1use anyhow::{Context, Result};
9use evdev::{Device, InputEventKind, Key};
10use std::collections::HashMap;
11use std::sync::OnceLock;
12use tokio_util::sync::CancellationToken;
13
14use crate::state::InputEvent;
15
16fn build_key_map() -> HashMap<&'static str, Key> {
17 let mut m = HashMap::new();
18
19 m.insert("numpad_0", Key::KEY_KP0);
21 m.insert("numpad_1", Key::KEY_KP1);
22 m.insert("numpad_2", Key::KEY_KP2);
23 m.insert("numpad_3", Key::KEY_KP3);
24 m.insert("numpad_4", Key::KEY_KP4);
25 m.insert("numpad_5", Key::KEY_KP5);
26 m.insert("numpad_6", Key::KEY_KP6);
27 m.insert("numpad_7", Key::KEY_KP7);
28 m.insert("numpad_8", Key::KEY_KP8);
29 m.insert("numpad_9", Key::KEY_KP9);
30 m.insert("numpad_enter", Key::KEY_KPENTER);
31 m.insert("numpad_decimal", Key::KEY_KPDOT);
32 m.insert("numpad_dot", Key::KEY_KPDOT);
33 m.insert("numpad_plus", Key::KEY_KPPLUS);
34 m.insert("numpad_add", Key::KEY_KPPLUS);
35 m.insert("numpad_minus", Key::KEY_KPMINUS);
36 m.insert("numpad_subtract", Key::KEY_KPMINUS);
37 m.insert("numpad_multiply", Key::KEY_KPASTERISK);
38 m.insert("numpad_divide", Key::KEY_KPSLASH);
39 m.insert("numpad_clear", Key::KEY_NUMLOCK); m.insert("numpad_equals", Key::KEY_KPEQUAL);
41
42 m.insert("right_option", Key::KEY_RIGHTALT);
44 m.insert("right_alt", Key::KEY_RIGHTALT);
45 m.insert("left_option", Key::KEY_LEFTALT);
46 m.insert("left_alt", Key::KEY_LEFTALT);
47 m.insert("right_command", Key::KEY_RIGHTMETA);
48 m.insert("right_cmd", Key::KEY_RIGHTMETA);
49 m.insert("left_command", Key::KEY_LEFTMETA);
50 m.insert("left_cmd", Key::KEY_LEFTMETA);
51 m.insert("right_shift", Key::KEY_RIGHTSHIFT);
52 m.insert("left_shift", Key::KEY_LEFTSHIFT);
53 m.insert("right_control", Key::KEY_RIGHTCTRL);
54 m.insert("right_ctrl", Key::KEY_RIGHTCTRL);
55 m.insert("left_control", Key::KEY_LEFTCTRL);
56 m.insert("left_ctrl", Key::KEY_LEFTCTRL);
57 m.insert("fn_key", Key::KEY_FN);
58 m.insert("fn", Key::KEY_FN);
59 m.insert("caps_lock", Key::KEY_CAPSLOCK);
60
61 m.insert("f1", Key::KEY_F1);
63 m.insert("f2", Key::KEY_F2);
64 m.insert("f3", Key::KEY_F3);
65 m.insert("f4", Key::KEY_F4);
66 m.insert("f5", Key::KEY_F5);
67 m.insert("f6", Key::KEY_F6);
68 m.insert("f7", Key::KEY_F7);
69 m.insert("f8", Key::KEY_F8);
70 m.insert("f9", Key::KEY_F9);
71 m.insert("f10", Key::KEY_F10);
72 m.insert("f11", Key::KEY_F11);
73 m.insert("f12", Key::KEY_F12);
74
75 m.insert("f13", Key::KEY_F13);
77 m.insert("f14", Key::KEY_F14);
78 m.insert("f15", Key::KEY_F15);
79 m.insert("f16", Key::KEY_F16);
80 m.insert("f17", Key::KEY_F17);
81 m.insert("f18", Key::KEY_F18);
82 m.insert("f19", Key::KEY_F19);
83 m.insert("f20", Key::KEY_F20);
84
85 m.insert("space", Key::KEY_SPACE);
87 m.insert("tab", Key::KEY_TAB);
88 m.insert("escape", Key::KEY_ESC);
89 m.insert("delete", Key::KEY_BACKSPACE);
90 m.insert("forward_delete", Key::KEY_DELETE);
91 m.insert("return_key", Key::KEY_ENTER);
92 m.insert("return", Key::KEY_ENTER);
93 m.insert("enter", Key::KEY_ENTER);
94 m.insert("home", Key::KEY_HOME);
95 m.insert("end", Key::KEY_END);
96 m.insert("page_up", Key::KEY_PAGEUP);
97 m.insert("page_down", Key::KEY_PAGEDOWN);
98 m.insert("up_arrow", Key::KEY_UP);
99 m.insert("down_arrow", Key::KEY_DOWN);
100 m.insert("left_arrow", Key::KEY_LEFT);
101 m.insert("right_arrow", Key::KEY_RIGHT);
102 m.insert("insert", Key::KEY_INSERT);
103 m.insert("print_screen", Key::KEY_SYSRQ);
104 m.insert("scroll_lock", Key::KEY_SCROLLLOCK);
105 m.insert("pause", Key::KEY_PAUSE);
106 m.insert("num_lock", Key::KEY_NUMLOCK);
107
108 m.insert("section", Key::KEY_102ND);
111 m.insert("grave", Key::KEY_GRAVE);
112 m.insert("minus", Key::KEY_MINUS);
113 m.insert("equal", Key::KEY_EQUAL);
114 m.insert("left_bracket", Key::KEY_LEFTBRACE);
115 m.insert("right_bracket", Key::KEY_RIGHTBRACE);
116 m.insert("backslash", Key::KEY_BACKSLASH);
117 m.insert("semicolon", Key::KEY_SEMICOLON);
118 m.insert("quote", Key::KEY_APOSTROPHE);
119 m.insert("comma", Key::KEY_COMMA);
120 m.insert("period", Key::KEY_DOT);
121 m.insert("slash", Key::KEY_SLASH);
122
123 m
124}
125
126static KEY_MAP: OnceLock<HashMap<&'static str, Key>> = OnceLock::new();
127
128pub(crate) fn get_key_map() -> &'static HashMap<&'static str, Key> {
129 KEY_MAP.get_or_init(build_key_map)
130}
131
132fn resolve_key(input: &str) -> Result<Key> {
134 let key_map = get_key_map();
135
136 if let Some(&key) = key_map.get(input) {
137 return Ok(key);
138 }
139
140 if let Some(hex) = input
142 .strip_prefix("0x")
143 .or_else(|| input.strip_prefix("0X"))
144 {
145 if let Ok(n) = u16::from_str_radix(hex, 16) {
146 return Ok(Key::new(n));
147 }
148 }
149
150 if let Ok(n) = input.parse::<u16>() {
152 return Ok(Key::new(n));
153 }
154
155 Err(anyhow::anyhow!(
156 "Unknown key name: '{}'. Use 'opencode-voice keys' to list valid names.",
157 input
158 ))
159}
160
161fn find_devices_with_key(key: Key) -> Vec<Device> {
163 evdev::enumerate()
164 .filter_map(|(_, device)| {
165 let supports_key = device
166 .supported_keys()
167 .map_or(false, |keys| keys.contains(key));
168 if supports_key {
169 Some(device)
170 } else {
171 None
172 }
173 })
174 .collect()
175}
176
177pub struct GlobalHotkey {
179 target_key: Key,
180 sender: tokio::sync::mpsc::UnboundedSender<InputEvent>,
181 cancel: CancellationToken,
182}
183
184impl GlobalHotkey {
185 pub fn new(
191 key_name: &str,
192 sender: tokio::sync::mpsc::UnboundedSender<InputEvent>,
193 cancel: CancellationToken,
194 ) -> Result<Self> {
195 let target_key =
196 resolve_key(key_name).with_context(|| format!("Invalid hotkey: {}", key_name))?;
197
198 let probe = find_devices_with_key(target_key);
200 if probe.is_empty() {
201 return Err(diagnose_no_devices(target_key));
202 }
203
204 Ok(GlobalHotkey {
205 target_key,
206 sender,
207 cancel,
208 })
209 }
210
211 pub fn run(&self) -> Result<()> {
217 let devices = find_devices_with_key(self.target_key);
220
221 for device in devices {
222 let target_key = self.target_key;
223 let sender = self.sender.clone();
224 let cancel = self.cancel.clone();
225
226 std::thread::spawn(move || {
227 listen_on_device(device, target_key, sender, cancel);
228 });
229 }
230
231 Ok(())
232 }
233}
234
235fn diagnose_no_devices(target_key: Key) -> anyhow::Error {
243 let accessible: Vec<_> = evdev::enumerate().collect();
244
245 if accessible.is_empty() {
246 return anyhow::anyhow!(
247 "No input devices accessible. Global hotkey requires read access to /dev/input/.\n \
248 Fix: sudo usermod -a -G input $USER (then log out and back in)"
249 );
250 }
251
252 let keyboards: Vec<_> = accessible
254 .iter()
255 .filter(|(_, d)| {
256 d.supported_keys()
257 .map_or(false, |keys| keys.contains(Key::KEY_SPACE))
258 })
259 .collect();
260
261 let total_device_nodes = std::fs::read_dir("/dev/input")
263 .map(|rd| {
264 rd.filter(|e| {
265 e.as_ref().map_or(false, |e| {
266 e.file_name().to_string_lossy().starts_with("event")
267 })
268 })
269 .count()
270 })
271 .unwrap_or(0);
272
273 if keyboards.is_empty() {
274 anyhow::anyhow!(
276 "Cannot access keyboard input devices ({} of {} /dev/input/event* devices accessible, \
277 but none are keyboards).\n \
278 Fix: sudo usermod -a -G input $USER (then log out and back in)\n \
279 Or run with --no-global for terminal-only input.",
280 accessible.len(),
281 total_device_nodes,
282 )
283 } else {
284 let kb_names: Vec<String> = keyboards
286 .iter()
287 .filter_map(|(_, d)| d.name().map(|s| s.to_string()))
288 .collect();
289 anyhow::anyhow!(
290 "Found {} keyboard(s) ({}) but none report support for key {:?}.\n \
291 Try a different key with --hotkey, or use --no-global for terminal-only input.",
292 keyboards.len(),
293 kb_names.join(", "),
294 target_key,
295 )
296 }
297}
298
299fn listen_on_device(
301 mut device: Device,
302 target_key: Key,
303 sender: tokio::sync::mpsc::UnboundedSender<InputEvent>,
304 cancel: CancellationToken,
305) {
306 let mut pressed = false;
307
308 loop {
309 if cancel.is_cancelled() {
310 break;
311 }
312
313 let events = match device.fetch_events() {
314 Ok(events) => events,
315 Err(_) => break, };
317
318 for ev in events {
319 if cancel.is_cancelled() {
320 return;
321 }
322
323 if let InputEventKind::Key(key) = ev.kind() {
324 if key == target_key {
325 match ev.value() {
326 1 => {
327 if !pressed {
329 pressed = true;
330 let _ = sender.send(InputEvent::KeyDown);
331 }
332 }
333 0 => {
334 pressed = false;
336 let _ = sender.send(InputEvent::KeyUp);
337 let _ = sender.send(InputEvent::Toggle);
338 }
339 _ => {} }
341 }
342 }
343 }
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350
351 #[test]
352 fn test_resolve_key_right_option() {
353 let result = resolve_key("right_option");
354 assert!(result.is_ok());
355 assert_eq!(result.unwrap(), Key::KEY_RIGHTALT);
356 }
357
358 #[test]
359 fn test_resolve_key_alias_right_alt() {
360 let k1 = resolve_key("right_option").unwrap();
361 let k2 = resolve_key("right_alt").unwrap();
362 assert_eq!(k1, k2);
363 }
364
365 #[test]
366 fn test_resolve_key_decimal_number() {
367 let result = resolve_key("57"); assert!(result.is_ok());
369 }
370
371 #[test]
372 fn test_resolve_key_hex_number() {
373 let result = resolve_key("0x39"); assert!(result.is_ok());
375 }
376
377 #[test]
378 fn test_resolve_key_unknown() {
379 let result = resolve_key("not_a_key");
380 assert!(result.is_err());
381 }
382
383 #[test]
384 fn test_resolve_key_space() {
385 assert_eq!(resolve_key("space").unwrap(), Key::KEY_SPACE);
386 }
387
388 #[test]
389 fn test_resolve_key_f1() {
390 assert_eq!(resolve_key("f1").unwrap(), Key::KEY_F1);
391 }
392
393 #[test]
394 fn test_resolve_key_f13() {
395 assert_eq!(resolve_key("f13").unwrap(), Key::KEY_F13);
396 }
397
398 #[test]
399 fn test_resolve_key_left_command() {
400 assert_eq!(resolve_key("left_command").unwrap(), Key::KEY_LEFTMETA);
401 }
402
403 #[test]
404 fn test_resolve_key_caps_lock() {
405 assert_eq!(resolve_key("caps_lock").unwrap(), Key::KEY_CAPSLOCK);
406 }
407
408 #[test]
409 fn test_resolve_key_escape() {
410 assert_eq!(resolve_key("escape").unwrap(), Key::KEY_ESC);
411 }
412
413 #[test]
414 fn test_key_map_has_60_plus_entries() {
415 let map = get_key_map();
416 assert!(
417 map.len() >= 60,
418 "KEY_MAP should have at least 60 entries, has {}",
419 map.len()
420 );
421 }
422
423 #[test]
424 fn test_resolve_key_modifiers() {
425 assert_eq!(resolve_key("left_alt").unwrap(), Key::KEY_LEFTALT);
426 assert_eq!(resolve_key("left_shift").unwrap(), Key::KEY_LEFTSHIFT);
427 assert_eq!(resolve_key("left_ctrl").unwrap(), Key::KEY_LEFTCTRL);
428 assert_eq!(resolve_key("right_shift").unwrap(), Key::KEY_RIGHTSHIFT);
429 assert_eq!(resolve_key("right_ctrl").unwrap(), Key::KEY_RIGHTCTRL);
430 }
431
432 #[test]
433 fn test_resolve_key_numpad() {
434 assert_eq!(resolve_key("numpad_0").unwrap(), Key::KEY_KP0);
435 assert_eq!(resolve_key("numpad_enter").unwrap(), Key::KEY_KPENTER);
436 assert_eq!(resolve_key("numpad_plus").unwrap(), Key::KEY_KPPLUS);
437 assert_eq!(resolve_key("numpad_add").unwrap(), Key::KEY_KPPLUS);
438 }
439}