Skip to main content

opencode_voice/input/
hotkey.rs

1//! Global hotkey support — shared key-name utilities and platform-specific backends.
2//!
3//! Key name lists and formatting live here. The actual hotkey listener
4//! (`GlobalHotkey`) is platform-specific:
5//! - macOS: `hotkey_macos.rs` (rdev / CGEventTap)
6//! - Linux: `hotkey_linux.rs` (evdev / /dev/input)
7
8#[cfg(target_os = "macos")]
9#[path = "hotkey_macos.rs"]
10mod platform;
11
12#[cfg(target_os = "linux")]
13#[path = "hotkey_linux.rs"]
14mod platform;
15
16// Re-export the platform-specific GlobalHotkey so callers use
17// `crate::input::hotkey::GlobalHotkey` regardless of platform.
18pub use platform::GlobalHotkey;
19
20/// Formats a key name for display (e.g., "right_option" → "Right Option").
21pub fn format_key_name(input: &str) -> String {
22    input
23        .split('_')
24        .map(|word| {
25            let mut chars = word.chars();
26            match chars.next() {
27                None => String::new(),
28                Some(c) => c.to_uppercase().to_string() + chars.as_str(),
29            }
30        })
31        .collect::<Vec<_>>()
32        .join(" ")
33}
34
35/// Returns a sorted list of all supported key names.
36///
37/// Delegates to the platform key map so the list reflects exactly which
38/// keys are available on this platform.
39pub fn list_key_names() -> Vec<&'static str> {
40    let mut names: Vec<&'static str> = platform::get_key_map().keys().copied().collect();
41    names.sort();
42    names
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    #[test]
50    fn test_format_key_name_right_option() {
51        assert_eq!(format_key_name("right_option"), "Right Option");
52    }
53
54    #[test]
55    fn test_format_key_name_f13() {
56        assert_eq!(format_key_name("f13"), "F13");
57    }
58
59    #[test]
60    fn test_format_key_name_numpad_enter() {
61        assert_eq!(format_key_name("numpad_enter"), "Numpad Enter");
62    }
63
64    #[test]
65    fn test_format_key_name_space() {
66        assert_eq!(format_key_name("space"), "Space");
67    }
68
69    #[test]
70    fn test_list_key_names_sorted() {
71        let names = list_key_names();
72        assert!(!names.is_empty());
73        assert!(names.windows(2).all(|w| w[0] <= w[1]));
74        assert!(names.contains(&"right_option"));
75        assert!(names.contains(&"space"));
76        assert!(names.contains(&"f1"));
77    }
78
79    #[test]
80    fn test_key_map_has_60_plus_entries() {
81        let names = list_key_names();
82        assert!(
83            names.len() >= 60,
84            "KEY_MAP should have at least 60 entries, has {}",
85            names.len()
86        );
87    }
88
89    #[test]
90    fn test_list_key_names_includes_section() {
91        let names = list_key_names();
92        assert!(names.contains(&"section"));
93    }
94
95    #[test]
96    fn test_list_key_names_includes_numpad_clear() {
97        let names = list_key_names();
98        assert!(names.contains(&"numpad_clear"));
99    }
100}