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 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 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 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 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}