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
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        // Segment the input without intermediate allocations by slicing `text` at kind boundaries.
144        // This keeps the journal cheap for frequent short inputs (single key presses) and also
145        // efficient for programmatic multi-character inserts.
146        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        // `suffix_runs` is expected to be in reverse order (from repeated `pop_back`).
340        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    // Keep this module independent from `crate::platform` so it can be built from the minimal lib target.
397    // VK_CONTROL = 0x11, VK_MENU (Alt) = 0x12.
398    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}