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 (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 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}