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 pop_suffix_whitespace(&mut self) -> Vec<InputRun> {
324 let mut suffix_runs: Vec<InputRun> = Vec::new();
325 while self
326 .runs
327 .back()
328 .is_some_and(|run| run.kind == RunKind::Whitespace)
329 {
330 let Some(run) = self.runs.pop_back() else {
331 break;
332 };
333 self.total_chars = self.total_chars.saturating_sub(run.text.chars().count());
334 suffix_runs.push(run);
335 }
336 suffix_runs
337 }
338
339 fn restore_suffix(&mut self, suffix_runs: &mut Vec<InputRun>) {
340 while let Some(run) = suffix_runs.pop() {
342 self.total_chars += run.text.chars().count();
343 self.runs.push_back(run);
344 }
345 }
346}
347
348#[cfg(windows)]
349#[derive(Debug)]
350struct DecodedText {
351 text: String,
352 layout: LayoutTag,
353}
354
355#[cfg(windows)]
356#[derive(Copy, Clone, Debug, Eq, PartialEq)]
357struct KeyboardStateOverrides {
358 shift_down: bool,
359 left_shift_down: bool,
360 right_shift_down: bool,
361 caps_lock_on: bool,
362}
363
364#[cfg(windows)]
365pub fn layout_tag_from_hkl(hkl: HKL) -> LayoutTag {
366 let hkl_raw = hkl.0 as usize;
367
368 if hkl_raw == 0 {
369 return LayoutTag::Unknown;
370 }
371
372 let lang_id = (hkl_raw & 0xFFFF) as u16;
373 let primary = lang_id & 0x03FF;
374
375 match primary {
376 LANG_ENGLISH_PRIMARY => LayoutTag::En,
377 LANG_RUSSIAN_PRIMARY => LayoutTag::Ru,
378 _ => LayoutTag::Other(lang_id),
379 }
380}
381
382#[cfg(windows)]
383fn current_foreground_layout_tag() -> LayoutTag {
384 let fg = unsafe { GetForegroundWindow() };
385 if fg.0.is_null() {
386 return LayoutTag::Unknown;
387 }
388
389 let tid = unsafe { GetWindowThreadProcessId(fg, None) };
390 let hkl = unsafe { GetKeyboardLayout(tid) };
391 layout_tag_from_hkl(hkl)
392}
393
394pub fn mark_last_token_autoconverted() {
395 with_journal_mut(|j| j.last_token_autoconverted = true);
396}
397
398#[cfg(any(test, windows))]
399#[must_use]
400pub fn last_token_autoconverted() -> bool {
401 with_journal(|j| j.last_token_autoconverted)
402}
403
404#[cfg(windows)]
405fn mods_ctrl_or_alt_down() -> bool {
406 let ctrl = unsafe { GetAsyncKeyState(0x11) }.cast_unsigned();
409 let alt = unsafe { GetAsyncKeyState(0x12) }.cast_unsigned();
410 (ctrl & 0x8000) != 0 || (alt & 0x8000) != 0
411}
412
413#[cfg(windows)]
414fn key_is_down(vk: VIRTUAL_KEY) -> bool {
415 let value = unsafe { GetAsyncKeyState(i32::from(vk.0)) }.cast_unsigned();
416 (value & 0x8000) != 0
417}
418
419#[cfg(windows)]
420fn key_is_toggled(vk: VIRTUAL_KEY) -> bool {
421 let value = unsafe { GetKeyState(i32::from(vk.0)) }.cast_unsigned();
422 (value & 0x0001) != 0
423}
424
425#[cfg(windows)]
426fn set_key_down_state(state: &mut [u8; 256], vk: VIRTUAL_KEY, is_down: bool) {
427 let idx = usize::from(vk.0);
428 if idx >= state.len() {
429 return;
430 }
431
432 if is_down {
433 state[idx] |= 0x80;
434 } else {
435 state[idx] &= !0x80;
436 }
437}
438
439#[cfg(windows)]
440fn set_key_toggle_state(state: &mut [u8; 256], vk: VIRTUAL_KEY, is_toggled: bool) {
441 let idx = usize::from(vk.0);
442 if idx >= state.len() {
443 return;
444 }
445
446 if is_toggled {
447 state[idx] |= 0x01;
448 } else {
449 state[idx] &= !0x01;
450 }
451}
452
453#[cfg(windows)]
454fn current_keyboard_state_overrides() -> KeyboardStateOverrides {
455 KeyboardStateOverrides {
456 shift_down: key_is_down(VK_SHIFT),
457 left_shift_down: key_is_down(VK_LSHIFT),
458 right_shift_down: key_is_down(VK_RSHIFT),
459 caps_lock_on: key_is_toggled(VK_CAPITAL),
460 }
461}
462
463#[cfg(windows)]
464fn apply_keyboard_state_overrides(state: &mut [u8; 256], overrides: KeyboardStateOverrides) {
465 set_key_down_state(state, VK_SHIFT, overrides.shift_down);
469 set_key_down_state(state, VK_LSHIFT, overrides.left_shift_down);
470 set_key_down_state(state, VK_RSHIFT, overrides.right_shift_down);
471 set_key_toggle_state(state, VK_CAPITAL, overrides.caps_lock_on);
472}
473
474#[cfg(windows)]
475fn decode_typed_text(kb: &KBDLLHOOKSTRUCT, vk: VIRTUAL_KEY) -> Option<DecodedText> {
476 let fg = unsafe { GetForegroundWindow() };
477 if fg.0.is_null() {
478 return None;
479 }
480
481 let tid = unsafe { GetWindowThreadProcessId(fg, None) };
482 let hkl = unsafe { GetKeyboardLayout(tid) };
483 let layout = layout_tag_from_hkl(hkl);
484
485 let mut state = [0u8; 256];
486 if unsafe { GetKeyboardState(&mut state) }.is_err() {
487 return None;
488 }
489
490 apply_keyboard_state_overrides(&mut state, current_keyboard_state_overrides());
491
492 let mut buf = [0u16; 8];
493 let rc = unsafe { ToUnicodeEx(u32::from(vk.0), kb.scanCode, &state, &mut buf, 0, Some(hkl)) };
494
495 if rc == -1 {
496 let _ =
497 unsafe { ToUnicodeEx(u32::from(vk.0), kb.scanCode, &state, &mut buf, 0, Some(hkl)) };
498 return None;
499 }
500
501 if rc <= 0 {
502 return None;
503 }
504
505 let rc = usize::try_from(rc).ok()?;
506 let s = String::from_utf16_lossy(&buf[..rc]);
507
508 if s.chars().any(char::is_control) {
509 return None;
510 }
511
512 Some(DecodedText { text: s, layout })
513}
514
515#[cfg(windows)]
516pub fn record_keydown(kb: &KBDLLHOOKSTRUCT, vk: u32) -> Option<String> {
517 if kb.flags.contains(LLKHF_INJECTED) {
518 return None;
519 }
520
521 let vk_u16 = u16::try_from(vk).ok()?;
522 let vk = VIRTUAL_KEY(vk_u16);
523
524 enum JournalAction {
525 Clear,
526 Backspace,
527 PushText {
528 text: String,
529 layout: LayoutTag,
530 origin: RunOrigin,
531 },
532 }
533
534 let mut action: Option<JournalAction> = None;
535 let mut output: Option<String> = None;
536
537 match vk {
538 VK_ESCAPE | VK_DELETE | VK_INSERT | VK_LEFT | VK_RIGHT | VK_UP | VK_DOWN | VK_HOME
539 | VK_END | VK_PRIOR | VK_NEXT => action = Some(JournalAction::Clear),
540 VK_BACK => action = Some(JournalAction::Backspace),
541 VK_RETURN => {
542 let layout = current_foreground_layout_tag();
543 output = Some("\n".to_string());
544 action = Some(JournalAction::PushText {
545 text: "\n".to_string(),
546 layout,
547 origin: RunOrigin::Physical,
548 });
549 }
550 VK_TAB => {
551 let layout = current_foreground_layout_tag();
552 output = Some("\t".to_string());
553 action = Some(JournalAction::PushText {
554 text: "\t".to_string(),
555 layout,
556 origin: RunOrigin::Physical,
557 });
558 }
559 _ => {}
560 }
561
562 if mods_ctrl_or_alt_down() {
563 action = Some(JournalAction::Clear);
564 }
565
566 if action.is_none() {
567 let decoded = decode_typed_text(kb, vk)?;
568 output = Some(decoded.text.clone());
569 action = Some(JournalAction::PushText {
570 text: decoded.text,
571 layout: decoded.layout,
572 origin: RunOrigin::Physical,
573 });
574 }
575
576 with_journal_mut(|j| {
577 j.invalidate_if_foreground_changed();
578 if let Some(action) = action {
579 match action {
580 JournalAction::Clear => j.clear(),
581 JournalAction::Backspace => j.backspace(),
582 JournalAction::PushText {
583 text,
584 layout,
585 origin,
586 } => {
587 if text.chars().any(char::is_alphanumeric) {
588 j.last_token_autoconverted = false;
589 }
590 j.push_text_internal(&text, layout, origin);
591 }
592 }
593 }
594 });
595
596 output
597}
598
599#[must_use]
600pub fn take_last_layout_run_with_suffix() -> Option<(InputRun, Vec<InputRun>)> {
601 with_journal_mut(|j| j.take_last_layout_run_with_suffix())
602}
603
604#[must_use]
605pub fn take_last_layout_sequence_with_suffix() -> Option<(Vec<InputRun>, Vec<InputRun>)> {
606 with_journal_mut(|j| j.take_last_layout_sequence_with_suffix())
607}
608
609#[cfg(test)]
610pub fn push_text(s: &str) {
611 with_journal_mut(|j| j.push_text_internal(s, LayoutTag::Unknown, RunOrigin::Programmatic));
612}
613
614pub fn push_run(run: InputRun) {
615 with_journal_mut(|j| j.push_run(run));
616}
617
618pub fn push_runs(runs: impl IntoIterator<Item = InputRun>) {
619 with_journal_mut(|j| j.push_runs(runs));
620}
621
622#[cfg(any(test, windows))]
623pub fn push_text_with_meta(text: &str, layout: LayoutTag, origin: RunOrigin) {
624 with_journal_mut(|j| j.push_text_internal(text, layout, origin));
625}
626
627#[cfg(test)]
628pub fn test_backspace() {
629 with_journal_mut(|j| j.backspace());
630}
631
632#[cfg(test)]
633pub fn runs_snapshot() -> Vec<InputRun> {
634 with_journal(|j| j.runs.iter().cloned().collect())
635}
636
637#[cfg(any(test, windows))]
638pub fn invalidate() {
639 with_journal_mut(|j| j.clear());
640}
641
642#[cfg(any(test, windows))]
643#[must_use]
644pub fn last_char_triggers_autoconvert() -> bool {
645 with_journal(|j| {
646 let Some(last) = j.last_char() else {
647 return false;
648 };
649
650 if matches!(last, '.' | ',' | '!' | '?' | ';' | ':') {
651 return j
652 .prev_char_before_last()
653 .is_some_and(|prev| !prev.is_whitespace());
654 }
655
656 if last.is_whitespace() {
657 return j
658 .prev_char_before_last()
659 .is_some_and(|prev| !prev.is_whitespace());
660 }
661
662 false
663 })
664}
665
666#[cfg(all(test, windows))]
667mod tests {
668 use super::*;
669
670 #[test]
671 fn keyboard_state_overrides_apply_caps_lock_toggle_without_touching_high_bit() {
672 let mut state = [0u8; 256];
673 apply_keyboard_state_overrides(
674 &mut state,
675 KeyboardStateOverrides {
676 shift_down: false,
677 left_shift_down: false,
678 right_shift_down: false,
679 caps_lock_on: true,
680 },
681 );
682
683 let caps = state[usize::from(VK_CAPITAL.0)];
684 assert_eq!(caps & 0x01, 0x01);
685 assert_eq!(caps & 0x80, 0x00);
686 }
687
688 #[test]
689 fn keyboard_state_overrides_preserve_shift_and_caps_lock_combination() {
690 let mut state = [0u8; 256];
691 apply_keyboard_state_overrides(
692 &mut state,
693 KeyboardStateOverrides {
694 shift_down: true,
695 left_shift_down: true,
696 right_shift_down: false,
697 caps_lock_on: true,
698 },
699 );
700
701 assert_eq!(state[usize::from(VK_SHIFT.0)] & 0x80, 0x80);
702 assert_eq!(state[usize::from(VK_LSHIFT.0)] & 0x80, 0x80);
703 assert_eq!(state[usize::from(VK_RSHIFT.0)] & 0x80, 0x00);
704 assert_eq!(state[usize::from(VK_CAPITAL.0)] & 0x01, 0x01);
705 }
706}