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