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 = [
"env_logger",
"log",
"serde",
"worf",
]

View file

@ -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());
}

View file

@ -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")

View file

@ -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"]

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
## 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)

View file

@ -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<u64>, 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::<u64>() {
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<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(
&config,
provider,
@ -314,6 +342,7 @@ fn show(config: Arc<RwLock<Config>>, provider: Arc<Mutex<PasswordProvider>>) ->
return show(
config,
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));
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<RwLock<Config>>, provider: Arc<Mutex<PasswordProvider>>) ->
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
struct WardenConfig {
custom_auto_types: HashMap<String, String>,
}
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)
}

View file

@ -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<PathBuf, Error> {
/// # Errors
///
/// 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> {
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<PathBuf, Error> {
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<Config, Error> {
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<Config, Error> {
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 {
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}"))),

View file

@ -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());
}