From b63b02830a6c93f7023649b8eb9856317e3153e3 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Sat, 12 Jul 2025 22:34:35 +0200 Subject: [PATCH] support custom auto typing fixes #91 --- Cargo.lock | 1 + examples/worf-hyprspace/src/main.rs | 2 +- examples/worf-hyprswitch/src/main.rs | 2 +- examples/worf-warden/Cargo.toml | 5 +- examples/worf-warden/Readme.md | 21 +++++++ examples/worf-warden/src/main.rs | 83 +++++++++++++++++++++------- worf/src/lib/config.rs | 50 +++++++++++------ worf/src/main.rs | 2 +- 8 files changed, 122 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 496717f..081c27a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2925,6 +2925,7 @@ version = "0.1.0" dependencies = [ "env_logger", "log", + "serde", "worf", ] diff --git a/examples/worf-hyprspace/src/main.rs b/examples/worf-hyprspace/src/main.rs index 29a3e92..900f4eb 100644 --- a/examples/worf-hyprspace/src/main.rs +++ b/examples/worf-hyprspace/src/main.rs @@ -471,7 +471,7 @@ fn main() -> Result<(), String> { .init(); let mut cfg = HyprSpaceConfig::parse(); - cfg.worf = worf::config::load_config(Some(&cfg.worf)).unwrap_or(cfg.worf); + cfg.worf = worf::config::load_worf_config(Some(&cfg.worf)).unwrap_or(cfg.worf); if cfg.worf.prompt().is_none() { cfg.worf.set_prompt(cfg.hypr_space_mode().to_string()); } diff --git a/examples/worf-hyprswitch/src/main.rs b/examples/worf-hyprswitch/src/main.rs index 0efbee3..40824eb 100644 --- a/examples/worf-hyprswitch/src/main.rs +++ b/examples/worf-hyprswitch/src/main.rs @@ -145,7 +145,7 @@ fn main() -> Result<(), String> { let args = config::parse_args(); let config = Arc::new(RwLock::new( - config::load_config(Some(&args)).unwrap_or(args), + config::load_worf_config(Some(&args)).unwrap_or(args), )); let cache_path = desktop::cache_file_path(&config.read().unwrap(), "worf-hyprswitch") diff --git a/examples/worf-warden/Cargo.toml b/examples/worf-warden/Cargo.toml index f7244ae..02f88f1 100644 --- a/examples/worf-warden/Cargo.toml +++ b/examples/worf-warden/Cargo.toml @@ -7,8 +7,5 @@ edition = "2024" worf = {path = "../../worf"} env_logger = "0.11.8" log = "0.4.27" +serde = { version = "1.0.219", features = ["derive"] } -# todo re-add this -#[features] -#default = [] # nothing enabled by default -#warden = ["worf-warden"] diff --git a/examples/worf-warden/Readme.md b/examples/worf-warden/Readme.md index 4e83a1e..4267694 100644 --- a/examples/worf-warden/Readme.md +++ b/examples/worf-warden/Readme.md @@ -9,4 +9,25 @@ Simple password manager build upon these additional tools aside worf 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` + +```toml +[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. +Demo = "$U\n$P" +# Will sleep 500ms before typing password +# Any underscore will be ignored +Delayed = "$U_\n_$S_500_$P" +``` + + + + ![example](../images/worf-warden.png) diff --git a/examples/worf-warden/src/main.rs b/examples/worf-warden/src/main.rs index d2e8fd0..1cfbc15 100644 --- a/examples/worf-warden/src/main.rs +++ b/examples/worf-warden/src/main.rs @@ -1,3 +1,4 @@ +use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, env, @@ -6,7 +7,6 @@ use std::{ thread::sleep, time::Duration, }; - use worf::{ config::{self, Config, CustomKeyHintLocation, Key}, desktop::{copy_to_clipboard, spawn_fork}, @@ -120,20 +120,44 @@ fn keyboard_type(text: &str) { .expect("Failed to execute ydotool"); } -fn keyboard_tab() { - Command::new("ydotool") - .arg("type") - .arg("\t") - .output() - .expect("Failed to execute ydotool"); +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" + + // Extract digits after "$S" + let num_part: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect(); + + if let Ok(number) = num_part.parse::() { + let right = &rest[num_part.len()..]; + return (left, Some(number), Some(right)); + } + } + + (cmd, None, None) } fn keyboard_return() { - Command::new("ydotool") - .arg("type") - .arg("\n") - .output() - .expect("Failed to execute ydotool"); + keyboard_type("\n"); +} + +fn keyboard_auto_type(cmd: &str, id: &str) -> Result<(), String> { + let user = rbw_get_user(id, false)?; + let pw = rbw_get_password(id, false)?; + + let ydo_string = cmd.replace('_', "").replace("$U", &user).replace("$P", &pw); + + 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)); + } + + if let Some(right) = right { + keyboard_type(right); + } + + Ok(()) } fn rbw(cmd: &str, args: Option>) -> Result { @@ -281,7 +305,11 @@ fn key_lock() -> KeyBinding { } } -fn show(config: Arc>, provider: Arc>) -> Result<(), String> { +fn show( + config: Arc>, + provider: Arc>, + warden_config: WardenConfig, +) -> Result<(), String> { match gui::show( &config, provider, @@ -314,6 +342,7 @@ fn show(config: Arc>, provider: Arc>) -> return show( config, Arc::new(Mutex::new(PasswordProvider::sub_provider(meta.ids)?)), + warden_config.clone(), ); } @@ -322,9 +351,13 @@ fn show(config: Arc>, provider: Arc>) -> sleep(Duration::from_millis(500)); if let Some(key) = selection.custom_key { if key == key_type_all() || key == key_type_all_and_enter() { - keyboard_type(&rbw_get_user(id, false)?); - keyboard_tab(); - keyboard_type(&rbw_get_password(id, false)?); + let default = "$U\t$P".to_owned(); + let typing = warden_config + .custom_auto_types + .get(id) + .or(warden_config.custom_auto_types.get(&selection.menu.label)) + .unwrap_or(&default); + keyboard_auto_type(typing, id)?; } else if key == key_type_user() || key == key_type_user_and_enter() { keyboard_type(&rbw_get_user(id, false)?); } else if key == key_type_password() || key == key_type_password_and_enter() { @@ -357,6 +390,11 @@ fn show(config: Arc>, provider: Arc>) -> } } +#[derive(Debug, Deserialize, Serialize, Clone)] +struct WardenConfig { + custom_auto_types: HashMap, +} + fn main() -> Result<(), String> { env_logger::Builder::new() .parse_filters(&env::var("RUST_LOG").unwrap_or_else(|_| "error".to_owned())) @@ -364,10 +402,13 @@ fn main() -> Result<(), String> { .init(); let args = config::parse_args(); - let config = Arc::new(RwLock::new( - config::load_config(Some(&args)).unwrap_or(args), + let worf_config = Arc::new(RwLock::new( + config::load_worf_config(Some(&args)).unwrap_or(args.clone()), )); + let warden_config: WardenConfig = config::load_config(Some(&args), "worf", "warden") + .map_err(|e| format!("failed to parse warden config {e}"))?; + if !groups().contains("input") { log::error!( "User must be in input group. 'sudo usermod -aG input $USER', then login again" @@ -381,6 +422,8 @@ fn main() -> Result<(), String> { } // todo eventually use a propper rust client for this, for now rbw is good enough - let provider = Arc::new(Mutex::new(PasswordProvider::new(&config.read().unwrap())?)); - show(config, provider) + let provider = Arc::new(Mutex::new(PasswordProvider::new( + &worf_config.read().unwrap(), + )?)); + show(worf_config, provider, warden_config) } diff --git a/worf/src/lib/config.rs b/worf/src/lib/config.rs index 0a045e1..a0ba267 100644 --- a/worf/src/lib/config.rs +++ b/worf/src/lib/config.rs @@ -1,6 +1,7 @@ use std::{env, fs, path::PathBuf, str::FromStr}; use clap::{Parser, ValueEnum}; +use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -950,11 +951,9 @@ fn style_path(full_path: Option<&String>) -> Result { /// # Errors /// /// Will return Err when it cannot resolve any path or no style is found -pub fn conf_path(full_path: Option<&String>) -> Result { - let alternative_paths = path_alternatives( - vec![dirs::config_dir()], - &PathBuf::from("worf").join("config"), - ); +pub fn conf_path(full_path: Option<&String>, folder: &str, name: &str) -> Result { + let alternative_paths = + path_alternatives(vec![dirs::config_dir()], &PathBuf::from(folder).join(name)); resolve_path(full_path, alternative_paths.into_iter().collect()) } @@ -996,22 +995,39 @@ pub fn resolve_path( /// * cannot parse the config file /// * no config file exists /// * config file and args cannot be merged -pub fn load_config(args_opt: Option<&Config>) -> Result { - let config_path = conf_path(args_opt.as_ref().and_then(|c| c.cfg_path.as_ref())); +pub fn load_worf_config(args_opt: Option<&Config>) -> Result { + let mut config = load_config(args_opt, "worf", "config")?; + if let Some(args) = args_opt { + let merge_result = merge_config_with_args(&mut config, args) + .map_err(|e| Error::ParsingError(format!("{e}")))?; + Ok(merge_result) + } else { + Ok(config) + } +} + +/// # Errors +/// +/// Will return Err when it +/// * cannot read the config file +/// * cannot parse the config file +/// * no config file exists +/// * config file and args cannot be merged +pub fn load_config( + args_opt: Option<&Config>, + folder: &str, + name: &str, +) -> Result { + let config_path = conf_path( + args_opt.as_ref().and_then(|c| c.cfg_path.as_ref()), + folder, + name, + ); match config_path { Ok(path) => { log::debug!("loading config from {}", path.display()); let toml_content = fs::read_to_string(path).map_err(|e| Error::Io(format!("{e}")))?; - let mut config: Config = - toml::from_str(&toml_content).map_err(|e| Error::ParsingError(format!("{e}")))?; - - if let Some(args) = args_opt { - let merge_result = merge_config_with_args(&mut config, args) - .map_err(|e| Error::ParsingError(format!("{e}")))?; - Ok(merge_result) - } else { - Ok(config) - } + toml::from_str(&toml_content).map_err(|e| Error::ParsingError(format!("{e}"))) } Err(e) => Err(Error::Io(format!("{e}"))), diff --git a/worf/src/main.rs b/worf/src/main.rs index 9bbdc70..80e9a9d 100644 --- a/worf/src/main.rs +++ b/worf/src/main.rs @@ -95,7 +95,7 @@ fn main() { .init(); let mut config = MainConfig::parse(); - config.worf = config::load_config(Some(&config.worf)).unwrap_or(config.worf); + config.worf = config::load_worf_config(Some(&config.worf)).unwrap_or(config.worf); if config.worf.prompt().is_none() { config.worf.set_prompt(config.show.to_string()); }