git.delta.rocks / fleet / refs/heads / push-kyumtlkprzyo

difftreelog

source

remowt/crates/remowt-ui-prompt/src/rofi.rs5.5 KiBsourcehistory
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	// Rofi can't run concurrently; serialize invocations.14	lock: Arc<Mutex<()>>,15}1617fn fixup_prompt(prompt: &str) -> &str {18	// Rofi always appends such suffix19	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		// write_all already flushes, just to be sure.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	// #[tokio::test]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}