Skip to main content

rust_switcher/input/
ring_buffer.rs

1use std::{
2    collections::VecDeque,
3    sync::{Mutex, OnceLock},
4};
5
6#[cfg(windows)]
7use windows::Win32::UI::{
8    Input::KeyboardAndMouse::{
9        GetAsyncKeyState, GetKeyState, GetKeyboardLayout, GetKeyboardState, HKL, ToUnicodeEx,
10        VIRTUAL_KEY, VK_BACK, VK_CAPITAL, VK_DELETE, VK_DOWN, VK_END, VK_ESCAPE, VK_HOME,
11        VK_INSERT, VK_LEFT, VK_LSHIFT, VK_NEXT, VK_PRIOR, VK_RETURN, VK_RIGHT, VK_RSHIFT, VK_SHIFT,
12        VK_TAB, VK_UP,
13    },
14    WindowsAndMessaging::{
15        GetForegroundWindow, GetWindowThreadProcessId, KBDLLHOOKSTRUCT, LLKHF_INJECTED,
16    },
17};
18
19static JOURNAL: OnceLock<Mutex<InputJournal>> = OnceLock::new();
20
21fn journal() -> &'static Mutex<InputJournal> {
22    JOURNAL.get_or_init(|| Mutex::new(InputJournal::new(100)))
23}
24
25fn with_journal_mut<R>(f: impl FnOnce(&mut InputJournal) -> R) -> R {
26    let mut guard = match journal().lock() {
27        Ok(g) => g,
28        Err(poison) => {
29            #[cfg(debug_assertions)]
30            tracing::warn!("input journal mutex was poisoned; continuing with inner value");
31            poison.into_inner()
32        }
33    };
34    f(&mut guard)
35}
36
37#[cfg(any(test, windows))]
38fn with_journal<R>(f: impl FnOnce(&InputJournal) -> R) -> R {
39    let guard = match journal().lock() {
40        Ok(g) => g,
41        Err(poison) => {
42            #[cfg(debug_assertions)]
43            tracing::warn!("input journal mutex was poisoned; continuing with inner value");
44            poison.into_inner()
45        }
46    };
47    f(&guard)
48}
49
50#[cfg(windows)]
51const LANG_ENGLISH_PRIMARY: u16 = 0x09;
52#[cfg(windows)]
53const LANG_RUSSIAN_PRIMARY: u16 = 0x19;
54
55#[derive(Copy, Clone, Debug, Eq, PartialEq)]
56pub enum LayoutTag {
57    Ru,
58    En,
59    Other(u16),
60    Unknown,
61}
62
63#[derive(Copy, Clone, Debug, Eq, PartialEq)]
64pub enum RunOrigin {
65    Physical,
66    Programmatic,
67}
68
69#[derive(Copy, Clone, Debug, Eq, PartialEq)]
70pub enum RunKind {
71    Text,
72    Whitespace,
73}
74
75#[derive(Clone, Debug, Eq, PartialEq)]
76pub struct InputRun {
77    pub text: String,
78    pub layout: LayoutTag,
79    pub origin: RunOrigin,
80    pub kind: RunKind,
81}
82
83#[derive(Debug, Default)]
84struct InputJournal {
85    runs: VecDeque<InputRun>,
86    cap_chars: usize,
87    total_chars: usize,
88    last_token_autoconverted: bool,
89    #[cfg(windows)]
90    last_fg_hwnd: isize,
91}
92
93impl InputJournal {
94    const fn new(cap_chars: usize) -> Self {
95        Self {
96            runs: VecDeque::new(),
97            cap_chars,
98            total_chars: 0,
99            last_token_autoconverted: false,
100            #[cfg(windows)]
101            last_fg_hwnd: 0,
102        }
103    }
104
105    #[cfg(any(test, windows))]
106    fn clear(&mut self) {
107        self.runs.clear();
108        self.total_chars = 0;
109        self.last_token_autoconverted = false;
110    }
111
112    fn append_segment(&mut self, text: &str, layout: LayoutTag, origin: RunOrigin, kind: RunKind) {
113        if text.is_empty() {
114            return;
115        }
116
117        if let Some(last) = self.runs.back_mut()
118            && last.layout == layout
119            && last.origin == origin
120            && last.kind == kind
121        {
122            last.text.push_str(text);
123            self.total_chars += text.chars().count();
124            self.enforce_cap_chars();
125            return;
126        }
127
128        self.total_chars += text.chars().count();
129        self.runs.push_back(InputRun {
130            text: text.to_string(),
131            layout,
132            origin,
133            kind,
134        });
135        self.enforce_cap_chars();
136    }
137
138    #[cfg(any(test, windows))]
139    fn push_text_internal(&mut self, text: &str, layout: LayoutTag, origin: RunOrigin) {
140        if text.is_empty() {
141            return;
142        }
143
144        // Segment the input without intermediate allocations by slicing `text` at kind boundaries.
145        // This keeps the journal cheap for frequent short inputs (single key presses) and also
146        // efficient for programmatic multi-character inserts.
147        let mut start = 0usize;
148        let mut current_kind: Option<RunKind> = None;
149
150        for (i, ch) in text.char_indices() {
151            let kind = if ch.is_whitespace() {
152                RunKind::Whitespace
153            } else {
154                RunKind::Text
155            };
156
157            match current_kind {
158                None => {
159                    start = i;
160                    current_kind = Some(kind);
161                }
162                Some(k) if k == kind => {}
163                Some(k) => {
164                    self.append_segment(&text[start..i], layout, origin, k);
165                    start = i;
166                    current_kind = Some(kind);
167                }
168            }
169        }
170
171        if let Some(kind) = current_kind {
172            self.append_segment(&text[start..], layout, origin, kind);
173        }
174    }
175
176    fn push_run(&mut self, run: InputRun) {
177        self.append_segment(&run.text, run.layout, run.origin, run.kind);
178    }
179
180    fn push_runs(&mut self, runs: impl IntoIterator<Item = InputRun>) {
181        for run in runs {
182            self.push_run(run);
183        }
184    }
185
186    fn enforce_cap_chars(&mut self) {
187        while self.total_chars > self.cap_chars {
188            let mut remove_front_run = false;
189
190            if let Some(front) = self.runs.front_mut() {
191                if let Some((idx, _)) = front.text.char_indices().nth(1) {
192                    front.text.drain(..idx);
193                } else {
194                    front.text.clear();
195                    remove_front_run = true;
196                }
197                self.total_chars = self.total_chars.saturating_sub(1);
198
199                if front.text.is_empty() {
200                    remove_front_run = true;
201                }
202            } else {
203                self.total_chars = 0;
204                break;
205            }
206
207            if remove_front_run {
208                let _ = self.runs.pop_front();
209            }
210        }
211    }
212
213    #[cfg(any(test, windows))]
214    fn backspace(&mut self) {
215        let mut pop_last = false;
216
217        if let Some(last) = self.runs.back_mut()
218            && let Some((idx, _)) = last.text.char_indices().last()
219        {
220            last.text.drain(idx..);
221            self.total_chars = self.total_chars.saturating_sub(1);
222            if last.text.is_empty() {
223                pop_last = true;
224            }
225        }
226
227        if pop_last {
228            let _ = self.runs.pop_back();
229        }
230    }
231
232    #[cfg(windows)]
233    fn invalidate_if_foreground_changed(&mut self) {
234        let fg = unsafe { GetForegroundWindow() };
235        let raw = fg.0 as isize;
236        if raw == 0 {
237            self.clear();
238            self.last_fg_hwnd = 0;
239            return;
240        }
241
242        if self.last_fg_hwnd == 0 {
243            self.last_fg_hwnd = raw;
244            return;
245        }
246
247        if self.last_fg_hwnd != raw {
248            self.clear();
249            self.last_fg_hwnd = raw;
250        }
251    }
252
253    #[cfg(any(test, windows))]
254    fn last_char(&self) -> Option<char> {
255        self.runs.back()?.text.chars().last()
256    }
257
258    #[cfg(any(test, windows))]
259    fn prev_char_before_last(&self) -> Option<char> {
260        let mut runs_it = self.runs.iter().rev();
261        let last_run = runs_it.next()?;
262
263        let mut chars = last_run.text.chars().rev();
264        let _ = chars.next()?;
265        if let Some(prev) = chars.next() {
266            return Some(prev);
267        }
268
269        for run in runs_it {
270            if let Some(ch) = run.text.chars().last() {
271                return Some(ch);
272            }
273        }
274
275        None
276    }
277
278    fn take_last_layout_run_with_suffix(&mut self) -> Option<(InputRun, Vec<InputRun>)> {
279        let mut suffix_runs = self.pop_suffix_whitespace();
280
281        if self.runs.back().is_none_or(|run| run.kind != RunKind::Text) {
282            self.restore_suffix(&mut suffix_runs);
283            return None;
284        }
285
286        let run = self.runs.pop_back()?;
287        self.total_chars = self.total_chars.saturating_sub(run.text.chars().count());
288        suffix_runs.reverse();
289        Some((run, suffix_runs))
290    }
291
292    fn take_last_layout_sequence_with_suffix(&mut self) -> Option<(Vec<InputRun>, Vec<InputRun>)> {
293        let mut suffix_runs = self.pop_suffix_whitespace();
294
295        if self.runs.back().is_none_or(|run| run.kind != RunKind::Text) {
296            self.restore_suffix(&mut suffix_runs);
297            return None;
298        }
299
300        let last = self.runs.back()?;
301        let target_layout = last.layout;
302        let target_origin = last.origin;
303        let mut seq_rev: Vec<InputRun> = Vec::new();
304        while let Some(run) = self.runs.back() {
305            if run.layout != target_layout || run.origin != target_origin {
306                break;
307            }
308            let run = self.runs.pop_back()?;
309            self.total_chars = self.total_chars.saturating_sub(run.text.chars().count());
310            seq_rev.push(run);
311        }
312
313        if seq_rev.is_empty() {
314            self.restore_suffix(&mut suffix_runs);
315            return None;
316        }
317
318        seq_rev.reverse();
319        suffix_runs.reverse();
320        Some((seq_rev, suffix_runs))
321    }
322
323    fn pop_suffix_whitespace(&mut self) -> Vec<InputRun> {
324        let mut suffix_runs: Vec<InputRun> = Vec::new();
325        while self
326            .runs
327            .back()
328            .is_some_and(|run| run.kind == RunKind::Whitespace)
329        {
330            let Some(run) = self.runs.pop_back() else {
331                break;
332            };
333            self.total_chars = self.total_chars.saturating_sub(run.text.chars().count());
334            suffix_runs.push(run);
335        }
336        suffix_runs
337    }
338
339    fn restore_suffix(&mut self, suffix_runs: &mut Vec<InputRun>) {
340        // `suffix_runs` is expected to be in reverse order (from repeated `pop_back`).
341        while let Some(run) = suffix_runs.pop() {
342            self.total_chars += run.text.chars().count();
343            self.runs.push_back(run);
344        }
345    }
346}
347
348#[cfg(windows)]
349#[derive(Debug)]
350struct DecodedText {
351    text: String,
352    layout: LayoutTag,
353}
354
355#[cfg(windows)]
356#[derive(Copy, Clone, Debug, Eq, PartialEq)]
357struct KeyboardStateOverrides {
358    shift_down: bool,
359    left_shift_down: bool,
360    right_shift_down: bool,
361    caps_lock_on: bool,
362}
363
364#[cfg(windows)]
365pub fn layout_tag_from_hkl(hkl: HKL) -> LayoutTag {
366    let hkl_raw = hkl.0 as usize;
367
368    if hkl_raw == 0 {
369        return LayoutTag::Unknown;
370    }
371
372    let lang_id = (hkl_raw & 0xFFFF) as u16;
373    let primary = lang_id & 0x03FF;
374
375    match primary {
376        LANG_ENGLISH_PRIMARY => LayoutTag::En,
377        LANG_RUSSIAN_PRIMARY => LayoutTag::Ru,
378        _ => LayoutTag::Other(lang_id),
379    }
380}
381
382#[cfg(windows)]
383fn current_foreground_layout_tag() -> LayoutTag {
384    let fg = unsafe { GetForegroundWindow() };
385    if fg.0.is_null() {
386        return LayoutTag::Unknown;
387    }
388
389    let tid = unsafe { GetWindowThreadProcessId(fg, None) };
390    let hkl = unsafe { GetKeyboardLayout(tid) };
391    layout_tag_from_hkl(hkl)
392}
393
394pub fn mark_last_token_autoconverted() {
395    with_journal_mut(|j| j.last_token_autoconverted = true);
396}
397
398#[cfg(any(test, windows))]
399#[must_use]
400pub fn last_token_autoconverted() -> bool {
401    with_journal(|j| j.last_token_autoconverted)
402}
403
404#[cfg(windows)]
405fn mods_ctrl_or_alt_down() -> bool {
406    // Keep this module independent from `crate::platform` so it can be built from the minimal lib target.
407    // VK_CONTROL = 0x11, VK_MENU (Alt) = 0x12.
408    let ctrl = unsafe { GetAsyncKeyState(0x11) }.cast_unsigned();
409    let alt = unsafe { GetAsyncKeyState(0x12) }.cast_unsigned();
410    (ctrl & 0x8000) != 0 || (alt & 0x8000) != 0
411}
412
413#[cfg(windows)]
414fn key_is_down(vk: VIRTUAL_KEY) -> bool {
415    let value = unsafe { GetAsyncKeyState(i32::from(vk.0)) }.cast_unsigned();
416    (value & 0x8000) != 0
417}
418
419#[cfg(windows)]
420fn key_is_toggled(vk: VIRTUAL_KEY) -> bool {
421    let value = unsafe { GetKeyState(i32::from(vk.0)) }.cast_unsigned();
422    (value & 0x0001) != 0
423}
424
425#[cfg(windows)]
426fn set_key_down_state(state: &mut [u8; 256], vk: VIRTUAL_KEY, is_down: bool) {
427    let idx = usize::from(vk.0);
428    if idx >= state.len() {
429        return;
430    }
431
432    if is_down {
433        state[idx] |= 0x80;
434    } else {
435        state[idx] &= !0x80;
436    }
437}
438
439#[cfg(windows)]
440fn set_key_toggle_state(state: &mut [u8; 256], vk: VIRTUAL_KEY, is_toggled: bool) {
441    let idx = usize::from(vk.0);
442    if idx >= state.len() {
443        return;
444    }
445
446    if is_toggled {
447        state[idx] |= 0x01;
448    } else {
449        state[idx] &= !0x01;
450    }
451}
452
453#[cfg(windows)]
454fn current_keyboard_state_overrides() -> KeyboardStateOverrides {
455    KeyboardStateOverrides {
456        shift_down: key_is_down(VK_SHIFT),
457        left_shift_down: key_is_down(VK_LSHIFT),
458        right_shift_down: key_is_down(VK_RSHIFT),
459        caps_lock_on: key_is_toggled(VK_CAPITAL),
460    }
461}
462
463#[cfg(windows)]
464fn apply_keyboard_state_overrides(state: &mut [u8; 256], overrides: KeyboardStateOverrides) {
465    // LL-hook decoding runs before the target thread's keyboard state is fully reflected in our
466    // thread-local `GetKeyboardState` snapshot. Patch in the physical modifier/toggle state that
467    // affects character case before calling `ToUnicodeEx`.
468    set_key_down_state(state, VK_SHIFT, overrides.shift_down);
469    set_key_down_state(state, VK_LSHIFT, overrides.left_shift_down);
470    set_key_down_state(state, VK_RSHIFT, overrides.right_shift_down);
471    set_key_toggle_state(state, VK_CAPITAL, overrides.caps_lock_on);
472}
473
474#[cfg(windows)]
475fn decode_typed_text(kb: &KBDLLHOOKSTRUCT, vk: VIRTUAL_KEY) -> Option<DecodedText> {
476    let fg = unsafe { GetForegroundWindow() };
477    if fg.0.is_null() {
478        return None;
479    }
480
481    let tid = unsafe { GetWindowThreadProcessId(fg, None) };
482    let hkl = unsafe { GetKeyboardLayout(tid) };
483    let layout = layout_tag_from_hkl(hkl);
484
485    let mut state = [0u8; 256];
486    if unsafe { GetKeyboardState(&mut state) }.is_err() {
487        return None;
488    }
489
490    apply_keyboard_state_overrides(&mut state, current_keyboard_state_overrides());
491
492    let mut buf = [0u16; 8];
493    let rc = unsafe { ToUnicodeEx(u32::from(vk.0), kb.scanCode, &state, &mut buf, 0, Some(hkl)) };
494
495    if rc == -1 {
496        let _ =
497            unsafe { ToUnicodeEx(u32::from(vk.0), kb.scanCode, &state, &mut buf, 0, Some(hkl)) };
498        return None;
499    }
500
501    if rc <= 0 {
502        return None;
503    }
504
505    let rc = usize::try_from(rc).ok()?;
506    let s = String::from_utf16_lossy(&buf[..rc]);
507
508    if s.chars().any(char::is_control) {
509        return None;
510    }
511
512    Some(DecodedText { text: s, layout })
513}
514
515#[cfg(windows)]
516pub fn record_keydown(kb: &KBDLLHOOKSTRUCT, vk: u32) -> Option<String> {
517    if kb.flags.contains(LLKHF_INJECTED) {
518        return None;
519    }
520
521    let vk_u16 = u16::try_from(vk).ok()?;
522    let vk = VIRTUAL_KEY(vk_u16);
523
524    enum JournalAction {
525        Clear,
526        Backspace,
527        PushText {
528            text: String,
529            layout: LayoutTag,
530            origin: RunOrigin,
531        },
532    }
533
534    let mut action: Option<JournalAction> = None;
535    let mut output: Option<String> = None;
536
537    match vk {
538        VK_ESCAPE | VK_DELETE | VK_INSERT | VK_LEFT | VK_RIGHT | VK_UP | VK_DOWN | VK_HOME
539        | VK_END | VK_PRIOR | VK_NEXT => action = Some(JournalAction::Clear),
540        VK_BACK => action = Some(JournalAction::Backspace),
541        VK_RETURN => {
542            let layout = current_foreground_layout_tag();
543            output = Some("\n".to_string());
544            action = Some(JournalAction::PushText {
545                text: "\n".to_string(),
546                layout,
547                origin: RunOrigin::Physical,
548            });
549        }
550        VK_TAB => {
551            let layout = current_foreground_layout_tag();
552            output = Some("\t".to_string());
553            action = Some(JournalAction::PushText {
554                text: "\t".to_string(),
555                layout,
556                origin: RunOrigin::Physical,
557            });
558        }
559        _ => {}
560    }
561
562    if mods_ctrl_or_alt_down() {
563        action = Some(JournalAction::Clear);
564    }
565
566    if action.is_none() {
567        let decoded = decode_typed_text(kb, vk)?;
568        output = Some(decoded.text.clone());
569        action = Some(JournalAction::PushText {
570            text: decoded.text,
571            layout: decoded.layout,
572            origin: RunOrigin::Physical,
573        });
574    }
575
576    with_journal_mut(|j| {
577        j.invalidate_if_foreground_changed();
578        if let Some(action) = action {
579            match action {
580                JournalAction::Clear => j.clear(),
581                JournalAction::Backspace => j.backspace(),
582                JournalAction::PushText {
583                    text,
584                    layout,
585                    origin,
586                } => {
587                    if text.chars().any(char::is_alphanumeric) {
588                        j.last_token_autoconverted = false;
589                    }
590                    j.push_text_internal(&text, layout, origin);
591                }
592            }
593        }
594    });
595
596    output
597}
598
599#[must_use]
600pub fn take_last_layout_run_with_suffix() -> Option<(InputRun, Vec<InputRun>)> {
601    with_journal_mut(|j| j.take_last_layout_run_with_suffix())
602}
603
604#[must_use]
605pub fn take_last_layout_sequence_with_suffix() -> Option<(Vec<InputRun>, Vec<InputRun>)> {
606    with_journal_mut(|j| j.take_last_layout_sequence_with_suffix())
607}
608
609#[cfg(test)]
610pub fn push_text(s: &str) {
611    with_journal_mut(|j| j.push_text_internal(s, LayoutTag::Unknown, RunOrigin::Programmatic));
612}
613
614pub fn push_run(run: InputRun) {
615    with_journal_mut(|j| j.push_run(run));
616}
617
618pub fn push_runs(runs: impl IntoIterator<Item = InputRun>) {
619    with_journal_mut(|j| j.push_runs(runs));
620}
621
622#[cfg(any(test, windows))]
623pub fn push_text_with_meta(text: &str, layout: LayoutTag, origin: RunOrigin) {
624    with_journal_mut(|j| j.push_text_internal(text, layout, origin));
625}
626
627#[cfg(test)]
628pub fn test_backspace() {
629    with_journal_mut(|j| j.backspace());
630}
631
632#[cfg(test)]
633pub fn runs_snapshot() -> Vec<InputRun> {
634    with_journal(|j| j.runs.iter().cloned().collect())
635}
636
637#[cfg(any(test, windows))]
638pub fn invalidate() {
639    with_journal_mut(|j| j.clear());
640}
641
642#[cfg(any(test, windows))]
643#[must_use]
644pub fn last_char_triggers_autoconvert() -> bool {
645    with_journal(|j| {
646        let Some(last) = j.last_char() else {
647            return false;
648        };
649
650        if matches!(last, '.' | ',' | '!' | '?' | ';' | ':') {
651            return j
652                .prev_char_before_last()
653                .is_some_and(|prev| !prev.is_whitespace());
654        }
655
656        if last.is_whitespace() {
657            return j
658                .prev_char_before_last()
659                .is_some_and(|prev| !prev.is_whitespace());
660        }
661
662        false
663    })
664}
665
666#[cfg(all(test, windows))]
667mod tests {
668    use super::*;
669
670    #[test]
671    fn keyboard_state_overrides_apply_caps_lock_toggle_without_touching_high_bit() {
672        let mut state = [0u8; 256];
673        apply_keyboard_state_overrides(
674            &mut state,
675            KeyboardStateOverrides {
676                shift_down: false,
677                left_shift_down: false,
678                right_shift_down: false,
679                caps_lock_on: true,
680            },
681        );
682
683        let caps = state[usize::from(VK_CAPITAL.0)];
684        assert_eq!(caps & 0x01, 0x01);
685        assert_eq!(caps & 0x80, 0x00);
686    }
687
688    #[test]
689    fn keyboard_state_overrides_preserve_shift_and_caps_lock_combination() {
690        let mut state = [0u8; 256];
691        apply_keyboard_state_overrides(
692            &mut state,
693            KeyboardStateOverrides {
694                shift_down: true,
695                left_shift_down: true,
696                right_shift_down: false,
697                caps_lock_on: true,
698            },
699        );
700
701        assert_eq!(state[usize::from(VK_SHIFT.0)] & 0x80, 0x80);
702        assert_eq!(state[usize::from(VK_LSHIFT.0)] & 0x80, 0x80);
703        assert_eq!(state[usize::from(VK_RSHIFT.0)] & 0x80, 0x00);
704        assert_eq!(state[usize::from(VK_CAPITAL.0)] & 0x01, 0x01);
705    }
706}