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
216bitflags::bitflags! {
217    /// The mouse event categories the active tracking mode reports (#129) — the
218    /// routing mask the frame carries so a frame-mode consumer sends an event to
219    /// the app (a wanted bit set) or keeps it local (selection/scrollback). It is
220    /// the single source `encode_mouse`'s restriction shares, so the wire mask and
221    /// the encode-time gate cannot drift.
222    #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
223    pub struct MouseEvents: u8 {
224        /// Button press (every protocol except `Off`).
225        const DOWN  = 1 << 0;
226        /// Button release (`?1000`+).
227        const UP    = 1 << 1;
228        /// Wheel turn (`?1000`+ — X10 excludes it).
229        const WHEEL = 1 << 2;
230        /// Motion while a button is held — drag (`?1002`+).
231        const DRAG  = 1 << 3;
232        /// Bare motion, no button held (`?1003`).
233        const MOVE  = 1 << 4;
234    }
235}
236
237impl MouseProtocol {
238    /// The event categories this protocol reports — the routing mask carried on
239    /// the frame (#129). This is the authoritative protocol→events table;
240    /// `encode_mouse` gates on the same mask so the two cannot diverge.
241    pub fn wanted_events(self) -> MouseEvents {
242        match self {
243            MouseProtocol::Off => MouseEvents::empty(),
244            MouseProtocol::X10 => MouseEvents::DOWN,
245            MouseProtocol::Normal => MouseEvents::DOWN | MouseEvents::UP | MouseEvents::WHEEL,
246            MouseProtocol::ButtonEvent => {
247                MouseEvents::DOWN | MouseEvents::UP | MouseEvents::WHEEL | MouseEvents::DRAG
248            }
249            MouseProtocol::AnyEvent => {
250                MouseEvents::DOWN
251                    | MouseEvents::UP
252                    | MouseEvents::WHEEL
253                    | MouseEvents::DRAG
254                    | MouseEvents::MOVE
255            }
256        }
257    }
258}
259
260/// Mouse coordinate encoding — *how* a report is framed (default X10 vs DEC
261/// `?1006` SGR).
262#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
263pub enum MouseEncoding {
264    /// X10 `CSI M Cb Cx Cy`, each value offset by 32 — breaks past column 223.
265    #[default]
266    Default,
267    /// `?1006` SGR `CSI < Cb ; Cx ; Cy M|m` — coords unbounded, release distinct.
268    Sgr,
269    /// `?1015` urxvt `CSI Cb ; Cx ; Cy M` — the Default byte semantics (Cb with
270    /// the +32 base, release loses button identity) as decimal params, always
271    /// terminated by `M`. Unbounded coords, no separate release form.
272    Urxvt,
273    /// `?1005` UTF-8 — the Default `CSI M Cb Cx Cy` framing but each value
274    /// UTF-8-encoded, so values past 127 become multi-byte (extends the range).
275    Utf8,
276    /// `?1016` SGR-pixels — the SGR framing but the coordinates are pixels
277    /// (from `MouseEvent::px`/`py`) instead of cells.
278    SgrPixels,
279}
280
281const ESC: u8 = 0x1b;
282
283/// Bit 0 of the kitty progressive-enhancement flags: disambiguate escape codes.
284const KITTY_DISAMBIGUATE: u8 = 0b1;
285
286/// Encode a key event to bytes, given whether DECCKM (application cursor keys)
287/// is active and the kitty keyboard-protocol flags. Returns `None` only for keys
288/// with no defined encoding.
289pub fn encode_key(
290    ev: &KeyEvent,
291    app_cursor: bool,
292    app_keypad: bool,
293    kitty_flags: u8,
294) -> Option<Vec<u8>> {
295    // Under the kitty protocol, events legacy cannot express (a modifier on a
296    // text key, a release/repeat) take the `CSI unicode ; mods : event u` form;
297    // everything else falls through to legacy (#23).
298    if kitty_flags != 0
299        && let Some(bytes) = kitty_encode(ev, kitty_flags)
300    {
301        return Some(bytes);
302    }
303    match ev.key {
304        Key::Char(c) => Some(encode_char(c, ev.mods)),
305        Key::Keypad(k) => Some(keypad_key(k, app_keypad)),
306        Key::Up => Some(cursor_key(b'A', ev.mods, app_cursor)),
307        Key::Down => Some(cursor_key(b'B', ev.mods, app_cursor)),
308        Key::Right => Some(cursor_key(b'C', ev.mods, app_cursor)),
309        Key::Left => Some(cursor_key(b'D', ev.mods, app_cursor)),
310        Key::Home => Some(cursor_key(b'H', ev.mods, app_cursor)),
311        Key::End => Some(cursor_key(b'F', ev.mods, app_cursor)),
312        Key::Insert => Some(tilde_key(2, ev.mods)),
313        Key::Delete => Some(tilde_key(3, ev.mods)),
314        Key::PageUp => Some(tilde_key(5, ev.mods)),
315        Key::PageDown => Some(tilde_key(6, ev.mods)),
316        Key::Enter => Some(vec![b'\r']),
317        Key::Backspace => Some(vec![0x7f]), // DEL, the PC-keyboard convention
318        Key::Escape => Some(vec![ESC]),
319        Key::Tab => {
320            if ev.mods.contains(Modifiers::SHIFT) {
321                Some(vec![ESC, b'[', b'Z']) // back-tab (CBT)
322            } else {
323                Some(vec![b'\t'])
324            }
325        }
326        Key::F(n) => function_key(n, ev.mods),
327    }
328}
329
330/// Bit 1 of the kitty flags: report event types (repeat / release).
331const KITTY_REPORT_EVENTS: u8 = 0b10;
332/// Bit 2 of the kitty flags: report alternate (shifted / base-layout) keys.
333const KITTY_ALTERNATE_KEYS: u8 = 0b100;
334/// Bit 3 of the kitty flags: report all keys (incl. printable) as escape codes.
335const KITTY_ALL_AS_ESCAPE: u8 = 0b1000;
336/// Bit 4 of the kitty flags: report the text a key produced.
337const KITTY_ASSOCIATED_TEXT: u8 = 0b10000;
338
339/// Kitty `CSI unicode ; mods : event u` encoding. Returns `None` to fall through
340/// to legacy when this event needs no kitty form (a plain press of an
341/// unmodified key under disambiguate, etc.). The functional-key codepoint table
342/// and the remaining flags grow this in later slices.
343fn kitty_encode(ev: &KeyEvent, flags: u8) -> Option<Vec<u8>> {
344    // Event sub-parameter — only reported when the report-events flag is on, and
345    // a plain press is the omitted default.
346    let event = if flags & KITTY_REPORT_EVENTS != 0 {
347        match ev.action {
348            KeyAction::Press => None,
349            KeyAction::Repeat => Some(2),
350            KeyAction::Release => Some(3),
351        }
352    } else {
353        None
354    };
355    let modified = ev.mods.kitty_param();
356    let disambiguate = flags & KITTY_DISAMBIGUATE != 0;
357
358    // Functional keys (arrows / nav / F-keys) keep their legacy escape form but
359    // gain the kitty `;mods:event` parameter when modified or evented; an
360    // unmodified press stays legacy.
361    if let Some((number, terminator)) = functional_key(ev.key) {
362        if event.is_none() && modified.is_none() {
363            return None; // legacy form
364        }
365        return Some(kitty_seq(number, modified, event, terminator));
366    }
367
368    // Codepoint keys. Escape is ambiguous (introduces sequences) → disambiguated
369    // even unmodified. Enter/Tab/Backspace are the documented *exceptions*: legacy
370    // unless modified or carrying a non-press event.
371    let codepoint = match ev.key {
372        Key::Escape => 27,
373        Key::Enter => 13,
374        Key::Tab => 9,
375        Key::Backspace => 127,
376        Key::Char(c) => c as u32,
377        _ => return None,
378    };
379    // Escape disambiguates even unmodified. All-as-escape sends *every* key in
380    // CSI u form — by here, functional keys are already handled, so the rest
381    // (Esc/Enter/Tab/Backspace/Char) all qualify. Otherwise a modifier or a
382    // non-press event is needed.
383    let all_as_escape = flags & KITTY_ALL_AS_ESCAPE != 0;
384    let always = (disambiguate && ev.key == Key::Escape) || all_as_escape;
385    if !always && event.is_none() && !(disambiguate && modified.is_some()) {
386        return None;
387    }
388    Some(kitty_csi_u(ev, codepoint, modified, event, flags))
389}
390
391/// The `CSI u` codepoint form, including the alternate-keys and associated-text
392/// sub-fields when their flags are active:
393/// `CSI codepoint[:shifted[:base]] [; mods[:event] [; text]] u`.
394fn kitty_csi_u(
395    ev: &KeyEvent,
396    codepoint: u32,
397    modified: Option<u8>,
398    event: Option<u8>,
399    flags: u8,
400) -> Vec<u8> {
401    let mut s = format!("\x1b[{codepoint}");
402
403    // Alternate keys (bit 2): codepoint : shifted : base.
404    if flags & KITTY_ALTERNATE_KEYS != 0 && (ev.shifted_key.is_some() || ev.base_key.is_some()) {
405        s.push(':');
406        if let Some(sh) = ev.shifted_key {
407            s.push_str(&(sh as u32).to_string());
408        }
409        if let Some(b) = ev.base_key {
410            s.push(':');
411            s.push_str(&(b as u32).to_string());
412        }
413    }
414
415    // The text sub-parameter (bit 4) forces the modifier field to be present.
416    let text = if flags & KITTY_ASSOCIATED_TEXT != 0 {
417        ev.text
418    } else {
419        None
420    };
421    if modified.is_some() || event.is_some() || text.is_some() {
422        s.push(';');
423        s.push_str(&modified.unwrap_or(1).to_string());
424        if let Some(e) = event {
425            s.push(':');
426            s.push_str(&e.to_string());
427        }
428    }
429    if let Some(txt) = text {
430        s.push(';');
431        s.push_str(&(txt as u32).to_string());
432    }
433
434    s.push('u');
435    s.into_bytes()
436}
437
438/// A functional key's legacy CSI form: `(leading number, terminator)` — e.g. Up
439/// is `(1, b'A')` → `CSI 1 A`, Delete is `(3, b'~')` → `CSI 3 ~`. `None` for keys
440/// that take the `CSI u` codepoint form instead.
441fn functional_key(key: Key) -> Option<(u32, u8)> {
442    Some(match key {
443        Key::Up => (1, b'A'),
444        Key::Down => (1, b'B'),
445        Key::Right => (1, b'C'),
446        Key::Left => (1, b'D'),
447        Key::Home => (1, b'H'),
448        Key::End => (1, b'F'),
449        Key::Insert => (2, b'~'),
450        Key::Delete => (3, b'~'),
451        Key::PageUp => (5, b'~'),
452        Key::PageDown => (6, b'~'),
453        Key::F(1) => (1, b'P'),
454        Key::F(2) => (1, b'Q'),
455        Key::F(3) => (1, b'R'),
456        Key::F(4) => (1, b'S'),
457        Key::F(5) => (15, b'~'),
458        Key::F(6) => (17, b'~'),
459        Key::F(7) => (18, b'~'),
460        Key::F(8) => (19, b'~'),
461        Key::F(9) => (20, b'~'),
462        Key::F(10) => (21, b'~'),
463        Key::F(11) => (23, b'~'),
464        Key::F(12) => (24, b'~'),
465        _ => return None,
466    })
467}
468
469/// Build `CSI <number> [; <param> [: <event>]] <terminator>` — the shared shape
470/// of both the `CSI u` codepoint form and the functional-key legacy form. The
471/// `;param` is emitted when modified or evented (param defaults to 1).
472fn kitty_seq(number: u32, modified: Option<u8>, event: Option<u8>, terminator: u8) -> Vec<u8> {
473    let mut s = format!("\x1b[{number}");
474    if modified.is_some() || event.is_some() {
475        s.push(';');
476        s.push_str(&modified.unwrap_or(1).to_string());
477        if let Some(e) = event {
478            s.push(':');
479            s.push_str(&e.to_string());
480        }
481    }
482    let mut v = s.into_bytes();
483    v.push(terminator);
484    v
485}
486
487/// A printable character with modifiers. Ctrl folds an ASCII letter to its
488/// control code; Alt (meta-sends-escape) prefixes ESC.
489fn encode_char(c: char, mods: Modifiers) -> Vec<u8> {
490    let mut out = Vec::new();
491    if mods.contains(Modifiers::ALT) {
492        out.push(ESC);
493    }
494    if mods.contains(Modifiers::CTRL) {
495        // Ctrl+letter → 0x01..=0x1a; Ctrl+@/[/\/]/^/_ → 0x00..0x1f.
496        let code = match c {
497            'a'..='z' => Some((c as u8 - b'a') + 1),
498            'A'..='Z' => Some((c as u8 - b'A') + 1),
499            '@' => Some(0),
500            '[' => Some(0x1b),
501            '\\' => Some(0x1c),
502            ']' => Some(0x1d),
503            '^' => Some(0x1e),
504            '_' => Some(0x1f),
505            ' ' => Some(0),
506            _ => None,
507        };
508        if let Some(b) = code {
509            out.push(b);
510            return out;
511        }
512    }
513    let mut buf = [0u8; 4];
514    out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
515    out
516}
517
518/// Cursor keys and Home/End. Unmodified: SS3 under DECCKM, else CSI. Modified:
519/// always the CSI `1;<mod>` form regardless of DECCKM (xterm rule).
520fn cursor_key(final_byte: u8, mods: Modifiers, app_cursor: bool) -> Vec<u8> {
521    match mods.csi_param() {
522        Some(param) => {
523            let mut v = vec![ESC, b'['];
524            v.extend_from_slice(b"1;");
525            v.extend_from_slice(param.to_string().as_bytes());
526            v.push(final_byte);
527            v
528        }
529        None if app_cursor => vec![ESC, b'O', final_byte],
530        None => vec![ESC, b'[', final_byte],
531    }
532}
533
534/// A numeric-keypad key (#83). In application-keypad mode it is the classic
535/// VT100/VT220 `SS3` sequence (`ESC O <final>`); in numeric mode it is the
536/// literal character. Sequences verified against the xterm ctlseqs DEC
537/// application-keypad table.
538fn keypad_key(k: KeypadKey, app_keypad: bool) -> Vec<u8> {
539    if app_keypad {
540        let final_byte = match k {
541            KeypadKey::Digit(n) => b'p' + n.min(9), // p=0 .. y=9
542            KeypadKey::Decimal => b'n',
543            KeypadKey::Enter => b'M',
544            KeypadKey::Add => b'k',
545            KeypadKey::Subtract => b'm',
546            KeypadKey::Multiply => b'j',
547            KeypadKey::Divide => b'o',
548            KeypadKey::Equal => b'X',
549        };
550        vec![ESC, b'O', final_byte]
551    } else {
552        let c = match k {
553            KeypadKey::Digit(n) => b'0' + n.min(9),
554            KeypadKey::Decimal => b'.',
555            KeypadKey::Enter => b'\r',
556            KeypadKey::Add => b'+',
557            KeypadKey::Subtract => b'-',
558            KeypadKey::Multiply => b'*',
559            KeypadKey::Divide => b'/',
560            KeypadKey::Equal => b'=',
561        };
562        vec![c]
563    }
564}
565
566/// Keys encoded as `CSI <n> ~` (Insert/Delete/PageUp/PageDown and F5+), with an
567/// optional `;<mod>` parameter.
568fn tilde_key(n: u8, mods: Modifiers) -> Vec<u8> {
569    let mut v = vec![ESC, b'['];
570    v.extend_from_slice(n.to_string().as_bytes());
571    if let Some(param) = mods.csi_param() {
572        v.push(b';');
573        v.extend_from_slice(param.to_string().as_bytes());
574    }
575    v.push(b'~');
576    v
577}
578
579/// Function keys. F1–F4 are SS3 `P/Q/R/S` (CSI `1;<mod>` form when modified);
580/// F5–F12 are tilde keys `15/17/18/19/20/21/23/24 ~`.
581fn function_key(n: u8, mods: Modifiers) -> Option<Vec<u8>> {
582    match n {
583        1..=4 => {
584            let letter = b'P' + (n - 1); // P, Q, R, S
585            match mods.csi_param() {
586                Some(param) => {
587                    let mut v = vec![ESC, b'[', b'1', b';'];
588                    v.extend_from_slice(param.to_string().as_bytes());
589                    v.push(letter);
590                    Some(v)
591                }
592                None => Some(vec![ESC, b'O', letter]),
593            }
594        }
595        5 => Some(tilde_key(15, mods)),
596        6 => Some(tilde_key(17, mods)),
597        7 => Some(tilde_key(18, mods)),
598        8 => Some(tilde_key(19, mods)),
599        9 => Some(tilde_key(20, mods)),
600        10 => Some(tilde_key(21, mods)),
601        11 => Some(tilde_key(23, mods)),
602        12 => Some(tilde_key(24, mods)),
603        _ => None,
604    }
605}
606
607/// Whether a button is one of the wheel directions (the 64-base wheel group).
608fn is_wheel(button: Option<MouseButton>) -> bool {
609    matches!(
610        button,
611        Some(
612            MouseButton::WheelUp
613                | MouseButton::WheelDown
614                | MouseButton::WheelLeft
615                | MouseButton::WheelRight
616        )
617    )
618}
619
620/// The event's category as a single [`MouseEvents`] bit — what the tracking mode
621/// must *want* for this event to report. Wheel releases are dropped before this
622/// (see `encode_mouse`), so a `Release` here is always a real button-up.
623fn event_category(ev: &MouseEvent) -> MouseEvents {
624    match ev.action {
625        MouseAction::Press if is_wheel(ev.button) => MouseEvents::WHEEL,
626        MouseAction::Press => MouseEvents::DOWN,
627        MouseAction::Release => MouseEvents::UP,
628        MouseAction::Motion if ev.button.is_some() => MouseEvents::DRAG,
629        MouseAction::Motion => MouseEvents::MOVE,
630    }
631}
632
633/// Encode a mouse event, given the active tracking mode and encoding. Returns
634/// `None` when reporting is off or the event is filtered out by the mode (e.g.
635/// a bare move under `?1000`).
636pub fn encode_mouse(ev: &MouseEvent, proto: MouseProtocol, enc: MouseEncoding) -> Option<Vec<u8>> {
637    // A wheel turn is a single press-like event; a release on a wheel button is
638    // not a real report (it would leak a bogus SGR `m` / an identity-less X10
639    // release), so drop it — independent of the tracking mode.
640    if ev.action == MouseAction::Release && is_wheel(ev.button) {
641        return None;
642    }
643    // The tracking mode gates which event categories report at all. This is the
644    // single source `MouseProtocol::wanted_events` — the same mask the frame
645    // carries for the consumer's routing (#129) — so the encode-time gate and the
646    // wire mask cannot drift. (Off wants nothing → None; X10 wants only DOWN, so
647    // its press-only/no-wheel restriction falls out here too.)
648    if !proto.wanted_events().contains(event_category(ev)) {
649        return None;
650    }
651    // X10 (?9) additionally carries no modifier bits in the button byte; the
652    // strip is applied at `mod_bits` below.
653    let x10 = proto == MouseProtocol::X10;
654
655    // Low button bits + wheel base.
656    let button_bits = match ev.button {
657        Some(MouseButton::Left) => 0,
658        Some(MouseButton::Middle) => 1,
659        Some(MouseButton::Right) => 2,
660        Some(MouseButton::WheelUp) => 64,
661        Some(MouseButton::WheelDown) => 65,
662        Some(MouseButton::WheelLeft) => 66,
663        Some(MouseButton::WheelRight) => 67,
664        Some(MouseButton::Back) => 128,
665        Some(MouseButton::Forward) => 129,
666        // Any other button by its X11 number, via the xterm bit translation:
667        // low 2 bits as-is, +64 for the wheel group, +128 for the extra group.
668        Some(MouseButton::Other(n)) => {
669            let n = n as usize;
670            (n & 3) | (if n & 4 != 0 { 64 } else { 0 }) | (if n & 8 != 0 { 128 } else { 0 })
671        }
672        None => 3, // motion with no button: the "no button" code
673    };
674    let motion = if ev.action == MouseAction::Motion {
675        32
676    } else {
677        0
678    };
679    // X10 carries no modifier bits; the others pack shift 4 / alt 8 / ctrl 16.
680    let mod_bits = if x10 {
681        0
682    } else {
683        (if ev.mods.contains(Modifiers::SHIFT) {
684            4
685        } else {
686            0
687        }) + (if ev.mods.contains(Modifiers::ALT) {
688            8
689        } else {
690            0
691        }) + (if ev.mods.contains(Modifiers::CTRL) {
692            16
693        } else {
694            0
695        })
696    };
697
698    let col1 = ev.col + 1;
699    let row1 = ev.row + 1;
700
701    match enc {
702        MouseEncoding::Sgr | MouseEncoding::SgrPixels => {
703            // SGR framing; `?1016` swaps cell coords for the consumer's pixels.
704            // SGR keeps the button identity on release; the terminator says which.
705            let cb = button_bits + motion + mod_bits;
706            let (x, y) = if enc == MouseEncoding::SgrPixels {
707                (ev.px + 1, ev.py + 1)
708            } else {
709                (col1, row1)
710            };
711            let final_byte = if ev.action == MouseAction::Release {
712                b'm'
713            } else {
714                b'M'
715            };
716            let mut v = vec![ESC, b'[', b'<'];
717            v.extend_from_slice(cb.to_string().as_bytes());
718            v.push(b';');
719            v.extend_from_slice(x.to_string().as_bytes());
720            v.push(b';');
721            v.extend_from_slice(y.to_string().as_bytes());
722            v.push(final_byte);
723            Some(v)
724        }
725        MouseEncoding::Default => {
726            // X10: release loses button identity (button bits = 3); all values +32.
727            let base = if ev.action == MouseAction::Release {
728                3
729            } else {
730                button_bits
731            };
732            let cb = base + motion + mod_bits + 32;
733            let cx = (col1 + 32).min(255) as u8;
734            let cy = (row1 + 32).min(255) as u8;
735            Some(vec![ESC, b'[', b'M', cb as u8, cx, cy])
736        }
737        MouseEncoding::Urxvt => {
738            // Default's Cb semantics (release → button 3, +32 base) but as decimal
739            // params and always terminated by `M`.
740            let base = if ev.action == MouseAction::Release {
741                3
742            } else {
743                button_bits
744            };
745            let cb = base + motion + mod_bits + 32;
746            let mut v = vec![ESC, b'['];
747            v.extend_from_slice(cb.to_string().as_bytes());
748            v.push(b';');
749            v.extend_from_slice(col1.to_string().as_bytes());
750            v.push(b';');
751            v.extend_from_slice(row1.to_string().as_bytes());
752            v.push(b'M');
753            Some(v)
754        }
755        MouseEncoding::Utf8 => {
756            // Default's CSI M framing, but each value UTF-8-encoded so it can
757            // exceed one byte (the 223-column fix that predates SGR).
758            let base = if ev.action == MouseAction::Release {
759                3
760            } else {
761                button_bits
762            };
763            let mut v = vec![ESC, b'[', b'M'];
764            push_utf8(&mut v, base + motion + mod_bits + 32);
765            push_utf8(&mut v, col1 + 32);
766            push_utf8(&mut v, row1 + 32);
767            Some(v)
768        }
769    }
770}
771
772/// Append `val` UTF-8-encoded (a single code point) — the ?1005 coordinate
773/// packing. Out-of-range values fall back to the replacement character.
774fn push_utf8(out: &mut Vec<u8>, val: usize) {
775    let c = char::from_u32(val as u32).unwrap_or('\u{fffd}');
776    let mut buf = [0u8; 4];
777    out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
778}
779
780/// Wrap pasted text in bracketed-paste markers when the mode is on, else return
781/// it raw. The markers let the app treat the payload as literal text, never as
782/// typed control sequences.
783pub fn encode_paste(text: &str, bracketed: bool) -> Vec<u8> {
784    if !bracketed {
785        return text.as_bytes().to_vec();
786    }
787    let mut v = Vec::with_capacity(text.len() + 12);
788    v.extend_from_slice(b"\x1b[200~");
789    v.extend_from_slice(text.as_bytes());
790    v.extend_from_slice(b"\x1b[201~");
791    v
792}
793
794/// Focus in/out report (`CSI I` / `CSI O`), or `None` when focus reporting
795/// (`?1004`) is off.
796pub fn encode_focus(focused: bool, enabled: bool) -> Option<Vec<u8>> {
797    if !enabled {
798        return None;
799    }
800    Some(vec![ESC, b'[', if focused { b'I' } else { b'O' }])
801}