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.rsdiffbeforeafterboth1use core::fmt;2use std::borrow::Cow;3use std::future::Future;4use std::result;56pub mod dbus;7pub mod rofi;8pub mod bifrost;910#[derive(thiserror::Error, Debug)]11pub enum Error {12 #[error("user has cancelled input")]13 Cancel,14 #[error("input error: {0}")]15 InputError(String),16}1718pub type Result<T, E = Error> = result::Result<T, E>;1920#[cfg_attr(feature = "dbus", derive(zbus::zvariant::Type))]21#[derive(serde::Serialize, serde::Deserialize, Clone)]22pub struct Source(pub Cow<'static, str>);23impl fmt::Display for Source {24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {25 write!(f, "<u>{}</u>", self.0)26 }27}2829pub trait Prompter: Send + Sync {30 fn prompt_radio(31 &self,32 prompt: &str,33 description: &str,34 source: &[Source],35 ) -> impl Future<Output = Result<bool>> + Send {36 let fut = self.prompt_enum(prompt, description, &["No", "Yes"], source);37 async { fut.await.map(|v| v == 1) }38 }39 fn prompt_enum(40 &self,41 prompt: &str,42 description: &str,43 variants: &[&str],44 source: &[Source],45 ) -> impl Future<Output = Result<u32>> + Send;46 fn prompt_text(47 &self,48 echo: bool,49 prompt: &str,50 description: &str,51 source: &[Source],52 ) -> impl Future<Output = Result<String>> + Send;53 fn display_text(54 &self,55 error: bool,56 description: &str,57 source: &[Source],58 ) -> impl Future<Output = Result<()>> + Send;59}60pub trait BlockingPrompter {61 fn prompt_radio(&self, prompt: &str, description: &str, source: &[Source]) -> Result<bool> {62 self.prompt_enum(prompt, description, &["No", "Yes"], source)63 .map(|v| v == 1)64 }65 fn prompt_enum(66 &self,67 prompt: &str,68 description: &str,69 variants: &[&str],70 source: &[Source],71 ) -> Result<u32>;72 fn prompt_text(73 &self,74 echo: bool,75 prompt: &str,76 description: &str,77 source: &[Source],78 ) -> Result<String>;79 fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()>;80}81impl<P> Prompter for &P82where83 P: Prompter,84{85 fn prompt_radio(86 &self,87 prompt: &str,88 description: &str,89 source: &[Source],90 ) -> impl Future<Output = Result<bool>> + Send {91 (*self).prompt_radio(prompt, description, source)92 }9394 fn prompt_enum(95 &self,96 prompt: &str,97 description: &str,98 variants: &[&str],99 source: &[Source],100 ) -> impl Future<Output = Result<u32>> + Send {101 (*self).prompt_enum(prompt, description, variants, source)102 }103104 fn prompt_text(105 &self,106 echo: bool,107 prompt: &str,108 description: &str,109 source: &[Source],110 ) -> impl Future<Output = Result<String>> + Send {111 (*self).prompt_text(echo, prompt, description, source)112 }113114 fn display_text(115 &self,116 error: bool,117 description: &str,118 source: &[Source],119 ) -> impl Future<Output = Result<()>> + Send {120 (*self).display_text(error, description, source)121 }122}123124pub struct PrependSourcePrompter<P> {125 pub prompter: P,126 pub source: Vec<Source>,127 pub description: String,128}129impl<P> PrependSourcePrompter<P> {130 fn source(&self, input: &[Source]) -> Vec<Source> {131 let mut out = self.source.clone();132 out.extend(input.iter().cloned());133 out134 }135 fn description(&self, input: &str) -> String {136 if self.description.is_empty() {137 input.to_owned()138 } else if input.is_empty() {139 self.description.to_owned()140 } else {141 format!("{input}\n\n{}", self.description)142 }143 }144}145impl<P> Prompter for PrependSourcePrompter<P>146where147 P: Prompter + Sync,148{149 async fn prompt_radio(150 &self,151 prompt: &str,152 description: &str,153 source: &[Source],154 ) -> Result<bool> {155 self.prompter156 .prompt_radio(prompt, &self.description(description), &self.source(source))157 .await158 }159160 async fn prompt_enum(161 &self,162 prompt: &str,163 description: &str,164 variants: &[&str],165 source: &[Source],166 ) -> Result<u32> {167 self.prompter168 .prompt_enum(169 prompt,170 dbg!(&self.description(description)),171 variants,172 &self.source(source),173 )174 .await175 }176177 async fn prompt_text(178 &self,179 echo: bool,180 prompt: &str,181 description: &str,182 source: &[Source],183 ) -> Result<String> {184 self.prompter185 .prompt_text(186 echo,187 prompt,188 &self.description(description),189 &self.source(source),190 )191 .await192 }193194 async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {195 self.prompter196 .display_text(error, &self.description(description), &self.source(source))197 .await198 }199}1use core::fmt;2use std::borrow::Cow;3use std::future::Future;4use std::result;56pub mod bifrost;7pub mod dbus;8pub mod rofi;910#[derive(thiserror::Error, Debug, serde::Serialize, serde::Deserialize)]11pub enum Error {12 #[error("user has cancelled input")]13 Cancel,14 #[error("input error: {0}")]15 InputError(String),16 #[error("unknown remote error: {0}")]17 Remote(String),18}1920pub type Result<T, E = Error> = result::Result<T, E>;2122#[cfg_attr(feature = "dbus", derive(zbus::zvariant::Type))]23#[derive(serde::Serialize, serde::Deserialize, Clone)]24pub struct Source(pub Cow<'static, str>);25impl fmt::Display for Source {26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {27 write!(f, "<u>{}</u>", self.0)28 }29}3031pub trait Prompter: Send + Sync {32 fn prompt_radio(33 &self,34 prompt: &str,35 description: &str,36 source: &[Source],37 ) -> impl Future<Output = Result<bool>> + Send {38 let fut = self.prompt_enum(prompt, description, &["No", "Yes"], source);39 async { fut.await.map(|v| v == 1) }40 }41 fn prompt_enum(42 &self,43 prompt: &str,44 description: &str,45 variants: &[&str],46 source: &[Source],47 ) -> impl Future<Output = Result<u32>> + Send;48 fn prompt_text(49 &self,50 echo: bool,51 prompt: &str,52 description: &str,53 source: &[Source],54 ) -> impl Future<Output = Result<String>> + Send;55 fn display_text(56 &self,57 error: bool,58 description: &str,59 source: &[Source],60 ) -> impl Future<Output = Result<()>> + Send;61}62pub trait BlockingPrompter {63 fn prompt_radio(&self, prompt: &str, description: &str, source: &[Source]) -> Result<bool> {64 self.prompt_enum(prompt, description, &["No", "Yes"], source)65 .map(|v| v == 1)66 }67 fn prompt_enum(68 &self,69 prompt: &str,70 description: &str,71 variants: &[&str],72 source: &[Source],73 ) -> Result<u32>;74 fn prompt_text(75 &self,76 echo: bool,77 prompt: &str,78 description: &str,79 source: &[Source],80 ) -> Result<String>;81 fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()>;82}83impl<P> Prompter for &P84where85 P: Prompter,86{87 fn prompt_radio(88 &self,89 prompt: &str,90 description: &str,91 source: &[Source],92 ) -> impl Future<Output = Result<bool>> + Send {93 (*self).prompt_radio(prompt, description, source)94 }9596 fn prompt_enum(97 &self,98 prompt: &str,99 description: &str,100 variants: &[&str],101 source: &[Source],102 ) -> impl Future<Output = Result<u32>> + Send {103 (*self).prompt_enum(prompt, description, variants, source)104 }105106 fn prompt_text(107 &self,108 echo: bool,109 prompt: &str,110 description: &str,111 source: &[Source],112 ) -> impl Future<Output = Result<String>> + Send {113 (*self).prompt_text(echo, prompt, description, source)114 }115116 fn display_text(117 &self,118 error: bool,119 description: &str,120 source: &[Source],121 ) -> impl Future<Output = Result<()>> + Send {122 (*self).display_text(error, description, source)123 }124}125126pub struct PrependSourcePrompter<P> {127 pub prompter: P,128 pub source: Vec<Source>,129 pub description: String,130}131impl<P> PrependSourcePrompter<P> {132 fn source(&self, input: &[Source]) -> Vec<Source> {133 let mut out = self.source.clone();134 out.extend(input.iter().cloned());135 out136 }137 fn description(&self, input: &str) -> String {138 if self.description.is_empty() {139 input.to_owned()140 } else if input.is_empty() {141 self.description.to_owned()142 } else {143 format!("{input}\n\n{}", self.description)144 }145 }146}147impl<P> Prompter for PrependSourcePrompter<P>148where149 P: Prompter + Sync,150{151 async fn prompt_radio(152 &self,153 prompt: &str,154 description: &str,155 source: &[Source],156 ) -> Result<bool> {157 self.prompter158 .prompt_radio(prompt, &self.description(description), &self.source(source))159 .await160 }161162 async fn prompt_enum(163 &self,164 prompt: &str,165 description: &str,166 variants: &[&str],167 source: &[Source],168 ) -> Result<u32> {169 self.prompter170 .prompt_enum(171 prompt,172 &self.description(description),173 variants,174 &self.source(source),175 )176 .await177 }178179 async fn prompt_text(180 &self,181 echo: bool,182 prompt: &str,183 description: &str,184 source: &[Source],185 ) -> Result<String> {186 self.prompter187 .prompt_text(188 echo,189 prompt,190 &self.description(description),191 &self.source(source),192 )193 .await194 }195196 async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {197 self.prompter198 .display_text(error, &self.description(description), &self.source(source))199 .await200 }201}crates/ui-prompt/src/rofi.rsdiffbeforeafterboth--- a/crates/ui-prompt/src/rofi.rs
+++ b/crates/ui-prompt/src/rofi.rs
@@ -6,195 +6,203 @@
use crate::{Error, Prompter, Result, Source};
+#[derive(Clone)]
pub struct RofiPrompter;
fn fixup_prompt(prompt: &str) -> &str {
- // Rofi always appends such suffix
- prompt.strip_suffix(": ").unwrap_or(prompt)
+ // Rofi always appends such suffix
+ prompt.strip_suffix(": ").unwrap_or(prompt)
}
+fn rofi_command() -> Command {
+ Command::new(option_env!("ROFI").unwrap_or("rofi"))
+}
+
impl Prompter for RofiPrompter {
- async fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> Result<u32> {
- trace!("rofi radio");
- let mut cmd = Command::new("rofi");
- let mesg = if source.is_empty() {
- description.to_owned()
- } else {
- let mut out = format!("{description}\n\n<b>Requested on ",);
- for (i, s) in source.iter().enumerate() {
- if i != 0 {
- out.push_str(" -> ");
- }
- out.push_str(&s.to_string());
- }
- out.push_str("</b>");
- out
- };
- cmd.args([
- "-dmenu",
- "-mesg",
- &mesg,
- "-sync",
- "-only-match",
- "-p",
- fixup_prompt(prompt),
- "-format",
- "i",
- "-markup-rows",
- ]);
- cmd.stdin(Stdio::piped());
- cmd.stdout(Stdio::piped());
- cmd.kill_on_drop(true);
- let mut child = cmd
- .spawn()
- .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ trace!("rofi radio");
+ let mut cmd = rofi_command();
+ let mesg = if source.is_empty() {
+ description.to_owned()
+ } else {
+ let mut out = format!("{description}\n\n<b>Requested on ",);
+ for (i, s) in source.iter().enumerate() {
+ if i != 0 {
+ out.push_str(" -> ");
+ }
+ out.push_str(&s.to_string());
+ }
+ out.push_str("</b>");
+ out
+ };
+ cmd.args([
+ "-dmenu",
+ "-mesg",
+ &mesg,
+ "-sync",
+ "-only-match",
+ "-p",
+ fixup_prompt(prompt),
+ "-format",
+ "i",
+ "-markup-rows",
+ ]);
+ cmd.stdin(Stdio::piped());
+ cmd.stdout(Stdio::piped());
+ cmd.kill_on_drop(true);
+ let mut child = cmd
+ .spawn()
+ .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
- let mut stdin = child.stdin.take().expect("stdin is piped");
- for var in variants {
- stdin
- .write_all(var.replace('\n', " ").as_bytes())
- .await
- .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;
- stdin
- .write_all(b"\n")
- .await
- .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;
- }
- // write_all already flushes, just to be sure.
- let _ = stdin.flush().await;
- drop(stdin);
+ let mut stdin = child.stdin.take().expect("stdin is piped");
+ for var in variants {
+ stdin
+ .write_all(var.replace('\n', " ").as_bytes())
+ .await
+ .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;
+ stdin
+ .write_all(b"\n")
+ .await
+ .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;
+ }
+ // write_all already flushes, just to be sure.
+ let _ = stdin.flush().await;
+ drop(stdin);
- let out = child
- .wait_with_output()
- .await
- .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
- let stdout = out
- .stdout
- .strip_suffix(b"\n")
- .unwrap_or(&out.stdout)
- .to_owned();
+ let out = child
+ .wait_with_output()
+ .await
+ .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
+ let stdout = out
+ .stdout
+ .strip_suffix(b"\n")
+ .unwrap_or(&out.stdout)
+ .to_owned();
- let id: u32 = String::from_utf8(stdout)
- .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?
- .parse()
- .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?;
- if id as usize >= variants.len() {
- return Err(Error::InputError("invalid rofi response".to_owned()));
- }
+ let id: u32 = String::from_utf8(stdout)
+ .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?
+ .parse()
+ .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?;
+ if id as usize >= variants.len() {
+ return Err(Error::InputError("invalid rofi response".to_owned()));
+ }
- Ok(id)
- }
+ Ok(id)
+ }
- async fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> Result<String> {
- trace!("rofi text");
- let mut cmd = Command::new("rofi");
- let mesg = if source.is_empty() {
- description.to_owned()
- } else {
- let mut out = format!("{description}\n\n<b>Requested on ",);
- for (i, s) in source.iter().enumerate() {
- if i != 0 {
- out.push_str(" -> ");
- }
- out.push_str(&s.to_string());
- }
- out.push_str("</b>");
- out
- };
- cmd.args(["-dmenu", "-mesg", &mesg, "-p", fixup_prompt(prompt)]);
- if !echo {
- cmd.arg("-password");
- }
- cmd.stdin(Stdio::null());
- cmd.stdout(Stdio::piped());
- cmd.kill_on_drop(true);
- let child = cmd
- .spawn()
- .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ trace!("rofi text");
+ let mut cmd = rofi_command();
+ let mesg = if source.is_empty() {
+ description.to_owned()
+ } else {
+ let mut out = format!("{description}\n\n<b>Requested on ",);
+ for (i, s) in source.iter().enumerate() {
+ if i != 0 {
+ out.push_str(" -> ");
+ }
+ out.push_str(&s.to_string());
+ }
+ out.push_str("</b>");
+ out
+ };
+ cmd.args(["-dmenu", "-mesg", &mesg, "-p", fixup_prompt(prompt)]);
+ if !echo {
+ cmd.arg("-password");
+ }
+ cmd.stdin(Stdio::null());
+ cmd.stdout(Stdio::piped());
+ cmd.kill_on_drop(true);
+ let child = cmd
+ .spawn()
+ .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
- let out = child
- .wait_with_output()
- .await
- .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
- let stdout = out
- .stdout
- .strip_suffix(b"\n")
- .unwrap_or(&out.stdout)
- .to_owned();
+ let out = child
+ .wait_with_output()
+ .await
+ .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
+ let stdout = out
+ .stdout
+ .strip_suffix(b"\n")
+ .unwrap_or(&out.stdout)
+ .to_owned();
- Ok(String::from_utf8_lossy(&stdout).to_string())
- }
+ Ok(String::from_utf8_lossy(&stdout).to_string())
+ }
- async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
- trace!("rofi display");
- let mut cmd = Command::new("rofi");
- let mut mesg = if source.is_empty() {
- description.to_owned()
- } else {
- let mut out = format!("{description}\n\n<b>Coming from ",);
- for s in source.iter() {
- out.push_str(&s.to_string());
- }
- out.push_str("</b>");
- out
- };
- if error {
- mesg.insert_str(0, "<span color=\"red\">");
- mesg.push_str("</span>");
- }
- cmd.args(["-e", &mesg, "-markup"]);
- cmd.stdin(Stdio::null());
- cmd.stdout(Stdio::null());
- cmd.kill_on_drop(true);
- let mut child = cmd
- .spawn()
- .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
+ async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ trace!("rofi display");
+ let mut cmd = rofi_command();
+ let mut mesg = if source.is_empty() {
+ description.to_owned()
+ } else {
+ let mut out = format!("{description}\n\n<b>Coming from ",);
+ for s in source.iter() {
+ out.push_str(&s.to_string());
+ }
+ out.push_str("</b>");
+ out
+ };
+ if error {
+ mesg.insert_str(0, "<span color=\"red\">");
+ mesg.push_str("</span>");
+ }
+ cmd.args(["-e", &mesg, "-markup"]);
+ cmd.stdin(Stdio::null());
+ cmd.stdout(Stdio::null());
+ cmd.kill_on_drop(true);
+ let mut child = cmd
+ .spawn()
+ .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
- child
- .wait()
- .await
- .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
+ child
+ .wait()
+ .await
+ .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
- Ok(())
- }
+ Ok(())
+ }
}
#[cfg(test)]
mod tests {
- use std::borrow::Cow;
+ use std::borrow::Cow;
- use crate::rofi::RofiPrompter;
- use crate::{PrependSourcePrompter, Prompter as _, Source};
+ use crate::rofi::RofiPrompter;
+ use crate::{PrependSourcePrompter, Prompter as _, Source};
- #[tokio::test]
- async fn test() {
- let prompter = PrependSourcePrompter {
- prompter: RofiPrompter,
- source: vec![Source(Cow::Borrowed("ssh"))],
- };
- prompter
- .prompt_radio("Enable", "Polkit needs access", &[])
- .await
- .expect("rofi");
- prompter
- .prompt_text(false, "Password", "Polkit needs access", &[])
- .await
- .expect("rofi");
- prompter
- .display_text(true, "Polkit needs access", &[])
- .await
- .expect("rofi");
- }
+ // #[tokio::test]
+ #[tokio::test]
+ #[ignore = "interactive"]
+ async fn test() {
+ let prompter = PrependSourcePrompter {
+ prompter: RofiPrompter,
+ description: "test".to_owned(),
+ source: vec![Source(Cow::Borrowed("ssh"))],
+ };
+ prompter
+ .prompt_radio("Enable", "Polkit needs access", &[])
+ .await
+ .expect("rofi");
+ prompter
+ .prompt_text(false, "Password", "Polkit needs access", &[])
+ .await
+ .expect("rofi");
+ prompter
+ .display_text(true, "Polkit needs access", &[])
+ .await
+ .expect("rofi");
+ }
}