diff --git a/Cargo.lock b/Cargo.lock index 7c58ee0..f060ef4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,6 +10,7 @@ dependencies = [ "env_logger", "evdev", "flate2", + "fuzzy-matcher", "itertools", "log", "nix", @@ -297,6 +298,15 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + [[package]] name = "heck" version = "0.4.1" @@ -694,6 +704,16 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "tokio" version = "1.24.2" diff --git a/swhkd/Cargo.toml b/swhkd/Cargo.toml index d9fc31e..7d0fb62 100644 --- a/swhkd/Cargo.toml +++ b/swhkd/Cargo.toml @@ -16,6 +16,7 @@ flate2 = "1.0.24" clap = { version = "4.1.0", features = ["derive"] } env_logger = "0.9.0" evdev = { version = "0.12.0", features = ["tokio"] } +fuzzy-matcher = "0.3.7" itertools = "0.10.3" log = "0.4.14" nix = "0.23.1" diff --git a/swhkd/src/daemon.rs b/swhkd/src/daemon.rs index 4c67f36..2e6ccc6 100644 --- a/swhkd/src/daemon.rs +++ b/swhkd/src/daemon.rs @@ -1,7 +1,10 @@ use crate::config::Value; -use clap::Parser; +use clap::{Parser, Subcommand}; use config::Hotkey; use evdev::{AttributeSet, Device, InputEventKind, Key}; +use fuzzy_matcher::skim::SkimMatcherV2; +use fuzzy_matcher::FuzzyMatcher; +use itertools::Itertools; use nix::{ sys::stat::{umask, Mode}, unistd::{Group, Uid}, @@ -45,10 +48,8 @@ impl KeyboardState { } } -/// Simple Wayland Hotkey Daemon -#[derive(Parser)] -#[command(version, about, long_about = None)] -struct Args { +#[derive(clap::Args)] +struct RunArgs { /// Set a custom config file path. #[arg(short = 'c', long, value_name = "FILE")] config: Option, @@ -61,9 +62,89 @@ struct Args { #[arg(short, long)] debug: bool, - /// Take a list of devices from the user - #[arg(short = 'D', long, num_args = 0.., value_delimiter = ' ')] + /// Take a list of device paths from the user + #[arg(short = 'D', long, num_args = 0.., value_delimiter = ' ', value_name = "PATH")] device: Vec, + + /// Fuzzy match one or more device names + #[arg(short = 'n', long, num_args = 0.., value_delimiter = ' ', value_name = "DEVICE_NAME")] + device_by_name: Vec, +} + +/// Simple Wayland Hotkey Daemon +#[derive(Parser)] +#[command(version, about, long_about = None)] +struct Args { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Run the hotkey daemon + Run(RunArgs), + + /// List the names and paths of available devices + ListDevices, +} + +fn list_device_names() { + for (path, device) in evdev::enumerate() { + if let Some(name) = device.name() { + print!("name {}, ", name) + } + println!("path: {}", path.display()) + } +} + +fn filter_keyboard_devices(paths: &[String], names: &[String]) -> Vec<(PathBuf, Device)> { + if paths.is_empty() && names.is_empty() { + log::trace!("Attempting to find all keyboard file descriptors."); + return evdev::enumerate().filter(|(_, dev)| check_device_is_keyboard(dev)).collect(); + } + + let mut keyboard_devices = vec![]; + + for in_path in paths { + if let Some(pair) = + evdev::enumerate().find(|(path, _device)| in_path == path.to_str().unwrap_or_default()) + { + keyboard_devices.push(pair); + } + } + + let matcher = SkimMatcherV2::default(); + + for in_name in names { + let possible_devices: Vec<_> = evdev::enumerate() + .filter(|(_path, device)| { + device.name().is_some_and(|n| matcher.fuzzy_match(n, in_name.as_str()).is_some()) + }) + .collect(); + match possible_devices.len() { + 0 => log::warn!("found no device with the name `{}`", in_name), + 1 => { + log::warn!( + "correcting device name `{}` to its closes match `{}`", + in_name, + possible_devices[0].1.name().unwrap_or_default() + ); + keyboard_devices.push(possible_devices.into_iter().next().unwrap()); + } + _ => { + log::warn!( + "found no device with the name `{}`, did you mean any of {}?", + in_name, + possible_devices + .iter() + .map(|(_, d)| format!("`{}`", d.name().unwrap_or_default())) + .join(", ") + ); + } + } + } + + keyboard_devices } #[tokio::main] @@ -72,6 +153,14 @@ async fn main() -> Result<(), Box> { let default_cooldown: u64 = 250; env::set_var("RUST_LOG", "swhkd=warn"); + let args = match args.command { + Commands::Run(run_args) => run_args, + Commands::ListDevices => { + list_device_names(); + return Ok(()); + } + }; + if args.debug { env::set_var("RUST_LOG", "swhkd=trace"); } @@ -112,16 +201,7 @@ async fn main() -> Result<(), Box> { let mut mode_stack: Vec = vec![0]; let arg_devices: Vec = args.device; - let keyboard_devices: Vec<_> = { - if arg_devices.is_empty() { - log::trace!("Attempting to find all keyboard file descriptors."); - evdev::enumerate().filter(|(_, dev)| check_device_is_keyboard(dev)).collect() - } else { - evdev::enumerate() - .filter(|(_, dev)| arg_devices.contains(&dev.name().unwrap_or("").to_string())) - .collect() - } - }; + let keyboard_devices = filter_keyboard_devices(&arg_devices, &args.device_by_name); if keyboard_devices.is_empty() { log::error!("No valid keyboard device was detected!");