keyboard.rs

  1use anyhow::Result;
  2use collections::HashMap;
  3use windows::Win32::UI::{
  4    Input::KeyboardAndMouse::{
  5        GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode,
  6        VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1,
  7        VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7,
  8        VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
  9    },
 10    WindowsAndMessaging::KL_NAMELENGTH,
 11};
 12use windows_core::HSTRING;
 13
 14use crate::{
 15    KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper,
 16};
 17
 18pub(crate) struct WindowsKeyboardLayout {
 19    id: String,
 20    name: String,
 21}
 22
 23pub(crate) struct WindowsKeyboardMapper {
 24    key_to_vkey: HashMap<String, (u16, bool)>,
 25    vkey_to_key: HashMap<u16, String>,
 26    vkey_to_shifted: HashMap<u16, String>,
 27}
 28
 29impl PlatformKeyboardLayout for WindowsKeyboardLayout {
 30    fn id(&self) -> &str {
 31        &self.id
 32    }
 33
 34    fn name(&self) -> &str {
 35        &self.name
 36    }
 37}
 38
 39impl PlatformKeyboardMapper for WindowsKeyboardMapper {
 40    fn map_key_equivalent(
 41        &self,
 42        mut keystroke: Keystroke,
 43        use_key_equivalents: bool,
 44    ) -> KeybindingKeystroke {
 45        let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents)
 46        else {
 47            return KeybindingKeystroke::from_keystroke(keystroke);
 48        };
 49        if shifted_key && keystroke.modifiers.shift {
 50            log::warn!(
 51                "Keystroke '{}' has both shift and a shifted key, this is likely a bug",
 52                keystroke.key
 53            );
 54        }
 55
 56        let shift = shifted_key || keystroke.modifiers.shift;
 57        keystroke.modifiers.shift = false;
 58
 59        let Some(key) = self.vkey_to_key.get(&vkey).cloned() else {
 60            log::error!(
 61                "Failed to map key equivalent '{:?}' to a valid key",
 62                keystroke
 63            );
 64            return KeybindingKeystroke::from_keystroke(keystroke);
 65        };
 66
 67        keystroke.key = if shift {
 68            let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else {
 69                log::error!(
 70                    "Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key",
 71                    keystroke,
 72                    vkey
 73                );
 74                return KeybindingKeystroke::from_keystroke(keystroke);
 75            };
 76            shifted_key
 77        } else {
 78            key.clone()
 79        };
 80
 81        let modifiers = Modifiers {
 82            shift,
 83            ..keystroke.modifiers
 84        };
 85
 86        KeybindingKeystroke::new(keystroke, modifiers, key)
 87    }
 88
 89    fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
 90        None
 91    }
 92}
 93
 94impl WindowsKeyboardLayout {
 95    pub(crate) fn new() -> Result<Self> {
 96        let mut buffer = [0u16; KL_NAMELENGTH as usize];
 97        unsafe { GetKeyboardLayoutNameW(&mut buffer)? };
 98        let id = HSTRING::from_wide(&buffer).to_string();
 99        let entry = windows_registry::LOCAL_MACHINE.open(format!(
100            "System\\CurrentControlSet\\Control\\Keyboard Layouts\\{}",
101            id
102        ))?;
103        let name = entry.get_hstring("Layout Text")?.to_string();
104        Ok(Self { id, name })
105    }
106
107    pub(crate) fn unknown() -> Self {
108        Self {
109            id: "unknown".to_string(),
110            name: "unknown".to_string(),
111        }
112    }
113
114    pub(crate) fn uses_altgr(&self) -> bool {
115        // Check if this is a known AltGr layout by examining the layout ID
116        // The layout ID is a hex string like "00000409" (US) or "00000407" (German)
117        // Extract the language ID (last 4 bytes)
118        let id_bytes = self.id.as_bytes();
119        if id_bytes.len() >= 4 {
120            let lang_id = &id_bytes[id_bytes.len() - 4..];
121            // List of keyboard layouts that use AltGr (non-exhaustive)
122            matches!(
123                lang_id,
124                b"0407" | // German
125                b"040C" | // French
126                b"040A" | // Spanish
127                b"0415" | // Polish
128                b"0413" | // Dutch
129                b"0816" | // Portuguese
130                b"041D" | // Swedish
131                b"0414" | // Norwegian
132                b"040B" | // Finnish
133                b"041F" | // Turkish
134                b"0419" | // Russian
135                b"0405" | // Czech
136                b"040E" | // Hungarian
137                b"0424" | // Slovenian
138                b"041B" | // Slovak
139                b"0418" // Romanian
140            )
141        } else {
142            false
143        }
144    }
145}
146
147impl WindowsKeyboardMapper {
148    pub(crate) fn new() -> Self {
149        let mut key_to_vkey = HashMap::default();
150        let mut vkey_to_key = HashMap::default();
151        let mut vkey_to_shifted = HashMap::default();
152        for vkey in CANDIDATE_VKEYS {
153            if let Some(key) = get_key_from_vkey(*vkey) {
154                key_to_vkey.insert(key.clone(), (vkey.0, false));
155                vkey_to_key.insert(vkey.0, key);
156            }
157            let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) };
158            if scan_code == 0 {
159                continue;
160            }
161            if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) {
162                key_to_vkey.insert(shifted_key.clone(), (vkey.0, true));
163                vkey_to_shifted.insert(vkey.0, shifted_key);
164            }
165        }
166        Self {
167            key_to_vkey,
168            vkey_to_key,
169            vkey_to_shifted,
170        }
171    }
172
173    fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> {
174        if use_key_equivalents {
175            get_vkey_from_key_with_us_layout(key)
176        } else {
177            self.key_to_vkey.get(key).cloned()
178        }
179    }
180}
181
182pub(crate) fn get_keystroke_key(
183    vkey: VIRTUAL_KEY,
184    scan_code: u32,
185    modifiers: &mut Modifiers,
186) -> Option<String> {
187    if modifiers.shift && need_to_convert_to_shifted_key(vkey) {
188        get_shifted_key(vkey, scan_code).inspect(|_| {
189            modifiers.shift = false;
190        })
191    } else {
192        get_key_from_vkey(vkey)
193    }
194}
195
196fn get_key_from_vkey(vkey: VIRTUAL_KEY) -> Option<String> {
197    let key_data = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_CHAR) };
198    if key_data == 0 {
199        return None;
200    }
201
202    // The high word contains dead key flag, the low word contains the character
203    let key = char::from_u32(key_data & 0xFFFF)?;
204
205    Some(key.to_ascii_lowercase().to_string())
206}
207
208#[inline]
209fn need_to_convert_to_shifted_key(vkey: VIRTUAL_KEY) -> bool {
210    matches!(
211        vkey,
212        VK_OEM_3
213            | VK_OEM_MINUS
214            | VK_OEM_PLUS
215            | VK_OEM_4
216            | VK_OEM_5
217            | VK_OEM_6
218            | VK_OEM_1
219            | VK_OEM_7
220            | VK_OEM_COMMA
221            | VK_OEM_PERIOD
222            | VK_OEM_2
223            | VK_OEM_102
224            | VK_OEM_8
225            | VK_ABNT_C1
226            | VK_0
227            | VK_1
228            | VK_2
229            | VK_3
230            | VK_4
231            | VK_5
232            | VK_6
233            | VK_7
234            | VK_8
235            | VK_9
236    )
237}
238
239fn get_shifted_key(vkey: VIRTUAL_KEY, scan_code: u32) -> Option<String> {
240    generate_key_char(vkey, scan_code, false, true, false)
241}
242
243pub(crate) fn generate_key_char(
244    vkey: VIRTUAL_KEY,
245    scan_code: u32,
246    control: bool,
247    shift: bool,
248    alt: bool,
249) -> Option<String> {
250    let mut state = [0; 256];
251    if control {
252        state[VK_CONTROL.0 as usize] = 0x80;
253    }
254    if shift {
255        state[VK_SHIFT.0 as usize] = 0x80;
256    }
257    if alt {
258        state[VK_MENU.0 as usize] = 0x80;
259    }
260
261    let mut buffer = [0; 8];
262    let len = unsafe { ToUnicode(vkey.0 as u32, scan_code, Some(&state), &mut buffer, 1 << 2) };
263
264    match len {
265        len if len > 0 => String::from_utf16(&buffer[..len as usize])
266            .ok()
267            .filter(|candidate| {
268                !candidate.is_empty() && !candidate.chars().next().unwrap().is_control()
269            }),
270        len if len < 0 => String::from_utf16(&buffer[..(-len as usize)]).ok(),
271        _ => None,
272    }
273}
274
275fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> {
276    match key {
277        // ` => VK_OEM_3
278        "`" => Some((VK_OEM_3.0, false)),
279        "~" => Some((VK_OEM_3.0, true)),
280        "1" => Some((VK_1.0, false)),
281        "!" => Some((VK_1.0, true)),
282        "2" => Some((VK_2.0, false)),
283        "@" => Some((VK_2.0, true)),
284        "3" => Some((VK_3.0, false)),
285        "#" => Some((VK_3.0, true)),
286        "4" => Some((VK_4.0, false)),
287        "$" => Some((VK_4.0, true)),
288        "5" => Some((VK_5.0, false)),
289        "%" => Some((VK_5.0, true)),
290        "6" => Some((VK_6.0, false)),
291        "^" => Some((VK_6.0, true)),
292        "7" => Some((VK_7.0, false)),
293        "&" => Some((VK_7.0, true)),
294        "8" => Some((VK_8.0, false)),
295        "*" => Some((VK_8.0, true)),
296        "9" => Some((VK_9.0, false)),
297        "(" => Some((VK_9.0, true)),
298        "0" => Some((VK_0.0, false)),
299        ")" => Some((VK_0.0, true)),
300        "-" => Some((VK_OEM_MINUS.0, false)),
301        "_" => Some((VK_OEM_MINUS.0, true)),
302        "=" => Some((VK_OEM_PLUS.0, false)),
303        "+" => Some((VK_OEM_PLUS.0, true)),
304        "[" => Some((VK_OEM_4.0, false)),
305        "{" => Some((VK_OEM_4.0, true)),
306        "]" => Some((VK_OEM_6.0, false)),
307        "}" => Some((VK_OEM_6.0, true)),
308        "\\" => Some((VK_OEM_5.0, false)),
309        "|" => Some((VK_OEM_5.0, true)),
310        ";" => Some((VK_OEM_1.0, false)),
311        ":" => Some((VK_OEM_1.0, true)),
312        "'" => Some((VK_OEM_7.0, false)),
313        "\"" => Some((VK_OEM_7.0, true)),
314        "," => Some((VK_OEM_COMMA.0, false)),
315        "<" => Some((VK_OEM_COMMA.0, true)),
316        "." => Some((VK_OEM_PERIOD.0, false)),
317        ">" => Some((VK_OEM_PERIOD.0, true)),
318        "/" => Some((VK_OEM_2.0, false)),
319        "?" => Some((VK_OEM_2.0, true)),
320        _ => None,
321    }
322}
323
324const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[
325    VK_OEM_3,
326    VK_OEM_MINUS,
327    VK_OEM_PLUS,
328    VK_OEM_4,
329    VK_OEM_5,
330    VK_OEM_6,
331    VK_OEM_1,
332    VK_OEM_7,
333    VK_OEM_COMMA,
334    VK_OEM_PERIOD,
335    VK_OEM_2,
336    VK_OEM_102,
337    VK_OEM_8,
338    VK_ABNT_C1,
339    VK_0,
340    VK_1,
341    VK_2,
342    VK_3,
343    VK_4,
344    VK_5,
345    VK_6,
346    VK_7,
347    VK_8,
348    VK_9,
349];
350
351#[cfg(test)]
352mod tests {
353    use crate::{Keystroke, Modifiers, PlatformKeyboardMapper, WindowsKeyboardMapper};
354
355    #[test]
356    fn test_keyboard_mapper() {
357        let mapper = WindowsKeyboardMapper::new();
358
359        // Normal case
360        let keystroke = Keystroke {
361            modifiers: Modifiers::control(),
362            key: "a".to_string(),
363            key_char: None,
364        };
365        let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
366        assert_eq!(*mapped.inner(), keystroke);
367        assert_eq!(mapped.key(), "a");
368        assert_eq!(*mapped.modifiers(), Modifiers::control());
369
370        // Shifted case, ctrl-$
371        let keystroke = Keystroke {
372            modifiers: Modifiers::control(),
373            key: "$".to_string(),
374            key_char: None,
375        };
376        let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
377        assert_eq!(*mapped.inner(), keystroke);
378        assert_eq!(mapped.key(), "4");
379        assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
380
381        // Shifted case, but shift is true
382        let keystroke = Keystroke {
383            modifiers: Modifiers::control_shift(),
384            key: "$".to_string(),
385            key_char: None,
386        };
387        let mapped = mapper.map_key_equivalent(keystroke, true);
388        assert_eq!(mapped.inner().modifiers, Modifiers::control());
389        assert_eq!(mapped.key(), "4");
390        assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
391
392        // Windows style
393        let keystroke = Keystroke {
394            modifiers: Modifiers::control_shift(),
395            key: "4".to_string(),
396            key_char: None,
397        };
398        let mapped = mapper.map_key_equivalent(keystroke, true);
399        assert_eq!(mapped.inner().modifiers, Modifiers::control());
400        assert_eq!(mapped.inner().key, "$");
401        assert_eq!(mapped.key(), "4");
402        assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
403    }
404}