1use 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 13 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 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}