improve worf-warden, support more keys

This commit is contained in:
Alexander Mohr 2025-05-07 22:07:08 +02:00
parent d9c53a3acf
commit dd46e210e1
5 changed files with 158 additions and 106 deletions

1
Cargo.lock generated
View file

@ -1916,7 +1916,6 @@ dependencies = [
name = "worf-warden" name = "worf-warden"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"env_logger", "env_logger",
"worf", "worf",
] ]

View file

@ -5,7 +5,6 @@ edition = "2024"
[dependencies] [dependencies]
worf = {path = "../../worf"} worf = {path = "../../worf"}
anyhow = "1.0.98"
env_logger = "0.11.8" env_logger = "0.11.8"
# todo re-add this # todo re-add this

View file

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

View file

@ -1,5 +1,4 @@
use anyhow::anyhow; use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::env; use std::env;
use std::process::Command; use std::process::Command;
use std::thread::sleep; use std::thread::sleep;
@ -26,65 +25,71 @@ fn split_at_tab(input: &str) -> Option<(&str, &str)> {
impl PasswordProvider { impl PasswordProvider {
fn new(config: &Config) -> Self { fn new(config: &Config) -> Self {
let output = Command::new("rbw") let output = rbw("list", Some(vec!["--fields", "id,name"]));
.arg("list") let items = match output {
.arg("--fields") Ok(output) => {
.arg("id,name") let mut items = output
.output() .lines()
.expect("Failed to execute command"); .filter_map(|s| split_at_tab(s))
.fold(
HashMap::new(),
|mut acc: HashMap<String, Vec<String>>, (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::<Vec<_>>();
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 fn sub_provider(ids: Vec<String>) -> Result<Self, String> {
.lines() let items = ids
.filter_map(|s| split_at_tab(s))
.fold(
HashMap::new(),
|mut acc: HashMap<String, Vec<String>>, (id, name)| {
acc.entry(name.to_owned()).or_default().push(id.to_owned());
acc
},
)
.iter() .iter()
.map(|(key, value)| { .map(|id| {
MenuItem::new( Ok(MenuItem::new(
key.clone(), rbw_get_user(id, false)?,
None, None,
None, None,
vec![], vec![],
None, None,
0.0, 0.0,
Some(MenuItemMetaData { Some(MenuItemMetaData {
ids: value.clone(), ids: vec![id.clone()],
}), }),
) ))
}) })
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, String>>()?;
gui::apply_sort(&mut items, &config.sort_order()); Ok(Self { items })
Self { items }
}
fn sub_provider(ids: Vec<String>) -> 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::<Vec<_>>(),
}
} }
} }
@ -122,44 +127,63 @@ fn keyboard_tab() {
Command::new("ydotool") Command::new("ydotool")
.arg("key") .arg("key")
.arg("-d") .arg("-d")
.arg("10") .arg("50")
.arg("15:1") .arg("15:1")
.arg("15:0") .arg("15:0")
.output() .output()
.expect("Failed to execute ydotool"); .expect("Failed to execute ydotool");
} }
fn rbw_get(id: &str, field: &str) -> String { fn rbw(cmd: &str, args: Option<Vec<&str>>) -> Result<String, String> {
let output = Command::new("rbw") let mut command = Command::new("rbw");
.arg("get") command.arg(cmd);
.arg(id)
.arg("--field") if let Some(args) = args {
.arg(field) for arg in args {
command.arg(arg);
}
}
let output = command
.output() .output()
.expect("Failed to execute command"); .map_err(|e| format!("Failed to execute command: {}", e))?;
String::from_utf8_lossy(&output.stdout) if !output.status.success() {
.trim_end() let stderr = String::from_utf8_lossy(&output.stderr);
.to_string() 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 { fn rbw_get(id: &str, field: &str, copy: bool) -> Result<String, String> {
rbw_get(id, "user") let mut args = vec![id, "--field", field];
if copy {
args.push("--clipboard");
}
rbw("get", Some(args))
} }
fn rbw_get_password(id: &str) -> String { fn rbw_get_user(id: &str, copy: bool) -> Result<String, String> {
rbw_get(id, "password") rbw_get(id, "user", copy)
} }
fn rbw_get_totp(id: &str) -> String { fn rbw_get_password(id: &str, copy: bool) -> Result<String, String> {
rbw_get(id, "totp") rbw_get(id, "password", copy)
}
fn rbw_get_totp(id: &str, copy: bool) -> Result<String, String> {
rbw_get(id, "totp", copy)
} }
fn key_type_all() -> KeyBinding { fn key_type_all() -> KeyBinding {
KeyBinding { KeyBinding {
key: Key::Num1, key: Key::Num1,
modifiers: Modifier::Alt, modifiers: Modifier::Alt,
label: "<b>Alt+1</b> Type User".to_string(), label: "<b>Alt+1</b> Type All".to_string(),
} }
} }
@ -183,11 +207,11 @@ fn key_type_totp() -> KeyBinding {
KeyBinding { KeyBinding {
key: Key::Num4, key: Key::Num4,
modifiers: Modifier::Alt, modifiers: Modifier::Alt,
label: "<b>Alt+3</b> Type Totp".to_string(), label: "<b>Alt+4</b> Type Totp".to_string(),
} }
} }
fn key_reload() -> KeyBinding { fn key_sync() -> KeyBinding {
KeyBinding { KeyBinding {
key: Key::R, key: Key::R,
modifiers: Modifier::Alt, modifiers: Modifier::Alt,
@ -199,7 +223,7 @@ fn key_urls() -> KeyBinding {
KeyBinding { KeyBinding {
key: Key::U, key: Key::U,
modifiers: Modifier::Alt, modifiers: Modifier::Alt,
label: "<b>Alt+u</b> Sync".to_string(), label: "<b>Alt+u</b> Urls".to_string(),
} }
} }
@ -207,7 +231,7 @@ fn key_names() -> KeyBinding {
KeyBinding { KeyBinding {
key: Key::N, key: Key::N,
modifiers: Modifier::Alt, modifiers: Modifier::Alt,
label: "<b>Alt+n</b> Sync".to_string(), label: "<b>Alt+n</b> NAmes".to_string(),
} }
} }
@ -215,15 +239,16 @@ fn key_folders() -> KeyBinding {
KeyBinding { KeyBinding {
key: Key::C, key: Key::C,
modifiers: Modifier::Alt, modifiers: Modifier::Alt,
label: "<b>Alt+c</b> Sync".to_string(), label: "<b>Alt+c</b> Folders".to_string(),
} }
} }
/// copies totp to clipboard
fn key_totp() -> KeyBinding { fn key_totp() -> KeyBinding {
KeyBinding { KeyBinding {
key: Key::T, key: Key::T,
modifiers: Modifier::Alt, modifiers: Modifier::Alt,
label: "<b>Alt+t</b> Sync".to_string(), label: "<b>Alt+t</b> Totp".to_string(),
} }
} }
@ -231,11 +256,11 @@ fn key_lock() -> KeyBinding {
KeyBinding { KeyBinding {
key: Key::L, key: Key::L,
modifiers: Modifier::Alt, modifiers: Modifier::Alt,
label: "<b>Alt+l</b> Sync".to_string(), label: "<b>Alt+l</b> Lock".to_string(),
} }
} }
fn show(config: Config, provider: PasswordProvider) -> anyhow::Result<()> { fn show(config: Config, provider: PasswordProvider) -> Result<(), String> {
match gui::show( match gui::show(
config.clone(), config.clone(),
provider, provider,
@ -246,7 +271,7 @@ fn show(config: Config, provider: PasswordProvider) -> anyhow::Result<()> {
key_type_user(), key_type_user(),
key_type_password(), key_type_password(),
key_type_totp(), key_type_totp(),
key_reload(), key_sync(),
key_urls(), key_urls(),
key_names(), key_names(),
key_folders(), key_folders(),
@ -257,35 +282,49 @@ fn show(config: Config, provider: PasswordProvider) -> anyhow::Result<()> {
Ok(selection) => { Ok(selection) => {
if let Some(meta) = selection.menu.data { if let Some(meta) = selection.menu.data {
if meta.ids.len() > 1 { 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)); sleep(Duration::from_millis(250));
if let Some(key) = selection.custom_key { if let Some(key) = selection.custom_key {
if key == key_type_all() { if key == key_type_all() {
keyboard_type(&rbw_get_user(&id)); keyboard_type(&rbw_get_user(id, false)?);
keyboard_tab(); keyboard_tab();
keyboard_type(&rbw_get_password(&id)); keyboard_type(&rbw_get_password(id, false)?);
} else if key == key_type_user() { } 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() { } 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() { } 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(()) Ok(())
} else { } 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() 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()))
.format_timestamp_micros() .format_timestamp_micros()

View file

@ -8,7 +8,7 @@ use crossbeam::channel;
use crossbeam::channel::Sender; use crossbeam::channel::Sender;
use gdk4::Display; use gdk4::Display;
use gdk4::gio::File; 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 gdk4::prelude::{Cast, DisplayExt, MonitorExt, SurfaceExt};
use gtk4::glib::ControlFlow; use gtk4::glib::ControlFlow;
use gtk4::prelude::{ use gtk4::prelude::{
@ -457,9 +457,10 @@ where
// Use glib's MainContext to handle the receiver asynchronously // Use glib's MainContext to handle the receiver asynchronously
let main_context = MainContext::default(); let main_context = MainContext::default();
let receiver_result = main_context.block_on(async { let receiver_result = main_context.block_on(async {
MainContext::default().spawn_local(async move { MainContext::default()
receiver.recv().map_err(|e| Error::Io(e.to_string())) .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()))) .await
.unwrap_or_else(|e| Err(Error::Io(e.to_string())))
}); });
receiver_result? receiver_result?
@ -566,14 +567,7 @@ fn build_ui<T, P>(
let animate_cfg = config.clone(); let animate_cfg = config.clone();
let animate_window = ui_elements.window.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| { animate_window.connect_is_active_notify(move |w| {
w.set_opacity(1.0); w.set_opacity(1.0);
window_show_resize(&animate_cfg.clone(), w); window_show_resize(&animate_cfg.clone(), w);
@ -822,9 +816,14 @@ fn handle_key_press<T: Clone + 'static + Send>(
} }
gdk4::Key::Return => { gdk4::Key::Return => {
let search_lock = ui.search_text.lock().unwrap(); let search_lock = ui.search_text.lock().unwrap();
if let Err(e) = if let Err(e) = handle_selected_item(
handle_selected_item(ui.clone(), meta.clone(), Some(&search_lock), None, meta.new_on_empty, None) ui.clone(),
{ meta.clone(),
Some(&search_lock),
None,
meta.new_on_empty,
None,
) {
log::error!("{e}"); log::error!("{e}");
} }
} }
@ -962,7 +961,7 @@ fn handle_selected_item<T>(
custom_key: Option<&KeyBinding>, custom_key: Option<&KeyBinding>,
) -> Result<(), String> ) -> Result<(), String>
where where
T: Clone + Send +'static, T: Clone + Send + 'static,
{ {
if let Some(selected_item) = item { if let Some(selected_item) = item {
send_selected_item(ui, meta, custom_key.map(|c| c.clone()), selected_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); let item = list_items.get(&s);
if let Some(selected_item) = item { if let Some(selected_item) = item {
if selected_item.visible { 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(()); return Ok(());
} }
} }
@ -1213,7 +1217,7 @@ fn set_menu_visibility_for_search<T: Clone>(
if config.sort_order() == SortOrder::Default { if config.sort_order() == SortOrder::Default {
return; return;
} }
{ {
if query.is_empty() { if query.is_empty() {
for (fb, menu_item) in items.iter_mut() { for (fb, menu_item) in items.iter_mut() {