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};89#[derive(Clone)]10pub struct RofiPrompter;1112fn fixup_prompt(prompt: &str) -> &str {13 // Rofi always appends such suffix14 prompt.strip_suffix(": ").unwrap_or(prompt)15}1617fn rofi_command() -> Command {18 Command::new(option_env!("ROFI").unwrap_or("rofi"))19}2021impl Prompter for RofiPrompter {22 async fn prompt_enum(23 &self,24 prompt: &str,25 description: &str,26 variants: &[&str],27 source: &[Source],28 ) -> Result<u32> {29 trace!("rofi radio");30 let mut cmd = rofi_command();31 let mesg = if source.is_empty() {32 description.to_owned()33 } else {34 let mut out = format!("{description}\n\n<b>Requested on ",);35 for (i, s) in source.iter().enumerate() {36 if i != 0 {37 out.push_str(" -> ");38 }39 out.push_str(&s.to_string());40 }41 out.push_str("</b>");42 out43 };44 cmd.args([45 "-dmenu",46 "-mesg",47 &mesg,48 "-sync",49 "-only-match",50 "-p",51 fixup_prompt(prompt),52 "-format",53 "i",54 "-markup-rows",55 ]);56 cmd.stdin(Stdio::piped());57 cmd.stdout(Stdio::piped());58 cmd.kill_on_drop(true);59 let mut child = cmd60 .spawn()61 .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;6263 let mut stdin = child.stdin.take().expect("stdin is piped");64 for var in variants {65 stdin66 .write_all(var.replace('\n', " ").as_bytes())67 .await68 .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;69 stdin70 .write_all(b"\n")71 .await72 .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;73 }74 // write_all already flushes, just to be sure.75 let _ = stdin.flush().await;76 drop(stdin);7778 let out = child79 .wait_with_output()80 .await81 .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;82 let stdout = out83 .stdout84 .strip_suffix(b"\n")85 .unwrap_or(&out.stdout)86 .to_owned();8788 let id: u32 = String::from_utf8(stdout)89 .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?90 .parse()91 .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?;92 if id as usize >= variants.len() {93 return Err(Error::InputError("invalid rofi response".to_owned()));94 }9596 Ok(id)97 }9899 async fn prompt_text(100 &self,101 echo: bool,102 prompt: &str,103 description: &str,104 source: &[Source],105 ) -> Result<String> {106 trace!("rofi text");107 let mut cmd = rofi_command();108 let mesg = if source.is_empty() {109 description.to_owned()110 } else {111 let mut out = format!("{description}\n\n<b>Requested on ",);112 for (i, s) in source.iter().enumerate() {113 if i != 0 {114 out.push_str(" -> ");115 }116 out.push_str(&s.to_string());117 }118 out.push_str("</b>");119 out120 };121 cmd.args(["-dmenu", "-mesg", &mesg, "-p", fixup_prompt(prompt)]);122 if !echo {123 cmd.arg("-password");124 }125 cmd.stdin(Stdio::null());126 cmd.stdout(Stdio::piped());127 cmd.kill_on_drop(true);128 let child = cmd129 .spawn()130 .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;131132 let out = child133 .wait_with_output()134 .await135 .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;136 let stdout = out137 .stdout138 .strip_suffix(b"\n")139 .unwrap_or(&out.stdout)140 .to_owned();141142 Ok(String::from_utf8_lossy(&stdout).to_string())143 }144145 async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {146 trace!("rofi display");147 let mut cmd = rofi_command();148 let mut mesg = if source.is_empty() {149 description.to_owned()150 } else {151 let mut out = format!("{description}\n\n<b>Coming from ",);152 for s in source.iter() {153 out.push_str(&s.to_string());154 }155 out.push_str("</b>");156 out157 };158 if error {159 mesg.insert_str(0, "<span color=\"red\">");160 mesg.push_str("</span>");161 }162 cmd.args(["-e", &mesg, "-markup"]);163 cmd.stdin(Stdio::null());164 cmd.stdout(Stdio::null());165 cmd.kill_on_drop(true);166 let mut child = cmd167 .spawn()168 .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;169170 child171 .wait()172 .await173 .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;174175 Ok(())176 }177}178179#[cfg(test)]180mod tests {181 use std::borrow::Cow;182183 use crate::rofi::RofiPrompter;184 use crate::{PrependSourcePrompter, Prompter as _, Source};185186 // #[tokio::test]187 #[tokio::test]188 #[ignore = "interactive"]189 async fn test() {190 let prompter = PrependSourcePrompter {191 prompter: RofiPrompter,192 description: "test".to_owned(),193 source: vec![Source(Cow::Borrowed("ssh"))],194 };195 prompter196 .prompt_radio("Enable", "Polkit needs access", &[])197 .await198 .expect("rofi");199 prompter200 .prompt_text(false, "Password", "Polkit needs access", &[])201 .await202 .expect("rofi");203 prompter204 .display_text(true, "Polkit needs access", &[])205 .await206 .expect("rofi");207 }208}