From 04d11c98d70ae6f178450b9c9b013c3875cec47c Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Fri, 1 Aug 2025 22:28:56 +0200 Subject: [PATCH] worf-warden improve auto type and config * auto type now supports inserting a totp key with $T * configuration allows setting a different type tool * improve documentation --- examples/worf-warden/Readme.md | 26 ++++-- examples/worf-warden/src/main.rs | 142 ++++++++++++++++++++++--------- 2 files changed, 123 insertions(+), 45 deletions(-) diff --git a/examples/worf-warden/Readme.md b/examples/worf-warden/Readme.md index 4267694..518ddf8 100644 --- a/examples/worf-warden/Readme.md +++ b/examples/worf-warden/Readme.md @@ -6,18 +6,34 @@ Simple password manager build upon these additional tools aside worf * [pinentry](https://www.gnupg.org/related_software/pinentry/index.en.html) is required to show a dialog show password entry * As worf warden * [ydotool](https://github.com/ReimuNotMoe/ydotool) + * ydotool is just the defaults, other tools can be configured via `--typing-cmd` or using this key in the config file. The idea it taken from https://github.com/mattydebie/bitwarden-rofi/blob/master/bwmenu ## Custom auto typing -* Auto typing supports custom keys. Just pass user name with `$U` and pw with `$P` - * I.e. the default is `$U\t$P` which is user, tab, password. - * This is using ydotool to type, so see their documentation for key input details. -### Example -`~/.config/worf/warden` +Custom key strokes are supported for auto typing. +For example this can be used for some websites like PayPal, +where `` after the username must be typed instead of `` + +Special variables: +* `$U` -- Username +* `$P` -- Password +* `$T` -- Two factor +* `$S` -- Sleep in milliseconds +* `_` -- All underscores are removed and used to make the string more readable + +The default is `$U\t$P` which is user, tab, password. +As the string is passed to the typing tool, see their documentation for special chars. + +## Configuration + +The location of the configuration file follows the same rules as worf itself. ```toml +typing_cmd = "ydotool" +typing_cmd_args = ["type"] + [custom_auto_types] # This will use User, enter, password for the demo entry. # You can use the id or the label as key, where id has higher precedence. diff --git a/examples/worf-warden/src/main.rs b/examples/worf-warden/src/main.rs index 6b5c015..9f82902 100644 --- a/examples/worf-warden/src/main.rs +++ b/examples/worf-warden/src/main.rs @@ -114,49 +114,92 @@ fn groups() -> String { .to_string() } -fn keyboard_type(text: &str) { - Command::new("ydotool") - .arg("type") - .arg(text) - .output() - .expect("Failed to execute ydotool"); +fn keyboard_type(text: &str, cfg: &WardenConfig) { + let mut cmd = Command::new(cfg.typing_cmd()); + for arg in cfg.typing_cmd_args() { + cmd.arg(arg); + } + cmd.arg(text); + + cmd.output() + .unwrap_or_else(|_| panic!("Failed to execute {}", cfg.typing_cmd())); } -fn parse_cmd(cmd: &str) -> (&str, Option, Option<&str>) { - if let Some(pos) = cmd.find("$S") { - let left = &cmd[..pos]; - let rest = &cmd[pos + 2..]; // Skip "$S" +fn keyboard_return(config: &WardenConfig) { + keyboard_type("\n", config); +} - // Extract digits after "$S" - let num_part: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect(); +fn keyboard_auto_type(cmd: &str, id: &str, config: &WardenConfig) -> Result<(), String> { + let mut input = cmd.replace('_', ""); - if let Ok(number) = num_part.parse::() { - let right = &rest[num_part.len()..]; - return (left, Some(number), Some(right)); + //.replace("$U", &user).replace("$P", &pw); + + // if input.contains("$T") { + // input = input.replace("$T", &rbw_get_totp(id, false)?); + // } + + while !input.is_empty() { + // Remove up to and including the first '$' + + if let Some(pos) = input.find('$') { + // Extract substring before '$' + let extracted = input[..pos].to_string(); + + // Remove extracted part + '$' from input + input.drain(..=pos); + + if !extracted.is_empty() { + keyboard_type(extracted.as_str(), config); + } } - } - (cmd, None, None) -} + // Match the next character + match input.chars().next() { + Some('S') => { + // Remove the 'S' command character + input.remove(0); -fn keyboard_return() { - keyboard_type("\n"); -} + // Collect digits following 'S' + let digits: String = input.chars().take_while(|c| c.is_ascii_digit()).collect(); -fn keyboard_auto_type(cmd: &str, id: &str) -> Result<(), String> { - let user = rbw_get_user(id, false)?; - let pw = rbw_get_password(id, false)?; + // Remove the digits from input + let len = digits.len(); + input.drain(..len); - let ydo_string = cmd.replace('_', "").replace("$U", &user).replace("$P", &pw); + // Parse and sleep + if let Ok(ms) = digits.parse::() { + sleep(Duration::from_millis(ms)); + } else { + log::error!("Failed to parse digits: {digits}"); + } + continue; + } - let (left, sleep_ms, right) = parse_cmd(&ydo_string); - keyboard_type(left); - if let Some(sleep_ms) = sleep_ms { - sleep(Duration::from_millis(sleep_ms)); - } + Some('U') => { + let user = rbw_get_user(id, false)?; + keyboard_type(&user, config); + } - if let Some(right) = right { - keyboard_type(right); + Some('P') => { + let pw = rbw_get_password(id, false)?; + keyboard_type(&pw, config); + } + + Some('T') => { + let totp = rbw_get_totp(id, false)?; + keyboard_type(&totp, config); + } + + Some(c) => { + log::error!("Unknown character found: {c}"); + } + + None => { + log::error!("No command found after '$'"); + } + } + + input.drain(..1); } Ok(()) @@ -359,13 +402,13 @@ fn show( .get(id) .or(warden_config.custom_auto_types.get(&selection.menu.label)) .unwrap_or(&default); - keyboard_auto_type(typing, id)?; + keyboard_auto_type(typing, id, &warden_config)?; } else if key == key_type_user() || key == key_type_user_and_enter() { - keyboard_type(&rbw_get_user(id, false)?); + keyboard_type(&rbw_get_user(id, false)?, &warden_config); } else if key == key_type_password() || key == key_type_password_and_enter() { - keyboard_type(&rbw_get_password(id, false)?); + keyboard_type(&rbw_get_password(id, false)?, &warden_config); } else if key == key_type_totp() || key == key_type_totp_and_enter() { - keyboard_type(&rbw_get_totp(id, false)?); + keyboard_type(&rbw_get_totp(id, false)?, &warden_config); } else if key == key_lock() { rbw("lock", None)?; } else if key == key_sync() { @@ -375,7 +418,7 @@ fn show( } if key.modifiers.contains(&Modifier::Shift) { - keyboard_return(); + keyboard_return(&warden_config); } } else { let pw = rbw_get_password(id, true)?; @@ -394,9 +437,23 @@ fn show( #[derive(Debug, Default, Deserialize, Serialize, Clone)] struct WardenConfig { + typing_cmd: Option, + typing_cmd_args: Option>, custom_auto_types: HashMap, } +impl WardenConfig { + fn typing_cmd(&self) -> String { + self.typing_cmd.clone().unwrap_or("ydotool".to_owned()) + } + + fn typing_cmd_args(&self) -> Vec { + self.typing_cmd_args + .clone() + .unwrap_or(vec!["type".to_owned()]) + } +} + #[derive(Debug, Parser, Clone)] struct WardenArgs { /// Configuration file for worf warden @@ -431,9 +488,14 @@ fn main() -> Result<(), String> { std::process::exit(1) } - // will exit if there is a daemon running already, so it's fine to call this everytime. - if let Err(e) = spawn_fork("ydotoold", None) { - log::error!("Failed to start ydotool daemon: {e}"); + // ydotool is our special default value, give it some love and start the daemon + // if other tools need this it must be run beforehand (or can be added here) + // in case another tool is added it might make sense to make it configurable + if warden_config.typing_cmd() == "ydotool" { + // will exit if there is a daemon running already, so it's fine to call this everytime. + if let Err(e) = spawn_fork("ydotoold", None) { + log::error!("Failed to start ydotool daemon: {e}"); + } } let worf_config = Arc::new(RwLock::new(cfg.worf.clone()));