Skip to main content

justerm_core/
input.rs

1//! Input encoding (#11): consumer events → the bytes an application expects.
2//!
3//! The inverse of `feed` — a key/mouse/paste/focus event becomes the byte
4//! sequence a TUI app reads on its stdin, decided by the DEC modes the engine
5//! tracks from the *output* stream (DECCKM, mouse tracking/encoding, focus,
6//! bracketed paste). The engine owns the modes; these functions are pure
7//! (event + modes → bytes), so the consumer's I/O stays its own concern.
8//!
9//! This is the **legacy xterm** baseline (the common-90% every TUI speaks). The
10//! kitty keyboard protocol (`CSI u` + a negotiated progressive-flag stack) is a
11//! stateful superset deferred to #23.
12
13use bitflags::bitflags;
14
15bitflags! {
16    /// Modifier keys held during an event. The bit values follow the **kitty**
17    /// scheme (the superset): Shift=1, Alt=2, Ctrl=4, Super=8, Hyper=16, Meta=32,
18    /// CapsLock=64, NumLock=128. Legacy xterm can only express the first three
19    /// plus Meta-at-8, so `csi_param` remaps; kitty uses the bits directly (#23).
20    #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21    pub struct Modifiers: u8 {
22        const SHIFT     = 1;
23        const ALT       = 2;
24        const CTRL      = 4;
25        const SUPER     = 8;
26        const HYPER     = 16;
27        const META      = 32;
28        const CAPS_LOCK = 64;
29        const NUM_LOCK  = 128;
30    }
31}
32
33impl Modifiers {
34    /// The legacy xterm CSI modifier parameter (`1 + bitmask`, Shift=1/Alt=2/
35    /// Ctrl=4/Meta=8), or `None` when none of the legacy-expressible modifiers is
36    /// held. Super/Hyper/CapsLock/NumLock have no legacy form and are dropped.
37    fn csi_param(self) -> Option<u8> {
38        let mut bits = 0u8;
39        if self.contains(Modifiers::SHIFT) {
40            bits |= 1;
41        }
42        if self.contains(Modifiers::ALT) {
43            bits |= 2;
44        }
45        if self.contains(Modifiers::CTRL) {
46            bits |= 4;
47        }
48        if self.contains(Modifiers::META) {
49            bits |= 8;
50        }
51        if bits == 0 { None } else { Some(1 + bits) }
52    }
53
54    /// The kitty CSI modifier parameter (`1 + bits`) — the bit values already
55    /// match the kitty scheme, so all eight modifiers are expressible (#23).
56    fn kitty_param(self) -> Option<u8> {
57        if self.is_empty() {
58            None
59        } else {
60            Some(1 + self.bits())
61        }
62    }
63}
64
65/// A numeric-keypad key. In application-keypad mode (DECNKM ?66 / DECKPAM, #74)
66/// these encode as the classic VT100/VT220 SS3 sequences; in numeric mode as the
67/// literal character. The consumer produces these for *raw* keypad identity — it
68/// owns NumLock / key-location resolution (#83).
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum KeypadKey {
71    /// A keypad digit, `0..=9`.
72    Digit(u8),
73    Decimal,
74    Enter,
75    Add,
76    Subtract,
77    Multiply,
78    Divide,
79    Equal,
80}
81
82/// A logical key press from the consumer (already decoded from the platform's
83/// keyboard event — justerm does not read hardware).
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum Key {
86    /// A printable character (the consumer's already-composed text).
87    Char(char),
88    /// A numeric-keypad key (encoded per application-keypad mode, #83).
89    Keypad(KeypadKey),
90    Up,
91    Down,
92    Right,
93    Left,
94    Home,
95    End,
96    PageUp,
97    PageDown,
98    Insert,
99    Delete,
100    Enter,
101    Tab,
102    Backspace,
103    Escape,
104    /// Function key `F(n)`, `n` in 1..=12.
105    F(u8),
106}
107
108/// Press / repeat / release. Legacy reports only presses; the kitty protocol's
109/// "report event types" flag (bit 1) carries repeat and release too (#23).
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
111pub enum KeyAction {
112    #[default]
113    Press,
114    Repeat,
115    Release,
116}
117
118/// A key event: a key, the modifiers held with it, its press/repeat/release type
119/// (defaults to `Press`), and consumer-supplied extras the kitty protocol's
120/// alternate-keys / associated-text flags report (all `None` for legacy).
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub struct KeyEvent {
123    pub key: Key,
124    pub mods: Modifiers,
125    pub action: KeyAction,
126    /// Kitty alternate-keys (bit 2): the codepoint Shift would produce, if it
127    /// differs from `key`.
128    pub shifted_key: Option<char>,
129    /// Kitty alternate-keys (bit 2): the codepoint at this key's position on the
130    /// base (standard) layout, if it differs from `key`.
131    pub base_key: Option<char>,
132    /// Kitty associated-text (bit 4): the text the key actually produced
133    /// (composed input / dead keys).
134    pub text: Option<char>,
135}
136
137impl Default for KeyEvent {
138    fn default() -> Self {
139        KeyEvent {
140            key: Key::Char('\0'),
141            mods: Modifiers::empty(),
142            action: KeyAction::Press,
143            shifted_key: None,
144            base_key: None,
145            text: None,
146        }
147    }
148}
149
150/// Which mouse button an event concerns. `None` on a [`MouseEvent`] means bare
151/// motion with no button held.
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum MouseButton {
154    Left,
155    Middle,
156    Right,
157    WheelUp,
158    WheelDown,
159    /// Horizontal scroll / tilt-wheel — xterm buttons 6 and 7, encoded in the
160    /// same 64-base wheel group as up/down.
161    WheelLeft,
162    WheelRight,
163    /// The thumb buttons — X11 buttons 8 and 9, the "back"/"forward" of the
164    /// 128-base extra group.
165    Back,
166    Forward,
167    /// Any further mouse button by its X11 number (gaming-mouse side buttons,
168    /// 10+). Encoded via the xterm bit formula; use the named variants above for
169    /// buttons that have one.
170    Other(u8),
171}
172
173/// What the mouse did.
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub enum MouseAction {
176    Press,
177    Release,
178    Motion,
179}
180
181/// A mouse event in viewport cell coordinates (0-based — the encoding shifts to
182/// 1-based on the wire).
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub struct MouseEvent {
185    /// The button, or `None` for bare motion (no button held).
186    pub button: Option<MouseButton>,
187    pub action: MouseAction,
188    pub col: usize,
189    pub row: usize,
190    /// 0-based pixel coordinates, used only by the `?1016` SGR-pixels encoding —
191    /// the consumer (which has the window geometry) supplies them; the engine
192    /// only formats them. Ignored by the cell-based encodings.
193    pub px: usize,
194    pub py: usize,
195    pub mods: Modifiers,
196}
197
198/// Mouse tracking mode — *what* the app asked to be reported (DEC `?1000` /
199/// `?1002` / `?1003`).
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
201pub enum MouseProtocol {
202    /// No reporting (default). `encode_mouse` returns `None`.
203    #[default]
204    Off,
205    /// `?9` — the original X10 protocol: button **press only**, no release, no
206    /// motion, no wheel, and no modifier bits.
207    X10,
208    /// `?1000` — button press and release only.
209    Normal,
210    /// `?1002` — also motion while a button is held (drag).
211    ButtonEvent,
212    /// `?1003` — also motion with no button held.
213    AnyEvent,
214}
215
216/// Mouse coordinate encoding — *how* a report is framed (default X10 vs DEC
217/// `?1006` SGR).
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
219pub enum MouseEncoding {
220    /// X10 `CSI M Cb Cx Cy`, each value offset by 32 — breaks past column 223.
221    #[default]
222    Default,
223    /// `?1006` SGR `CSI < Cb ; Cx ; Cy M|m` — coords unbounded, release distinct.
224    Sgr,
225    /// `?1015` urxvt `CSI Cb ; Cx ; Cy M` — the Default byte semantics (Cb with
226    /// the +32 base, release loses button identity) as decimal params, always
227    /// terminated by `M`. Unbounded coords, no separate release form.
228    Urxvt,
229    /// `?1005` UTF-8 — the Default `CSI M Cb Cx Cy` framing but each value
230    /// UTF-8-encoded, so values past 127 become multi-byte (extends the range).
231    Utf8,
232    /// `?1016` SGR-pixels — the SGR framing but the coordinates are pixels
233    /// (from `MouseEvent::px`/`py`) instead of cells.
234    SgrPixels,
235}
236
237const ESC: u8 = 0x1b;
238
239/// Bit 0 of the kitty progressive-enhancement flags: disambiguate escape codes.
240const KITTY_DISAMBIGUATE: u8 = 0b1;
241
242/// Encode a key event to bytes, given whether DECCKM (application cursor keys)
243/// is active and the kitty keyboard-protocol flags. Returns `None` only for keys
244/// with no defined encoding.
245pub fn encode_key(
246    ev: &KeyEvent,
247    app_cursor: bool,
248    app_keypad: bool,
249    kitty_flags: u8,
250) -> Option<Vec<u8>> {
251    // Under the kitty protocol, events legacy cannot express (a modifier on a
252    // text key, a release/repeat) take the `CSI unicode ; mods : event u` form;
253    // everything else falls through to legacy (#23).
254    if kitty_flags != 0
255        && let Some(bytes) = kitty_encode(ev, kitty_flags)
256    {
257        return Some(bytes);
258    }
259    match ev.key {
260        Key::Char(c) => Some(encode_char(c, ev.mods)),
261        Key::Keypad(k) => Some(keypad_key(k, app_keypad)),
262        Key::Up => Some(cursor_key(b'A', ev.mods, app_cursor)),
263        Key::Down => Some(cursor_key(b'B', ev.mods, app_cursor)),
264        Key::Right => Some(cursor_key(b'C', ev.mods, app_cursor)),
265        Key::Left => Some(cursor_key(b'D', ev.mods, app_cursor)),
266        Key::Home => Some(cursor_key(b'H', ev.mods, app_cursor)),
267        Key::End => Some(cursor_key(b'F', ev.mods, app_cursor)),
268        Key::Insert => Some(tilde_key(2, ev.mods)),
269        Key::Delete => Some(tilde_key(3, ev.mods)),
270        Key::PageUp => Some(tilde_key(5, ev.mods)),
271        Key::PageDown => Some(tilde_key(6, ev.mods)),
272        Key::Enter => Some(vec![b'\r']),
273        Key::Backspace => Some(vec![0x7f]), // DEL, the PC-keyboard convention
274        Key::Escape => Some(vec![ESC]),
275        Key::Tab => {
276            if ev.mods.contains(Modifiers::SHIFT) {
277                Some(vec![ESC, b'[', b'Z']) // back-tab (CBT)
278            } else {
279                Some(vec![b'\t'])
280            }
281        }
282        Key::F(n) => function_key(n, ev.mods),
283    }
284}
285
286/// Bit 1 of the kitty flags: report event types (repeat / release).
287const KITTY_REPORT_EVENTS: u8 = 0b10;
288/// Bit 2 of the kitty flags: report alternate (shifted / base-layout) keys.
289const KITTY_ALTERNATE_KEYS: u8 = 0b100;
290/// Bit 3 of the kitty flags: report all keys (incl. printable) as escape codes.
291const KITTY_ALL_AS_ESCAPE: u8 = 0b1000;
292/// Bit 4 of the kitty flags: report the text a key produced.
293const KITTY_ASSOCIATED_TEXT: u8 = 0b10000;
294
295/// Kitty `CSI unicode ; mods : event u` encoding. Returns `None` to fall through
296/// to legacy when this event needs no kitty form (a plain press of an
297/// unmodified key under disambiguate, etc.). The functional-key codepoint table
298/// and the remaining flags grow this in later slices.
299fn kitty_encode(ev: &KeyEvent, flags: u8) -> Option<Vec<u8>> {
300    // Event sub-parameter — only reported when the report-events flag is on, and
301    // a plain press is the omitted default.
302    let event = if flags & KITTY_REPORT_EVENTS != 0 {
303        match ev.action {
304            KeyAction::Press => None,
305            KeyAction::Repeat => Some(2),
306            KeyAction::Release => Some(3),
307        }
308    } else {
309        None
310    };
311    let modified = ev.mods.kitty_param();
312    let disambiguate = flags & KITTY_DISAMBIGUATE != 0;
313
314    // Functional keys (arrows / nav / F-keys) keep their legacy escape form but
315    // gain the kitty `;mods:event` parameter when modified or evented; an
316    // unmodified press stays legacy.
317    if let Some((number, terminator)) = functional_key(ev.key) {
318        if event.is_none() && modified.is_none() {
319            return None; // legacy form
320        }
321        return Some(kitty_seq(number, modified, event, terminator));
322    }
323
324    // Codepoint keys. Escape is ambiguous (introduces sequences) → disambiguated
325    // even unmodified. Enter/Tab/Backspace are the documented *exceptions*: legacy
326    // unless modified or carrying a non-press event.
327    let codepoint = match ev.key {
328        Key::Escape => 27,
329        Key::Enter => 13,
330        Key::Tab => 9,
331        Key::Backspace => 127,
332        Key::Char(c) => c as u32,
333        _ => return None,
334    };
335    // Escape disambiguates even unmodified. All-as-escape sends *every* key in
336    // CSI u form — by here, functional keys are already handled, so the rest
337    // (Esc/Enter/Tab/Backspace/Char) all qualify. Otherwise a modifier or a
338    // non-press event is needed.
339    let all_as_escape = flags & KITTY_ALL_AS_ESCAPE != 0;
340    let always = (disambiguate && ev.key == Key::Escape) || all_as_escape;
341    if !always && event.is_none() && !(disambiguate && modified.is_some()) {
342        return None;
343    }
344    Some(kitty_csi_u(ev, codepoint, modified, event, flags))
345}
346
347/// The `CSI u` codepoint form, including the alternate-keys and associated-text
348/// sub-fields when their flags are active:
349/// `CSI codepoint[:shifted[:base]] [; mods[:event] [; text]] u`.
350fn kitty_csi_u(
351    ev: &KeyEvent,
352    codepoint: u32,
353    modified: Option<u8>,
354    event: Option<u8>,
355    flags: u8,
356) -> Vec<u8> {
357    let mut s = format!("\x1b[{codepoint}");
358
359    // Alternate keys (bit 2): codepoint : shifted : base.
360    if flags & KITTY_ALTERNATE_KEYS != 0 && (ev.shifted_key.is_some() || ev.base_key.is_some()) {
361        s.push(':');
362        if let Some(sh) = ev.shifted_key {
363            s.push_str(&(sh as u32).to_string());
364        }
365        if let Some(b) = ev.base_key {
366            s.push(':');
367            s.push_str(&(b as u32).to_string());
368        }
369    }
370
371    // The text sub-parameter (bit 4) forces the modifier field to be present.
372    let text = if flags & KITTY_ASSOCIATED_TEXT != 0 {
373        ev.text
374    } else {
375        None
376    };
377    if modified.is_some() || event.is_some() || text.is_some() {
378        s.push(';');
379        s.push_str(&modified.unwrap_or(1).to_string());
380        if let Some(e) = event {
381            s.push(':');
382            s.push_str(&e.to_string());
383        }
384    }
385    if let Some(txt) = text {
386        s.push(';');
387        s.push_str(&(txt as u32).to_string());
388    }
389
390    s.push('u');
391    s.into_bytes()
392}
393
394/// A functional key's legacy CSI form: `(leading number, terminator)` — e.g. Up
395/// is `(1, b'A')` → `CSI 1 A`, Delete is `(3, b'~')` → `CSI 3 ~`. `None` for keys
396/// that take the `CSI u` codepoint form instead.
397fn functional_key(key: Key) -> Option<(u32, u8)> {
398    Some(match key {
399        Key::Up => (1, b'A'),
400        Key::Down => (1, b'B'),
401        Key::Right => (1, b'C'),
402        Key::Left => (1, b'D'),
403        Key::Home => (1, b'H'),
404        Key::End => (1, b'F'),
405        Key::Insert => (2, b'~'),
406        Key::Delete => (3, b'~'),
407        Key::PageUp => (5, b'~'),
408        Key::PageDown => (6, b'~'),
409        Key::F(1) => (1, b'P'),
410        Key::F(2) => (1, b'Q'),
411        Key::F(3) => (1, b'R'),
412        Key::F(4) => (1, b'S'),
413        Key::F(5) => (15, b'~'),
414        Key::F(6) => (17, b'~'),
415        Key::F(7) => (18, b'~'),
416        Key::F(8) => (19, b'~'),
417        Key::F(9) => (20, b'~'),
418        Key::F(10) => (21, b'~'),
419        Key::F(11) => (23, b'~'),
420        Key::F(12) => (24, b'~'),
421        _ => return None,
422    })
423}
424
425/// Build `CSI <number> [; <param> [: <event>]] <terminator>` — the shared shape
426/// of both the `CSI u` codepoint form and the functional-key legacy form. The
427/// `;param` is emitted when modified or evented (param defaults to 1).
428fn kitty_seq(number: u32, modified: Option<u8>, event: Option<u8>, terminator: u8) -> Vec<u8> {
429    let mut s = format!("\x1b[{number}");
430    if modified.is_some() || event.is_some() {
431        s.push(';');
432        s.push_str(&modified.unwrap_or(1).to_string());
433        if let Some(e) = event {
434            s.push(':');
435            s.push_str(&e.to_string());
436        }
437    }
438    let mut v = s.into_bytes();
439    v.push(terminator);
440    v
441}
442
443/// A printable character with modifiers. Ctrl folds an ASCII letter to its
444/// control code; Alt (meta-sends-escape) prefixes ESC.
445fn encode_char(c: char, mods: Modifiers) -> Vec<u8> {
446    let mut out = Vec::new();
447    if mods.contains(Modifiers::ALT) {
448        out.push(ESC);
449    }
450    if mods.contains(Modifiers::CTRL) {
451        // Ctrl+letter → 0x01..=0x1a; Ctrl+@/[/\/]/^/_ → 0x00..0x1f.
452        let code = match c {
453            'a'..='z' => Some((c as u8 - b'a') + 1),
454            'A'..='Z' => Some((c as u8 - b'A') + 1),
455            '@' => Some(0),
456            '[' => Some(0x1b),
457            '\\' => Some(0x1c),
458            ']' => Some(0x1d),
459            '^' => Some(0x1e),
460            '_' => Some(0x1f),
461            ' ' => Some(0),
462            _ => None,
463        };
464        if let Some(b) = code {
465            out.push(b);
466            return out;
467        }
468    }
469    let mut buf = [0u8; 4];
470    out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
471    out
472}
473
474/// Cursor keys and Home/End. Unmodified: SS3 under DECCKM, else CSI. Modified:
475/// always the CSI `1;<mod>` form regardless of DECCKM (xterm rule).
476fn cursor_key(final_byte: u8, mods: Modifiers, app_cursor: bool) -> Vec<u8> {
477    match mods.csi_param() {
478        Some(param) => {
479            let mut v = vec![ESC, b'['];
480            v.extend_from_slice(b"1;");
481            v.extend_from_slice(param.to_string().as_bytes());
482            v.push(final_byte);
483            v
484        }
485        None if app_cursor => vec![ESC, b'O', final_byte],
486        None => vec![ESC, b'[', final_byte],
487    }
488}
489
490/// A numeric-keypad key (#83). In application-keypad mode it is the classic
491/// VT100/VT220 `SS3` sequence (`ESC O <final>`); in numeric mode it is the
492/// literal character. Sequences verified against the xterm ctlseqs DEC
493/// application-keypad table.
494fn keypad_key(k: KeypadKey, app_keypad: bool) -> Vec<u8> {
495    if app_keypad {
496        let final_byte = match k {
497            KeypadKey::Digit(n) => b'p' + n.min(9), // p=0 .. y=9
498            KeypadKey::Decimal => b'n',
499            KeypadKey::Enter => b'M',
500            KeypadKey::Add => b'k',
501            KeypadKey::Subtract => b'm',
502            KeypadKey::Multiply => b'j',
503            KeypadKey::Divide => b'o',
504            KeypadKey::Equal => b'X',
505        };
506        vec![ESC, b'O', final_byte]
507    } else {
508        let c = match k {
509            KeypadKey::Digit(n) => b'0' + n.min(9),
510            KeypadKey::Decimal => b'.',
511            KeypadKey::Enter => b'\r',
512            KeypadKey::Add => b'+',
513            KeypadKey::Subtract => b'-',
514            KeypadKey::Multiply => b'*',
515            KeypadKey::Divide => b'/',
516            KeypadKey::Equal => b'=',
517        };
518        vec![c]
519    }
520}
521
522/// Keys encoded as `CSI <n> ~` (Insert/Delete/PageUp/PageDown and F5+), with an
523/// optional `;<mod>` parameter.
524fn tilde_key(n: u8, mods: Modifiers) -> Vec<u8> {
525    let mut v = vec![ESC, b'['];
526    v.extend_from_slice(n.to_string().as_bytes());
527    if let Some(param) = mods.csi_param() {
528        v.push(b';');
529        v.extend_from_slice(param.to_string().as_bytes());
530    }
531    v.push(b'~');
532    v
533}
534
535/// Function keys. F1–F4 are SS3 `P/Q/R/S` (CSI `1;<mod>` form when modified);
536/// F5–F12 are tilde keys `15/17/18/19/20/21/23/24 ~`.
537fn function_key(n: u8, mods: Modifiers) -> Option<Vec<u8>> {
538    match n {
539        1..=4 => {
540            let letter = b'P' + (n - 1); // P, Q, R, S
541            match mods.csi_param() {
542                Some(param) => {
543                    let mut v = vec![ESC, b'[', b'1', b';'];
544                    v.extend_from_slice(param.to_string().as_bytes());
545                    v.push(letter);
546                    Some(v)
547                }
548                None => Some(vec![ESC, b'O', letter]),
549            }
550        }
551        5 => Some(tilde_key(15, mods)),
552        6 => Some(tilde_key(17, mods)),
553        7 => Some(tilde_key(18, mods)),
554        8 => Some(tilde_key(19, mods)),
555        9 => Some(tilde_key(20, mods)),
556        10 => Some(tilde_key(21, mods)),
557        11 => Some(tilde_key(23, mods)),
558        12 => Some(tilde_key(24, mods)),
559        _ => None,
560    }
561}
562
563/// Encode a mouse event, given the active tracking mode and encoding. Returns
564/// `None` when reporting is off or the event is filtered out by the mode (e.g.
565/// a bare move under `?1000`).
566pub fn encode_mouse(ev: &MouseEvent, proto: MouseProtocol, enc: MouseEncoding) -> Option<Vec<u8>> {
567    if proto == MouseProtocol::Off {
568        return None;
569    }
570    // X10 (?9): a button *press* only — no release, no motion, no wheel — and the
571    // button byte carries no modifier bits (xterm.js CoreMouseService X10
572    // `restrict`). The modifier strip is applied at `mod_bits` below.
573    let x10 = proto == MouseProtocol::X10;
574    if x10
575        && (ev.action != MouseAction::Press
576            || matches!(
577                ev.button,
578                Some(
579                    MouseButton::WheelUp
580                        | MouseButton::WheelDown
581                        | MouseButton::WheelLeft
582                        | MouseButton::WheelRight
583                )
584            ))
585    {
586        return None;
587    }
588    // A wheel turn is a single press-like event; a release on a wheel button is
589    // not a real report (it would leak a bogus SGR `m` / an identity-less X10
590    // release), so drop it.
591    if ev.action == MouseAction::Release
592        && matches!(
593            ev.button,
594            Some(
595                MouseButton::WheelUp
596                    | MouseButton::WheelDown
597                    | MouseButton::WheelLeft
598                    | MouseButton::WheelRight
599            )
600        )
601    {
602        return None;
603    }
604    // Mode gates which events report at all.
605    match ev.action {
606        MouseAction::Press | MouseAction::Release => {}
607        MouseAction::Motion => match (proto, ev.button) {
608            // Drag (button held) needs ButtonEvent or AnyEvent.
609            (MouseProtocol::ButtonEvent | MouseProtocol::AnyEvent, Some(_)) => {}
610            // Bare motion needs AnyEvent.
611            (MouseProtocol::AnyEvent, None) => {}
612            _ => return None,
613        },
614    }
615
616    // Low button bits + wheel base.
617    let button_bits = match ev.button {
618        Some(MouseButton::Left) => 0,
619        Some(MouseButton::Middle) => 1,
620        Some(MouseButton::Right) => 2,
621        Some(MouseButton::WheelUp) => 64,
622        Some(MouseButton::WheelDown) => 65,
623        Some(MouseButton::WheelLeft) => 66,
624        Some(MouseButton::WheelRight) => 67,
625        Some(MouseButton::Back) => 128,
626        Some(MouseButton::Forward) => 129,
627        // Any other button by its X11 number, via the xterm bit translation:
628        // low 2 bits as-is, +64 for the wheel group, +128 for the extra group.
629        Some(MouseButton::Other(n)) => {
630            let n = n as usize;
631            (n & 3) | (if n & 4 != 0 { 64 } else { 0 }) | (if n & 8 != 0 { 128 } else { 0 })
632        }
633        None => 3, // motion with no button: the "no button" code
634    };
635    let motion = if ev.action == MouseAction::Motion {
636        32
637    } else {
638        0
639    };
640    // X10 carries no modifier bits; the others pack shift 4 / alt 8 / ctrl 16.
641    let mod_bits = if x10 {
642        0
643    } else {
644        (if ev.mods.contains(Modifiers::SHIFT) {
645            4
646        } else {
647            0
648        }) + (if ev.mods.contains(Modifiers::ALT) {
649            8
650        } else {
651            0
652        }) + (if ev.mods.contains(Modifiers::CTRL) {
653            16
654        } else {
655            0
656        })
657    };
658
659    let col1 = ev.col + 1;
660    let row1 = ev.row + 1;
661
662    match enc {
663        MouseEncoding::Sgr | MouseEncoding::SgrPixels => {
664            // SGR framing; `?1016` swaps cell coords for the consumer's pixels.
665            // SGR keeps the button identity on release; the terminator says which.
666            let cb = button_bits + motion + mod_bits;
667            let (x, y) = if enc == MouseEncoding::SgrPixels {
668                (ev.px + 1, ev.py + 1)
669            } else {
670                (col1, row1)
671            };
672            let final_byte = if ev.action == MouseAction::Release {
673                b'm'
674            } else {
675                b'M'
676            };
677            let mut v = vec![ESC, b'[', b'<'];
678            v.extend_from_slice(cb.to_string().as_bytes());
679            v.push(b';');
680            v.extend_from_slice(x.to_string().as_bytes());
681            v.push(b';');
682            v.extend_from_slice(y.to_string().as_bytes());
683            v.push(final_byte);
684            Some(v)
685        }
686        MouseEncoding::Default => {
687            // X10: release loses button identity (button bits = 3); all values +32.
688            let base = if ev.action == MouseAction::Release {
689                3
690            } else {
691                button_bits
692            };
693            let cb = base + motion + mod_bits + 32;
694            let cx = (col1 + 32).min(255) as u8;
695            let cy = (row1 + 32).min(255) as u8;
696            Some(vec![ESC, b'[', b'M', cb as u8, cx, cy])
697        }
698        MouseEncoding::Urxvt => {
699            // Default's Cb semantics (release → button 3, +32 base) but as decimal
700            // params and always terminated by `M`.
701            let base = if ev.action == MouseAction::Release {
702                3
703            } else {
704                button_bits
705            };
706            let cb = base + motion + mod_bits + 32;
707            let mut v = vec![ESC, b'['];
708            v.extend_from_slice(cb.to_string().as_bytes());
709            v.push(b';');
710            v.extend_from_slice(col1.to_string().as_bytes());
711            v.push(b';');
712            v.extend_from_slice(row1.to_string().as_bytes());
713            v.push(b'M');
714            Some(v)
715        }
716        MouseEncoding::Utf8 => {
717            // Default's CSI M framing, but each value UTF-8-encoded so it can
718            // exceed one byte (the 223-column fix that predates SGR).
719            let base = if ev.action == MouseAction::Release {
720                3
721            } else {
722                button_bits
723            };
724            let mut v = vec![ESC, b'[', b'M'];
725            push_utf8(&mut v, base + motion + mod_bits + 32);
726            push_utf8(&mut v, col1 + 32);
727            push_utf8(&mut v, row1 + 32);
728            Some(v)
729        }
730    }
731}
732
733/// Append `val` UTF-8-encoded (a single code point) — the ?1005 coordinate
734/// packing. Out-of-range values fall back to the replacement character.
735fn push_utf8(out: &mut Vec<u8>, val: usize) {
736    let c = char::from_u32(val as u32).unwrap_or('\u{fffd}');
737    let mut buf = [0u8; 4];
738    out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
739}
740
741/// Wrap pasted text in bracketed-paste markers when the mode is on, else return
742/// it raw. The markers let the app treat the payload as literal text, never as
743/// typed control sequences.
744pub fn encode_paste(text: &str, bracketed: bool) -> Vec<u8> {
745    if !bracketed {
746        return text.as_bytes().to_vec();
747    }
748    let mut v = Vec::with_capacity(text.len() + 12);
749    v.extend_from_slice(b"\x1b[200~");
750    v.extend_from_slice(text.as_bytes());
751    v.extend_from_slice(b"\x1b[201~");
752    v
753}
754
755/// Focus in/out report (`CSI I` / `CSI O`), or `None` when focus reporting
756/// (`?1004`) is off.
757pub fn encode_focus(focused: bool, enabled: bool) -> Option<Vec<u8>> {
758    if !enabled {
759        return None;
760    }
761    Some(vec![ESC, b'[', if focused { b'I' } else { b'O' }])
762}