Skip to main content

win_text_inject/
modifiers.rs

1//! Release physically-held modifier keys before synthesizing input.
2//!
3//! In a push-to-talk dictation app a modifier is held *by construction* at the moment injection
4//! fires. `SendInput` does not reset keyboard state:
5//!
6//! > This function does not reset the keyboard's current state. Any keys that are already pressed
7//! > when the function is called might interfere with the events that this function generates.
8//!
9//! So a user holding Right-Alt turns a synthesized Ctrl+V into AltGr+V, which is a different
10//! character on many layouts. No dictation tool surveyed does this.
11
12use windows::Win32::UI::Input::KeyboardAndMouse::{
13    GetAsyncKeyState, SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP,
14    VIRTUAL_KEY, VK_LCONTROL, VK_LMENU, VK_LSHIFT, VK_LWIN, VK_RCONTROL, VK_RMENU, VK_RSHIFT,
15    VK_RWIN,
16};
17
18use crate::sendinput::{tagged_keyboard_input, INJECT_TAG};
19use crate::Error;
20
21/// Every modifier that can alter the meaning of a synthesized chord.
22const MODIFIERS: [VIRTUAL_KEY; 8] = [
23    VK_LSHIFT,
24    VK_RSHIFT,
25    VK_LCONTROL,
26    VK_RCONTROL,
27    VK_LMENU,
28    VK_RMENU,
29    VK_LWIN,
30    VK_RWIN,
31];
32
33/// High bit of `GetAsyncKeyState` means the key is currently physically down.
34const KEY_DOWN_MASK: u16 = 0x8000;
35
36fn is_down(vk: VIRTUAL_KEY) -> bool {
37    (unsafe { GetAsyncKeyState(vk.0 as i32) } as u16 & KEY_DOWN_MASK) != 0
38}
39
40/// Synthesize key-up for every modifier currently held.
41///
42/// Returns the modifiers that were released. They are deliberately **not** restored afterwards:
43/// re-pressing a modifier the user has since physically released leaves it stuck down forever,
44/// which is a far worse failure than a lost modifier.
45pub fn sanitize() -> Result<Vec<VIRTUAL_KEY>, Error> {
46    let held: Vec<VIRTUAL_KEY> = MODIFIERS.into_iter().filter(|vk| is_down(*vk)).collect();
47    if held.is_empty() {
48        return Ok(held);
49    }
50
51    let inputs: Vec<INPUT> = held
52        .iter()
53        .map(|vk| INPUT {
54            r#type: INPUT_KEYBOARD,
55            Anonymous: INPUT_0 {
56                ki: KEYBDINPUT {
57                    wVk: *vk,
58                    wScan: 0,
59                    dwFlags: KEYEVENTF_KEYUP,
60                    time: 0,
61                    dwExtraInfo: INJECT_TAG,
62                },
63            },
64        })
65        .collect();
66
67    let sent = unsafe { SendInput(&inputs, std::mem::size_of::<INPUT>() as i32) };
68    if sent as usize != inputs.len() {
69        return Err(Error::SendInputBlocked);
70    }
71    Ok(held)
72}
73
74/// True if any modifier is currently held. Cheap pre-check for callers that want to log or delay.
75pub fn any_held() -> bool {
76    MODIFIERS.into_iter().any(is_down)
77}
78
79/// Build a tagged key-up event for one virtual key, exposed for chord construction.
80pub(crate) fn key_up(vk: VIRTUAL_KEY) -> INPUT {
81    tagged_keyboard_input(vk, KEYEVENTF_KEYUP)
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn modifier_set_covers_both_sides_of_every_modifier() {
90        assert_eq!(MODIFIERS.len(), 8);
91        for pair in [
92            (VK_LSHIFT, VK_RSHIFT),
93            (VK_LCONTROL, VK_RCONTROL),
94            (VK_LMENU, VK_RMENU),
95            (VK_LWIN, VK_RWIN),
96        ] {
97            assert!(MODIFIERS.contains(&pair.0));
98            assert!(MODIFIERS.contains(&pair.1));
99        }
100    }
101
102    #[test]
103    fn any_held_agrees_with_per_key_state() {
104        // Whatever the live keyboard state is, the aggregate must match the individual checks.
105        let individually = MODIFIERS.into_iter().any(is_down);
106        assert_eq!(any_held(), individually);
107    }
108}