Skip to main content

rust_switcher/input/
ring_buffer.rs

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    // Keep this module independent from `crate::platform` so it can be built from the minimal lib target.
373    // VK_CONTROL = 0x11, VK_MENU (Alt) = 0x12.
374    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}