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