mod parse;
use std::collections::HashSet;
use std::path::Path;
use std::time::Duration;
use evdev::{Device, EventType, InputEventKind, Key};
use tokio::sync::mpsc;
use tracing::{debug, info, warn};
use crate::{Command, HotkeyConfig};
pub use parse::{parse_hotkey, HotkeyBinding};
const HOTKEY_MAX_RETRIES: u32 = 10;
const HOTKEY_INITIAL_DELAY: Duration = Duration::from_secs(1);
struct HotkeyAction {
binding: HotkeyBinding,
command: Command,
}
pub async fn start_hotkey_listener(config: &HotkeyConfig, cmd_tx: mpsc::Sender<Command>) {
let mut actions = Vec::new();
if let Some(ref s) = config.toggle {
match parse_hotkey(s) {
Ok(binding) => {
info!("hotkey: toggle = {s}");
actions.push(HotkeyAction {
binding,
command: Command::Toggle,
});
}
Err(e) => warn!("invalid toggle hotkey '{s}': {e}"),
}
}
if let Some(ref s) = config.cancel {
match parse_hotkey(s) {
Ok(binding) => {
info!("hotkey: cancel = {s}");
actions.push(HotkeyAction {
binding,
command: Command::Cancel,
});
}
Err(e) => warn!("invalid cancel hotkey '{s}': {e}"),
}
}
if let Some(ref s) = config.command {
match parse_hotkey(s) {
Ok(binding) => {
info!("hotkey: command = {s}");
actions.push(HotkeyAction {
binding,
command: Command::CommandMode,
});
}
Err(e) => warn!("invalid command hotkey '{s}': {e}"),
}
}
if actions.is_empty() {
debug!("no hotkeys configured");
return;
}
let mut delay = HOTKEY_INITIAL_DELAY;
let mut devices = Vec::new();
for attempt in 1..=HOTKEY_MAX_RETRIES {
match enumerate_keyboards() {
Ok(d) if !d.is_empty() => {
if attempt > 1 {
info!("found {} keyboard device(s) (attempt {attempt})", d.len());
}
devices = d;
break;
}
Ok(_) => {
if attempt == HOTKEY_MAX_RETRIES {
warn!(
"no keyboard input devices found after {HOTKEY_MAX_RETRIES} attempts — hotkeys disabled"
);
return;
}
info!(
"no keyboard devices found (attempt {attempt}/{HOTKEY_MAX_RETRIES}) — retrying in {delay:?}"
);
}
Err(e) => {
if attempt == HOTKEY_MAX_RETRIES {
warn!(
"failed to enumerate input devices after {HOTKEY_MAX_RETRIES} attempts: {e} — hotkeys disabled"
);
return;
}
info!(
"failed to enumerate input devices (attempt {attempt}/{HOTKEY_MAX_RETRIES}): {e} — retrying in {delay:?}"
);
}
}
tokio::time::sleep(delay).await;
delay = (delay * 2).min(Duration::from_secs(10));
}
info!(
"hotkey listener monitoring {} keyboard device(s)",
devices.len()
);
for device in devices {
let name = device.name().unwrap_or("unknown").to_string();
let actions_clone: Vec<(Vec<Key>, Key, Command)> = actions
.iter()
.map(|a| {
(
a.binding.modifiers.clone(),
a.binding.trigger,
a.command.clone(),
)
})
.collect();
let tx = cmd_tx.clone();
tokio::spawn(async move {
if let Err(e) = listen_device(device, &actions_clone, tx).await {
debug!("hotkey listener for '{name}' stopped: {e}");
}
});
}
}
fn enumerate_keyboards() -> anyhow::Result<Vec<Device>> {
let mut keyboards = Vec::new();
let input_dir = Path::new("/dev/input");
if !input_dir.exists() {
anyhow::bail!("/dev/input does not exist");
}
for entry in std::fs::read_dir(input_dir)? {
let entry = entry?;
let path = entry.path();
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if !name.starts_with("event") {
continue;
}
match Device::open(&path) {
Ok(device) => {
if let Some(keys) = device.supported_keys() {
if keys.contains(Key::KEY_A) && keys.contains(Key::KEY_LEFTMETA) {
let dev_name = device.name().unwrap_or("unknown").to_string();
debug!("found keyboard: {} ({})", dev_name, path.display());
keyboards.push(device);
}
}
}
Err(e) => {
debug!("cannot open {}: {e}", path.display());
}
}
}
Ok(keyboards)
}
async fn listen_device(
device: Device,
actions: &[(Vec<Key>, Key, Command)],
cmd_tx: mpsc::Sender<Command>,
) -> anyhow::Result<()> {
let mut held_keys: HashSet<Key> = HashSet::new();
let mut stream = device.into_event_stream()?;
loop {
let event = stream.next_event().await?;
if event.event_type() != EventType::KEY {
continue;
}
let key = match event.kind() {
InputEventKind::Key(k) => k,
_ => continue,
};
match event.value() {
1 => {
held_keys.insert(key);
for (modifiers, trigger, command) in actions {
if key == *trigger && modifiers_held(&held_keys, modifiers) {
debug!("hotkey matched: {:?}", command);
let _ = cmd_tx.send(command.clone()).await;
}
}
}
0 => {
held_keys.remove(&key);
}
_ => {} }
}
}
fn modifiers_held(held: &HashSet<Key>, required: &[Key]) -> bool {
required.iter().all(|m| {
match *m {
Key::KEY_LEFTMETA => {
held.contains(&Key::KEY_LEFTMETA) || held.contains(&Key::KEY_RIGHTMETA)
}
Key::KEY_LEFTALT => {
held.contains(&Key::KEY_LEFTALT) || held.contains(&Key::KEY_RIGHTALT)
}
Key::KEY_LEFTCTRL => {
held.contains(&Key::KEY_LEFTCTRL) || held.contains(&Key::KEY_RIGHTCTRL)
}
Key::KEY_LEFTSHIFT => {
held.contains(&Key::KEY_LEFTSHIFT) || held.contains(&Key::KEY_RIGHTSHIFT)
}
other => held.contains(&other),
}
})
}