Skip to main content

win_text_inject/
sendinput.rs

1//! Synthesized keyboard input, tagged so the sender's own low-level hook can ignore it.
2
3use windows::Win32::UI::Input::KeyboardAndMouse::{
4    SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYBD_EVENT_FLAGS, KEYEVENTF_KEYUP,
5    KEYEVENTF_UNICODE, VIRTUAL_KEY,
6};
7
8use crate::Error;
9
10/// Marker written to `dwExtraInfo` on every event this crate synthesizes.
11///
12/// A dictation app installs a `WH_KEYBOARD_LL` hook to detect its hotkey. Without a tag, the
13/// synthesized paste chord re-enters that hook and can retrigger the hotkey. Callers should skip
14/// events whose `dwExtraInfo` equals this value, in addition to checking `LLKHF_INJECTED`.
15pub const INJECT_TAG: usize = 0x57_54_49_4A; // "WTIJ"
16
17pub(crate) fn tagged_keyboard_input(vk: VIRTUAL_KEY, flags: KEYBD_EVENT_FLAGS) -> INPUT {
18    INPUT {
19        r#type: INPUT_KEYBOARD,
20        Anonymous: INPUT_0 {
21            ki: KEYBDINPUT {
22                wVk: vk,
23                wScan: 0,
24                dwFlags: flags,
25                time: 0,
26                dwExtraInfo: INJECT_TAG,
27            },
28        },
29    }
30}
31
32fn unicode_unit(unit: u16, key_up: bool) -> INPUT {
33    let flags = if key_up {
34        KEYEVENTF_UNICODE | KEYEVENTF_KEYUP
35    } else {
36        KEYEVENTF_UNICODE
37    };
38    INPUT {
39        r#type: INPUT_KEYBOARD,
40        Anonymous: INPUT_0 {
41            ki: KEYBDINPUT {
42                // Must be zero when KEYEVENTF_UNICODE is set.
43                wVk: VIRTUAL_KEY(0),
44                wScan: unit,
45                dwFlags: flags,
46                time: 0,
47                dwExtraInfo: INJECT_TAG,
48            },
49        },
50    }
51}
52
53/// Build the full event sequence for `text` as UTF-16 code units.
54///
55/// Characters outside the BMP become a surrogate pair, and each half is sent as its own
56/// down/up pair — four events for one glyph. Receivers reassemble them; there is no way to send a
57/// non-BMP character as a single event.
58pub(crate) fn unicode_inputs(text: &str) -> Vec<INPUT> {
59    let mut inputs = Vec::with_capacity(text.len() * 2);
60    for unit in text.encode_utf16() {
61        inputs.push(unicode_unit(unit, false));
62        inputs.push(unicode_unit(unit, true));
63    }
64    inputs
65}
66
67/// Send events in chunks so a long transcript does not occupy the input queue in one burst.
68const CHUNK: usize = 256;
69
70pub(crate) fn send(inputs: &[INPUT]) -> Result<(), Error> {
71    if inputs.is_empty() {
72        return Ok(());
73    }
74    for chunk in inputs.chunks(CHUNK) {
75        let sent = unsafe { SendInput(chunk, std::mem::size_of::<INPUT>() as i32) };
76        // A short count here is almost always UIPI silently refusing the injection.
77        if sent as usize != chunk.len() {
78            return Err(Error::SendInputBlocked);
79        }
80    }
81    Ok(())
82}
83
84/// Type `text` directly as Unicode input, bypassing the clipboard entirely.
85///
86/// Layout-independent, and the only option for targets that block programmatic paste (password
87/// managers, some VDI clients). Slow for long text: throughput is bounded by the receiving
88/// application's message pump, not by this function.
89pub fn type_text(text: &str) -> Result<(), Error> {
90    send(&unicode_inputs(text))
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    fn scans(text: &str) -> Vec<u16> {
98        unicode_inputs(text)
99            .iter()
100            .map(|i| unsafe { i.Anonymous.ki.wScan })
101            .collect()
102    }
103
104    #[test]
105    fn each_code_unit_produces_a_down_and_an_up() {
106        assert_eq!(unicode_inputs("abc").len(), 6);
107    }
108
109    #[test]
110    fn non_bmp_characters_produce_four_events() {
111        // One emoji is a surrogate pair, so two code units, so four events.
112        assert_eq!(unicode_inputs("\u{1F600}").len(), 4);
113        assert_eq!(scans("\u{1F600}"), vec![0xD83D, 0xD83D, 0xDE00, 0xDE00]);
114    }
115
116    #[test]
117    fn unicode_events_must_not_carry_a_virtual_key() {
118        for input in unicode_inputs("hi\u{1F600}") {
119            assert_eq!(unsafe { input.Anonymous.ki.wVk }, VIRTUAL_KEY(0));
120        }
121    }
122
123    #[test]
124    fn every_event_is_tagged_for_hook_filtering() {
125        for input in unicode_inputs("hi") {
126            assert_eq!(unsafe { input.Anonymous.ki.dwExtraInfo }, INJECT_TAG);
127        }
128    }
129
130    #[test]
131    fn empty_text_produces_no_events() {
132        assert!(unicode_inputs("").is_empty());
133        assert!(send(&[]).is_ok());
134    }
135
136    #[test]
137    fn alternating_events_are_down_then_up() {
138        let inputs = unicode_inputs("ab");
139        let ups: Vec<bool> = inputs
140            .iter()
141            .map(|i| unsafe { i.Anonymous.ki.dwFlags }.contains(KEYEVENTF_KEYUP))
142            .collect();
143        assert_eq!(ups, vec![false, true, false, true]);
144    }
145}