worf-warden support elements with multiple entires

This commit is contained in:
Alexander Mohr 2025-05-06 22:21:08 +02:00
parent 344433e697
commit a3d4fc8551
5 changed files with 212 additions and 122 deletions

1
Cargo.lock generated
View file

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

View file

@ -1,12 +1,13 @@
#!/bin/bash #!/bin/bash
# A list of options, one per line # A list of options, one per line
options="Option 1 options=""
Option 2 for i in $(seq 1 2000); do
Option 3" options+="Option $i"$'\n'
done
# Pipe options to wofi and capture the selection # 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) #selection=$(echo "$options" | wofi --show dmenu)
# Do something with the selection # Do something with the selection

View file

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

View file

@ -1,53 +1,102 @@
use anyhow::anyhow;
use std::collections::{HashMap, HashSet};
use std::env;
use std::process::Command; use std::process::Command;
use std::thread::sleep; use std::thread::sleep;
use std::time::Duration; use std::time::Duration;
use worf_lib::config::Config; use worf_lib::config::Config;
use worf_lib::desktop::spawn_fork; use worf_lib::desktop::spawn_fork;
use worf_lib::gui::{ItemProvider, Key, KeyBinding, MenuItem, Modifier}; use worf_lib::gui::{ItemProvider, Key, KeyBinding, MenuItem, Modifier};
use worf_lib::{config, gui}; use worf_lib::{config, gui};
#[derive(Clone)]
struct MenuItemMetaData {
ids: Vec<String>,
}
#[derive(Clone)] #[derive(Clone)]
struct PasswordProvider { struct PasswordProvider {
items: Vec<MenuItem<String>>, items: Vec<MenuItem<MenuItemMetaData>>,
}
fn split_at_tab(input: &str) -> Option<(&str, &str)> {
let mut parts = input.splitn(2, '\t');
Some((parts.next()?, parts.next()?))
} }
impl PasswordProvider { impl PasswordProvider {
fn new(config: &Config) -> Self { fn new(config: &Config) -> Self {
let output = Command::new("rbw") let output = Command::new("rbw")
.arg("list") .arg("list")
.arg("--fields")
.arg("id,name")
.output() .output()
.expect("Failed to execute command"); .expect("Failed to execute command");
let stdout = String::from_utf8_lossy(&output.stdout); let stdout = String::from_utf8_lossy(&output.stdout);
// todo the own solution should support images. let mut items = stdout
let mut items: Vec<_> = stdout
.lines() .lines()
.map(|line| { .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( MenuItem::new(
line.to_owned(), key.clone(),
None, None,
None, None,
vec![], vec![],
None, None,
0.0, 0.0,
Some(String::new()), Some(MenuItemMetaData {
ids: value.clone(),
}),
) )
}) })
.collect(); .collect::<Vec<_>>();
gui::apply_sort(&mut items, &config.sort_order()); gui::apply_sort(&mut items, &config.sort_order());
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<_>>(),
}
}
} }
impl ItemProvider<String> for PasswordProvider { impl ItemProvider<MenuItemMetaData> for PasswordProvider {
fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<String>>) { fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<MenuItemMetaData>>) {
(false, self.items.clone()) (false, self.items.clone())
} }
fn get_sub_elements(&mut self, _: &MenuItem<String>) -> (bool, Option<Vec<MenuItem<String>>>) { fn get_sub_elements(
&mut self,
_: &MenuItem<MenuItemMetaData>,
) -> (bool, Option<Vec<MenuItem<MenuItemMetaData>>>) {
(false, None) (false, None)
} }
} }
@ -80,10 +129,10 @@ fn keyboard_tab() {
.expect("Failed to execute ydotool"); .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") let output = Command::new("rbw")
.arg("get") .arg("get")
.arg(name) .arg(id)
.arg("--field") .arg("--field")
.arg(field) .arg(field)
.output() .output()
@ -94,23 +143,157 @@ fn rbw_get(name: &str, field: &str) -> String {
.to_string() .to_string()
} }
fn rbw_get_user(name: &str) -> String { fn rbw_get_user(id: &str) -> String {
rbw_get(name, "user") rbw_get(id, "user")
} }
fn rbw_get_password(name: &str) -> String { fn rbw_get_password(id: &str) -> String {
rbw_get(name, "password") rbw_get(id, "password")
} }
fn rbw_get_totp(name: &str) -> String { fn rbw_get_totp(id: &str) -> String {
rbw_get(name, "totp") rbw_get(id, "totp")
}
fn key_type_all() -> KeyBinding {
KeyBinding {
key: Key::Num1,
modifiers: Modifier::Alt,
label: "<b>Alt+1</b> Type User".to_string(),
}
}
fn key_type_user() -> KeyBinding {
KeyBinding {
key: Key::Num2,
modifiers: Modifier::Alt,
label: "<b>Alt+2</b> Type User".to_string(),
}
}
fn key_type_password() -> KeyBinding {
KeyBinding {
key: Key::Num3,
modifiers: Modifier::Alt,
label: "<b>Alt+3</b> Type Password".to_string(),
}
}
fn key_type_totp() -> KeyBinding {
KeyBinding {
key: Key::Num4,
modifiers: Modifier::Alt,
label: "<b>Alt+3</b> Type Totp".to_string(),
}
}
fn key_reload() -> KeyBinding {
KeyBinding {
key: Key::R,
modifiers: Modifier::Alt,
label: "<b>Alt+r</b> Sync".to_string(),
}
}
fn key_urls() -> KeyBinding {
KeyBinding {
key: Key::U,
modifiers: Modifier::Alt,
label: "<b>Alt+u</b> Sync".to_string(),
}
}
fn key_names() -> KeyBinding {
KeyBinding {
key: Key::N,
modifiers: Modifier::Alt,
label: "<b>Alt+n</b> Sync".to_string(),
}
}
fn key_folders() -> KeyBinding {
KeyBinding {
key: Key::C,
modifiers: Modifier::Alt,
label: "<b>Alt+c</b> Sync".to_string(),
}
}
fn key_totp() -> KeyBinding {
KeyBinding {
key: Key::T,
modifiers: Modifier::Alt,
label: "<b>Alt+t</b> Sync".to_string(),
}
}
fn key_lock() -> KeyBinding {
KeyBinding {
key: Key::L,
modifiers: Modifier::Alt,
label: "<b>Alt+l</b> 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<()> { 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 args = config::parse_args();
let config = config::load_config(Some(&args)).unwrap_or(args); let config = config::load_config(Some(&args)).unwrap_or(args);
if !groups().contains("input") { if !groups().contains("input") {
eprintln!("User must be in input group. 'sudo usermod -aG input $USER', then login again"); eprintln!("User must be in input group. 'sudo usermod -aG input $USER', then login again");
std::process::exit(1) 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 // todo eventually use a propper rust client for this, for now rbw is good enough
let provider = PasswordProvider::new(&config); let provider = PasswordProvider::new(&config);
let type_all = KeyBinding { show(config, provider)
key: Key::Num1,
modifiers: Modifier::Alt,
label: "<b>Alt+1</b> Type All".to_string(),
};
let type_user = KeyBinding {
key: Key::Num2,
modifiers: Modifier::Alt,
label: "<b>Alt+2</b> Type User".to_string(),
};
let type_password = KeyBinding {
key: Key::Num3,
modifiers: Modifier::Alt,
label: "<b>Alt+3</b> Type Password".to_string(),
};
let type_totp = KeyBinding {
key: Key::Num4,
modifiers: Modifier::Alt,
label: "<b>Alt+3</b> Type Totp".to_string(),
};
let reload = KeyBinding {
key: Key::R,
modifiers: Modifier::Alt,
label: "<b>Alt+r</b> Sync".to_string(),
};
let urls = KeyBinding {
key: Key::U, // switch view to urls
modifiers: Modifier::Alt,
label: "<b>Alt+u</b> Sync".to_string(),
};
let names = KeyBinding {
key: Key::N, // switch view to names
modifiers: Modifier::Alt,
label: "<b>Alt+n</b> Sync".to_string(),
};
let folders = KeyBinding {
key: Key::C, // switch view to folders
modifiers: Modifier::Alt,
label: "<b>Alt+c</b> Sync".to_string(),
};
let totp = KeyBinding {
key: Key::T,
modifiers: Modifier::Alt, // switch view to totp
label: "<b>Alt+t</b> Sync".to_string(),
};
let lock = KeyBinding {
key: Key::L,
modifiers: Modifier::Alt,
label: "<b>Alt+l</b> 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(())
} }

View file

@ -598,6 +598,7 @@ impl DMenuProvider {
.lines() .lines()
.map(String::from) .map(String::from)
.map(|s| MenuItem::new(s.clone(), None, None, vec![], None, 0.0, None)) .map(|s| MenuItem::new(s.clone(), None, None, vec![], None, 0.0, None))
.rev()
.collect(); .collect();
gui::apply_sort(&mut items, sort_order); gui::apply_sort(&mut items, sort_order);