support custom auto typing fixes #91

This commit is contained in:
Alexander Mohr 2025-07-12 22:34:35 +02:00
parent 024e24d4de
commit b63b02830a
8 changed files with 122 additions and 44 deletions

1
Cargo.lock generated
View file

@ -2925,6 +2925,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"env_logger", "env_logger",
"log", "log",
"serde",
"worf", "worf",
] ]

View file

@ -471,7 +471,7 @@ fn main() -> Result<(), String> {
.init(); .init();
let mut cfg = HyprSpaceConfig::parse(); 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() { if cfg.worf.prompt().is_none() {
cfg.worf.set_prompt(cfg.hypr_space_mode().to_string()); cfg.worf.set_prompt(cfg.hypr_space_mode().to_string());
} }

View file

@ -145,7 +145,7 @@ fn main() -> Result<(), String> {
let args = config::parse_args(); let args = config::parse_args();
let config = Arc::new(RwLock::new( 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") let cache_path = desktop::cache_file_path(&config.read().unwrap(), "worf-hyprswitch")

View file

@ -7,8 +7,5 @@ edition = "2024"
worf = {path = "../../worf"} worf = {path = "../../worf"}
env_logger = "0.11.8" env_logger = "0.11.8"
log = "0.4.27" log = "0.4.27"
serde = { version = "1.0.219", features = ["derive"] }
# todo re-add this
#[features]
#default = [] # nothing enabled by default
#warden = ["worf-warden"]

View file

@ -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 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) ![example](../images/worf-warden.png)

View file

@ -1,3 +1,4 @@
use serde::{Deserialize, Serialize};
use std::{ use std::{
collections::HashMap, collections::HashMap,
env, env,
@ -6,7 +7,6 @@ use std::{
thread::sleep, thread::sleep,
time::Duration, time::Duration,
}; };
use worf::{ use worf::{
config::{self, Config, CustomKeyHintLocation, Key}, config::{self, Config, CustomKeyHintLocation, Key},
desktop::{copy_to_clipboard, spawn_fork}, desktop::{copy_to_clipboard, spawn_fork},
@ -120,20 +120,44 @@ fn keyboard_type(text: &str) {
.expect("Failed to execute ydotool"); .expect("Failed to execute ydotool");
} }
fn keyboard_tab() { fn parse_cmd(cmd: &str) -> (&str, Option<u64>, Option<&str>) {
Command::new("ydotool") if let Some(pos) = cmd.find("$S") {
.arg("type") let left = &cmd[..pos];
.arg("\t") let rest = &cmd[pos + 2..]; // Skip "$S"
.output()
.expect("Failed to execute ydotool"); // 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::<u64>() {
let right = &rest[num_part.len()..];
return (left, Some(number), Some(right));
}
}
(cmd, None, None)
} }
fn keyboard_return() { fn keyboard_return() {
Command::new("ydotool") keyboard_type("\n");
.arg("type") }
.arg("\n")
.output() fn keyboard_auto_type(cmd: &str, id: &str) -> Result<(), String> {
.expect("Failed to execute ydotool"); 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<Vec<&str>>) -> Result<String, String> { fn rbw(cmd: &str, args: Option<Vec<&str>>) -> Result<String, String> {
@ -281,7 +305,11 @@ fn key_lock() -> KeyBinding {
} }
} }
fn show(config: Arc<RwLock<Config>>, provider: Arc<Mutex<PasswordProvider>>) -> Result<(), String> { fn show(
config: Arc<RwLock<Config>>,
provider: Arc<Mutex<PasswordProvider>>,
warden_config: WardenConfig,
) -> Result<(), String> {
match gui::show( match gui::show(
&config, &config,
provider, provider,
@ -314,6 +342,7 @@ fn show(config: Arc<RwLock<Config>>, provider: Arc<Mutex<PasswordProvider>>) ->
return show( return show(
config, config,
Arc::new(Mutex::new(PasswordProvider::sub_provider(meta.ids)?)), Arc::new(Mutex::new(PasswordProvider::sub_provider(meta.ids)?)),
warden_config.clone(),
); );
} }
@ -322,9 +351,13 @@ fn show(config: Arc<RwLock<Config>>, provider: Arc<Mutex<PasswordProvider>>) ->
sleep(Duration::from_millis(500)); sleep(Duration::from_millis(500));
if let Some(key) = selection.custom_key { if let Some(key) = selection.custom_key {
if key == key_type_all() || key == key_type_all_and_enter() { if key == key_type_all() || key == key_type_all_and_enter() {
keyboard_type(&rbw_get_user(id, false)?); let default = "$U\t$P".to_owned();
keyboard_tab(); let typing = warden_config
keyboard_type(&rbw_get_password(id, false)?); .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() { } 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)?);
} else if key == key_type_password() || key == key_type_password_and_enter() { } else if key == key_type_password() || key == key_type_password_and_enter() {
@ -357,6 +390,11 @@ fn show(config: Arc<RwLock<Config>>, provider: Arc<Mutex<PasswordProvider>>) ->
} }
} }
#[derive(Debug, Deserialize, Serialize, Clone)]
struct WardenConfig {
custom_auto_types: HashMap<String, String>,
}
fn main() -> Result<(), String> { fn main() -> Result<(), String> {
env_logger::Builder::new() env_logger::Builder::new()
.parse_filters(&env::var("RUST_LOG").unwrap_or_else(|_| "error".to_owned())) .parse_filters(&env::var("RUST_LOG").unwrap_or_else(|_| "error".to_owned()))
@ -364,10 +402,13 @@ fn main() -> Result<(), String> {
.init(); .init();
let args = config::parse_args(); let args = config::parse_args();
let config = Arc::new(RwLock::new( let worf_config = Arc::new(RwLock::new(
config::load_config(Some(&args)).unwrap_or(args), 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") { if !groups().contains("input") {
log::error!( log::error!(
"User must be in input group. 'sudo usermod -aG input $USER', then login again" "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 // 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())?)); let provider = Arc::new(Mutex::new(PasswordProvider::new(
show(config, provider) &worf_config.read().unwrap(),
)?));
show(worf_config, provider, warden_config)
} }

View file

@ -1,6 +1,7 @@
use std::{env, fs, path::PathBuf, str::FromStr}; use std::{env, fs, path::PathBuf, str::FromStr};
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
@ -950,11 +951,9 @@ fn style_path(full_path: Option<&String>) -> Result<PathBuf, Error> {
/// # Errors /// # Errors
/// ///
/// Will return Err when it cannot resolve any path or no style is found /// Will return Err when it cannot resolve any path or no style is found
pub fn conf_path(full_path: Option<&String>) -> Result<PathBuf, Error> { pub fn conf_path(full_path: Option<&String>, folder: &str, name: &str) -> Result<PathBuf, Error> {
let alternative_paths = path_alternatives( let alternative_paths =
vec![dirs::config_dir()], path_alternatives(vec![dirs::config_dir()], &PathBuf::from(folder).join(name));
&PathBuf::from("worf").join("config"),
);
resolve_path(full_path, alternative_paths.into_iter().collect()) resolve_path(full_path, alternative_paths.into_iter().collect())
} }
@ -996,22 +995,39 @@ pub fn resolve_path(
/// * cannot parse the config file /// * cannot parse the config file
/// * no config file exists /// * no config file exists
/// * config file and args cannot be merged /// * config file and args cannot be merged
pub fn load_config(args_opt: Option<&Config>) -> Result<Config, Error> { pub fn load_worf_config(args_opt: Option<&Config>) -> Result<Config, Error> {
let config_path = conf_path(args_opt.as_ref().and_then(|c| c.cfg_path.as_ref())); 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<T: DeserializeOwned>(
args_opt: Option<&Config>,
folder: &str,
name: &str,
) -> Result<T, Error> {
let config_path = conf_path(
args_opt.as_ref().and_then(|c| c.cfg_path.as_ref()),
folder,
name,
);
match config_path { match config_path {
Ok(path) => { Ok(path) => {
log::debug!("loading config from {}", path.display()); log::debug!("loading config from {}", path.display());
let toml_content = fs::read_to_string(path).map_err(|e| Error::Io(format!("{e}")))?; 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}")))
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)
}
} }
Err(e) => Err(Error::Io(format!("{e}"))), Err(e) => Err(Error::Io(format!("{e}"))),

View file

@ -95,7 +95,7 @@ fn main() {
.init(); .init();
let mut config = MainConfig::parse(); 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() { if config.worf.prompt().is_none() {
config.worf.set_prompt(config.show.to_string()); config.worf.set_prompt(config.show.to_string());
} }