1use std::process::Stdio;2use std::sync::Arc;34use tokio::io::AsyncWriteExt;5use tokio::process::Command;6use tokio::sync::Mutex;7use tracing::trace;89use crate::{Error, Prompter, Result, Source};1011#[derive(Clone, Default)]12pub struct RofiPrompter {13 14 lock: Arc<Mutex<()>>,15}1617fn fixup_prompt(prompt: &str) -> &str {18 19 prompt.strip_suffix(": ").unwrap_or(prompt)20}2122fn rofi_command() -> Command {23 Command::new(option_env!("ROFI").unwrap_or("rofi"))24}2526impl Prompter for RofiPrompter {27 async fn prompt_enum(28 &self,29 prompt: &str,30 description: &str,31 variants: &[&str],32 source: &[Source],33 ) -> Result<u32> {34 trace!("rofi radio");35 let _guard = self.lock.lock().await;36 let mut cmd = rofi_command();37 let mesg = if source.is_empty() {38 description.to_owned()39 } else {40 let mut out = format!("{description}\n\n<b>Requested on ",);41 for (i, s) in source.iter().enumerate() {42 if i != 0 {43 out.push_str(" -> ");44 }45 out.push_str(&s.to_string());46 }47 out.push_str("</b>");48 out49 };50 cmd.args([51 "-dmenu",52 "-mesg",53 &mesg,54 "-sync",55 "-no-custom",56 "-p",57 fixup_prompt(prompt),58 "-format",59 "i",60 "-markup-rows",61 ]);62 cmd.stdin(Stdio::piped());63 cmd.stdout(Stdio::piped());64 cmd.kill_on_drop(true);65 let mut child = cmd66 .spawn()67 .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;6869 let mut stdin = child.stdin.take().expect("stdin is piped");70 for var in variants {71 stdin72 .write_all(var.replace('\n', " ").as_bytes())73 .await74 .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;75 stdin76 .write_all(b"\n")77 .await78 .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;79 }80 81 let _ = stdin.flush().await;82 drop(stdin);8384 let out = child85 .wait_with_output()86 .await87 .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;88 match out.status.code() {89 Some(0) => {}90 Some(1) => return Err(Error::Cancel),91 other => {92 return Err(Error::InputError(format!(93 "rofi exited with status {other:?}"94 )));95 }96 }97 let stdout = out98 .stdout99 .strip_suffix(b"\n")100 .unwrap_or(&out.stdout)101 .to_owned();102103 let id: u32 = String::from_utf8(stdout)104 .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?105 .parse()106 .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?;107 if id as usize >= variants.len() {108 return Err(Error::InputError("invalid rofi response".to_owned()));109 }110111 Ok(id)112 }113114 async fn prompt_text(115 &self,116 echo: bool,117 prompt: &str,118 description: &str,119 source: &[Source],120 ) -> Result<String> {121 trace!("rofi text");122 let _guard = self.lock.lock().await;123 let mut cmd = rofi_command();124 let mesg = if source.is_empty() {125 description.to_owned()126 } else {127 let mut out = format!("{description}\n\n<b>Requested on ",);128 for (i, s) in source.iter().enumerate() {129 if i != 0 {130 out.push_str(" -> ");131 }132 out.push_str(&s.to_string());133 }134 out.push_str("</b>");135 out136 };137 cmd.args(["-dmenu", "-mesg", &mesg, "-p", fixup_prompt(prompt)]);138 if !echo {139 cmd.arg("-password");140 }141 cmd.stdin(Stdio::null());142 cmd.stdout(Stdio::piped());143 cmd.kill_on_drop(true);144 let child = cmd145 .spawn()146 .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;147148 let out = child149 .wait_with_output()150 .await151 .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;152 match out.status.code() {153 Some(0) => {}154 Some(1) => return Err(Error::Cancel),155 other => {156 return Err(Error::InputError(format!(157 "rofi exited with status {other:?}"158 )));159 }160 }161 let stdout = out162 .stdout163 .strip_suffix(b"\n")164 .unwrap_or(&out.stdout)165 .to_owned();166167 Ok(String::from_utf8_lossy(&stdout).to_string())168 }169170 async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {171 trace!("rofi display");172 let _guard = self.lock.lock().await;173 let mut cmd = rofi_command();174 let mut mesg = if source.is_empty() {175 description.to_owned()176 } else {177 let mut out = format!("{description}\n\n<b>Coming from ",);178 for s in source.iter() {179 out.push_str(&s.to_string());180 }181 out.push_str("</b>");182 out183 };184 if error {185 mesg.insert_str(0, "<span color=\"red\">");186 mesg.push_str("</span>");187 }188 cmd.args(["-e", &mesg, "-markup"]);189 cmd.stdin(Stdio::null());190 cmd.stdout(Stdio::null());191 cmd.kill_on_drop(true);192 let mut child = cmd193 .spawn()194 .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;195196 child197 .wait()198 .await199 .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;200201 Ok(())202 }203}204205#[cfg(test)]206mod tests {207 use std::borrow::Cow;208209 use crate::rofi::RofiPrompter;210 use crate::{PrependSourcePrompter, Prompter as _, Source};211212 213 #[tokio::test]214 #[ignore = "interactive"]215 async fn test() {216 let prompter = PrependSourcePrompter {217 prompter: RofiPrompter::default(),218 description: "test".to_owned(),219 source: vec![Source(Cow::Borrowed("ssh"))],220 };221 prompter222 .prompt_radio("Enable", "Polkit needs access", &[])223 .await224 .expect("rofi");225 prompter226 .prompt_text(false, "Password", "Polkit needs access", &[])227 .await228 .expect("rofi");229 prompter230 .display_text(true, "Polkit needs access", &[])231 .await232 .expect("rofi");233 }234}