difftreelog
feat(ui-prompt) declarative bifrost endpoint
in: trunk
5 files changed
crates/ui-prompt/Cargo.tomldiffbeforeafterboth--- a/crates/ui-prompt/Cargo.toml
+++ b/crates/ui-prompt/Cargo.toml
@@ -5,11 +5,13 @@
[dependencies]
bifrostlink.workspace = true
-serde = "1.0.204"
+bifrostlink-macros.workspace = true
+serde.workspace = true
+serde_json.workspace = true
thiserror = "1.0.63"
-tokio = { version = "1.39.2", features = ["io-util", "macros", "process", "rt"] }
-tracing = "0.1.40"
-zbus = { version = "4.4.0", optional = true }
+tokio = { workspace = true, features = ["io-util", "macros", "process", "rt"] }
+tracing.workspace = true
+zbus = { workspace = true, optional = true }
[features]
default = ["dbus"]
crates/ui-prompt/src/bifrost.rsdiffbeforeafterboth--- a/crates/ui-prompt/src/bifrost.rs
+++ b/crates/ui-prompt/src/bifrost.rs
@@ -1,72 +1,73 @@
-use bifrostlink::error::ErrorT;
-use bifrostlink::{request, AddressT, Rpc};
+use bifrostlink::{Config, Rpc};
+use bifrostlink_macros::endpoints;
use serde::{Deserialize, Serialize};
use crate::{Error, Prompter, Source};
-pub struct BifrostPrompter<A: AddressT, E: ErrorT> {
- pub address: A,
- pub rpc: Rpc<A, E>,
-}
+pub struct PromptEndpoints<P>(pub P);
-#[derive(Serialize, Deserialize)]
-struct EnumRequest {
- prompt: String,
- description: String,
- variants: Vec<String>,
- source: Vec<Source>,
-}
-#[derive(Serialize, Deserialize)]
-struct EnumResponse {
- value: u32,
-}
-request!(EnumRequest => EnumResponse);
+#[endpoints(ns = 2)]
+impl<P> PromptEndpoints<P>
+where
+ P: Prompter + Send + Sync + 'static,
+{
+ #[endpoints(id = 1, cancel)]
+ async fn prompt_enum(
+ &self,
+ prompt: String,
+ description: String,
+ variants: Vec<String>,
+ source: Vec<Source>,
+ ) -> Result<u32, Error> {
+ let variants: Vec<&str> = variants.iter().map(|v| v.as_str()).collect();
+ self.0
+ .prompt_enum(&prompt, &description, &variants, &source)
+ .await
+ }
-#[derive(Serialize, Deserialize)]
-struct TextRequest {
- echo: bool,
- prompt: String,
- description: String,
- source: Vec<Source>,
-}
-#[derive(Serialize, Deserialize)]
-struct TextResponse {
- value: String,
-}
-request!(TextRequest => TextResponse);
+ #[endpoints(id = 2, cancel)]
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: String,
+ description: String,
+ source: Vec<Source>,
+ ) -> Result<String, Error> {
+ self.0
+ .prompt_text(echo, &prompt, &description, &source)
+ .await
+ }
-#[derive(Serialize, Deserialize)]
-struct DisplayRequest {
- error: bool,
- description: String,
- source: Vec<Source>,
+ #[endpoints(id = 3, cancel)]
+ async fn display_text(
+ &self,
+ error: bool,
+ description: String,
+ source: Vec<Source>,
+ ) -> Result<(), Error> {
+ self.0.display_text(error, &description, &source).await
+ }
}
-request!(DisplayRequest => ());
-impl<A: AddressT, E: ErrorT> Prompter for BifrostPrompter<A, E>
+impl<C: Config> Prompter for PromptEndpointsClient<C>
where
- crate::Error: From<E>,
+ Error: ToString,
{
async fn prompt_enum(
&self,
prompt: &str,
description: &str,
variants: &[&str],
- source: &[crate::Source],
+ source: &[Source],
) -> crate::Result<u32> {
- let res = self
- .rpc
- .request(
- self.address.clone(),
- &EnumRequest {
- prompt: prompt.to_owned(),
- description: description.to_owned(),
- variants: variants.into_iter().map(|v| (*v).to_owned()).collect(),
- source: source.to_vec(),
- },
- )
- .await?;
- Ok(res.value)
+ self.prompt_enum(
+ prompt.to_owned(),
+ description.to_owned(),
+ variants.iter().map(|v| (*v).to_owned()).collect(),
+ source.to_vec(),
+ )
+ .await
+ .map_err(|e| Error::Remote(e.to_string()))?
}
async fn prompt_text(
@@ -74,93 +75,35 @@
echo: bool,
prompt: &str,
description: &str,
- source: &[crate::Source],
+ source: &[Source],
) -> crate::Result<String> {
- let res = self
- .rpc
- .request(
- self.address.clone(),
- &TextRequest {
- echo,
- prompt: prompt.to_owned(),
- description: description.to_owned(),
- source: source.to_vec(),
- },
- )
- .await?;
- Ok(res.value)
+ self.prompt_text(
+ echo,
+ prompt.to_owned(),
+ description.to_owned(),
+ source.to_vec(),
+ )
+ .await
+ .map_err(|e| Error::Remote(e.to_string()))?
}
async fn display_text(
&self,
error: bool,
description: &str,
- source: &[crate::Source],
+ source: &[Source],
) -> crate::Result<()> {
- self.rpc
- .request(
- self.address.clone(),
- &DisplayRequest {
- error,
- description: description.to_owned(),
- source: source.to_vec(),
- },
- )
- .await?;
- Ok(())
+ self.display_text(error, description.to_owned(), source.to_vec())
+ .await
+ .map_err(|e| Error::Remote(e.to_string()))?
}
}
-pub fn handle_bifrost_prompts<
- P: Prompter + Clone + 'static,
- A: AddressT,
- E: ErrorT + From<Error>,
->(
- rpc: &Rpc<A, E>,
- prompt: P,
-) {
- rpc.register_request_handler(true, {
- let prompt = prompt.clone();
- move |_addr, req: EnumRequest| {
- let prompt = prompt.clone();
- async move {
- let i = prompt
- .prompt_enum(
- &req.prompt,
- &req.description,
- &req.variants.iter().map(|v| v.as_str()).collect::<Vec<_>>(),
- &req.source,
- )
- .await?;
-
- Ok(EnumResponse { value: i })
- }
- }
- });
- rpc.register_request_handler(true, {
- let prompt = prompt.clone();
- move |_addr, req: TextRequest| {
- let prompt = prompt.clone();
- async move {
- let i = prompt
- .prompt_text(req.echo, &req.prompt, &req.description, &req.source)
- .await?;
-
- Ok(TextResponse { value: i })
- }
- }
- });
- rpc.register_request_handler(true, {
- let prompt = prompt.clone();
- move |_addr, req: DisplayRequest| {
- let prompt = prompt.clone();
- async move {
- prompt
- .display_text(req.error, &req.description, &req.source)
- .await?;
-
- Ok(())
- }
- }
- });
+pub fn serve_prompts<P, C>(rpc: &mut Rpc<C>, prompt: P)
+where
+ P: Prompter + Send + Sync + 'static,
+ C: Config,
+ C::Error: From<Error>,
+{
+ PromptEndpoints(prompt).register_endpoints(rpc);
}
crates/ui-prompt/src/dbus.rsdiffbeforeafterboth--- a/crates/ui-prompt/src/dbus.rs
+++ b/crates/ui-prompt/src/dbus.rs
@@ -6,128 +6,130 @@
use crate::{Error, Prompter};
pub struct DbusPrompterInterface<P>(pub P);
+
#[interface(name = "lach.PolkitInputHandler")]
impl<P: Prompter + Send + Sync + 'static> DbusPrompterInterface<P> {
- async fn prompt_radio(
- &self,
- prompt: &str,
- description: &str,
- source: Vec<Source>,
- ) -> fdo::Result<bool> {
- Ok(self.0.prompt_radio(prompt, description, &source).await?)
- }
- async fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: Vec<Source>,
- ) -> fdo::Result<String> {
- Ok(self
- .0
- .prompt_text(echo, prompt, description, &source)
- .await?)
- }
- async fn display_text(
- &self,
- error: bool,
- description: &str,
- source: Vec<Source>,
- ) -> fdo::Result<()> {
- Ok(self.0.display_text(error, description, &source).await?)
- }
+ async fn prompt_radio(
+ &self,
+ prompt: &str,
+ description: &str,
+ source: Vec<Source>,
+ ) -> fdo::Result<bool> {
+ Ok(self.0.prompt_radio(prompt, description, &source).await?)
+ }
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: Vec<Source>,
+ ) -> fdo::Result<String> {
+ Ok(self
+ .0
+ .prompt_text(echo, prompt, description, &source)
+ .await?)
+ }
+ async fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: Vec<Source>,
+ ) -> fdo::Result<()> {
+ Ok(self.0.display_text(error, description, &source).await?)
+ }
}
#[proxy(interface = "lach.PolkitInputHandler")]
-trait DbusPrompter {
- async fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> fdo::Result<u32>;
- async fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> fdo::Result<String>;
- async fn display_text(
- &self,
- error: bool,
- description: &str,
- source: &[Source],
- ) -> fdo::Result<()>;
+pub trait DbusPrompter {
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> fdo::Result<u32>;
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> fdo::Result<String>;
+ async fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: &[Source],
+ ) -> fdo::Result<()>;
}
impl Prompter for DbusPrompterProxy<'_> {
- async fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> Result<u32> {
- Ok(self
- .prompt_enum(prompt, description, variants, source)
- .await?)
- }
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ Ok(self
+ .prompt_enum(prompt, description, variants, source)
+ .await?)
+ }
- async fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> Result<String> {
- Ok(self.prompt_text(echo, prompt, description, source).await?)
- }
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ Ok(self.prompt_text(echo, prompt, description, source).await?)
+ }
- async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
- Ok(self.display_text(error, description, source).await?)
- }
+ async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ Ok(self.display_text(error, description, source).await?)
+ }
}
impl BlockingPrompter for DbusPrompterProxyBlocking<'_> {
- fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> Result<u32> {
- Ok(self.prompt_enum(prompt, description, variants, 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(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> Result<String> {
- Ok(self.prompt_text(echo, prompt, description, source)?)
- }
+ fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ Ok(self.prompt_text(echo, prompt, description, source)?)
+ }
- fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
- Ok(self.display_text(error, description, source)?)
- }
+ fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ Ok(self.display_text(error, description, source)?)
+ }
}
impl From<fdo::Error> for Error {
- fn from(value: fdo::Error) -> Self {
- if matches!(value, fdo::Error::NoReply(_)) {
- return Self::Cancel;
- }
- Self::InputError(format!("{value}"))
- }
+ fn from(value: fdo::Error) -> Self {
+ if matches!(value, fdo::Error::NoReply(_)) {
+ return Self::Cancel;
+ }
+ Self::InputError(format!("{value}"))
+ }
}
impl From<Error> for fdo::Error {
- fn from(value: Error) -> Self {
- match value {
- Error::Cancel => fdo::Error::NoReply("input was cancelled".to_owned()),
- Error::InputError(e) => fdo::Error::Failed(e),
- }
- }
+ fn from(value: Error) -> Self {
+ match value {
+ Error::Cancel => fdo::Error::NoReply("input was cancelled".to_owned()),
+ Error::Remote(e) => fdo::Error::NoReply(format!("remote error occured: {e}")),
+ Error::InputError(e) => fdo::Error::Failed(e),
+ }
+ }
}
crates/ui-prompt/src/lib.rsdiffbeforeafterboth--- a/crates/ui-prompt/src/lib.rs
+++ b/crates/ui-prompt/src/lib.rs
@@ -3,16 +3,18 @@
use std::future::Future;
use std::result;
+pub mod bifrost;
pub mod dbus;
pub mod rofi;
-pub mod bifrost;
-#[derive(thiserror::Error, Debug)]
+#[derive(thiserror::Error, Debug, serde::Serialize, serde::Deserialize)]
pub enum Error {
- #[error("user has cancelled input")]
- Cancel,
- #[error("input error: {0}")]
- InputError(String),
+ #[error("user has cancelled input")]
+ Cancel,
+ #[error("input error: {0}")]
+ InputError(String),
+ #[error("unknown remote error: {0}")]
+ Remote(String),
}
pub type Result<T, E = Error> = result::Result<T, E>;
@@ -21,179 +23,179 @@
#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct Source(pub Cow<'static, str>);
impl fmt::Display for Source {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "<u>{}</u>", self.0)
- }
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "<u>{}</u>", self.0)
+ }
}
pub trait Prompter: Send + Sync {
- fn prompt_radio(
- &self,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> 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,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> impl Future<Output = Result<String>> + Send;
- fn display_text(
- &self,
- error: bool,
- description: &str,
- source: &[Source],
- ) -> impl Future<Output = Result<()>> + Send;
+ fn prompt_radio(
+ &self,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> 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,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<String>> + Send;
+ fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<()>> + Send;
}
pub trait BlockingPrompter {
- 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,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> Result<String>;
- fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()>;
+ 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,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String>;
+ fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()>;
}
impl<P> Prompter for &P
where
- P: Prompter,
+ P: Prompter,
{
- fn prompt_radio(
- &self,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> impl Future<Output = Result<bool>> + Send {
- (*self).prompt_radio(prompt, description, source)
- }
+ fn prompt_radio(
+ &self,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<bool>> + Send {
+ (*self).prompt_radio(prompt, description, source)
+ }
- fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> impl Future<Output = Result<u32>> + Send {
- (*self).prompt_enum(prompt, description, variants, source)
- }
+ fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> impl Future<Output = Result<u32>> + Send {
+ (*self).prompt_enum(prompt, description, variants, source)
+ }
- fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> impl Future<Output = Result<String>> + Send {
- (*self).prompt_text(echo, prompt, description, source)
- }
+ fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<String>> + Send {
+ (*self).prompt_text(echo, prompt, description, source)
+ }
- fn display_text(
- &self,
- error: bool,
- description: &str,
- source: &[Source],
- ) -> impl Future<Output = Result<()>> + Send {
- (*self).display_text(error, description, source)
- }
+ fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<()>> + Send {
+ (*self).display_text(error, description, source)
+ }
}
pub struct PrependSourcePrompter<P> {
- pub prompter: P,
- pub source: Vec<Source>,
- pub description: String,
+ pub prompter: P,
+ pub source: Vec<Source>,
+ pub description: String,
}
impl<P> PrependSourcePrompter<P> {
- fn source(&self, input: &[Source]) -> Vec<Source> {
- let mut out = self.source.clone();
- out.extend(input.iter().cloned());
- out
- }
- fn description(&self, input: &str) -> String {
- if self.description.is_empty() {
- input.to_owned()
- } else if input.is_empty() {
- self.description.to_owned()
- } else {
- format!("{input}\n\n{}", self.description)
- }
- }
+ fn source(&self, input: &[Source]) -> Vec<Source> {
+ let mut out = self.source.clone();
+ out.extend(input.iter().cloned());
+ out
+ }
+ fn description(&self, input: &str) -> String {
+ if self.description.is_empty() {
+ input.to_owned()
+ } else if input.is_empty() {
+ self.description.to_owned()
+ } else {
+ format!("{input}\n\n{}", self.description)
+ }
+ }
}
impl<P> Prompter for PrependSourcePrompter<P>
where
- P: Prompter + Sync,
+ P: Prompter + Sync,
{
- async fn prompt_radio(
- &self,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> Result<bool> {
- self.prompter
- .prompt_radio(prompt, &self.description(description), &self.source(source))
- .await
- }
+ async fn prompt_radio(
+ &self,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<bool> {
+ self.prompter
+ .prompt_radio(prompt, &self.description(description), &self.source(source))
+ .await
+ }
- async fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> Result<u32> {
- self.prompter
- .prompt_enum(
- prompt,
- dbg!(&self.description(description)),
- variants,
- &self.source(source),
- )
- .await
- }
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ self.prompter
+ .prompt_enum(
+ prompt,
+ &self.description(description),
+ variants,
+ &self.source(source),
+ )
+ .await
+ }
- async fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> Result<String> {
- self.prompter
- .prompt_text(
- echo,
- prompt,
- &self.description(description),
- &self.source(source),
- )
- .await
- }
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ self.prompter
+ .prompt_text(
+ echo,
+ prompt,
+ &self.description(description),
+ &self.source(source),
+ )
+ .await
+ }
- async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
- self.prompter
- .display_text(error, &self.description(description), &self.source(source))
- .await
- }
+ async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ self.prompter
+ .display_text(error, &self.description(description), &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 (i, s) in source.iter().enumerate() {31 if i != 0 {32 out.push_str(" -> ");33 }34 out.push_str(&s.to_string());35 }36 out.push_str("</b>");37 out38 };39 cmd.args([40 "-dmenu",41 "-mesg",42 &mesg,43 "-sync",44 "-only-match",45 "-p",46 fixup_prompt(prompt),47 "-format",48 "i",49 "-markup-rows",50 ]);51 cmd.stdin(Stdio::piped());52 cmd.stdout(Stdio::piped());53 cmd.kill_on_drop(true);54 let mut child = cmd55 .spawn()56 .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;5758 let mut stdin = child.stdin.take().expect("stdin is piped");59 for var in variants {60 stdin61 .write_all(var.replace('\n', " ").as_bytes())62 .await63 .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;64 stdin65 .write_all(b"\n")66 .await67 .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;68 }69 // write_all already flushes, just to be sure.70 let _ = stdin.flush().await;71 drop(stdin);7273 let out = child74 .wait_with_output()75 .await76 .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;77 let stdout = out78 .stdout79 .strip_suffix(b"\n")80 .unwrap_or(&out.stdout)81 .to_owned();8283 let id: u32 = String::from_utf8(stdout)84 .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?85 .parse()86 .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?;87 if id as usize >= variants.len() {88 return Err(Error::InputError("invalid rofi response".to_owned()));89 }9091 Ok(id)92 }9394 async fn prompt_text(95 &self,96 echo: bool,97 prompt: &str,98 description: &str,99 source: &[Source],100 ) -> Result<String> {101 trace!("rofi text");102 let mut cmd = Command::new("rofi");103 let mesg = if source.is_empty() {104 description.to_owned()105 } else {106 let mut out = format!("{description}\n\n<b>Requested on ",);107 for (i, s) in source.iter().enumerate() {108 if i != 0 {109 out.push_str(" -> ");110 }111 out.push_str(&s.to_string());112 }113 out.push_str("</b>");114 out115 };116 cmd.args(["-dmenu", "-mesg", &mesg, "-p", fixup_prompt(prompt)]);117 if !echo {118 cmd.arg("-password");119 }120 cmd.stdin(Stdio::null());121 cmd.stdout(Stdio::piped());122 cmd.kill_on_drop(true);123 let child = cmd124 .spawn()125 .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;126127 let out = child128 .wait_with_output()129 .await130 .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;131 let stdout = out132 .stdout133 .strip_suffix(b"\n")134 .unwrap_or(&out.stdout)135 .to_owned();136137 Ok(String::from_utf8_lossy(&stdout).to_string())138 }139140 async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {141 trace!("rofi display");142 let mut cmd = Command::new("rofi");143 let mut mesg = if source.is_empty() {144 description.to_owned()145 } else {146 let mut out = format!("{description}\n\n<b>Coming from ",);147 for s in source.iter() {148 out.push_str(&s.to_string());149 }150 out.push_str("</b>");151 out152 };153 if error {154 mesg.insert_str(0, "<span color=\"red\">");155 mesg.push_str("</span>");156 }157 cmd.args(["-e", &mesg, "-markup"]);158 cmd.stdin(Stdio::null());159 cmd.stdout(Stdio::null());160 cmd.kill_on_drop(true);161 let mut child = cmd162 .spawn()163 .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;164165 child166 .wait()167 .await168 .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;169170 Ok(())171 }172}173174#[cfg(test)]175mod tests {176 use std::borrow::Cow;177178 use crate::rofi::RofiPrompter;179 use crate::{PrependSourcePrompter, Prompter as _, Source};180181 #[tokio::test]182 async fn test() {183 let prompter = PrependSourcePrompter {184 prompter: RofiPrompter,185 source: vec![Source(Cow::Borrowed("ssh"))],186 };187 prompter188 .prompt_radio("Enable", "Polkit needs access", &[])189 .await190 .expect("rofi");191 prompter192 .prompt_text(false, "Password", "Polkit needs access", &[])193 .await194 .expect("rofi");195 prompter196 .display_text(true, "Polkit needs access", &[])197 .await198 .expect("rofi");199 }200}