diff --git a/Cargo.lock b/Cargo.lock index bbbf1dd..3502650 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1916,7 +1916,6 @@ dependencies = [ name = "worf-warden" version = "0.1.0" dependencies = [ - "anyhow", "env_logger", "worf", ] diff --git a/examples/worf-warden/Cargo.toml b/examples/worf-warden/Cargo.toml index 1b05755..99b56d4 100644 --- a/examples/worf-warden/Cargo.toml +++ b/examples/worf-warden/Cargo.toml @@ -5,7 +5,6 @@ edition = "2024" [dependencies] worf = {path = "../../worf"} -anyhow = "1.0.98" env_logger = "0.11.8" # todo re-add this diff --git a/examples/worf-warden/Readme.md b/examples/worf-warden/Readme.md new file mode 100644 index 0000000..f826768 --- /dev/null +++ b/examples/worf-warden/Readme.md @@ -0,0 +1,11 @@ +# Worf Warden + +Simple password manager build upon +* [rbw](https://github.com/doy/rbw) + * TOTP needs this PR https://github.com/doy/rbw/pull/247 +* [ydotool](https://github.com/ReimuNotMoe/ydotool) + +The idea it taken from https://github.com/mattydebie/bitwarden-rofi/blob/master/bwmenu + +Additional dependencies +* https://www.gnupg.org/related_software/pinentry/index.en.html diff --git a/examples/worf-warden/src/main.rs b/examples/worf-warden/src/main.rs index 0321326..4bd9c45 100644 --- a/examples/worf-warden/src/main.rs +++ b/examples/worf-warden/src/main.rs @@ -1,5 +1,4 @@ -use anyhow::anyhow; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::env; use std::process::Command; use std::thread::sleep; @@ -26,65 +25,71 @@ fn split_at_tab(input: &str) -> Option<(&str, &str)> { 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 output = rbw("list", Some(vec!["--fields", "id,name"])); + let items = match output { + Ok(output) => { + let mut items = output + .lines() + .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( + key.clone(), + None, + None, + vec![], + None, + 0.0, + Some(MenuItemMetaData { ids: value.clone() }), + ) + }) + .collect::>(); + gui::apply_sort(&mut items, &config.sort_order()); + items + } + Err(error) => { + let item = MenuItem::new( + format!("Error from rbw: {error}"), + None, + None, + vec![], + None, + 0.0, + None, + ); + vec![item] + } + }; - let stdout = String::from_utf8_lossy(&output.stdout); + Self { items } + } - let mut items = stdout - .lines() - .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 - }, - ) + fn sub_provider(ids: Vec) -> Result { + let items = ids .iter() - .map(|(key, value)| { - MenuItem::new( - key.clone(), + .map(|id| { + Ok(MenuItem::new( + rbw_get_user(id, false)?, None, None, vec![], None, 0.0, Some(MenuItemMetaData { - ids: value.clone(), + ids: vec![id.clone()], }), - ) + )) }) - .collect::>(); + .collect::, String>>()?; - 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::>(), - } + Ok(Self { items }) } } @@ -122,44 +127,63 @@ fn keyboard_tab() { Command::new("ydotool") .arg("key") .arg("-d") - .arg("10") + .arg("50") .arg("15:1") .arg("15:0") .output() .expect("Failed to execute ydotool"); } -fn rbw_get(id: &str, field: &str) -> String { - let output = Command::new("rbw") - .arg("get") - .arg(id) - .arg("--field") - .arg(field) +fn rbw(cmd: &str, args: Option>) -> Result { + let mut command = Command::new("rbw"); + command.arg(cmd); + + if let Some(args) = args { + for arg in args { + command.arg(arg); + } + } + + let output = command .output() - .expect("Failed to execute command"); + .map_err(|e| format!("Failed to execute command: {}", e))?; - String::from_utf8_lossy(&output.stdout) - .trim_end() - .to_string() + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("rbw command failed: {}", stderr.trim())); + } + + let stdout = + String::from_utf8(output.stdout).map_err(|e| format!("Invalid UTF-8 output: {}", e))?; + + Ok(stdout.trim().to_string()) } -fn rbw_get_user(id: &str) -> String { - rbw_get(id, "user") +fn rbw_get(id: &str, field: &str, copy: bool) -> Result { + let mut args = vec![id, "--field", field]; + if copy { + args.push("--clipboard"); + } + rbw("get", Some(args)) } -fn rbw_get_password(id: &str) -> String { - rbw_get(id, "password") +fn rbw_get_user(id: &str, copy: bool) -> Result { + rbw_get(id, "user", copy) } -fn rbw_get_totp(id: &str) -> String { - rbw_get(id, "totp") +fn rbw_get_password(id: &str, copy: bool) -> Result { + rbw_get(id, "password", copy) +} + +fn rbw_get_totp(id: &str, copy: bool) -> Result { + rbw_get(id, "totp", copy) } fn key_type_all() -> KeyBinding { KeyBinding { key: Key::Num1, modifiers: Modifier::Alt, - label: "Alt+1 Type User".to_string(), + label: "Alt+1 Type All".to_string(), } } @@ -183,11 +207,11 @@ fn key_type_totp() -> KeyBinding { KeyBinding { key: Key::Num4, modifiers: Modifier::Alt, - label: "Alt+3 Type Totp".to_string(), + label: "Alt+4 Type Totp".to_string(), } } -fn key_reload() -> KeyBinding { +fn key_sync() -> KeyBinding { KeyBinding { key: Key::R, modifiers: Modifier::Alt, @@ -199,7 +223,7 @@ fn key_urls() -> KeyBinding { KeyBinding { key: Key::U, modifiers: Modifier::Alt, - label: "Alt+u Sync".to_string(), + label: "Alt+u Urls".to_string(), } } @@ -207,7 +231,7 @@ fn key_names() -> KeyBinding { KeyBinding { key: Key::N, modifiers: Modifier::Alt, - label: "Alt+n Sync".to_string(), + label: "Alt+n NAmes".to_string(), } } @@ -215,15 +239,16 @@ fn key_folders() -> KeyBinding { KeyBinding { key: Key::C, modifiers: Modifier::Alt, - label: "Alt+c Sync".to_string(), + label: "Alt+c Folders".to_string(), } } +/// copies totp to clipboard fn key_totp() -> KeyBinding { KeyBinding { key: Key::T, modifiers: Modifier::Alt, - label: "Alt+t Sync".to_string(), + label: "Alt+t Totp".to_string(), } } @@ -231,11 +256,11 @@ fn key_lock() -> KeyBinding { KeyBinding { key: Key::L, modifiers: Modifier::Alt, - label: "Alt+l Sync".to_string(), + label: "Alt+l Lock".to_string(), } } -fn show(config: Config, provider: PasswordProvider) -> anyhow::Result<()> { +fn show(config: Config, provider: PasswordProvider) -> Result<(), String> { match gui::show( config.clone(), provider, @@ -246,7 +271,7 @@ fn show(config: Config, provider: PasswordProvider) -> anyhow::Result<()> { key_type_user(), key_type_password(), key_type_totp(), - key_reload(), + key_sync(), key_urls(), key_names(), key_folders(), @@ -257,35 +282,49 @@ fn show(config: Config, provider: PasswordProvider) -> anyhow::Result<()> { Ok(selection) => { if let Some(meta) = selection.menu.data { if meta.ids.len() > 1 { - return show(config, PasswordProvider::sub_provider(meta.ids)); + return show(config, PasswordProvider::sub_provider(meta.ids)?); } - let id = meta.ids.iter().next().unwrap_or(&selection.menu.label); + let id = meta.ids.first().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_type(&rbw_get_user(id, false)?); keyboard_tab(); - keyboard_type(&rbw_get_password(&id)); + keyboard_type(&rbw_get_password(id, false)?); } else if key == key_type_user() { - keyboard_type(&rbw_get_user(&id)); + keyboard_type(&rbw_get_user(id, false)?); } else if key == key_type_password() { - keyboard_type(&rbw_get_password(&id)); + keyboard_type(&rbw_get_password(id, false)?); } else if key == key_type_totp() { - keyboard_type(&rbw_get_totp(&id)); + keyboard_type(&rbw_get_totp(id, false)?); + } else if key == key_lock() { + rbw("lock", None)?; + } else if key == key_sync() { + rbw("sync", None)?; + } else if key == key_urls() { + todo!("key urls"); + } else if key == key_names() { + todo!("key names"); + } else if key == key_folders() { + todo!("key folders"); + } else if key == key_totp() { + rbw_get_totp(id, true)?; } + } else { + rbw_get_password(id, true)?; } Ok(()) } else { - Err(anyhow!("missing meta data")) + Err("missing meta data".to_owned()) } } - Err(e) => return Err(anyhow::anyhow!(e)), + Err(e) => Err(e.to_string()), } } -fn main() -> anyhow::Result<()> { +fn main() -> Result<(), String> { env_logger::Builder::new() .parse_filters(&env::var("RUST_LOG").unwrap_or_else(|_| "error".to_owned())) .format_timestamp_micros() diff --git a/worf/src/lib/gui.rs b/worf/src/lib/gui.rs index 83ed0a8..9149c4a 100644 --- a/worf/src/lib/gui.rs +++ b/worf/src/lib/gui.rs @@ -8,7 +8,7 @@ use crossbeam::channel; use crossbeam::channel::Sender; use gdk4::Display; use gdk4::gio::File; -use gdk4::glib::{Propagation, timeout_add_local, MainContext}; +use gdk4::glib::{MainContext, Propagation, timeout_add_local}; use gdk4::prelude::{Cast, DisplayExt, MonitorExt, SurfaceExt}; use gtk4::glib::ControlFlow; use gtk4::prelude::{ @@ -457,9 +457,10 @@ where // Use glib's MainContext to handle the receiver asynchronously let main_context = MainContext::default(); let receiver_result = main_context.block_on(async { - MainContext::default().spawn_local(async move { - receiver.recv().map_err(|e| Error::Io(e.to_string())) - }).await.unwrap_or_else(|e| Err(Error::Io(e.to_string()))) + MainContext::default() + .spawn_local(async move { receiver.recv().map_err(|e| Error::Io(e.to_string())) }) + .await + .unwrap_or_else(|e| Err(Error::Io(e.to_string()))) }); receiver_result? @@ -566,14 +567,7 @@ fn build_ui( let animate_cfg = config.clone(); let animate_window = ui_elements.window.clone(); - // timeout_add_local(Duration::from_millis(1), move || { - // if !animate_window.is_active() { - // return ControlFlow::Continue; - // } - // animate_window.set_opacity(1.0); - // window_show_resize(&animate_cfg.clone(), &animate_window); - // ControlFlow::Break - // }); + animate_window.connect_is_active_notify(move |w| { w.set_opacity(1.0); window_show_resize(&animate_cfg.clone(), w); @@ -822,9 +816,14 @@ fn handle_key_press( } gdk4::Key::Return => { let search_lock = ui.search_text.lock().unwrap(); - if let Err(e) = - handle_selected_item(ui.clone(), meta.clone(), Some(&search_lock), None, meta.new_on_empty, None) - { + if let Err(e) = handle_selected_item( + ui.clone(), + meta.clone(), + Some(&search_lock), + None, + meta.new_on_empty, + None, + ) { log::error!("{e}"); } } @@ -962,7 +961,7 @@ fn handle_selected_item( custom_key: Option<&KeyBinding>, ) -> Result<(), String> where - T: Clone + Send +'static, + T: Clone + Send + 'static, { if let Some(selected_item) = item { send_selected_item(ui, meta, custom_key.map(|c| c.clone()), selected_item); @@ -972,7 +971,12 @@ where let item = list_items.get(&s); if let Some(selected_item) = item { if selected_item.visible { - send_selected_item(ui.clone(), meta, custom_key.map(|c| c.clone()), selected_item.clone()); + send_selected_item( + ui.clone(), + meta, + custom_key.map(|c| c.clone()), + selected_item.clone(), + ); return Ok(()); } } @@ -1213,7 +1217,7 @@ fn set_menu_visibility_for_search( if config.sort_order() == SortOrder::Default { return; } - + { if query.is_empty() { for (fb, menu_item) in items.iter_mut() {