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> {
13 let mut m = HashMap::new();
14
15 m.insert("numpad_0", Key::Kp0);
17 m.insert("numpad_1", Key::Kp1);
18 m.insert("numpad_2", Key::Kp2);
19 m.insert("numpad_3", Key::Kp3);
20 m.insert("numpad_4", Key::Kp4);
21 m.insert("numpad_5", Key::Kp5);
22 m.insert("numpad_6", Key::Kp6);
23 m.insert("numpad_7", Key::Kp7);
24 m.insert("numpad_8", Key::Kp8);
25 m.insert("numpad_9", Key::Kp9);
26 m.insert("numpad_enter", Key::KpReturn);
27 m.insert("numpad_decimal", Key::Unknown(65));
29 m.insert("numpad_dot", Key::Unknown(65));
30 m.insert("numpad_plus", Key::KpPlus);
32 m.insert("numpad_add", Key::KpPlus);
33 m.insert("numpad_minus", Key::KpMinus);
34 m.insert("numpad_subtract", Key::KpMinus);
35 m.insert("numpad_multiply", Key::KpMultiply);
36 m.insert("numpad_divide", Key::KpDivide);
37 m.insert("numpad_clear", Key::Unknown(71));
39 m.insert("numpad_equals", Key::Unknown(81));
41
42 m.insert("right_option", Key::AltGr);
44 m.insert("right_alt", Key::AltGr); m.insert("left_option", Key::Alt);
46 m.insert("left_alt", Key::Alt); m.insert("right_command", Key::MetaRight);
48 m.insert("right_cmd", Key::MetaRight); m.insert("left_command", Key::MetaLeft);
50 m.insert("left_cmd", Key::MetaLeft); m.insert("right_shift", Key::ShiftRight);
52 m.insert("left_shift", Key::ShiftLeft);
53 m.insert("right_control", Key::ControlRight);
54 m.insert("right_ctrl", Key::ControlRight); m.insert("left_control", Key::ControlLeft);
56 m.insert("left_ctrl", Key::ControlLeft); m.insert("fn_key", Key::Function);
58 m.insert("fn", Key::Function); m.insert("caps_lock", Key::CapsLock);
60
61 m.insert("f1", Key::F1);
63 m.insert("f2", Key::F2);
64 m.insert("f3", Key::F3);
65 m.insert("f4", Key::F4);
66 m.insert("f5", Key::F5);
67 m.insert("f6", Key::F6);
68 m.insert("f7", Key::F7);
69 m.insert("f8", Key::F8);
70 m.insert("f9", Key::F9);
71 m.insert("f10", Key::F10);
72 m.insert("f11", Key::F11);
73 m.insert("f12", Key::F12);
74
75 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)); m.insert("space", Key::Space);
88 m.insert("tab", Key::Tab);
89 m.insert("escape", Key::Escape);
90 m.insert("delete", Key::Backspace);
92 m.insert("forward_delete", Key::Delete);
94 m.insert("return_key", Key::Return);
95 m.insert("return", Key::Return);
96 m.insert("enter", Key::Return);
97 m.insert("home", Key::Home);
98 m.insert("end", Key::End);
99 m.insert("page_up", Key::PageUp);
100 m.insert("page_down", Key::PageDown);
101 m.insert("up_arrow", Key::UpArrow);
102 m.insert("down_arrow", Key::DownArrow);
103 m.insert("left_arrow", Key::LeftArrow);
104 m.insert("right_arrow", Key::RightArrow);
105 m.insert("insert", Key::Insert);
106 m.insert("print_screen", Key::PrintScreen);
107 m.insert("scroll_lock", Key::ScrollLock);
108 m.insert("pause", Key::Pause);
109 m.insert("num_lock", Key::NumLock);
110
111 m.insert("section", Key::Unknown(10));
114 m.insert("grave", Key::BackQuote);
115 m.insert("minus", Key::Minus);
116 m.insert("equal", Key::Equal);
117 m.insert("left_bracket", Key::LeftBracket);
118 m.insert("right_bracket", Key::RightBracket);
119 m.insert("backslash", Key::BackSlash);
120 m.insert("semicolon", Key::SemiColon);
121 m.insert("quote", Key::Quote);
122 m.insert("comma", Key::Comma);
123 m.insert("period", Key::Dot);
124 m.insert("slash", Key::Slash);
125
126 m
127}
128
129static KEY_MAP: OnceLock<HashMap<&'static str, Key>> = OnceLock::new();
130
131fn get_key_map() -> &'static HashMap<&'static str, Key> {
132 KEY_MAP.get_or_init(build_key_map)
133}
134
135pub fn resolve_key(input: &str) -> Result<Key> {
139 let key_map = get_key_map();
140
141 if let Some(&key) = key_map.get(input) {
143 return Ok(key);
144 }
145
146 if let Some(hex) = input
148 .strip_prefix("0x")
149 .or_else(|| input.strip_prefix("0X"))
150 {
151 if let Ok(n) = u32::from_str_radix(hex, 16) {
152 return Ok(Key::Unknown(n));
153 }
154 }
155
156 if let Ok(n) = input.parse::<u32>() {
158 return Ok(Key::Unknown(n));
159 }
160
161 Err(anyhow::anyhow!(
162 "Unknown key name: '{}'. Use 'opencode-voice keys' to list valid names.",
163 input
164 ))
165}
166
167pub fn format_key_name(input: &str) -> String {
169 input
170 .split('_')
171 .map(|word| {
172 let mut chars = word.chars();
173 match chars.next() {
174 None => String::new(),
175 Some(c) => c.to_uppercase().to_string() + chars.as_str(),
176 }
177 })
178 .collect::<Vec<_>>()
179 .join(" ")
180}
181
182pub fn list_key_names() -> Vec<&'static str> {
184 let mut names: Vec<&'static str> = get_key_map().keys().copied().collect();
185 names.sort();
186 names
187}
188
189pub struct GlobalHotkey {
191 target_key: Key,
192 sender: tokio::sync::mpsc::UnboundedSender<InputEvent>,
193 cancel: CancellationToken,
194}
195
196impl GlobalHotkey {
197 pub fn new(
198 key_name: &str,
199 sender: tokio::sync::mpsc::UnboundedSender<InputEvent>,
200 cancel: CancellationToken,
201 ) -> Result<Self> {
202 let target_key =
203 resolve_key(key_name).with_context(|| format!("Invalid hotkey: {}", key_name))?;
204 Ok(GlobalHotkey {
205 target_key,
206 sender,
207 cancel,
208 })
209 }
210
211 pub fn run(&self) -> Result<()> {
215 let target_key = self.target_key;
216 let sender = self.sender.clone();
217 let cancel = self.cancel.clone();
218 let pressed = Arc::new(Mutex::new(false));
219
220 let (result_tx, result_rx) = std::sync::mpsc::channel::<Result<()>>();
221 let result_tx_clone = result_tx.clone();
222
223 std::thread::spawn(move || {
224 let result = rdev::listen(move |event: Event| {
225 if cancel.is_cancelled() {
226 return;
227 }
228
229 match &event.event_type {
230 EventType::KeyPress(key) => {
231 if *key == target_key {
232 let mut p = pressed.lock().unwrap();
233 if !*p {
234 *p = true;
235 let _ = sender.send(InputEvent::KeyDown);
236 }
237 }
238 }
239 EventType::KeyRelease(key) => {
240 if *key == target_key {
241 let mut p = pressed.lock().unwrap();
242 *p = false;
243 let _ = sender.send(InputEvent::KeyUp);
245 let _ = sender.send(InputEvent::Toggle);
246 }
247 }
248 _ => {}
249 }
250 });
251
252 match result {
253 Ok(_) => {}
254 Err(e) => {
255 let msg = format_rdev_error(&e);
256 let _ = result_tx_clone.send(Err(anyhow::anyhow!("{}", msg)));
257 }
258 }
259 });
260
261 std::thread::sleep(std::time::Duration::from_millis(100));
263 if let Ok(Err(e)) = result_rx.try_recv() {
264 return Err(e);
265 }
266
267 Ok(())
268 }
269}
270
271fn format_rdev_error(error: &rdev::ListenError) -> String {
272 let msg = format!("{:?}", error);
273 if msg.contains("FailedToOpenX11")
274 || msg.contains("AccessDenied")
275 || msg.contains("PermissionDenied")
276 || msg.contains("EventTapError")
277 {
278 #[cfg(target_os = "macos")]
279 return "Accessibility permission required for global hotkey.\n \
280 Go to: System Settings → Privacy & Security → Accessibility\n \
281 Enable your terminal app (Terminal, iTerm2, etc.)"
282 .to_string();
283 #[cfg(not(target_os = "macos"))]
284 return "Input monitoring permission required.\n \
285 Add your user to the 'input' group: sudo usermod -a -G input $USER\n \
286 Or run with appropriate permissions."
287 .to_string();
288 }
289 format!("Global hotkey error: {}", msg)
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn test_resolve_key_right_option() {
298 let result = resolve_key("right_option");
299 assert!(result.is_ok());
300 assert_eq!(result.unwrap(), Key::AltGr);
301 }
302
303 #[test]
304 fn test_resolve_key_alias_right_alt() {
305 let k1 = resolve_key("right_option").unwrap();
307 let k2 = resolve_key("right_alt").unwrap();
308 assert_eq!(format!("{:?}", k1), format!("{:?}", k2));
309 }
310
311 #[test]
312 fn test_resolve_key_decimal_number() {
313 let result = resolve_key("65");
314 assert!(result.is_ok());
315 assert!(matches!(result.unwrap(), Key::Unknown(65)));
316 }
317
318 #[test]
319 fn test_resolve_key_hex_number() {
320 let result = resolve_key("0x41");
321 assert!(result.is_ok());
322 assert!(matches!(result.unwrap(), Key::Unknown(65)));
323 }
324
325 #[test]
326 fn test_resolve_key_unknown() {
327 let result = resolve_key("not_a_key");
328 assert!(result.is_err());
329 }
330
331 #[test]
332 fn test_resolve_key_space() {
333 let result = resolve_key("space");
334 assert!(result.is_ok());
335 assert_eq!(result.unwrap(), Key::Space);
336 }
337
338 #[test]
339 fn test_resolve_key_f1() {
340 let result = resolve_key("f1");
341 assert!(result.is_ok());
342 assert_eq!(result.unwrap(), Key::F1);
343 }
344
345 #[test]
346 fn test_resolve_key_f13() {
347 let result = resolve_key("f13");
349 assert!(result.is_ok());
350 assert!(matches!(result.unwrap(), Key::Unknown(105)));
351 }
352
353 #[test]
354 fn test_resolve_key_numpad_0() {
355 let result = resolve_key("numpad_0");
356 assert!(result.is_ok());
357 assert_eq!(result.unwrap(), Key::Kp0);
358 }
359
360 #[test]
361 fn test_resolve_key_numpad_enter() {
362 let result = resolve_key("numpad_enter");
363 assert!(result.is_ok());
364 assert_eq!(result.unwrap(), Key::KpReturn);
365 }
366
367 #[test]
368 fn test_resolve_key_hex_uppercase() {
369 let result = resolve_key("0X41");
370 assert!(result.is_ok());
371 assert!(matches!(result.unwrap(), Key::Unknown(65)));
372 }
373
374 #[test]
375 fn test_format_key_name_right_option() {
376 assert_eq!(format_key_name("right_option"), "Right Option");
377 }
378
379 #[test]
380 fn test_format_key_name_f13() {
381 assert_eq!(format_key_name("f13"), "F13");
382 }
383
384 #[test]
385 fn test_format_key_name_numpad_enter() {
386 assert_eq!(format_key_name("numpad_enter"), "Numpad Enter");
387 }
388
389 #[test]
390 fn test_format_key_name_space() {
391 assert_eq!(format_key_name("space"), "Space");
392 }
393
394 #[test]
395 fn test_list_key_names_sorted() {
396 let names = list_key_names();
397 assert!(!names.is_empty());
398 assert!(names.windows(2).all(|w| w[0] <= w[1]));
399 assert!(names.contains(&"right_option"));
400 assert!(names.contains(&"space"));
401 assert!(names.contains(&"f1"));
402 }
403
404 #[test]
405 fn test_key_map_has_60_plus_entries() {
406 let map = get_key_map();
407 assert!(
408 map.len() >= 60,
409 "KEY_MAP should have at least 60 entries, has {}",
410 map.len()
411 );
412 }
413
414 #[test]
415 fn test_resolve_key_left_command() {
416 let result = resolve_key("left_command");
417 assert!(result.is_ok());
418 assert_eq!(result.unwrap(), Key::MetaLeft);
419 }
420
421 #[test]
422 fn test_resolve_key_caps_lock() {
423 let result = resolve_key("caps_lock");
424 assert!(result.is_ok());
425 assert_eq!(result.unwrap(), Key::CapsLock);
426 }
427
428 #[test]
429 fn test_resolve_key_escape() {
430 let result = resolve_key("escape");
431 assert!(result.is_ok());
432 assert_eq!(result.unwrap(), Key::Escape);
433 }
434}