1use anyhow::{Context, Result};
4use rdev::{Event, EventType, Key};
5use std::collections::HashMap;
6use std::sync::{Arc, Mutex, OnceLock};
7use tokio_util::sync::CancellationToken;
8
9use crate::state::InputEvent;
10
11fn build_key_map() -> HashMap<&'static str, Key> {
17 let mut m = HashMap::new();
18
19 #[cfg(target_os = "macos")]
25 {
26 m.insert("numpad_0", Key::Unknown(82)); m.insert("numpad_1", Key::Unknown(83)); m.insert("numpad_2", Key::Unknown(84)); m.insert("numpad_3", Key::Unknown(85)); m.insert("numpad_4", Key::Unknown(86)); m.insert("numpad_5", Key::Unknown(87)); m.insert("numpad_6", Key::Unknown(88)); m.insert("numpad_7", Key::Unknown(89)); m.insert("numpad_8", Key::Unknown(91)); m.insert("numpad_9", Key::Unknown(92)); m.insert("numpad_enter", Key::Unknown(76)); m.insert("numpad_decimal", Key::Unknown(65)); m.insert("numpad_dot", Key::Unknown(65)); m.insert("numpad_plus", Key::Unknown(69)); m.insert("numpad_add", Key::Unknown(69)); m.insert("numpad_minus", Key::Unknown(78)); m.insert("numpad_subtract", Key::Unknown(78)); m.insert("numpad_multiply", Key::Unknown(67)); m.insert("numpad_divide", Key::Unknown(75)); m.insert("numpad_clear", Key::Unknown(71)); m.insert("numpad_equals", Key::Unknown(81)); }
48 #[cfg(target_os = "linux")]
49 {
50 m.insert("numpad_0", Key::Kp0);
51 m.insert("numpad_1", Key::Kp1);
52 m.insert("numpad_2", Key::Kp2);
53 m.insert("numpad_3", Key::Kp3);
54 m.insert("numpad_4", Key::Kp4);
55 m.insert("numpad_5", Key::Kp5);
56 m.insert("numpad_6", Key::Kp6);
57 m.insert("numpad_7", Key::Kp7);
58 m.insert("numpad_8", Key::Kp8);
59 m.insert("numpad_9", Key::Kp9);
60 m.insert("numpad_enter", Key::KpReturn);
61 m.insert("numpad_decimal", Key::KpDelete); m.insert("numpad_dot", Key::KpDelete);
63 m.insert("numpad_plus", Key::KpPlus);
64 m.insert("numpad_add", Key::KpPlus);
65 m.insert("numpad_minus", Key::KpMinus);
66 m.insert("numpad_subtract", Key::KpMinus);
67 m.insert("numpad_multiply", Key::KpMultiply);
68 m.insert("numpad_divide", Key::KpDivide);
69 m.insert("numpad_clear", Key::NumLock); m.insert("numpad_equals", Key::Unknown(125)); }
72
73 m.insert("right_option", Key::AltGr);
75 m.insert("right_alt", Key::AltGr); m.insert("left_option", Key::Alt);
77 m.insert("left_alt", Key::Alt); m.insert("right_command", Key::MetaRight);
79 m.insert("right_cmd", Key::MetaRight); m.insert("left_command", Key::MetaLeft);
81 m.insert("left_cmd", Key::MetaLeft); m.insert("right_shift", Key::ShiftRight);
83 m.insert("left_shift", Key::ShiftLeft);
84 m.insert("right_control", Key::ControlRight);
85 m.insert("right_ctrl", Key::ControlRight); m.insert("left_control", Key::ControlLeft);
87 m.insert("left_ctrl", Key::ControlLeft); m.insert("fn_key", Key::Function);
89 m.insert("fn", Key::Function); m.insert("caps_lock", Key::CapsLock);
91
92 m.insert("f1", Key::F1);
94 m.insert("f2", Key::F2);
95 m.insert("f3", Key::F3);
96 m.insert("f4", Key::F4);
97 m.insert("f5", Key::F5);
98 m.insert("f6", Key::F6);
99 m.insert("f7", Key::F7);
100 m.insert("f8", Key::F8);
101 m.insert("f9", Key::F9);
102 m.insert("f10", Key::F10);
103 m.insert("f11", Key::F11);
104 m.insert("f12", Key::F12);
105
106 #[cfg(target_os = "macos")]
111 {
112 m.insert("f13", Key::Unknown(105)); m.insert("f14", Key::Unknown(107)); m.insert("f15", Key::Unknown(113)); m.insert("f16", Key::Unknown(106)); m.insert("f17", Key::Unknown(64)); m.insert("f18", Key::Unknown(79)); m.insert("f19", Key::Unknown(80)); m.insert("f20", Key::Unknown(90)); }
121 #[cfg(target_os = "linux")]
122 {
123 m.insert("f13", Key::Unknown(191)); m.insert("f14", Key::Unknown(192)); m.insert("f15", Key::Unknown(193)); m.insert("f16", Key::Unknown(194)); m.insert("f17", Key::Unknown(195)); m.insert("f18", Key::Unknown(196)); m.insert("f19", Key::Unknown(197)); m.insert("f20", Key::Unknown(198)); }
132
133 m.insert("space", Key::Space);
135 m.insert("tab", Key::Tab);
136 m.insert("escape", Key::Escape);
137 m.insert("delete", Key::Backspace);
139 m.insert("forward_delete", Key::Delete);
141 m.insert("return_key", Key::Return);
142 m.insert("return", Key::Return);
143 m.insert("enter", Key::Return);
144 m.insert("home", Key::Home);
145 m.insert("end", Key::End);
146 m.insert("page_up", Key::PageUp);
147 m.insert("page_down", Key::PageDown);
148 m.insert("up_arrow", Key::UpArrow);
149 m.insert("down_arrow", Key::DownArrow);
150 m.insert("left_arrow", Key::LeftArrow);
151 m.insert("right_arrow", Key::RightArrow);
152 m.insert("insert", Key::Insert);
153 m.insert("print_screen", Key::PrintScreen);
154 m.insert("scroll_lock", Key::ScrollLock);
155 m.insert("pause", Key::Pause);
156 m.insert("num_lock", Key::NumLock);
157
158 #[cfg(target_os = "macos")]
162 m.insert("section", Key::Unknown(10));
163 #[cfg(target_os = "linux")]
164 m.insert("section", Key::IntlBackslash);
165 m.insert("grave", Key::BackQuote);
166 m.insert("minus", Key::Minus);
167 m.insert("equal", Key::Equal);
168 m.insert("left_bracket", Key::LeftBracket);
169 m.insert("right_bracket", Key::RightBracket);
170 m.insert("backslash", Key::BackSlash);
171 m.insert("semicolon", Key::SemiColon);
172 m.insert("quote", Key::Quote);
173 m.insert("comma", Key::Comma);
174 m.insert("period", Key::Dot);
175 m.insert("slash", Key::Slash);
176
177 m
178}
179
180static KEY_MAP: OnceLock<HashMap<&'static str, Key>> = OnceLock::new();
181
182fn get_key_map() -> &'static HashMap<&'static str, Key> {
183 KEY_MAP.get_or_init(build_key_map)
184}
185
186pub fn resolve_key(input: &str) -> Result<Key> {
190 let key_map = get_key_map();
191
192 if let Some(&key) = key_map.get(input) {
194 return Ok(key);
195 }
196
197 if let Some(hex) = input
199 .strip_prefix("0x")
200 .or_else(|| input.strip_prefix("0X"))
201 {
202 if let Ok(n) = u32::from_str_radix(hex, 16) {
203 return Ok(Key::Unknown(n));
204 }
205 }
206
207 if let Ok(n) = input.parse::<u32>() {
209 return Ok(Key::Unknown(n));
210 }
211
212 Err(anyhow::anyhow!(
213 "Unknown key name: '{}'. Use 'opencode-voice keys' to list valid names.",
214 input
215 ))
216}
217
218pub fn format_key_name(input: &str) -> String {
220 input
221 .split('_')
222 .map(|word| {
223 let mut chars = word.chars();
224 match chars.next() {
225 None => String::new(),
226 Some(c) => c.to_uppercase().to_string() + chars.as_str(),
227 }
228 })
229 .collect::<Vec<_>>()
230 .join(" ")
231}
232
233pub fn list_key_names() -> Vec<&'static str> {
235 let mut names: Vec<&'static str> = get_key_map().keys().copied().collect();
236 names.sort();
237 names
238}
239
240pub struct GlobalHotkey {
242 target_key: Key,
243 sender: tokio::sync::mpsc::UnboundedSender<InputEvent>,
244 cancel: CancellationToken,
245}
246
247impl GlobalHotkey {
248 pub fn new(
249 key_name: &str,
250 sender: tokio::sync::mpsc::UnboundedSender<InputEvent>,
251 cancel: CancellationToken,
252 ) -> Result<Self> {
253 let target_key =
254 resolve_key(key_name).with_context(|| format!("Invalid hotkey: {}", key_name))?;
255 Ok(GlobalHotkey {
256 target_key,
257 sender,
258 cancel,
259 })
260 }
261
262 pub fn run(&self) -> Result<()> {
266 let target_key = self.target_key;
267 let sender = self.sender.clone();
268 let cancel = self.cancel.clone();
269 let pressed = Arc::new(Mutex::new(false));
270
271 let (result_tx, result_rx) = std::sync::mpsc::channel::<Result<()>>();
272 let result_tx_clone = result_tx.clone();
273
274 std::thread::spawn(move || {
275 let result = rdev::listen(move |event: Event| {
276 if cancel.is_cancelled() {
277 return;
278 }
279
280 match &event.event_type {
281 EventType::KeyPress(key) => {
282 if *key == target_key {
283 let mut p = pressed.lock().unwrap();
284 if !*p {
285 *p = true;
286 let _ = sender.send(InputEvent::KeyDown);
287 }
288 }
289 }
290 EventType::KeyRelease(key) => {
291 if *key == target_key {
292 let mut p = pressed.lock().unwrap();
293 *p = false;
294 let _ = sender.send(InputEvent::KeyUp);
296 let _ = sender.send(InputEvent::Toggle);
297 }
298 }
299 _ => {}
300 }
301 });
302
303 match result {
304 Ok(_) => {}
305 Err(e) => {
306 let msg = format_rdev_error(&e);
307 let _ = result_tx_clone.send(Err(anyhow::anyhow!("{}", msg)));
308 }
309 }
310 });
311
312 std::thread::sleep(std::time::Duration::from_millis(100));
314 if let Ok(Err(e)) = result_rx.try_recv() {
315 return Err(e);
316 }
317
318 Ok(())
319 }
320}
321
322fn format_rdev_error(error: &rdev::ListenError) -> String {
323 let msg = format!("{:?}", error);
324 if msg.contains("FailedToOpenX11")
325 || msg.contains("AccessDenied")
326 || msg.contains("PermissionDenied")
327 || msg.contains("EventTapError")
328 {
329 #[cfg(target_os = "macos")]
330 return "Accessibility permission required for global hotkey.\n \
331 Go to: System Settings → Privacy & Security → Accessibility\n \
332 Enable your terminal app (Terminal, iTerm2, etc.)"
333 .to_string();
334 #[cfg(not(target_os = "macos"))]
335 return "Input monitoring permission required.\n \
336 Add your user to the 'input' group: sudo usermod -a -G input $USER\n \
337 Or run with appropriate permissions."
338 .to_string();
339 }
340 format!("Global hotkey error: {}", msg)
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 #[test]
348 fn test_resolve_key_right_option() {
349 let result = resolve_key("right_option");
350 assert!(result.is_ok());
351 assert_eq!(result.unwrap(), Key::AltGr);
352 }
353
354 #[test]
355 fn test_resolve_key_alias_right_alt() {
356 let k1 = resolve_key("right_option").unwrap();
358 let k2 = resolve_key("right_alt").unwrap();
359 assert_eq!(format!("{:?}", k1), format!("{:?}", k2));
360 }
361
362 #[test]
363 fn test_resolve_key_decimal_number() {
364 let result = resolve_key("65");
365 assert!(result.is_ok());
366 assert!(matches!(result.unwrap(), Key::Unknown(65)));
367 }
368
369 #[test]
370 fn test_resolve_key_hex_number() {
371 let result = resolve_key("0x41");
372 assert!(result.is_ok());
373 assert!(matches!(result.unwrap(), Key::Unknown(65)));
374 }
375
376 #[test]
377 fn test_resolve_key_unknown() {
378 let result = resolve_key("not_a_key");
379 assert!(result.is_err());
380 }
381
382 #[test]
383 fn test_resolve_key_space() {
384 let result = resolve_key("space");
385 assert!(result.is_ok());
386 assert_eq!(result.unwrap(), Key::Space);
387 }
388
389 #[test]
390 fn test_resolve_key_f1() {
391 let result = resolve_key("f1");
392 assert!(result.is_ok());
393 assert_eq!(result.unwrap(), Key::F1);
394 }
395
396 #[test]
397 fn test_resolve_key_f13() {
398 let result = resolve_key("f13");
399 assert!(result.is_ok());
400 #[cfg(target_os = "macos")]
401 assert!(matches!(result.unwrap(), Key::Unknown(105))); #[cfg(target_os = "linux")]
403 assert!(matches!(result.unwrap(), Key::Unknown(191))); }
405
406 #[test]
407 fn test_resolve_key_numpad_0() {
408 let result = resolve_key("numpad_0");
409 assert!(result.is_ok());
410 #[cfg(target_os = "macos")]
411 assert!(matches!(result.unwrap(), Key::Unknown(82))); #[cfg(target_os = "linux")]
413 assert_eq!(result.unwrap(), Key::Kp0);
414 }
415
416 #[test]
417 fn test_resolve_key_numpad_enter() {
418 let result = resolve_key("numpad_enter");
419 assert!(result.is_ok());
420 #[cfg(target_os = "macos")]
421 assert!(matches!(result.unwrap(), Key::Unknown(76))); #[cfg(target_os = "linux")]
423 assert_eq!(result.unwrap(), Key::KpReturn);
424 }
425
426 #[test]
427 fn test_resolve_key_hex_uppercase() {
428 let result = resolve_key("0X41");
429 assert!(result.is_ok());
430 assert!(matches!(result.unwrap(), Key::Unknown(65)));
431 }
432
433 #[test]
434 fn test_format_key_name_right_option() {
435 assert_eq!(format_key_name("right_option"), "Right Option");
436 }
437
438 #[test]
439 fn test_format_key_name_f13() {
440 assert_eq!(format_key_name("f13"), "F13");
441 }
442
443 #[test]
444 fn test_format_key_name_numpad_enter() {
445 assert_eq!(format_key_name("numpad_enter"), "Numpad Enter");
446 }
447
448 #[test]
449 fn test_format_key_name_space() {
450 assert_eq!(format_key_name("space"), "Space");
451 }
452
453 #[test]
454 fn test_list_key_names_sorted() {
455 let names = list_key_names();
456 assert!(!names.is_empty());
457 assert!(names.windows(2).all(|w| w[0] <= w[1]));
458 assert!(names.contains(&"right_option"));
459 assert!(names.contains(&"space"));
460 assert!(names.contains(&"f1"));
461 }
462
463 #[test]
464 fn test_key_map_has_60_plus_entries() {
465 let map = get_key_map();
466 assert!(
467 map.len() >= 60,
468 "KEY_MAP should have at least 60 entries, has {}",
469 map.len()
470 );
471 }
472
473 #[test]
474 fn test_resolve_key_left_command() {
475 let result = resolve_key("left_command");
476 assert!(result.is_ok());
477 assert_eq!(result.unwrap(), Key::MetaLeft);
478 }
479
480 #[test]
481 fn test_resolve_key_caps_lock() {
482 let result = resolve_key("caps_lock");
483 assert!(result.is_ok());
484 assert_eq!(result.unwrap(), Key::CapsLock);
485 }
486
487 #[test]
488 fn test_resolve_key_escape() {
489 let result = resolve_key("escape");
490 assert!(result.is_ok());
491 assert_eq!(result.unwrap(), Key::Escape);
492 }
493}