Skip to main content

rust_switcher/input/
ring_buffer.rs

1#[cfg(test)]
2use std::sync::MutexGuard;
3use std::{
4    collections::{HashMap, VecDeque},
5    sync::{Mutex, OnceLock},
6};
7
8#[cfg(windows)]
9use windows::Win32::{
10    System::SystemInformation::GetTickCount64,
11    UI::{
12        Input::KeyboardAndMouse::{
13            GetAsyncKeyState, GetKeyState, GetKeyboardLayout, GetKeyboardState, HKL, ToUnicodeEx,
14            VIRTUAL_KEY, VK_BACK, VK_CAPITAL, VK_DELETE, VK_DOWN, VK_END, VK_ESCAPE, VK_HOME,
15            VK_INSERT, VK_LEFT, VK_LSHIFT, VK_NEXT, VK_PRIOR, VK_RETURN, VK_RIGHT, VK_RSHIFT,
16            VK_SHIFT, VK_TAB, VK_UP,
17        },
18        WindowsAndMessaging::{
19            GUITHREADINFO, GetForegroundWindow, GetGUIThreadInfo, GetWindowThreadProcessId,
20            KBDLLHOOKSTRUCT, LLKHF_INJECTED,
21        },
22    },
23};
24
25static JOURNAL: OnceLock<Mutex<InputJournal>> = OnceLock::new();
26#[cfg(windows)]
27static JOURNAL_CACHE: OnceLock<Mutex<HashMap<FocusCacheKey, CachedJournal>>> = OnceLock::new();
28
29#[cfg(test)]
30static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
31
32#[cfg(windows)]
33const FOREGROUND_CACHE_TTL_MS: u64 = 2 * 60 * 1000;
34
35#[cfg(windows)]
36fn allow_injected_input_for_e2e() -> bool {
37    cfg!(debug_assertions) && std::env::var_os("RUST_SWITCHER_E2E_ALLOW_INJECTED").is_some()
38}
39
40fn journal() -> &'static Mutex<InputJournal> {
41    JOURNAL.get_or_init(|| Mutex::new(InputJournal::new(100)))
42}
43
44#[cfg(test)]
45pub fn test_guard() -> MutexGuard<'static, ()> {
46    TEST_LOCK
47        .get_or_init(|| Mutex::new(()))
48        .lock()
49        .unwrap_or_else(|e| e.into_inner())
50}
51
52fn with_journal_mut<R>(f: impl FnOnce(&mut InputJournal) -> R) -> R {
53    let mut guard = match journal().lock() {
54        Ok(g) => g,
55        Err(poison) => {
56            #[cfg(debug_assertions)]
57            tracing::warn!("input journal mutex was poisoned; continuing with inner value");
58            poison.into_inner()
59        }
60    };
61    f(&mut guard)
62}
63
64#[cfg(windows)]
65fn journal_cache() -> &'static Mutex<HashMap<FocusCacheKey, CachedJournal>> {
66    JOURNAL_CACHE.get_or_init(|| Mutex::new(HashMap::new()))
67}
68
69#[cfg(windows)]
70fn with_journal_cache_mut<R>(f: impl FnOnce(&mut HashMap<FocusCacheKey, CachedJournal>) -> R) -> R {
71    let mut guard = match journal_cache().lock() {
72        Ok(g) => g,
73        Err(poison) => {
74            #[cfg(debug_assertions)]
75            tracing::warn!("input journal cache mutex was poisoned; continuing with inner value");
76            poison.into_inner()
77        }
78    };
79    f(&mut guard)
80}
81
82#[cfg(windows)]
83fn prune_stale_foreground_cache(cache: &mut HashMap<FocusCacheKey, CachedJournal>, now_ms: u64) {
84    cache.retain(|_, cached| now_ms.saturating_sub(cached.updated_ms) <= FOREGROUND_CACHE_TTL_MS);
85}
86
87#[cfg(any(test, windows))]
88fn with_journal<R>(f: impl FnOnce(&InputJournal) -> R) -> R {
89    let guard = match journal().lock() {
90        Ok(g) => g,
91        Err(poison) => {
92            #[cfg(debug_assertions)]
93            tracing::warn!("input journal mutex was poisoned; continuing with inner value");
94            poison.into_inner()
95        }
96    };
97    f(&guard)
98}
99
100#[cfg(windows)]
101const LANG_ENGLISH_PRIMARY: u16 = 0x09;
102#[cfg(windows)]
103const LANG_RUSSIAN_PRIMARY: u16 = 0x19;
104
105#[derive(Copy, Clone, Debug, Eq, PartialEq)]
106pub enum LayoutTag {
107    Ru,
108    En,
109    Other(u16),
110    Unknown,
111}
112
113#[derive(Copy, Clone, Debug, Eq, PartialEq)]
114pub enum RunOrigin {
115    Physical,
116    Programmatic,
117}
118
119#[derive(Copy, Clone, Debug, Eq, PartialEq)]
120pub enum RunKind {
121    Text,
122    Whitespace,
123}
124
125#[derive(Clone, Debug, Eq, PartialEq)]
126pub struct InputRun {
127    pub text: String,
128    pub layout: LayoutTag,
129    pub origin: RunOrigin,
130    pub kind: RunKind,
131}
132
133#[cfg(windows)]
134#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
135struct CaretRectSignature {
136    left: i32,
137    top: i32,
138    right: i32,
139    bottom: i32,
140}
141
142#[cfg(windows)]
143#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
144struct FocusCacheKey {
145    foreground_hwnd: isize,
146    focus_hwnd: isize,
147    caret_hwnd: isize,
148    caret_rect: CaretRectSignature,
149}
150
151#[cfg(windows)]
152#[derive(Clone, Debug)]
153struct CachedJournal {
154    journal: InputJournal,
155    updated_ms: u64,
156}
157
158#[cfg(windows)]
159fn same_focus_identity(a: FocusCacheKey, b: FocusCacheKey) -> bool {
160    a.foreground_hwnd == b.foreground_hwnd
161        && a.focus_hwnd == b.focus_hwnd
162        && a.caret_hwnd == b.caret_hwnd
163}
164
165#[derive(Clone, Debug, Default)]
166struct InputJournal {
167    runs: VecDeque<InputRun>,
168    cap_chars: usize,
169    total_chars: usize,
170    caret_from_end: usize,
171    last_token_autoconverted: bool,
172    #[cfg(windows)]
173    last_fg_hwnd: isize,
174    #[cfg(windows)]
175    last_focus_key: Option<FocusCacheKey>,
176}
177
178impl InputJournal {
179    const fn new(cap_chars: usize) -> Self {
180        Self {
181            runs: VecDeque::new(),
182            cap_chars,
183            total_chars: 0,
184            caret_from_end: 0,
185            last_token_autoconverted: false,
186            #[cfg(windows)]
187            last_fg_hwnd: 0,
188            #[cfg(windows)]
189            last_focus_key: None,
190        }
191    }
192
193    #[cfg(any(test, windows))]
194    fn clear(&mut self) {
195        self.runs.clear();
196        self.total_chars = 0;
197        self.caret_from_end = 0;
198        self.last_token_autoconverted = false;
199    }
200
201    fn append_segment_end(
202        &mut self,
203        text: &str,
204        layout: LayoutTag,
205        origin: RunOrigin,
206        kind: RunKind,
207    ) {
208        if text.is_empty() {
209            return;
210        }
211
212        if let Some(last) = self.runs.back_mut()
213            && last.layout == layout
214            && last.origin == origin
215            && last.kind == kind
216        {
217            last.text.push_str(text);
218            self.total_chars += text.chars().count();
219            self.enforce_cap_chars();
220            return;
221        }
222
223        self.total_chars += text.chars().count();
224        self.runs.push_back(InputRun {
225            text: text.to_string(),
226            layout,
227            origin,
228            kind,
229        });
230        self.enforce_cap_chars();
231    }
232
233    fn insert_run_before_caret(&mut self, run: InputRun) {
234        if run.text.is_empty() {
235            return;
236        }
237
238        let caret_from_end = self.caret_from_end.min(self.total_chars);
239        let suffix_runs = self.detach_suffix(caret_from_end);
240
241        self.append_segment_end(&run.text, run.layout, run.origin, run.kind);
242        self.restore_suffix_after_caret(suffix_runs);
243    }
244
245    fn restore_suffix_after_caret(&mut self, suffix_runs: Vec<InputRun>) {
246        let suffix_len = suffix_runs.iter().map(|run| run.text.chars().count()).sum();
247
248        for run in suffix_runs {
249            self.append_segment_end(&run.text, run.layout, run.origin, run.kind);
250        }
251
252        self.caret_from_end = suffix_len;
253    }
254
255    #[cfg(any(test, windows))]
256    fn push_text_internal(&mut self, text: &str, layout: LayoutTag, origin: RunOrigin) {
257        if text.is_empty() {
258            return;
259        }
260
261        // Segment the input without intermediate allocations by slicing `text` at kind boundaries.
262        // This keeps the journal cheap for frequent short inputs (single key presses) and also
263        // efficient for programmatic multi-character inserts.
264        let mut start = 0usize;
265        let mut current_kind: Option<RunKind> = None;
266
267        for (i, ch) in text.char_indices() {
268            let kind = if ch.is_whitespace() {
269                RunKind::Whitespace
270            } else {
271                RunKind::Text
272            };
273
274            match current_kind {
275                None => {
276                    start = i;
277                    current_kind = Some(kind);
278                }
279                Some(k) if k == kind => {}
280                Some(k) => {
281                    self.insert_run_before_caret(InputRun {
282                        text: text[start..i].to_string(),
283                        layout,
284                        origin,
285                        kind: k,
286                    });
287                    start = i;
288                    current_kind = Some(kind);
289                }
290            }
291        }
292
293        if let Some(kind) = current_kind {
294            self.insert_run_before_caret(InputRun {
295                text: text[start..].to_string(),
296                layout,
297                origin,
298                kind,
299            });
300        }
301    }
302
303    fn push_run(&mut self, run: InputRun) {
304        self.insert_run_before_caret(run);
305    }
306
307    fn push_runs(&mut self, runs: impl IntoIterator<Item = InputRun>) {
308        for run in runs {
309            self.push_run(run);
310        }
311    }
312
313    fn enforce_cap_chars(&mut self) {
314        while self.total_chars > self.cap_chars {
315            let mut remove_front_run = false;
316
317            if let Some(front) = self.runs.front_mut() {
318                if let Some((idx, _)) = front.text.char_indices().nth(1) {
319                    front.text.drain(..idx);
320                } else {
321                    front.text.clear();
322                    remove_front_run = true;
323                }
324                self.total_chars = self.total_chars.saturating_sub(1);
325
326                if front.text.is_empty() {
327                    remove_front_run = true;
328                }
329            } else {
330                self.total_chars = 0;
331                break;
332            }
333
334            if remove_front_run {
335                let _ = self.runs.pop_front();
336            }
337        }
338
339        self.caret_from_end = self.caret_from_end.min(self.total_chars);
340    }
341
342    #[cfg(any(test, windows))]
343    fn backspace(&mut self) {
344        let caret_from_end = self.caret_from_end.min(self.total_chars);
345        let suffix_runs = self.detach_suffix(caret_from_end);
346        let mut pop_last = false;
347
348        if let Some(last) = self.runs.back_mut()
349            && let Some((idx, _)) = last.text.char_indices().last()
350        {
351            last.text.drain(idx..);
352            self.total_chars = self.total_chars.saturating_sub(1);
353            if last.text.is_empty() {
354                pop_last = true;
355            }
356        }
357
358        if pop_last {
359            let _ = self.runs.pop_back();
360        }
361
362        self.restore_suffix_after_caret(suffix_runs);
363    }
364
365    #[cfg(any(test, windows))]
366    fn move_caret_left(&mut self) {
367        self.caret_from_end = self.caret_from_end.saturating_add(1).min(self.total_chars);
368    }
369
370    #[cfg(any(test, windows))]
371    fn move_caret_right(&mut self) {
372        self.caret_from_end = self.caret_from_end.saturating_sub(1);
373    }
374
375    #[cfg(windows)]
376    fn move_caret_end(&mut self) {
377        self.caret_from_end = 0;
378    }
379
380    fn detach_suffix(&mut self, count: usize) -> Vec<InputRun> {
381        let mut remaining = count.min(self.total_chars);
382        let mut suffix_rev = Vec::new();
383
384        while remaining > 0 {
385            let Some(mut run) = self.runs.pop_back() else {
386                self.total_chars = 0;
387                break;
388            };
389
390            let run_len = run.text.chars().count();
391            if run_len <= remaining {
392                self.total_chars = self.total_chars.saturating_sub(run_len);
393                remaining -= run_len;
394                suffix_rev.push(run);
395                continue;
396            }
397
398            let split_chars = run_len - remaining;
399            let Some((split_idx, _)) = run.text.char_indices().nth(split_chars) else {
400                self.runs.push_back(run);
401                break;
402            };
403            let suffix_text = run.text.split_off(split_idx);
404            let suffix_run = InputRun {
405                text: suffix_text,
406                layout: run.layout,
407                origin: run.origin,
408                kind: run.kind,
409            };
410            self.runs.push_back(run);
411            self.total_chars = self.total_chars.saturating_sub(remaining);
412            suffix_rev.push(suffix_run);
413            remaining = 0;
414        }
415
416        suffix_rev.reverse();
417        suffix_rev
418    }
419
420    #[cfg(windows)]
421    fn invalidate_if_foreground_changed(&mut self) {
422        let fg = unsafe { GetForegroundWindow() };
423        let raw = fg.0 as isize;
424        let key = current_focus_cache_key(fg);
425        self.switch_foreground(raw, key, unsafe { GetTickCount64() });
426    }
427
428    #[cfg(windows)]
429    fn switch_foreground(&mut self, raw: isize, key: Option<FocusCacheKey>, now_ms: u64) {
430        if raw == self.last_fg_hwnd && key == self.last_focus_key {
431            return;
432        }
433
434        if raw == self.last_fg_hwnd
435            && match (self.last_focus_key, key) {
436                (Some(prev), Some(next)) => same_focus_identity(prev, next),
437                (None, None) => true,
438                (Some(_), None) | (None, Some(_)) => true,
439            }
440        {
441            self.last_focus_key = key;
442            return;
443        }
444
445        with_journal_cache_mut(|cache| {
446            prune_stale_foreground_cache(cache, now_ms);
447
448            if self.last_fg_hwnd != 0
449                && let Some(last_key) = self.last_focus_key
450            {
451                cache.insert(
452                    last_key,
453                    CachedJournal {
454                        journal: self.clone(),
455                        updated_ms: now_ms,
456                    },
457                );
458            }
459
460            if raw == 0 {
461                self.clear();
462                self.last_fg_hwnd = 0;
463                self.last_focus_key = None;
464                return;
465            }
466
467            if let Some(key) = key
468                && let Some(cached) = cache.remove(&key)
469            {
470                *self = cached.journal;
471                self.last_fg_hwnd = raw;
472                self.last_focus_key = Some(key);
473                self.enforce_cap_chars();
474                return;
475            }
476
477            self.clear();
478            self.last_fg_hwnd = raw;
479            self.last_focus_key = key;
480        });
481    }
482
483    #[cfg(any(test, windows))]
484    fn last_char(&self) -> Option<char> {
485        self.chars_before_caret_rev().next()
486    }
487
488    #[cfg(any(test, windows))]
489    fn prev_char_before_last(&self) -> Option<char> {
490        self.chars_before_caret_rev().nth(1)
491    }
492
493    #[cfg(any(test, windows))]
494    fn chars_before_caret_rev(&self) -> impl Iterator<Item = char> + '_ {
495        let prefix_len = self.total_chars.saturating_sub(self.caret_from_end);
496        self.runs
497            .iter()
498            .flat_map(|run| run.text.chars())
499            .take(prefix_len)
500            .collect::<Vec<_>>()
501            .into_iter()
502            .rev()
503    }
504
505    fn take_last_layout_run_with_suffix(&mut self) -> Option<(InputRun, Vec<InputRun>)> {
506        let caret_from_end = self.caret_from_end.min(self.total_chars);
507        let suffix_after_caret = self.detach_suffix(caret_from_end);
508        self.caret_from_end = 0;
509
510        let mut suffix_runs = self.pop_suffix_whitespace();
511
512        let result = if self.runs.back().is_none_or(|run| run.kind != RunKind::Text) {
513            self.restore_suffix(&mut suffix_runs);
514            None
515        } else if let Some(run) = self.runs.pop_back() {
516            self.total_chars = self.total_chars.saturating_sub(run.text.chars().count());
517            suffix_runs.reverse();
518            Some((run, suffix_runs))
519        } else {
520            self.restore_suffix(&mut suffix_runs);
521            None
522        };
523
524        self.restore_suffix_after_caret(suffix_after_caret);
525        result
526    }
527
528    #[cfg(test)]
529    fn take_last_layout_sequence_with_suffix(&mut self) -> Option<(Vec<InputRun>, Vec<InputRun>)> {
530        let caret_from_end = self.caret_from_end.min(self.total_chars);
531        let suffix_after_caret = self.detach_suffix(caret_from_end);
532        self.caret_from_end = 0;
533
534        let mut suffix_runs = self.pop_suffix_whitespace();
535
536        let result = if self.runs.back().is_none_or(|run| run.kind != RunKind::Text) {
537            self.restore_suffix(&mut suffix_runs);
538            None
539        } else {
540            let Some(last) = self.runs.back() else {
541                self.restore_suffix(&mut suffix_runs);
542                self.restore_suffix_after_caret(suffix_after_caret);
543                return None;
544            };
545            let target_layout = last.layout;
546            let target_origin = last.origin;
547            let mut seq_rev: Vec<InputRun> = Vec::new();
548            while let Some(run) = self.runs.back() {
549                if run.layout != target_layout || run.origin != target_origin {
550                    break;
551                }
552                let Some(run) = self.runs.pop_back() else {
553                    break;
554                };
555                self.total_chars = self.total_chars.saturating_sub(run.text.chars().count());
556                seq_rev.push(run);
557            }
558
559            if seq_rev.is_empty() {
560                self.restore_suffix(&mut suffix_runs);
561                None
562            } else {
563                seq_rev.reverse();
564                suffix_runs.reverse();
565                Some((seq_rev, suffix_runs))
566            }
567        };
568
569        self.restore_suffix_after_caret(suffix_after_caret);
570        result
571    }
572
573    #[cfg(test)]
574    fn take_last_programmatic_sequence_with_suffix(
575        &mut self,
576    ) -> Option<(Vec<InputRun>, Vec<InputRun>)> {
577        let caret_from_end = self.caret_from_end.min(self.total_chars);
578        let suffix_after_caret = self.detach_suffix(caret_from_end);
579        self.caret_from_end = 0;
580
581        let mut suffix_runs = self.pop_suffix_whitespace();
582
583        let result = if self
584            .runs
585            .back()
586            .is_none_or(|run| run.origin != RunOrigin::Programmatic)
587        {
588            self.restore_suffix(&mut suffix_runs);
589            None
590        } else {
591            let mut seq_rev: Vec<InputRun> = Vec::new();
592            while let Some(run) = self.runs.back() {
593                if run.origin != RunOrigin::Programmatic {
594                    break;
595                }
596                let Some(run) = self.runs.pop_back() else {
597                    break;
598                };
599                self.total_chars = self.total_chars.saturating_sub(run.text.chars().count());
600                seq_rev.push(run);
601            }
602
603            if seq_rev.is_empty() {
604                self.restore_suffix(&mut suffix_runs);
605                None
606            } else {
607                seq_rev.reverse();
608                suffix_runs.reverse();
609                Some((seq_rev, suffix_runs))
610            }
611        };
612
613        self.restore_suffix_after_caret(suffix_after_caret);
614        result
615    }
616
617    fn take_last_sequence_with_suffix(&mut self) -> Option<(Vec<InputRun>, Vec<InputRun>)> {
618        let mut suffix_runs = self.pop_suffix_whitespace();
619
620        if self.runs.back().is_none_or(|run| run.kind != RunKind::Text) {
621            self.restore_suffix(&mut suffix_runs);
622            return None;
623        }
624
625        let mut seq_rev = Vec::new();
626        let last = self.runs.back()?;
627
628        if last.origin == RunOrigin::Programmatic {
629            while self
630                .runs
631                .back()
632                .is_some_and(|run| run.origin == RunOrigin::Programmatic)
633            {
634                let run = self.runs.pop_back()?;
635                self.total_chars = self.total_chars.saturating_sub(run.text.chars().count());
636                seq_rev.push(run);
637            }
638
639            let prefix_layout = self
640                .runs
641                .iter()
642                .rev()
643                .find(|run| run.origin == RunOrigin::Physical && run.kind == RunKind::Text)
644                .map(|run| run.layout);
645
646            if let Some(layout) = prefix_layout {
647                while self
648                    .runs
649                    .back()
650                    .is_some_and(|run| run.origin == RunOrigin::Physical && run.layout == layout)
651                {
652                    let run = self.runs.pop_back()?;
653                    self.total_chars = self.total_chars.saturating_sub(run.text.chars().count());
654                    seq_rev.push(run);
655                }
656            }
657        } else {
658            let target_layout = last.layout;
659            let target_origin = last.origin;
660            while let Some(run) = self.runs.back() {
661                if run.layout != target_layout || run.origin != target_origin {
662                    break;
663                }
664                let run = self.runs.pop_back()?;
665                self.total_chars = self.total_chars.saturating_sub(run.text.chars().count());
666                seq_rev.push(run);
667            }
668        }
669
670        if seq_rev.is_empty() {
671            self.restore_suffix(&mut suffix_runs);
672            return None;
673        }
674
675        seq_rev.reverse();
676        suffix_runs.reverse();
677        Some((seq_rev, suffix_runs))
678    }
679
680    fn pop_suffix_whitespace(&mut self) -> Vec<InputRun> {
681        let mut suffix_runs: Vec<InputRun> = Vec::new();
682        while self
683            .runs
684            .back()
685            .is_some_and(|run| run.kind == RunKind::Whitespace)
686        {
687            let Some(run) = self.runs.pop_back() else {
688                break;
689            };
690            self.total_chars = self.total_chars.saturating_sub(run.text.chars().count());
691            suffix_runs.push(run);
692        }
693        suffix_runs
694    }
695
696    fn restore_suffix(&mut self, suffix_runs: &mut Vec<InputRun>) {
697        // `suffix_runs` is expected to be in reverse order (from repeated `pop_back`).
698        while let Some(run) = suffix_runs.pop() {
699            self.total_chars += run.text.chars().count();
700            self.runs.push_back(run);
701        }
702    }
703}
704
705#[cfg(windows)]
706fn current_focus_cache_key(foreground: windows::Win32::Foundation::HWND) -> Option<FocusCacheKey> {
707    if foreground.0.is_null() {
708        return None;
709    }
710
711    let tid = unsafe { GetWindowThreadProcessId(foreground, None) };
712    if tid == 0 {
713        return None;
714    }
715
716    let mut info = GUITHREADINFO {
717        cbSize: std::mem::size_of::<GUITHREADINFO>() as u32,
718        ..Default::default()
719    };
720
721    if unsafe { GetGUIThreadInfo(tid, &mut info) }.is_err()
722        || info.hwndFocus.0.is_null()
723        || info.hwndCaret.0.is_null()
724    {
725        return None;
726    }
727
728    Some(FocusCacheKey {
729        foreground_hwnd: foreground.0 as isize,
730        focus_hwnd: info.hwndFocus.0 as isize,
731        caret_hwnd: info.hwndCaret.0 as isize,
732        caret_rect: CaretRectSignature {
733            left: info.rcCaret.left,
734            top: info.rcCaret.top,
735            right: info.rcCaret.right,
736            bottom: info.rcCaret.bottom,
737        },
738    })
739}
740
741#[cfg(windows)]
742#[derive(Debug)]
743struct DecodedText {
744    text: String,
745    layout: LayoutTag,
746}
747
748#[cfg(windows)]
749#[derive(Copy, Clone, Debug, Eq, PartialEq)]
750struct KeyboardStateOverrides {
751    shift_down: bool,
752    left_shift_down: bool,
753    right_shift_down: bool,
754    caps_lock_on: bool,
755}
756
757#[cfg(windows)]
758pub fn layout_tag_from_hkl(hkl: HKL) -> LayoutTag {
759    let hkl_raw = hkl.0 as usize;
760
761    if hkl_raw == 0 {
762        return LayoutTag::Unknown;
763    }
764
765    let lang_id = (hkl_raw & 0xFFFF) as u16;
766    let primary = lang_id & 0x03FF;
767
768    match primary {
769        LANG_ENGLISH_PRIMARY => LayoutTag::En,
770        LANG_RUSSIAN_PRIMARY => LayoutTag::Ru,
771        _ => LayoutTag::Other(lang_id),
772    }
773}
774
775#[cfg(windows)]
776fn current_foreground_layout_tag() -> LayoutTag {
777    let fg = unsafe { GetForegroundWindow() };
778    if fg.0.is_null() {
779        return LayoutTag::Unknown;
780    }
781
782    let tid = unsafe { GetWindowThreadProcessId(fg, None) };
783    let hkl = unsafe { GetKeyboardLayout(tid) };
784    layout_tag_from_hkl(hkl)
785}
786
787pub fn mark_last_token_autoconverted() {
788    with_journal_mut(|j| j.last_token_autoconverted = true);
789}
790
791#[cfg(any(test, windows))]
792#[must_use]
793pub fn last_token_autoconverted() -> bool {
794    with_journal(|j| j.last_token_autoconverted)
795}
796
797#[cfg(windows)]
798fn mods_ctrl_or_alt_down() -> bool {
799    // Keep this module independent from `crate::platform` so it can be built from the minimal lib target.
800    // VK_CONTROL = 0x11, VK_MENU (Alt) = 0x12.
801    let ctrl = unsafe { GetAsyncKeyState(0x11) }.cast_unsigned();
802    let alt = unsafe { GetAsyncKeyState(0x12) }.cast_unsigned();
803    (ctrl & 0x8000) != 0 || (alt & 0x8000) != 0
804}
805
806#[cfg(windows)]
807fn is_modifier_vk(vk: VIRTUAL_KEY) -> bool {
808    matches!(vk.0, 0xA0..=0xA5 | 0x5B | 0x5C)
809}
810
811#[cfg(windows)]
812fn should_clear_for_ctrl_alt_combo(vk: VIRTUAL_KEY, ctrl_or_alt_down: bool) -> bool {
813    ctrl_or_alt_down && !is_modifier_vk(vk)
814}
815
816#[cfg(windows)]
817fn key_is_down(vk: VIRTUAL_KEY) -> bool {
818    let value = unsafe { GetAsyncKeyState(i32::from(vk.0)) }.cast_unsigned();
819    (value & 0x8000) != 0
820}
821
822#[cfg(windows)]
823fn key_is_toggled(vk: VIRTUAL_KEY) -> bool {
824    let value = unsafe { GetKeyState(i32::from(vk.0)) }.cast_unsigned();
825    (value & 0x0001) != 0
826}
827
828#[cfg(windows)]
829fn set_key_down_state(state: &mut [u8; 256], vk: VIRTUAL_KEY, is_down: bool) {
830    let idx = usize::from(vk.0);
831    if idx >= state.len() {
832        return;
833    }
834
835    if is_down {
836        state[idx] |= 0x80;
837    } else {
838        state[idx] &= !0x80;
839    }
840}
841
842#[cfg(windows)]
843fn set_key_toggle_state(state: &mut [u8; 256], vk: VIRTUAL_KEY, is_toggled: bool) {
844    let idx = usize::from(vk.0);
845    if idx >= state.len() {
846        return;
847    }
848
849    if is_toggled {
850        state[idx] |= 0x01;
851    } else {
852        state[idx] &= !0x01;
853    }
854}
855
856#[cfg(windows)]
857fn current_keyboard_state_overrides() -> KeyboardStateOverrides {
858    KeyboardStateOverrides {
859        shift_down: key_is_down(VK_SHIFT),
860        left_shift_down: key_is_down(VK_LSHIFT),
861        right_shift_down: key_is_down(VK_RSHIFT),
862        caps_lock_on: key_is_toggled(VK_CAPITAL),
863    }
864}
865
866#[cfg(windows)]
867fn apply_keyboard_state_overrides(state: &mut [u8; 256], overrides: KeyboardStateOverrides) {
868    // LL-hook decoding runs before the target thread's keyboard state is fully reflected in our
869    // thread-local `GetKeyboardState` snapshot. Patch in the physical modifier/toggle state that
870    // affects character case before calling `ToUnicodeEx`.
871    set_key_down_state(state, VK_SHIFT, overrides.shift_down);
872    set_key_down_state(state, VK_LSHIFT, overrides.left_shift_down);
873    set_key_down_state(state, VK_RSHIFT, overrides.right_shift_down);
874    set_key_toggle_state(state, VK_CAPITAL, overrides.caps_lock_on);
875}
876
877#[cfg(windows)]
878fn decode_typed_text(kb: &KBDLLHOOKSTRUCT, vk: VIRTUAL_KEY) -> Option<DecodedText> {
879    let fg = unsafe { GetForegroundWindow() };
880    if fg.0.is_null() {
881        return None;
882    }
883
884    let tid = unsafe { GetWindowThreadProcessId(fg, None) };
885    let hkl = unsafe { GetKeyboardLayout(tid) };
886    let layout = layout_tag_from_hkl(hkl);
887
888    let mut state = [0u8; 256];
889    if unsafe { GetKeyboardState(&mut state) }.is_err() {
890        return None;
891    }
892
893    apply_keyboard_state_overrides(&mut state, current_keyboard_state_overrides());
894
895    let mut buf = [0u16; 8];
896    let rc = unsafe { ToUnicodeEx(u32::from(vk.0), kb.scanCode, &state, &mut buf, 0, Some(hkl)) };
897
898    if rc == -1 {
899        let _ =
900            unsafe { ToUnicodeEx(u32::from(vk.0), kb.scanCode, &state, &mut buf, 0, Some(hkl)) };
901        return None;
902    }
903
904    if rc <= 0 {
905        return None;
906    }
907
908    let rc = usize::try_from(rc).ok()?;
909    let s = String::from_utf16_lossy(&buf[..rc]);
910
911    if s.chars().any(char::is_control) {
912        return None;
913    }
914
915    Some(DecodedText { text: s, layout })
916}
917
918#[cfg(windows)]
919pub fn record_keydown(kb: &KBDLLHOOKSTRUCT, vk: u32) -> Option<String> {
920    if kb.flags.contains(LLKHF_INJECTED) && !allow_injected_input_for_e2e() {
921        return None;
922    }
923
924    let vk_u16 = u16::try_from(vk).ok()?;
925    let vk = VIRTUAL_KEY(vk_u16);
926
927    enum JournalAction {
928        Clear,
929        Backspace,
930        MoveCaretLeft,
931        MoveCaretRight,
932        MoveCaretEnd,
933        PushText {
934            text: String,
935            layout: LayoutTag,
936            origin: RunOrigin,
937        },
938    }
939
940    let mut action: Option<JournalAction> = None;
941    let mut output: Option<String> = None;
942
943    match vk {
944        VK_ESCAPE | VK_DELETE | VK_INSERT | VK_UP | VK_DOWN | VK_HOME | VK_PRIOR | VK_NEXT => {
945            action = Some(JournalAction::Clear);
946        }
947        VK_LEFT => action = Some(JournalAction::MoveCaretLeft),
948        VK_RIGHT => action = Some(JournalAction::MoveCaretRight),
949        VK_END => action = Some(JournalAction::MoveCaretEnd),
950        VK_BACK => action = Some(JournalAction::Backspace),
951        VK_RETURN => {
952            let layout = current_foreground_layout_tag();
953            output = Some("\n".to_string());
954            action = Some(JournalAction::PushText {
955                text: "\n".to_string(),
956                layout,
957                origin: RunOrigin::Physical,
958            });
959        }
960        VK_TAB => {
961            let layout = current_foreground_layout_tag();
962            output = Some("\t".to_string());
963            action = Some(JournalAction::PushText {
964                text: "\t".to_string(),
965                layout,
966                origin: RunOrigin::Physical,
967            });
968        }
969        _ => {}
970    }
971
972    if should_clear_for_ctrl_alt_combo(vk, mods_ctrl_or_alt_down()) {
973        action = Some(JournalAction::Clear);
974    }
975
976    if action.is_none() {
977        let decoded = decode_typed_text(kb, vk)?;
978        output = Some(decoded.text.clone());
979        action = Some(JournalAction::PushText {
980            text: decoded.text,
981            layout: decoded.layout,
982            origin: RunOrigin::Physical,
983        });
984    }
985
986    with_journal_mut(|j| {
987        j.invalidate_if_foreground_changed();
988        if let Some(action) = action {
989            match action {
990                JournalAction::Clear => j.clear(),
991                JournalAction::Backspace => j.backspace(),
992                JournalAction::MoveCaretLeft => j.move_caret_left(),
993                JournalAction::MoveCaretRight => j.move_caret_right(),
994                JournalAction::MoveCaretEnd => j.move_caret_end(),
995                JournalAction::PushText {
996                    text,
997                    layout,
998                    origin,
999                } => {
1000                    if text.chars().any(char::is_alphanumeric) {
1001                        j.last_token_autoconverted = false;
1002                    }
1003                    j.push_text_internal(&text, layout, origin);
1004                }
1005            }
1006        }
1007    });
1008
1009    output
1010}
1011
1012#[must_use]
1013pub fn take_last_layout_run_with_suffix() -> Option<(InputRun, Vec<InputRun>)> {
1014    with_journal_mut(|j| {
1015        #[cfg(windows)]
1016        if j.last_fg_hwnd != 0 {
1017            j.invalidate_if_foreground_changed();
1018        }
1019        j.take_last_layout_run_with_suffix()
1020    })
1021}
1022
1023#[cfg(test)]
1024#[must_use]
1025pub fn take_last_layout_sequence_with_suffix() -> Option<(Vec<InputRun>, Vec<InputRun>)> {
1026    with_journal_mut(|j| j.take_last_layout_sequence_with_suffix())
1027}
1028
1029#[cfg(test)]
1030#[must_use]
1031pub fn take_last_programmatic_sequence_with_suffix() -> Option<(Vec<InputRun>, Vec<InputRun>)> {
1032    with_journal_mut(|j| j.take_last_programmatic_sequence_with_suffix())
1033}
1034
1035#[must_use]
1036pub fn take_last_sequence_with_suffix() -> Option<(Vec<InputRun>, Vec<InputRun>)> {
1037    with_journal_mut(|j| {
1038        #[cfg(windows)]
1039        if j.last_fg_hwnd != 0 {
1040            j.invalidate_if_foreground_changed();
1041        }
1042        j.take_last_sequence_with_suffix()
1043    })
1044}
1045
1046#[cfg(test)]
1047pub fn push_text(s: &str) {
1048    with_journal_mut(|j| j.push_text_internal(s, LayoutTag::Unknown, RunOrigin::Programmatic));
1049}
1050
1051pub fn push_run(run: InputRun) {
1052    with_journal_mut(|j| j.push_run(run));
1053}
1054
1055pub fn push_runs(runs: impl IntoIterator<Item = InputRun>) {
1056    with_journal_mut(|j| j.push_runs(runs));
1057}
1058
1059#[cfg(test)]
1060pub fn test_backspace() {
1061    with_journal_mut(|j| j.backspace());
1062}
1063
1064#[cfg(test)]
1065pub fn test_move_caret_left(count: usize) {
1066    with_journal_mut(|j| {
1067        for _ in 0..count {
1068            j.move_caret_left();
1069        }
1070    });
1071}
1072
1073#[cfg(test)]
1074pub fn test_move_caret_right(count: usize) {
1075    with_journal_mut(|j| {
1076        for _ in 0..count {
1077            j.move_caret_right();
1078        }
1079    });
1080}
1081
1082#[cfg(test)]
1083pub fn runs_snapshot() -> Vec<InputRun> {
1084    with_journal(|j| j.runs.iter().cloned().collect())
1085}
1086
1087#[cfg(any(test, windows))]
1088pub fn invalidate() {
1089    with_journal_mut(|j| {
1090        j.clear();
1091        #[cfg(windows)]
1092        {
1093            j.last_fg_hwnd = 0;
1094            j.last_focus_key = None;
1095        }
1096    });
1097    #[cfg(all(test, windows))]
1098    clear_foreground_cache_for_test();
1099}
1100
1101#[cfg(all(test, windows))]
1102pub fn clear_foreground_cache_for_test() {
1103    with_journal_cache_mut(HashMap::clear);
1104}
1105
1106#[cfg(all(test, windows))]
1107pub fn test_switch_foreground(raw: isize, now_ms: u64) {
1108    with_journal_mut(|j| j.switch_foreground(raw, focus_key_for_test(raw, 0), now_ms));
1109}
1110
1111#[cfg(all(test, windows))]
1112fn focus_key_for_test(raw: isize, caret_left: i32) -> Option<FocusCacheKey> {
1113    (raw != 0).then_some(FocusCacheKey {
1114        foreground_hwnd: raw,
1115        focus_hwnd: raw + 10_000,
1116        caret_hwnd: raw + 20_000,
1117        caret_rect: CaretRectSignature {
1118            left: caret_left,
1119            top: 10,
1120            right: caret_left + 1,
1121            bottom: 30,
1122        },
1123    })
1124}
1125
1126#[cfg(all(test, windows))]
1127fn test_switch_foreground_at(raw: isize, caret_left: i32, now_ms: u64) {
1128    with_journal_mut(|j| j.switch_foreground(raw, focus_key_for_test(raw, caret_left), now_ms));
1129}
1130
1131#[cfg(all(test, windows))]
1132fn test_switch_foreground_control(
1133    raw: isize,
1134    focus_hwnd: isize,
1135    caret_hwnd: isize,
1136    caret_left: i32,
1137    now_ms: u64,
1138) {
1139    let key = (raw != 0).then_some(FocusCacheKey {
1140        foreground_hwnd: raw,
1141        focus_hwnd,
1142        caret_hwnd,
1143        caret_rect: CaretRectSignature {
1144            left: caret_left,
1145            top: 10,
1146            right: caret_left + 1,
1147            bottom: 30,
1148        },
1149    });
1150    with_journal_mut(|j| j.switch_foreground(raw, key, now_ms));
1151}
1152
1153#[cfg(all(test, windows))]
1154fn test_switch_foreground_without_signature(raw: isize, now_ms: u64) {
1155    with_journal_mut(|j| j.switch_foreground(raw, None, now_ms));
1156}
1157
1158#[cfg(all(test, windows))]
1159#[must_use]
1160pub fn raw_foreground_for_test() -> isize {
1161    with_journal(|j| j.last_fg_hwnd)
1162}
1163
1164#[cfg(any(test, windows))]
1165#[must_use]
1166pub fn last_char_triggers_autoconvert() -> bool {
1167    with_journal(|j| {
1168        let Some(last) = j.last_char() else {
1169            return false;
1170        };
1171
1172        if matches!(last, '.' | ',' | '!' | '?' | ';' | ':') {
1173            return j
1174                .prev_char_before_last()
1175                .is_some_and(|prev| !prev.is_whitespace());
1176        }
1177
1178        if last.is_whitespace() {
1179            return j
1180                .prev_char_before_last()
1181                .is_some_and(|prev| !prev.is_whitespace());
1182        }
1183
1184        false
1185    })
1186}
1187
1188#[cfg(all(test, windows))]
1189mod tests {
1190    use super::*;
1191
1192    fn test_run(text: &str, layout: LayoutTag) -> InputRun {
1193        InputRun {
1194            text: text.to_string(),
1195            layout,
1196            origin: RunOrigin::Physical,
1197            kind: RunKind::Text,
1198        }
1199    }
1200
1201    fn test_space() -> InputRun {
1202        InputRun {
1203            text: " ".to_string(),
1204            layout: LayoutTag::En,
1205            origin: RunOrigin::Physical,
1206            kind: RunKind::Whitespace,
1207        }
1208    }
1209
1210    fn test_programmatic_text(text: &str, layout: LayoutTag) -> InputRun {
1211        InputRun {
1212            text: text.to_string(),
1213            layout,
1214            origin: RunOrigin::Programmatic,
1215            kind: RunKind::Text,
1216        }
1217    }
1218
1219    fn test_programmatic_space(layout: LayoutTag) -> InputRun {
1220        InputRun {
1221            text: " ".to_string(),
1222            layout,
1223            origin: RunOrigin::Programmatic,
1224            kind: RunKind::Whitespace,
1225        }
1226    }
1227
1228    fn take_last_word_after_focus_refresh(
1229        raw: isize,
1230        caret_left: i32,
1231        now_ms: u64,
1232    ) -> Option<InputRun> {
1233        with_journal_mut(|j| {
1234            j.switch_foreground(raw, focus_key_for_test(raw, caret_left), now_ms);
1235            j.take_last_layout_run_with_suffix().map(|(run, suffix)| {
1236                j.push_runs(suffix);
1237                run
1238            })
1239        })
1240    }
1241
1242    fn take_last_sequence_after_focus_refresh(
1243        raw: isize,
1244        caret_left: i32,
1245        now_ms: u64,
1246    ) -> Option<Vec<InputRun>> {
1247        with_journal_mut(|j| {
1248            j.switch_foreground(raw, focus_key_for_test(raw, caret_left), now_ms);
1249            j.take_last_sequence_with_suffix().map(|(runs, suffix)| {
1250                j.push_runs(suffix);
1251                runs
1252            })
1253        })
1254    }
1255
1256    #[test]
1257    fn foreground_switch_restores_recent_window_journal() {
1258        let _guard = test_guard();
1259        invalidate();
1260        clear_foreground_cache_for_test();
1261
1262        test_switch_foreground(1001, 1_000);
1263        push_run(test_run("first", LayoutTag::En));
1264
1265        test_switch_foreground(2002, 1_100);
1266        assert!(runs_snapshot().is_empty());
1267        push_run(test_run("second", LayoutTag::Ru));
1268
1269        test_switch_foreground(1001, 1_200);
1270        assert_eq!(raw_foreground_for_test(), 1001);
1271        assert_eq!(
1272            runs_snapshot()
1273                .iter()
1274                .map(|run| run.text.as_str())
1275                .collect::<String>(),
1276            "first"
1277        );
1278
1279        test_switch_foreground(2002, 1_300);
1280        assert_eq!(raw_foreground_for_test(), 2002);
1281        assert_eq!(
1282            runs_snapshot()
1283                .iter()
1284                .map(|run| run.text.as_str())
1285                .collect::<String>(),
1286            "second"
1287        );
1288    }
1289
1290    #[test]
1291    fn foreground_switch_drops_stale_window_journal_after_ttl() {
1292        let _guard = test_guard();
1293        invalidate();
1294        clear_foreground_cache_for_test();
1295
1296        test_switch_foreground(1001, 1_000);
1297        push_run(test_run("stale", LayoutTag::En));
1298
1299        test_switch_foreground(2002, 2_000);
1300        test_switch_foreground(1001, 2_000 + FOREGROUND_CACHE_TTL_MS + 1);
1301
1302        assert_eq!(raw_foreground_for_test(), 1001);
1303        assert!(runs_snapshot().is_empty());
1304    }
1305
1306    #[test]
1307    fn foreground_switch_requires_matching_caret_signature_to_restore() {
1308        let _guard = test_guard();
1309        invalidate();
1310        clear_foreground_cache_for_test();
1311
1312        test_switch_foreground_at(1001, 10, 1_000);
1313        push_run(test_run("first", LayoutTag::En));
1314
1315        test_switch_foreground(2002, 1_100);
1316        assert!(runs_snapshot().is_empty());
1317
1318        test_switch_foreground_at(1001, 50, 1_200);
1319        assert_eq!(raw_foreground_for_test(), 1001);
1320        assert!(runs_snapshot().is_empty());
1321    }
1322
1323    #[test]
1324    fn foreground_switch_same_control_caret_movement_keeps_current_session() {
1325        let _guard = test_guard();
1326        invalidate();
1327        clear_foreground_cache_for_test();
1328
1329        test_switch_foreground_at(1001, 10, 1_000);
1330        push_run(test_run("first", LayoutTag::En));
1331
1332        test_switch_foreground_at(1001, 50, 1_100);
1333        assert_eq!(
1334            runs_snapshot()
1335                .iter()
1336                .map(|run| run.text.as_str())
1337                .collect::<String>(),
1338            "first"
1339        );
1340
1341        test_switch_foreground(2002, 1_200);
1342        test_switch_foreground_at(1001, 50, 1_300);
1343        assert_eq!(
1344            runs_snapshot()
1345                .iter()
1346                .map(|run| run.text.as_str())
1347                .collect::<String>(),
1348            "first"
1349        );
1350
1351        test_switch_foreground(2002, 1_400);
1352        test_switch_foreground_at(1001, 10, 1_500);
1353        assert!(runs_snapshot().is_empty());
1354    }
1355
1356    #[test]
1357    fn foreground_switch_restores_distinct_controls_in_same_window() {
1358        let _guard = test_guard();
1359        invalidate();
1360        clear_foreground_cache_for_test();
1361
1362        test_switch_foreground_control(1001, 11, 21, 10, 1_000);
1363        push_run(test_run("first", LayoutTag::En));
1364
1365        test_switch_foreground_control(1001, 12, 22, 10, 1_100);
1366        assert!(runs_snapshot().is_empty());
1367        push_run(test_run("second", LayoutTag::Ru));
1368
1369        test_switch_foreground_control(1001, 11, 21, 10, 1_200);
1370        assert_eq!(
1371            runs_snapshot()
1372                .iter()
1373                .map(|run| run.text.as_str())
1374                .collect::<String>(),
1375            "first"
1376        );
1377
1378        test_switch_foreground_control(1001, 12, 22, 10, 1_300);
1379        assert_eq!(
1380            runs_snapshot()
1381                .iter()
1382                .map(|run| run.text.as_str())
1383                .collect::<String>(),
1384            "second"
1385        );
1386    }
1387
1388    #[test]
1389    fn foreground_switch_without_signature_keeps_current_session_but_does_not_restore() {
1390        let _guard = test_guard();
1391        invalidate();
1392        clear_foreground_cache_for_test();
1393
1394        test_switch_foreground_without_signature(1001, 1_000);
1395        push_run(test_run("unsupported", LayoutTag::En));
1396
1397        test_switch_foreground_without_signature(1001, 1_100);
1398        assert_eq!(
1399            runs_snapshot()
1400                .iter()
1401                .map(|run| run.text.as_str())
1402                .collect::<String>(),
1403            "unsupported"
1404        );
1405
1406        test_switch_foreground(2002, 1_200);
1407        assert!(runs_snapshot().is_empty());
1408
1409        test_switch_foreground_without_signature(1001, 1_300);
1410        assert_eq!(raw_foreground_for_test(), 1001);
1411        assert!(runs_snapshot().is_empty());
1412    }
1413
1414    #[test]
1415    fn command_style_last_word_replacement_survives_same_control_caret_signature_change() {
1416        let _guard = test_guard();
1417        invalidate();
1418        clear_foreground_cache_for_test();
1419
1420        test_switch_foreground_at(1001, 10, 1_000);
1421        push_runs([
1422            test_run("ghbdtn", LayoutTag::En),
1423            test_space(),
1424            test_run("rfr", LayoutTag::En),
1425            test_space(),
1426            test_run("ltkf", LayoutTag::En),
1427        ]);
1428
1429        let word = take_last_word_after_focus_refresh(1001, 50, 1_100)
1430            .expect("last word should survive caret movement");
1431        assert_eq!(word.text, "ltkf");
1432
1433        push_run(test_programmatic_text("дела", LayoutTag::Ru));
1434
1435        let sequence = take_last_sequence_after_focus_refresh(1001, 70, 1_200)
1436            .expect("sequence should remain available after replacement");
1437        assert_eq!(
1438            sequence
1439                .iter()
1440                .map(|run| run.text.as_str())
1441                .collect::<String>(),
1442            "ghbdtn rfr дела"
1443        );
1444    }
1445
1446    #[test]
1447    fn command_style_sequence_replacement_keeps_word_and_sequence_commands_available() {
1448        let _guard = test_guard();
1449        invalidate();
1450        clear_foreground_cache_for_test();
1451
1452        test_switch_foreground_at(1001, 10, 1_000);
1453        push_runs([
1454            test_run("ghbdtn", LayoutTag::En),
1455            test_space(),
1456            test_run("rfr", LayoutTag::En),
1457            test_space(),
1458            test_run("ltkf", LayoutTag::En),
1459        ]);
1460
1461        let sequence = take_last_sequence_after_focus_refresh(1001, 50, 1_100)
1462            .expect("sequence should survive caret movement");
1463        assert_eq!(
1464            sequence
1465                .iter()
1466                .map(|run| run.text.as_str())
1467                .collect::<String>(),
1468            "ghbdtn rfr ltkf"
1469        );
1470
1471        push_runs([
1472            test_programmatic_text("привет", LayoutTag::Ru),
1473            test_programmatic_space(LayoutTag::Ru),
1474            test_programmatic_text("как", LayoutTag::Ru),
1475            test_programmatic_space(LayoutTag::Ru),
1476            test_programmatic_text("дела", LayoutTag::Ru),
1477        ]);
1478
1479        let word = take_last_word_after_focus_refresh(1001, 80, 1_200)
1480            .expect("last word should remain available after sequence replacement");
1481        assert_eq!(word.text, "дела");
1482        push_run(word);
1483
1484        let sequence = take_last_sequence_after_focus_refresh(1001, 90, 1_300)
1485            .expect("sequence should remain available after sequence replacement");
1486        assert_eq!(
1487            sequence
1488                .iter()
1489                .map(|run| run.text.as_str())
1490                .collect::<String>(),
1491            "привет как дела"
1492        );
1493    }
1494
1495    #[test]
1496    fn ctrl_or_alt_combo_preserves_modifier_only_layout_switch_chords() {
1497        assert!(!should_clear_for_ctrl_alt_combo(VK_LSHIFT, true));
1498        assert!(!should_clear_for_ctrl_alt_combo(VK_RSHIFT, true));
1499        assert!(!should_clear_for_ctrl_alt_combo(VIRTUAL_KEY(0xA4), true));
1500        assert!(!should_clear_for_ctrl_alt_combo(VIRTUAL_KEY(0xA5), true));
1501    }
1502
1503    #[test]
1504    fn ctrl_or_alt_combo_still_clears_non_modifier_keys() {
1505        assert!(should_clear_for_ctrl_alt_combo(
1506            VIRTUAL_KEY(u16::from(b'A')),
1507            true
1508        ));
1509        assert!(should_clear_for_ctrl_alt_combo(VK_TAB, true));
1510        assert!(!should_clear_for_ctrl_alt_combo(
1511            VIRTUAL_KEY(u16::from(b'A')),
1512            false
1513        ));
1514    }
1515
1516    #[test]
1517    fn keyboard_state_overrides_apply_caps_lock_toggle_without_touching_high_bit() {
1518        let mut state = [0u8; 256];
1519        apply_keyboard_state_overrides(
1520            &mut state,
1521            KeyboardStateOverrides {
1522                shift_down: false,
1523                left_shift_down: false,
1524                right_shift_down: false,
1525                caps_lock_on: true,
1526            },
1527        );
1528
1529        let caps = state[usize::from(VK_CAPITAL.0)];
1530        assert_eq!(caps & 0x01, 0x01);
1531        assert_eq!(caps & 0x80, 0x00);
1532    }
1533
1534    #[test]
1535    fn keyboard_state_overrides_preserve_shift_and_caps_lock_combination() {
1536        let mut state = [0u8; 256];
1537        apply_keyboard_state_overrides(
1538            &mut state,
1539            KeyboardStateOverrides {
1540                shift_down: true,
1541                left_shift_down: true,
1542                right_shift_down: false,
1543                caps_lock_on: true,
1544            },
1545        );
1546
1547        assert_eq!(state[usize::from(VK_SHIFT.0)] & 0x80, 0x80);
1548        assert_eq!(state[usize::from(VK_LSHIFT.0)] & 0x80, 0x80);
1549        assert_eq!(state[usize::from(VK_RSHIFT.0)] & 0x80, 0x00);
1550        assert_eq!(state[usize::from(VK_CAPITAL.0)] & 0x01, 0x01);
1551    }
1552}