diff --git a/Cargo.lock b/Cargo.lock index b7d67f8..bbbf1dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1917,6 +1917,7 @@ name = "worf-warden" version = "0.1.0" dependencies = [ "anyhow", + "env_logger", "worf", ] diff --git a/examples/dmenu.sh b/examples/dmenu.sh index 50991d4..1173480 100755 --- a/examples/dmenu.sh +++ b/examples/dmenu.sh @@ -1,12 +1,13 @@ #!/bin/bash # A list of options, one per line -options="Option 1 -Option 2 -Option 3" +options="" +for i in $(seq 1 2000); do + options+="Option $i"$'\n' +done # Pipe options to wofi and capture the selection -selection=$(echo "$options" | cargo run -- --show dmenu) +selection=$(echo "$options" | cargo run --bin worf -- --show dmenu --sort-order default) #selection=$(echo "$options" | wofi --show dmenu) # Do something with the selection diff --git a/examples/worf-warden/Cargo.toml b/examples/worf-warden/Cargo.toml index b51c50e..1b05755 100644 --- a/examples/worf-warden/Cargo.toml +++ b/examples/worf-warden/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] worf = {path = "../../worf"} anyhow = "1.0.98" +env_logger = "0.11.8" # todo re-add this #[features] diff --git a/examples/worf-warden/src/main.rs b/examples/worf-warden/src/main.rs index 0243399..0321326 100644 --- a/examples/worf-warden/src/main.rs +++ b/examples/worf-warden/src/main.rs @@ -1,53 +1,102 @@ +use anyhow::anyhow; +use std::collections::{HashMap, HashSet}; +use std::env; use std::process::Command; use std::thread::sleep; use std::time::Duration; - use worf_lib::config::Config; use worf_lib::desktop::spawn_fork; use worf_lib::gui::{ItemProvider, Key, KeyBinding, MenuItem, Modifier}; use worf_lib::{config, gui}; +#[derive(Clone)] +struct MenuItemMetaData { + ids: Vec, +} + #[derive(Clone)] struct PasswordProvider { - items: Vec>, + items: Vec>, +} + +fn split_at_tab(input: &str) -> Option<(&str, &str)> { + let mut parts = input.splitn(2, '\t'); + Some((parts.next()?, parts.next()?)) } impl PasswordProvider { fn new(config: &Config) -> Self { let output = Command::new("rbw") .arg("list") + .arg("--fields") + .arg("id,name") .output() .expect("Failed to execute command"); let stdout = String::from_utf8_lossy(&output.stdout); - // todo the own solution should support images. - let mut items: Vec<_> = stdout + let mut items = stdout .lines() - .map(|line| { + .filter_map(|s| split_at_tab(s)) + .fold( + HashMap::new(), + |mut acc: HashMap>, (id, name)| { + acc.entry(name.to_owned()).or_default().push(id.to_owned()); + acc + }, + ) + .iter() + .map(|(key, value)| { MenuItem::new( - line.to_owned(), + key.clone(), None, None, vec![], None, 0.0, - Some(String::new()), + Some(MenuItemMetaData { + ids: value.clone(), + }), ) }) - .collect(); + .collect::>(); + gui::apply_sort(&mut items, &config.sort_order()); Self { items } } + + fn sub_provider(ids: Vec) -> Self { + Self { + items: ids + .iter() + .map(|id| { + MenuItem::new( + rbw_get_user(id), + None, + None, + vec![], + None, + 0.0, + Some(MenuItemMetaData { + ids: vec![id.clone()], + }), + ) + }) + .collect::>(), + } + } } -impl ItemProvider for PasswordProvider { - fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec>) { +impl ItemProvider for PasswordProvider { + fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec>) { (false, self.items.clone()) } - fn get_sub_elements(&mut self, _: &MenuItem) -> (bool, Option>>) { + fn get_sub_elements( + &mut self, + _: &MenuItem, + ) -> (bool, Option>>) { (false, None) } } @@ -80,10 +129,10 @@ fn keyboard_tab() { .expect("Failed to execute ydotool"); } -fn rbw_get(name: &str, field: &str) -> String { +fn rbw_get(id: &str, field: &str) -> String { let output = Command::new("rbw") .arg("get") - .arg(name) + .arg(id) .arg("--field") .arg(field) .output() @@ -94,23 +143,157 @@ fn rbw_get(name: &str, field: &str) -> String { .to_string() } -fn rbw_get_user(name: &str) -> String { - rbw_get(name, "user") +fn rbw_get_user(id: &str) -> String { + rbw_get(id, "user") } -fn rbw_get_password(name: &str) -> String { - rbw_get(name, "password") +fn rbw_get_password(id: &str) -> String { + rbw_get(id, "password") } -fn rbw_get_totp(name: &str) -> String { - rbw_get(name, "totp") +fn rbw_get_totp(id: &str) -> String { + rbw_get(id, "totp") +} + +fn key_type_all() -> KeyBinding { + KeyBinding { + key: Key::Num1, + modifiers: Modifier::Alt, + label: "Alt+1 Type User".to_string(), + } +} + +fn key_type_user() -> KeyBinding { + KeyBinding { + key: Key::Num2, + modifiers: Modifier::Alt, + label: "Alt+2 Type User".to_string(), + } +} + +fn key_type_password() -> KeyBinding { + KeyBinding { + key: Key::Num3, + modifiers: Modifier::Alt, + label: "Alt+3 Type Password".to_string(), + } +} + +fn key_type_totp() -> KeyBinding { + KeyBinding { + key: Key::Num4, + modifiers: Modifier::Alt, + label: "Alt+3 Type Totp".to_string(), + } +} + +fn key_reload() -> KeyBinding { + KeyBinding { + key: Key::R, + modifiers: Modifier::Alt, + label: "Alt+r Sync".to_string(), + } +} + +fn key_urls() -> KeyBinding { + KeyBinding { + key: Key::U, + modifiers: Modifier::Alt, + label: "Alt+u Sync".to_string(), + } +} + +fn key_names() -> KeyBinding { + KeyBinding { + key: Key::N, + modifiers: Modifier::Alt, + label: "Alt+n Sync".to_string(), + } +} + +fn key_folders() -> KeyBinding { + KeyBinding { + key: Key::C, + modifiers: Modifier::Alt, + label: "Alt+c Sync".to_string(), + } +} + +fn key_totp() -> KeyBinding { + KeyBinding { + key: Key::T, + modifiers: Modifier::Alt, + label: "Alt+t Sync".to_string(), + } +} + +fn key_lock() -> KeyBinding { + KeyBinding { + key: Key::L, + modifiers: Modifier::Alt, + label: "Alt+l Sync".to_string(), + } +} + +fn show(config: Config, provider: PasswordProvider) -> anyhow::Result<()> { + match gui::show( + config.clone(), + provider, + false, + None, + Some(vec![ + key_type_all(), + key_type_user(), + key_type_password(), + key_type_totp(), + key_reload(), + key_urls(), + key_names(), + key_folders(), + key_totp(), + key_lock(), + ]), + ) { + Ok(selection) => { + if let Some(meta) = selection.menu.data { + if meta.ids.len() > 1 { + return show(config, PasswordProvider::sub_provider(meta.ids)); + } + + let id = meta.ids.iter().next().unwrap_or(&selection.menu.label); + + sleep(Duration::from_millis(250)); + if let Some(key) = selection.custom_key { + if key == key_type_all() { + keyboard_type(&rbw_get_user(&id)); + keyboard_tab(); + keyboard_type(&rbw_get_password(&id)); + } else if key == key_type_user() { + keyboard_type(&rbw_get_user(&id)); + } else if key == key_type_password() { + keyboard_type(&rbw_get_password(&id)); + } else if key == key_type_totp() { + keyboard_type(&rbw_get_totp(&id)); + } + } + Ok(()) + } else { + Err(anyhow!("missing meta data")) + } + } + Err(e) => return Err(anyhow::anyhow!(e)), + } } fn main() -> anyhow::Result<()> { + env_logger::Builder::new() + .parse_filters(&env::var("RUST_LOG").unwrap_or_else(|_| "error".to_owned())) + .format_timestamp_micros() + .init(); let args = config::parse_args(); let config = config::load_config(Some(&args)).unwrap_or(args); - + if !groups().contains("input") { eprintln!("User must be in input group. 'sudo usermod -aG input $USER', then login again"); std::process::exit(1) @@ -122,102 +305,5 @@ fn main() -> anyhow::Result<()> { // todo eventually use a propper rust client for this, for now rbw is good enough let provider = PasswordProvider::new(&config); - let type_all = KeyBinding { - key: Key::Num1, - modifiers: Modifier::Alt, - label: "Alt+1 Type All".to_string(), - }; - - let type_user = KeyBinding { - key: Key::Num2, - modifiers: Modifier::Alt, - label: "Alt+2 Type User".to_string(), - }; - - let type_password = KeyBinding { - key: Key::Num3, - modifiers: Modifier::Alt, - label: "Alt+3 Type Password".to_string(), - }; - - let type_totp = KeyBinding { - key: Key::Num4, - modifiers: Modifier::Alt, - label: "Alt+3 Type Totp".to_string(), - }; - - let reload = KeyBinding { - key: Key::R, - modifiers: Modifier::Alt, - label: "Alt+r Sync".to_string(), - }; - - let urls = KeyBinding { - key: Key::U, // switch view to urls - modifiers: Modifier::Alt, - label: "Alt+u Sync".to_string(), - }; - - let names = KeyBinding { - key: Key::N, // switch view to names - modifiers: Modifier::Alt, - label: "Alt+n Sync".to_string(), - }; - - let folders = KeyBinding { - key: Key::C, // switch view to folders - modifiers: Modifier::Alt, - label: "Alt+c Sync".to_string(), - }; - - let totp = KeyBinding { - key: Key::T, - modifiers: Modifier::Alt, // switch view to totp - label: "Alt+t Sync".to_string(), - }; - - let lock = KeyBinding { - key: Key::L, - modifiers: Modifier::Alt, - label: "Alt+l Sync".to_string(), - }; - - match gui::show( - config, - provider, - false, - None, - Some(vec![ - type_all.clone(), - type_user.clone(), - type_password.clone(), - type_totp.clone(), - reload.clone(), - urls.clone(), - names.clone(), - folders.clone(), - totp.clone(), - lock.clone(), - ]), - ) { - Ok(selection) => { - let id = selection.menu.label.replace("\n", ""); - sleep(Duration::from_millis(250)); - if let Some(key) = selection.custom_key { - if key == type_all { - keyboard_type(&rbw_get_user(&id)); - keyboard_tab(); - keyboard_type(&rbw_get_password(&id)); - } else if key == type_user { - keyboard_type(&rbw_get_user(&id)); - } else if key == type_password { - keyboard_type(&rbw_get_password(&id)); - } else if key == type_totp { - keyboard_type(&rbw_get_totp(&id)); - } - } - } - Err(e) => return Err(anyhow::anyhow!(e)), - } - Ok(()) + show(config, provider) } diff --git a/worf/src/lib/mode.rs b/worf/src/lib/mode.rs index 57ae8e5..6786e10 100644 --- a/worf/src/lib/mode.rs +++ b/worf/src/lib/mode.rs @@ -598,6 +598,7 @@ impl DMenuProvider { .lines() .map(String::from) .map(|s| MenuItem::new(s.clone(), None, None, vec![], None, 0.0, None)) + .rev() .collect(); gui::apply_sort(&mut items, sort_order);