difftreelog
feat enum prompt variant
in: trunk
11 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -351,9 +351,9 @@
[[package]]
name = "clap"
-version = "4.5.11"
+version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3"
+checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc"
dependencies = [
"clap_builder",
"clap_derive",
@@ -361,9 +361,9 @@
[[package]]
name = "clap_builder"
-version = "4.5.11"
+version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa"
+checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99"
dependencies = [
"anstream",
"anstyle",
@@ -373,9 +373,9 @@
[[package]]
name = "clap_derive"
-version = "4.5.11"
+version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e"
+checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
dependencies = [
"heck",
"proc-macro2",
@@ -863,24 +863,6 @@
]
[[package]]
-name = "polkit-agent"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "pam-client",
- "polkit-shared",
- "rand",
- "serde",
- "tokio",
- "tracing",
- "tracing-subscriber",
- "ui-prompt",
- "uuid",
- "zbus",
- "zbus_polkit",
-]
-
-[[package]]
name = "polkit-backend"
version = "0.1.0"
dependencies = [
@@ -1013,6 +995,29 @@
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]]
+name = "remowt-agent"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "pam-client",
+ "polkit-shared",
+ "rand",
+ "serde",
+ "tokio",
+ "tracing",
+ "tracing-subscriber",
+ "ui-prompt",
+ "uuid",
+ "zbus",
+ "zbus_polkit",
+]
+
+[[package]]
+name = "remowt-ssh"
+version = "0.1.0"
+
+[[package]]
name = "rpassword"
version = "6.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
Cargo.tomldiffbeforeafterboth--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,3 +1,7 @@
[workspace]
members = ["cmds/*", "crates/*"]
resolver = "2"
+
+[workspace.packages]
+bifrostlink = { path = "../bifrostlink/crates/bifrostlink" }
+bifrostlink-ports = { path = "../bifrostlink/crates/bifrostlink-ports" }
cmds/polkit-agent/Cargo.tomldiffbeforeafterboth--- a/cmds/polkit-agent/Cargo.toml
+++ /dev/null
@@ -1,18 +0,0 @@
-[package]
-name = "polkit-agent"
-version = "0.1.0"
-edition = "2021"
-
-[dependencies]
-anyhow = "1.0.86"
-pam-client = "0.5.0"
-polkit-shared = { version = "0.1.0", path = "../../crates/polkit-shared" }
-rand = "0.8.5"
-serde = { version = "1.0.204", features = ["derive"] }
-tokio = { version = "1.39.2", features = ["rt-multi-thread", "fs", "macros"] }
-tracing = "0.1.40"
-tracing-subscriber = "0.3.18"
-ui-prompt = { version = "0.1.0", path = "../../crates/ui-prompt" }
-uuid = { version = "1.10.0", features = ["v4"] }
-zbus = { version = "4.4.0", features = ["tokio"] }
-zbus_polkit = { version = "4.0.0", features = ["tokio"] }
cmds/polkit-agent/src/main.rsdiffbeforeafterboth--- a/cmds/polkit-agent/src/main.rs
+++ /dev/null
@@ -1,177 +0,0 @@
-use std::collections::HashMap;
-use std::future;
-use std::marker::PhantomData;
-use std::sync::{Mutex, RwLock};
-
-use polkit_shared::{BackendRequest, Identity};
-use tokio::runtime::Handle;
-use tokio::task::{AbortHandle, JoinHandle, LocalSet};
-use tracing::trace;
-use ui_prompt::dbus::DbusPrompterInterface;
-use ui_prompt::rofi::RofiPrompter;
-use ui_prompt::Prompter;
-use zbus::zvariant::{OwnedValue, Str};
-use zbus::ObjectServer;
-use zbus::{interface, proxy, Connection};
-use zbus_polkit::policykit1::Subject;
-
-struct TemporaryPrompterInterface<P: Prompter + Send + Sync + 'static> {
- connection: Connection,
- path: String,
- _marker: PhantomData<P>,
-}
-impl<P: Prompter + Send + Sync + 'static> TemporaryPrompterInterface<P> {
- async fn new(connection: Connection, prompter: P) -> Self {
- let path = format!(
- "/remowt/prompters/{}",
- uuid::Uuid::new_v4().to_string().replace("-", "_")
- );
- let _ = connection
- .object_server()
- .at(path.clone(), DbusPrompterInterface(prompter))
- .await;
- Self {
- connection,
- path,
- _marker: PhantomData,
- }
- }
-}
-impl<P: Prompter + Send + Sync + 'static> Drop for TemporaryPrompterInterface<P> {
- fn drop(&mut self) {
- // FIXME: block_in_place prevents to moving to current_thread runtime
- // There should be a blocking way to remove ObjectServer listener.
- // As far as I can see, it is only async because of async RwLock, shouldn't it be
- // just a sync lock?
- tokio::task::block_in_place(move || {
- Handle::current().block_on(async {
- let _ = self
- .connection
- .object_server()
- .remove::<DbusPrompterInterface<P>, String>(self.path.clone())
- .await;
- });
- });
- }
-}
-
-struct Agent {
- helper: PolkitHelperProxy<'static>,
- tasks: Mutex<HashMap<String, AbortHandle>>,
- connection: Connection,
-}
-impl Agent {
- async fn new(connection: Connection) -> anyhow::Result<Self> {
- Ok(Self {
- helper: PolkitHelperProxy::new(&connection).await?,
- tasks: Mutex::new(HashMap::new()),
- connection,
- })
- }
-}
-
-#[interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]
-impl Agent {
- /// BeginAuthentication method
- #[allow(clippy::too_many_arguments)]
- async fn begin_authentication(
- &mut self,
- action_id: String,
- message: String,
- icon_name: String,
- details: HashMap<String, String>,
- cookie: String,
- identities: Vec<Identity>,
- ) -> zbus::fdo::Result<()> {
- trace!("begin auth");
- let task = {
- let connection = self.connection.clone();
- let helper = self.helper.clone();
- let cookie = cookie.clone();
- tokio::task::spawn(async move {
- trace!("conversation task");
- let prompter = TemporaryPrompterInterface::new(connection, RofiPrompter).await;
- helper
- .init_conversation(
- BackendRequest {
- cookie: cookie.to_owned(),
- environment: HashMap::new(),
- prompter_path: prompter.path.clone(),
- // TODO: Let user choose
- identity: identities.get(0).expect("first always exists").clone(),
- }, // cookie.to_owned(), HashMap::new(), prompter.path.clone()
- )
- .await?;
- println!("ASKED");
- dbg!(action_id, message, icon_name, details, cookie, identities);
-
- Ok(())
- })
- };
-
- self.tasks
- .lock()
- .unwrap()
- .insert(cookie.clone(), task.abort_handle());
- let result = task.await.expect("join error");
- // The only way to no reach this line, is to either panic in previous line, or if authorization cancelled,
- // while cancellation will remove task by itself.
- // TODO: But still it would be better to have abort guard, which will remove it from HashMap
- self.tasks.lock().unwrap().remove(&cookie);
-
- result
- }
-
- /// CancelAuthentication method
- async fn cancel_authentication(&self, cookie: &str) -> zbus::fdo::Result<()> {
- trace!("cancel auth");
- if let Some(abort) = self.tasks.lock().unwrap().remove(cookie) {
- abort.abort();
- }
- // debug!("Authentication cancled ! {cookie}");
- Ok(())
- }
-}
-
-const OBJ_PATH: &str = "/0lach/polkitAgent";
-#[tokio::main]
-async fn main() -> anyhow::Result<()> {
- tracing_subscriber::fmt::init();
-
- trace!("started");
- let conn = Connection::system().await?;
-
- let proxy = zbus_polkit::policykit1::AuthorityProxy::new(&conn).await?;
- conn.object_server()
- .at(OBJ_PATH, Agent::new(conn.clone()).await?)
- .await?;
-
- let session_id = std::env::var("XDG_SESSION_ID")?;
- let mut details = HashMap::new();
- let val: OwnedValue = {
- let wrapped: Str<'_> = session_id.into();
- wrapped.into()
- };
- details.insert("session-id".to_string(), val);
- proxy
- .register_authentication_agent(
- &Subject {
- subject_kind: "unix-session".to_string(),
- subject_details: details,
- },
- "C",
- OBJ_PATH,
- )
- .await?;
-
- future::pending().await
-}
-
-#[proxy(
- interface = "lach.PolkitHelper",
- default_service = "lach.polkit.helper1",
- default_path = "/lach/PolkitHelper"
-)]
-trait PolkitHelper {
- fn init_conversation(&self, request: BackendRequest) -> zbus::Result<()>;
-}
cmds/remowt-agent/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/cmds/remowt-agent/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "remowt-agent"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+anyhow = "1.0.86"
+clap = { version = "4.5.13", features = ["derive"] }
+pam-client = "0.5.0"
+polkit-shared = { version = "0.1.0", path = "../../crates/polkit-shared" }
+rand = "0.8.5"
+serde = { version = "1.0.204", features = ["derive"] }
+tokio = { version = "1.39.2", features = ["rt-multi-thread", "fs", "macros"] }
+tracing = "0.1.40"
+tracing-subscriber = "0.3.18"
+ui-prompt = { version = "0.1.0", path = "../../crates/ui-prompt" }
+uuid = { version = "1.10.0", features = ["v4"] }
+zbus = { version = "4.4.0", features = ["tokio"] }
+zbus_polkit = { version = "4.0.0", features = ["tokio"] }
cmds/remowt-agent/src/main.rsdiffbeforeafterboth--- /dev/null
+++ b/cmds/remowt-agent/src/main.rs
@@ -0,0 +1,197 @@
+use std::collections::HashMap;
+use std::io::{stdout, Write};
+use std::marker::PhantomData;
+use std::sync::{Mutex, RwLock};
+use std::{future, process};
+
+use clap::Parser;
+use polkit_shared::{BackendRequest, Identity};
+use tokio::runtime::Handle;
+use tokio::task::{AbortHandle, JoinHandle, LocalSet};
+use tracing::trace;
+use ui_prompt::dbus::DbusPrompterInterface;
+use ui_prompt::rofi::RofiPrompter;
+use ui_prompt::Prompter;
+use zbus::zvariant::{OwnedValue, Str};
+use zbus::ObjectServer;
+use zbus::{interface, proxy, Connection};
+use zbus_polkit::policykit1::Subject;
+
+struct TemporaryPrompterInterface<P: Prompter + Send + Sync + 'static> {
+ connection: Connection,
+ path: String,
+ _marker: PhantomData<P>,
+}
+impl<P: Prompter + Send + Sync + 'static> TemporaryPrompterInterface<P> {
+ async fn new(connection: Connection, prompter: P) -> Self {
+ let path = format!(
+ "/remowt/prompters/{}",
+ uuid::Uuid::new_v4().to_string().replace("-", "_")
+ );
+ let _ = connection
+ .object_server()
+ .at(path.clone(), DbusPrompterInterface(prompter))
+ .await;
+ Self {
+ connection,
+ path,
+ _marker: PhantomData,
+ }
+ }
+}
+impl<P: Prompter + Send + Sync + 'static> Drop for TemporaryPrompterInterface<P> {
+ fn drop(&mut self) {
+ // FIXME: block_in_place prevents to moving to current_thread runtime
+ // There should be a blocking way to remove ObjectServer listener.
+ // As far as I can see, it is only async because of async RwLock, shouldn't it be
+ // just a sync lock?
+ tokio::task::block_in_place(move || {
+ Handle::current().block_on(async {
+ let _ = self
+ .connection
+ .object_server()
+ .remove::<DbusPrompterInterface<P>, String>(self.path.clone())
+ .await;
+ });
+ });
+ }
+}
+
+struct Agent {
+ helper: PolkitHelperProxy<'static>,
+ tasks: Mutex<HashMap<String, AbortHandle>>,
+ connection: Connection,
+}
+impl Agent {
+ async fn new(connection: Connection) -> anyhow::Result<Self> {
+ Ok(Self {
+ helper: PolkitHelperProxy::new(&connection).await?,
+ tasks: Mutex::new(HashMap::new()),
+ connection,
+ })
+ }
+}
+
+#[interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]
+impl Agent {
+ /// BeginAuthentication method
+ #[allow(clippy::too_many_arguments)]
+ async fn begin_authentication(
+ &mut self,
+ action_id: String,
+ message: String,
+ icon_name: String,
+ details: HashMap<String, String>,
+ cookie: String,
+ identities: Vec<Identity>,
+ ) -> zbus::fdo::Result<()> {
+ trace!("begin auth");
+ let task = {
+ let connection = self.connection.clone();
+ let helper = self.helper.clone();
+ let cookie = cookie.clone();
+ tokio::task::spawn(async move {
+ trace!("conversation task");
+ let prompter = TemporaryPrompterInterface::new(connection, RofiPrompter).await;
+ helper
+ .init_conversation(
+ BackendRequest {
+ cookie: cookie.to_owned(),
+ environment: HashMap::new(),
+ prompter_path: prompter.path.clone(),
+ // TODO: Let user choose
+ identity: identities.get(0).expect("first always exists").clone(),
+ }, // cookie.to_owned(), HashMap::new(), prompter.path.clone()
+ )
+ .await?;
+ println!("ASKED");
+ dbg!(action_id, message, icon_name, details, cookie, identities);
+
+ Ok(())
+ })
+ };
+
+ self.tasks
+ .lock()
+ .unwrap()
+ .insert(cookie.clone(), task.abort_handle());
+ let result = task.await.expect("join error");
+ // The only way to no reach this line, is to either panic in previous line, or if authorization cancelled,
+ // while cancellation will remove task by itself.
+ // TODO: But still it would be better to have abort guard, which will remove it from HashMap
+ self.tasks.lock().unwrap().remove(&cookie);
+
+ result
+ }
+
+ /// CancelAuthentication method
+ async fn cancel_authentication(&self, cookie: &str) -> zbus::fdo::Result<()> {
+ trace!("cancel auth");
+ if let Some(abort) = self.tasks.lock().unwrap().remove(cookie) {
+ abort.abort();
+ }
+ // debug!("Authentication cancled ! {cookie}");
+ Ok(())
+ }
+}
+
+const OBJ_PATH: &str = "/0lach/polkitAgent";
+
+#[proxy(
+ interface = "lach.PolkitHelper",
+ default_service = "lach.polkit.helper1",
+ default_path = "/lach/PolkitHelper"
+)]
+trait PolkitHelper {
+ fn init_conversation(&self, request: BackendRequest) -> zbus::Result<()>;
+}
+
+#[derive(Parser)]
+enum Opts {
+ Agent,
+ AskPass { description: String },
+}
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ tracing_subscriber::fmt::init();
+ let opts = Opts::parse();
+
+ match opts {
+ Opts::Agent => {
+ trace!("started");
+ let conn = Connection::system().await?;
+
+ let proxy = zbus_polkit::policykit1::AuthorityProxy::new(&conn).await?;
+ conn.object_server()
+ .at(OBJ_PATH, Agent::new(conn.clone()).await?)
+ .await?;
+
+ let session_id = std::env::var("XDG_SESSION_ID")?;
+ let mut details = HashMap::new();
+ let val: OwnedValue = {
+ let wrapped: Str<'_> = session_id.into();
+ wrapped.into()
+ };
+ details.insert("session-id".to_string(), val);
+ proxy
+ .register_authentication_agent(
+ &Subject {
+ subject_kind: "unix-session".to_string(),
+ subject_details: details,
+ },
+ "C",
+ OBJ_PATH,
+ )
+ .await?;
+ }
+ Opts::AskPass { description } => {
+ let password = RofiPrompter
+ .prompt_text(false, &description, "SSH password request", &[])
+ .await?;
+ stdout().lock().write_all(password.as_bytes())?;
+ }
+ }
+
+ future::pending().await
+}
cmds/remowt-ssh/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/cmds/remowt-ssh/Cargo.toml
@@ -0,0 +1,6 @@
+[package]
+name = "remowt-ssh"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
cmds/remowt-ssh/src/main.rsdiffbeforeafterboth--- /dev/null
+++ b/cmds/remowt-ssh/src/main.rs
@@ -0,0 +1,3 @@
+fn main() {
+ println!("Hello, world!");
+}
crates/ui-prompt/src/dbus.rsdiffbeforeafterboth--- a/crates/ui-prompt/src/dbus.rs
+++ b/crates/ui-prompt/src/dbus.rs
@@ -40,12 +40,13 @@
#[proxy(interface = "lach.PolkitInputHandler")]
trait DbusPrompter {
- async fn prompt_radio(
+ async fn prompt_enum(
&self,
prompt: &str,
description: &str,
+ variants: &[&str],
source: &[Source],
- ) -> fdo::Result<bool>;
+ ) -> fdo::Result<u32>;
async fn prompt_text(
&self,
echo: bool,
@@ -62,13 +63,16 @@
}
impl Prompter for DbusPrompterProxy<'_> {
- async fn prompt_radio(
+ async fn prompt_enum(
&self,
prompt: &str,
description: &str,
+ variants: &[&str],
source: &[Source],
- ) -> Result<bool> {
- Ok(self.prompt_radio(prompt, description, source).await?)
+ ) -> Result<u32> {
+ Ok(self
+ .prompt_enum(prompt, description, variants, source)
+ .await?)
}
async fn prompt_text(
@@ -86,8 +90,14 @@
}
}
impl BlockingPrompter for DbusPrompterProxyBlocking<'_> {
- fn prompt_radio(&self, prompt: &str, description: &str, source: &[Source]) -> Result<bool> {
- Ok(self.prompt_radio(prompt, description, source)?)
+ fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ Ok(self.prompt_enum(prompt, description, variants, source)?)
}
fn prompt_text(
crates/ui-prompt/src/lib.rsdiffbeforeafterboth--- a/crates/ui-prompt/src/lib.rs
+++ b/crates/ui-prompt/src/lib.rs
@@ -31,7 +31,17 @@
prompt: &str,
description: &str,
source: &[Source],
- ) -> impl Future<Output = Result<bool>> + Send;
+ ) -> impl Future<Output = Result<bool>> + Send {
+ let fut = self.prompt_enum(prompt, description, &["No", "Yes"], source);
+ async { fut.await.map(|v| v == 1) }
+ }
+ fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> impl Future<Output = Result<u32>> + Send;
fn prompt_text(
&self,
echo: bool,
@@ -47,7 +57,17 @@
) -> impl Future<Output = Result<()>> + Send;
}
pub trait BlockingPrompter {
- fn prompt_radio(&self, prompt: &str, description: &str, source: &[Source]) -> Result<bool>;
+ fn prompt_radio(&self, prompt: &str, description: &str, source: &[Source]) -> Result<bool> {
+ self.prompt_enum(prompt, description, &["No", "Yes"], source)
+ .map(|v| v == 1)
+ }
+ fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32>;
fn prompt_text(
&self,
echo: bool,
@@ -73,14 +93,15 @@
where
P: Prompter + Sync,
{
- async fn prompt_radio(
+ async fn prompt_enum(
&self,
prompt: &str,
description: &str,
+ variants: &[&str],
source: &[Source],
- ) -> Result<bool> {
+ ) -> Result<u32> {
self.prompter
- .prompt_radio(prompt, description, &self.source(source))
+ .prompt_enum(prompt, description, variants, &self.source(source))
.await
}
crates/ui-prompt/src/rofi.rsdiffbeforeafterboth1use std::process::Stdio;23use tokio::io::AsyncWriteExt;4use tokio::process::Command;5use tracing::trace;67use crate::{Error, Prompter, Result, Source};89pub struct RofiPrompter;1011fn fixup_prompt(prompt: &str) -> &str {12 // Rofi always appends such suffix13 prompt.strip_suffix(": ").unwrap_or(prompt)14}1516impl Prompter for RofiPrompter {17 async fn prompt_enum(18 &self,19 prompt: &str,20 description: &str,21 variants: &[&str],22 source: &[Source],23 ) -> Result<u32> {24 trace!("rofi radio");25 let mut cmd = Command::new("rofi");26 let mesg = if source.is_empty() {27 description.to_owned()28 } else {29 let mut out = format!("{description}\n\n<b>Requested on ",);30 for s in source.iter() {31 out.push_str(&s.to_string());32 }33 out.push_str("</b>");34 out35 };36 cmd.args([37 "-dmenu",38 "-mesg",39 &mesg,40 "-sync",41 "-only-match",42 "-p",43 fixup_prompt(prompt),44 "-format",45 "i",46 ]);47 cmd.stdin(Stdio::piped());48 cmd.stdout(Stdio::piped());49 cmd.kill_on_drop(true);50 let mut child = cmd51 .spawn()52 .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;5354 let mut stdin = child.stdin.take().expect("stdin is piped");55 for var in variants {56 stdin57 .write_all(var.replace('\n', " ").as_bytes())58 .await59 .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;60 stdin61 .write_all(b"\n")62 .await63 .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;64 }65 // write_all already flushes, just to be sure.66 let _ = stdin.flush().await;67 drop(stdin);6869 let out = child70 .wait_with_output()71 .await72 .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;73 let stdout = out74 .stdout75 .strip_suffix(b"\n")76 .unwrap_or(&out.stdout)77 .to_owned();7879 let id: u32 = String::from_utf8(stdout)80 .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?81 .parse()82 .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?;83 if id as usize >= variants.len() {84 return Err(Error::InputError("invalid rofi response".to_owned()));85 }8687 Ok(id)88 }8990 async fn prompt_text(91 &self,92 echo: bool,93 prompt: &str,94 description: &str,95 source: &[Source],96 ) -> Result<String> {97 trace!("rofi text");98 let mut cmd = Command::new("rofi");99 let mesg = if source.is_empty() {100 description.to_owned()101 } else {102 let mut out = format!("{description}\n\n<b>Requested on ",);103 for s in source.iter() {104 out.push_str(&s.to_string());105 }106 out.push_str("</b>");107 out108 };109 cmd.args(["-dmenu", "-mesg", &mesg, "-p", fixup_prompt(prompt)]);110 if !echo {111 cmd.arg("-password");112 }113 cmd.stdin(Stdio::null());114 cmd.stdout(Stdio::piped());115 cmd.kill_on_drop(true);116 let child = cmd117 .spawn()118 .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;119120 let out = child121 .wait_with_output()122 .await123 .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;124 let stdout = out125 .stdout126 .strip_suffix(b"\n")127 .unwrap_or(&out.stdout)128 .to_owned();129130 Ok(String::from_utf8_lossy(&stdout).to_string())131 }132133 async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {134 trace!("rofi display");135 let mut cmd = Command::new("rofi");136 let mut mesg = if source.is_empty() {137 description.to_owned()138 } else {139 let mut out = format!("{description}\n\n<b>Coming from ",);140 for s in source.iter() {141 out.push_str(&s.to_string());142 }143 out.push_str("</b>");144 out145 };146 if error {147 mesg.insert_str(0, "<span color=\"red\">");148 mesg.push_str("</span>");149 }150 cmd.args(["-e", &mesg, "-markup"]);151 cmd.stdin(Stdio::null());152 cmd.stdout(Stdio::null());153 cmd.kill_on_drop(true);154 let mut child = cmd155 .spawn()156 .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;157158 child159 .wait()160 .await161 .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;162163 Ok(())164 }165}166167#[cfg(test)]168mod tests {169 use std::borrow::Cow;170171 use crate::rofi::RofiPrompter;172 use crate::{PrependSourcePrompter, Prompter as _, Source};173174 #[tokio::test]175 async fn test() {176 let prompter = PrependSourcePrompter {177 prompter: RofiPrompter,178 source: vec![Source(Cow::Borrowed("ssh"))],179 };180 prompter181 .prompt_radio("Enable", "Polkit needs access", &[])182 .await183 .expect("rofi");184 prompter185 .prompt_text(false, "Password", "Polkit needs access", &[])186 .await187 .expect("rofi");188 prompter189 .display_text(true, "Polkit needs access", &[])190 .await191 .expect("rofi");192 }193}