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