Skip to main content

openlogi_core/config/
function_key.rs

1//! The programmable top row: Esc, then F1–F19.
2//!
3//! These are the only keys a [`KeyTrigger`](super::KeyTrigger) can name, and
4//! every process that touches one speaks the same three vocabularies for
5//! them: the macOS virtual keycode the OS hook reports on every platform, the
6//! lowercase name a trigger is written with in `config.toml`, and the legend
7//! printed on the key. The table lives here once — a second copy that drifts
8//! makes every saved binding miss.
9
10/// A key of the programmable top row.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
12pub enum FunctionKey {
13    /// Escape.
14    Esc,
15    /// F1.
16    F1,
17    /// F2.
18    F2,
19    /// F3.
20    F3,
21    /// F4.
22    F4,
23    /// F5.
24    F5,
25    /// F6.
26    F6,
27    /// F7.
28    F7,
29    /// F8.
30    F8,
31    /// F9.
32    F9,
33    /// F10.
34    F10,
35    /// F11.
36    F11,
37    /// F12.
38    F12,
39    /// F13.
40    F13,
41    /// F14.
42    F14,
43    /// F15.
44    F15,
45    /// F16.
46    F16,
47    /// F17.
48    F17,
49    /// F18.
50    F18,
51    /// F19.
52    F19,
53}
54
55impl FunctionKey {
56    /// Every key, in the order the row is laid out.
57    pub const ALL: [Self; 20] = [
58        Self::Esc,
59        Self::F1,
60        Self::F2,
61        Self::F3,
62        Self::F4,
63        Self::F5,
64        Self::F6,
65        Self::F7,
66        Self::F8,
67        Self::F9,
68        Self::F10,
69        Self::F11,
70        Self::F12,
71        Self::F13,
72        Self::F14,
73        Self::F15,
74        Self::F16,
75        Self::F17,
76        Self::F18,
77        Self::F19,
78    ];
79
80    /// The macOS virtual keycode (`kVK_*`). Key events and triggers carry this
81    /// keycode on every platform, so a config written on one host binds the
82    /// same key on another.
83    #[must_use]
84    pub const fn keycode(self) -> u16 {
85        match self {
86            Self::Esc => 0x35,
87            Self::F1 => 0x7A,
88            Self::F2 => 0x78,
89            Self::F3 => 0x63,
90            Self::F4 => 0x76,
91            Self::F5 => 0x60,
92            Self::F6 => 0x61,
93            Self::F7 => 0x62,
94            Self::F8 => 0x64,
95            Self::F9 => 0x65,
96            Self::F10 => 0x6D,
97            Self::F11 => 0x67,
98            Self::F12 => 0x6F,
99            Self::F13 => 0x69,
100            Self::F14 => 0x6B,
101            Self::F15 => 0x71,
102            Self::F16 => 0x6A,
103            Self::F17 => 0x40,
104            Self::F18 => 0x4F,
105            Self::F19 => 0x50,
106        }
107    }
108
109    /// The key a macOS virtual keycode names, if it is on this row.
110    #[must_use]
111    pub fn from_keycode(keycode: u16) -> Option<Self> {
112        Self::ALL.into_iter().find(|key| key.keycode() == keycode)
113    }
114
115    /// The name a trigger is written with: `esc`, `f1` … `f19`.
116    #[must_use]
117    pub const fn name(self) -> &'static str {
118        match self {
119            Self::Esc => "esc",
120            Self::F1 => "f1",
121            Self::F2 => "f2",
122            Self::F3 => "f3",
123            Self::F4 => "f4",
124            Self::F5 => "f5",
125            Self::F6 => "f6",
126            Self::F7 => "f7",
127            Self::F8 => "f8",
128            Self::F9 => "f9",
129            Self::F10 => "f10",
130            Self::F11 => "f11",
131            Self::F12 => "f12",
132            Self::F13 => "f13",
133            Self::F14 => "f14",
134            Self::F15 => "f15",
135            Self::F16 => "f16",
136            Self::F17 => "f17",
137            Self::F18 => "f18",
138            Self::F19 => "f19",
139        }
140    }
141
142    /// The key a trigger name spells, case-insensitively.
143    #[must_use]
144    pub fn from_name(name: &str) -> Option<Self> {
145        Self::ALL
146            .into_iter()
147            .find(|key| key.name().eq_ignore_ascii_case(name))
148    }
149
150    /// The legend printed on the key: `Esc`, `F1` … `F19`.
151    #[must_use]
152    pub const fn label(self) -> &'static str {
153        match self {
154            Self::Esc => "Esc",
155            Self::F1 => "F1",
156            Self::F2 => "F2",
157            Self::F3 => "F3",
158            Self::F4 => "F4",
159            Self::F5 => "F5",
160            Self::F6 => "F6",
161            Self::F7 => "F7",
162            Self::F8 => "F8",
163            Self::F9 => "F9",
164            Self::F10 => "F10",
165            Self::F11 => "F11",
166            Self::F12 => "F12",
167            Self::F13 => "F13",
168            Self::F14 => "F14",
169            Self::F15 => "F15",
170            Self::F16 => "F16",
171            Self::F17 => "F17",
172            Self::F18 => "F18",
173            Self::F19 => "F19",
174        }
175    }
176
177    /// The `n`th F key, counting from 1 — how a platform whose own keycodes
178    /// number the F keys consecutively finds the key it saw. `None` past F19.
179    #[must_use]
180    pub fn nth_f(n: u16) -> Option<Self> {
181        match n {
182            0 => None,
183            n => Self::ALL.get(usize::from(n)).copied(),
184        }
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use std::collections::BTreeSet;
191
192    use super::FunctionKey;
193
194    #[test]
195    fn every_key_round_trips_through_each_vocabulary() {
196        for key in FunctionKey::ALL {
197            assert_eq!(FunctionKey::from_keycode(key.keycode()), Some(key));
198            assert_eq!(FunctionKey::from_name(key.name()), Some(key));
199        }
200        // A keycode or a name entered twice would shadow a key silently.
201        let keycodes: BTreeSet<u16> = FunctionKey::ALL.iter().map(|key| key.keycode()).collect();
202        let names: BTreeSet<&str> = FunctionKey::ALL.iter().map(|key| key.name()).collect();
203        assert_eq!(keycodes.len(), FunctionKey::ALL.len());
204        assert_eq!(names.len(), FunctionKey::ALL.len());
205    }
206
207    #[test]
208    fn the_nth_f_key_counts_from_one_and_stops_at_f19() {
209        assert_eq!(FunctionKey::nth_f(0), None, "Esc is not an F key");
210        assert_eq!(FunctionKey::nth_f(1), Some(FunctionKey::F1));
211        assert_eq!(FunctionKey::nth_f(19), Some(FunctionKey::F19));
212        assert_eq!(FunctionKey::nth_f(20), None);
213    }
214
215    #[test]
216    fn names_parse_case_insensitively() {
217        assert_eq!(FunctionKey::from_name("F12"), Some(FunctionKey::F12));
218        assert_eq!(FunctionKey::from_name("Esc"), Some(FunctionKey::Esc));
219        assert_eq!(FunctionKey::from_name("f20"), None);
220    }
221}