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///
70/// **`#[non_exhaustive]` (#843).** A keypad namespace grows; a consumer *constructs*
71/// these to hand to the encoder rather than matching on them, so an addition costs
72/// it nothing.
73#[non_exhaustive]
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum KeypadKey {
76    /// A keypad digit, `0..=9`.
77    Digit(u8),
78    Decimal,
79    Enter,
80    Add,
81    Subtract,
82    Multiply,
83    Divide,
84    Equal,
85}
86
87/// A logical key press from the consumer (already decoded from the platform's
88/// keyboard event — justerm does not read hardware).
89///
90/// **`#[non_exhaustive]` (#843).** Key namespaces grow — media keys, the kitty
91/// protocol's additions — and the traffic here runs inward: a consumer builds a
92/// `Key` for [`crate::Engine::encode_key`] rather than matching one we hand it, so
93/// the attribute costs it nothing and makes the next VT slice additive.
94#[non_exhaustive]
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum Key {
97    /// A printable character (the consumer's already-composed text).
98    Char(char),
99    /// A numeric-keypad key (encoded per application-keypad mode, #83).
100    Keypad(KeypadKey),
101    Up,
102    Down,
103    Right,
104    Left,
105    Home,
106    End,
107    PageUp,
108    PageDown,
109    Insert,
110    Delete,
111    Enter,
112    Tab,
113    Backspace,
114    Escape,
115    /// Function key `F(n)`, `n` in 1..=12.
116    F(u8),
117}
118
119/// Press / repeat / release. Legacy reports only presses; the kitty protocol's
120/// "report event types" flag (bit 1) carries repeat and release too (#23).
121///
122/// **Deliberately exhaustive (#843).** `Press` / `Repeat` / `Release` is the kitty
123/// keyboard protocol's event space, closed. Left exhaustive on purpose, not by
124/// omission.
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
126pub enum KeyAction {
127    #[default]
128    Press,
129    Repeat,
130    Release,
131}
132
133/// A key event: a key, the modifiers held with it, its press/repeat/release type
134/// (defaults to `Press`), and consumer-supplied extras the kitty protocol's
135/// alternate-keys / associated-text flags report (all `None` for legacy).
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub struct KeyEvent {
138    pub key: Key,
139    pub mods: Modifiers,
140    pub action: KeyAction,
141    /// Kitty alternate-keys (bit 2): the codepoint Shift would produce, if it
142    /// differs from `key`.
143    pub shifted_key: Option<char>,
144    /// Kitty alternate-keys (bit 2): the codepoint at this key's position on the
145    /// base (standard) layout, if it differs from `key`.
146    pub base_key: Option<char>,
147    /// Kitty associated-text (bit 4): the text the key actually produced
148    /// (composed input / dead keys).
149    pub text: Option<char>,
150}
151
152impl Default for KeyEvent {
153    fn default() -> Self {
154        KeyEvent {
155            key: Key::Char('\0'),
156            mods: Modifiers::empty(),
157            action: KeyAction::Press,
158            shifted_key: None,
159            base_key: None,
160            text: None,
161        }
162    }
163}
164
165/// Which mouse button an event concerns. `None` on a [`MouseEvent`] means bare
166/// motion with no button held.
167///
168/// **Deliberately exhaustive (#843), and it is the case worth reading.** The set is
169/// already open *at the data level* — [`MouseButton::Other`] carries any code we do
170/// not name — so the attribute would add nothing a caller could use. That is what
171/// separates this type from [`Key`], which has no catch-all: `Char` and `F` are
172/// semantics, not escape hatches, so an unnamed key has nowhere to go and the
173/// attribute is the only way to keep adding one cheap.
174///
175/// A second argument stood here and was **withdrawn as false** — that naming a
176/// future `Button8` would stop `Other(8)` being produced and break a consumer
177/// matching on it. Three things are wrong with it, and the first is four lines
178/// above: X11 buttons **8 and 9 are already named**, as `Back` and `Forward`, and
179/// `Other` is documented as `10+`. The encoder then collapses the distinction
180/// anyway — `Some(Back)` and `Other(8)` both emit `128` — and nothing outside this
181/// crate matches a `MouseButton` we hand it, because nothing is ever handed one.
182/// Left in view rather than deleted: a doc-comment whose own example contradicts
183/// the enum beside it is worse than no comment, and this one shipped through a
184/// completeness pass before a refuting one caught it.
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186pub enum MouseButton {
187    Left,
188    Middle,
189    Right,
190    WheelUp,
191    WheelDown,
192    /// Horizontal scroll / tilt-wheel — xterm buttons 6 and 7, encoded in the
193    /// same 64-base wheel group as up/down.
194    WheelLeft,
195    WheelRight,
196    /// The thumb buttons — X11 buttons 8 and 9, the "back"/"forward" of the
197    /// 128-base extra group.
198    Back,
199    Forward,
200    /// Any further mouse button by its X11 number (gaming-mouse side buttons,
201    /// 10+). Encoded via the xterm bit formula; use the named variants above for
202    /// buttons that have one.
203    Other(u8),
204}
205
206/// What the mouse did.
207///
208/// **Deliberately exhaustive (#843).** `Press` / `Release` / `Motion` is what the
209/// mouse protocols **justerm models** report, closed — and the qualifier matters,
210/// because the unqualified version of this sentence is false. `?1001` hilite
211/// tracking reports something else entirely (`ctlseqs.txt`, the Hilite Mouse
212/// Tracking entry), and `MouseProtocol` does not model it — a crate-private type,
213/// as rustdoc confirms by refusing to link it from here. So the closure is scoped
214/// to the protocols that type names, and grows only if it does.
215///
216/// It is left exhaustive on both of #843's axes anyway: closed as scoped, and
217/// inward-only — nothing public hands one of these outward, so the attribute would
218/// cost a consumer nothing and buy nothing either.
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub enum MouseAction {
221    Press,
222    Release,
223    Motion,
224}
225
226/// A mouse event in viewport cell coordinates (0-based — the encoding shifts to
227/// 1-based on the wire).
228#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub struct MouseEvent {
230    /// The button, or `None` for bare motion (no button held).
231    pub button: Option<MouseButton>,
232    pub action: MouseAction,
233    pub col: usize,
234    pub row: usize,
235    /// 0-based pixel coordinates, used only by the `?1016` SGR-pixels encoding —
236    /// the consumer (which has the window geometry) supplies them; the engine
237    /// only formats them. Ignored by the cell-based encodings.
238    pub px: usize,
239    pub py: usize,
240    pub mods: Modifiers,
241}
242
243/// Mouse tracking mode — *what* the app asked to be reported (DEC `?1000` /
244/// `?1002` / `?1003`).
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
246pub enum MouseProtocol {
247    /// No reporting (default). `encode_mouse` returns `None`.
248    #[default]
249    Off,
250    /// `?9` — the original X10 protocol: button **press only**, no release, no
251    /// motion, no wheel, and no modifier bits.
252    X10,
253    /// `?1000` — button press and release only.
254    Normal,
255    /// `?1002` — also motion while a button is held (drag).
256    ButtonEvent,
257    /// `?1003` — also motion with no button held.
258    AnyEvent,
259}
260
261bitflags::bitflags! {
262    /// The mouse event categories the active tracking mode reports (#129) — the
263    /// routing mask the frame carries so a frame-mode consumer sends an event to
264    /// the app (a wanted bit set) or keeps it local (selection/scrollback). It is
265    /// the single source `encode_mouse`'s restriction shares, so the wire mask and
266    /// the encode-time gate cannot drift.
267    #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
268    pub struct MouseEvents: u8 {
269        /// Button press (every protocol except `Off`).
270        const DOWN  = 1 << 0;
271        /// Button release (`?1000`+).
272        const UP    = 1 << 1;
273        /// Wheel turn (`?1000`+ — X10 excludes it).
274        const WHEEL = 1 << 2;
275        /// Motion while a button is held — drag (`?1002`+).
276        const DRAG  = 1 << 3;
277        /// Bare motion, no button held (`?1003`).
278        const MOVE  = 1 << 4;
279    }
280}
281
282impl MouseProtocol {
283    /// The event categories this protocol reports — the routing mask carried on
284    /// the frame (#129). This is the authoritative protocol→events table;
285    /// `encode_mouse` gates on the same mask so the two cannot diverge.
286    pub fn wanted_events(self) -> MouseEvents {
287        match self {
288            MouseProtocol::Off => MouseEvents::empty(),
289            MouseProtocol::X10 => MouseEvents::DOWN,
290            MouseProtocol::Normal => MouseEvents::DOWN | MouseEvents::UP | MouseEvents::WHEEL,
291            MouseProtocol::ButtonEvent => {
292                MouseEvents::DOWN | MouseEvents::UP | MouseEvents::WHEEL | MouseEvents::DRAG
293            }
294            MouseProtocol::AnyEvent => {
295                MouseEvents::DOWN
296                    | MouseEvents::UP
297                    | MouseEvents::WHEEL
298                    | MouseEvents::DRAG
299                    | MouseEvents::MOVE
300            }
301        }
302    }
303}
304
305/// Mouse coordinate encoding — *how* a report is framed (default X10 vs DEC
306/// `?1006` SGR).
307#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
308pub enum MouseEncoding {
309    /// X10 `CSI M Cb Cx Cy`, each value offset by 32 — breaks past column 223.
310    #[default]
311    Default,
312    /// `?1006` SGR `CSI < Cb ; Cx ; Cy M|m` — coords unbounded, release distinct.
313    Sgr,
314    /// `?1015` urxvt `CSI Cb ; Cx ; Cy M` — the Default byte semantics (Cb with
315    /// the +32 base, release loses button identity) as decimal params, always
316    /// terminated by `M`. Unbounded coords, no separate release form.
317    Urxvt,
318    /// `?1005` UTF-8 — the Default `CSI M Cb Cx Cy` framing but each value
319    /// UTF-8-encoded, so values past 127 become multi-byte (extends the range).
320    Utf8,
321    /// `?1016` SGR-pixels — the SGR framing but the coordinates are pixels
322    /// (from `MouseEvent::px`/`py`) instead of cells.
323    SgrPixels,
324}
325
326const ESC: u8 = 0x1b;
327
328/// Bit 0 of the kitty progressive-enhancement flags: disambiguate escape codes.
329const KITTY_DISAMBIGUATE: u8 = 0b1;
330
331/// Encode a key event to bytes, given whether DECCKM (application cursor keys)
332/// is active and the kitty keyboard-protocol flags. Returns `None` only for keys
333/// with no defined encoding.
334pub fn encode_key(
335    ev: &KeyEvent,
336    app_cursor: bool,
337    app_keypad: bool,
338    kitty_flags: u8,
339) -> Option<Vec<u8>> {
340    // Under the kitty protocol, events legacy cannot express (a modifier on a
341    // text key, a release/repeat) take the `CSI unicode ; mods : event u` form;
342    // everything else falls through to legacy (#23).
343    if kitty_flags != 0
344        && let Some(bytes) = kitty_encode(ev, kitty_flags)
345    {
346        return Some(bytes);
347    }
348    match ev.key {
349        Key::Char(c) => Some(encode_char(c, ev.mods)),
350        Key::Keypad(k) => Some(keypad_key(k, app_keypad)),
351        Key::Up => Some(cursor_key(b'A', ev.mods, app_cursor)),
352        Key::Down => Some(cursor_key(b'B', ev.mods, app_cursor)),
353        Key::Right => Some(cursor_key(b'C', ev.mods, app_cursor)),
354        Key::Left => Some(cursor_key(b'D', ev.mods, app_cursor)),
355        Key::Home => Some(cursor_key(b'H', ev.mods, app_cursor)),
356        Key::End => Some(cursor_key(b'F', ev.mods, app_cursor)),
357        Key::Insert => Some(tilde_key(2, ev.mods)),
358        Key::Delete => Some(tilde_key(3, ev.mods)),
359        Key::PageUp => Some(tilde_key(5, ev.mods)),
360        Key::PageDown => Some(tilde_key(6, ev.mods)),
361        Key::Enter => Some(vec![b'\r']),
362        Key::Backspace => Some(vec![0x7f]), // DEL, the PC-keyboard convention
363        Key::Escape => Some(vec![ESC]),
364        Key::Tab => {
365            if ev.mods.contains(Modifiers::SHIFT) {
366                Some(vec![ESC, b'[', b'Z']) // back-tab (CBT)
367            } else {
368                Some(vec![b'\t'])
369            }
370        }
371        Key::F(n) => function_key(n, ev.mods),
372    }
373}
374
375/// Bit 1 of the kitty flags: report event types (repeat / release).
376const KITTY_REPORT_EVENTS: u8 = 0b10;
377/// Bit 2 of the kitty flags: report alternate (shifted / base-layout) keys.
378const KITTY_ALTERNATE_KEYS: u8 = 0b100;
379/// Bit 3 of the kitty flags: report all keys (incl. printable) as escape codes.
380const KITTY_ALL_AS_ESCAPE: u8 = 0b1000;
381/// Bit 4 of the kitty flags: report the text a key produced.
382const KITTY_ASSOCIATED_TEXT: u8 = 0b10000;
383
384/// Kitty `CSI unicode ; mods : event u` encoding. Returns `None` to fall through
385/// to legacy when this event needs no kitty form (a plain press of an
386/// unmodified key under disambiguate, etc.). The functional-key codepoint table
387/// and the remaining flags grow this in later slices.
388fn kitty_encode(ev: &KeyEvent, flags: u8) -> Option<Vec<u8>> {
389    // Event sub-parameter — only reported when the report-events flag is on, and
390    // a plain press is the omitted default.
391    let event = if flags & KITTY_REPORT_EVENTS != 0 {
392        match ev.action {
393            KeyAction::Press => None,
394            KeyAction::Repeat => Some(2),
395            KeyAction::Release => Some(3),
396        }
397    } else {
398        None
399    };
400    let modified = ev.mods.kitty_param();
401    let disambiguate = flags & KITTY_DISAMBIGUATE != 0;
402
403    // Functional keys (arrows / nav / F-keys) keep their legacy escape form but
404    // gain the kitty `;mods:event` parameter when modified or evented; an
405    // unmodified press stays legacy.
406    if let Some((number, terminator)) = functional_key(ev.key) {
407        if event.is_none() && modified.is_none() {
408            return None; // legacy form
409        }
410        return Some(kitty_seq(number, modified, event, terminator));
411    }
412
413    // Codepoint keys. Escape is ambiguous (introduces sequences) → disambiguated
414    // even unmodified. Enter/Tab/Backspace are the documented *exceptions*: legacy
415    // unless modified or carrying a non-press event.
416    let codepoint = match ev.key {
417        Key::Escape => 27,
418        Key::Enter => 13,
419        Key::Tab => 9,
420        Key::Backspace => 127,
421        Key::Char(c) => c as u32,
422        _ => return None,
423    };
424    // Escape disambiguates even unmodified. All-as-escape sends *every* key in
425    // CSI u form — by here, functional keys are already handled, so the rest
426    // (Esc/Enter/Tab/Backspace/Char) all qualify. Otherwise a modifier or a
427    // non-press event is needed.
428    let all_as_escape = flags & KITTY_ALL_AS_ESCAPE != 0;
429    let always = (disambiguate && ev.key == Key::Escape) || all_as_escape;
430    if !always && event.is_none() && !(disambiguate && modified.is_some()) {
431        return None;
432    }
433    Some(kitty_csi_u(ev, codepoint, modified, event, flags))
434}
435
436/// The `CSI u` codepoint form, including the alternate-keys and associated-text
437/// sub-fields when their flags are active:
438/// `CSI codepoint[:shifted[:base]] [; mods[:event] [; text]] u`.
439fn kitty_csi_u(
440    ev: &KeyEvent,
441    codepoint: u32,
442    modified: Option<u8>,
443    event: Option<u8>,
444    flags: u8,
445) -> Vec<u8> {
446    let mut s = format!("\x1b[{codepoint}");
447
448    // Alternate keys (bit 2): codepoint : shifted : base.
449    if flags & KITTY_ALTERNATE_KEYS != 0 && (ev.shifted_key.is_some() || ev.base_key.is_some()) {
450        s.push(':');
451        if let Some(sh) = ev.shifted_key {
452            s.push_str(&(sh as u32).to_string());
453        }
454        if let Some(b) = ev.base_key {
455            s.push(':');
456            s.push_str(&(b as u32).to_string());
457        }
458    }
459
460    // The text sub-parameter (bit 4) forces the modifier field to be present.
461    let text = if flags & KITTY_ASSOCIATED_TEXT != 0 {
462        ev.text
463    } else {
464        None
465    };
466    if modified.is_some() || event.is_some() || text.is_some() {
467        s.push(';');
468        s.push_str(&modified.unwrap_or(1).to_string());
469        if let Some(e) = event {
470            s.push(':');
471            s.push_str(&e.to_string());
472        }
473    }
474    if let Some(txt) = text {
475        s.push(';');
476        s.push_str(&(txt as u32).to_string());
477    }
478
479    s.push('u');
480    s.into_bytes()
481}
482
483/// A functional key's legacy CSI form: `(leading number, terminator)` — e.g. Up
484/// is `(1, b'A')` → `CSI 1 A`, Delete is `(3, b'~')` → `CSI 3 ~`. `None` for keys
485/// that take the `CSI u` codepoint form instead.
486fn functional_key(key: Key) -> Option<(u32, u8)> {
487    Some(match key {
488        Key::Up => (1, b'A'),
489        Key::Down => (1, b'B'),
490        Key::Right => (1, b'C'),
491        Key::Left => (1, b'D'),
492        Key::Home => (1, b'H'),
493        Key::End => (1, b'F'),
494        Key::Insert => (2, b'~'),
495        Key::Delete => (3, b'~'),
496        Key::PageUp => (5, b'~'),
497        Key::PageDown => (6, b'~'),
498        Key::F(1) => (1, b'P'),
499        Key::F(2) => (1, b'Q'),
500        Key::F(3) => (1, b'R'),
501        Key::F(4) => (1, b'S'),
502        Key::F(5) => (15, b'~'),
503        Key::F(6) => (17, b'~'),
504        Key::F(7) => (18, b'~'),
505        Key::F(8) => (19, b'~'),
506        Key::F(9) => (20, b'~'),
507        Key::F(10) => (21, b'~'),
508        Key::F(11) => (23, b'~'),
509        Key::F(12) => (24, b'~'),
510        _ => return None,
511    })
512}
513
514/// Build `CSI <number> [; <param> [: <event>]] <terminator>` — the shared shape
515/// of both the `CSI u` codepoint form and the functional-key legacy form. The
516/// `;param` is emitted when modified or evented (param defaults to 1).
517fn kitty_seq(number: u32, modified: Option<u8>, event: Option<u8>, terminator: u8) -> Vec<u8> {
518    let mut s = format!("\x1b[{number}");
519    if modified.is_some() || event.is_some() {
520        s.push(';');
521        s.push_str(&modified.unwrap_or(1).to_string());
522        if let Some(e) = event {
523            s.push(':');
524            s.push_str(&e.to_string());
525        }
526    }
527    let mut v = s.into_bytes();
528    v.push(terminator);
529    v
530}
531
532/// A printable character with modifiers. Ctrl folds an ASCII letter to its
533/// control code; Alt (meta-sends-escape) prefixes ESC.
534fn encode_char(c: char, mods: Modifiers) -> Vec<u8> {
535    let mut out = Vec::new();
536    if mods.contains(Modifiers::ALT) {
537        out.push(ESC);
538    }
539    if mods.contains(Modifiers::CTRL) {
540        // Ctrl+letter → 0x01..=0x1a; Ctrl+@/[/\/]/^/_ → 0x00..0x1f.
541        let code = match c {
542            'a'..='z' => Some((c as u8 - b'a') + 1),
543            'A'..='Z' => Some((c as u8 - b'A') + 1),
544            '@' => Some(0),
545            '[' => Some(0x1b),
546            '\\' => Some(0x1c),
547            ']' => Some(0x1d),
548            '^' => Some(0x1e),
549            '_' => Some(0x1f),
550            ' ' => Some(0),
551            _ => None,
552        };
553        if let Some(b) = code {
554            out.push(b);
555            return out;
556        }
557    }
558    let mut buf = [0u8; 4];
559    out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
560    out
561}
562
563/// Cursor keys and Home/End. Unmodified: SS3 under DECCKM, else CSI. Modified:
564/// always the CSI `1;<mod>` form regardless of DECCKM (xterm rule).
565fn cursor_key(final_byte: u8, mods: Modifiers, app_cursor: bool) -> Vec<u8> {
566    match mods.csi_param() {
567        Some(param) => {
568            let mut v = vec![ESC, b'['];
569            v.extend_from_slice(b"1;");
570            v.extend_from_slice(param.to_string().as_bytes());
571            v.push(final_byte);
572            v
573        }
574        None if app_cursor => vec![ESC, b'O', final_byte],
575        None => vec![ESC, b'[', final_byte],
576    }
577}
578
579/// A numeric-keypad key (#83). In application-keypad mode it is the classic
580/// VT100/VT220 `SS3` sequence (`ESC O <final>`); in numeric mode it is the
581/// literal character. Sequences verified against the xterm ctlseqs DEC
582/// application-keypad table.
583fn keypad_key(k: KeypadKey, app_keypad: bool) -> Vec<u8> {
584    if app_keypad {
585        let final_byte = match k {
586            KeypadKey::Digit(n) => b'p' + n.min(9), // p=0 .. y=9
587            KeypadKey::Decimal => b'n',
588            KeypadKey::Enter => b'M',
589            KeypadKey::Add => b'k',
590            KeypadKey::Subtract => b'm',
591            KeypadKey::Multiply => b'j',
592            KeypadKey::Divide => b'o',
593            KeypadKey::Equal => b'X',
594        };
595        vec![ESC, b'O', final_byte]
596    } else {
597        let c = match k {
598            KeypadKey::Digit(n) => b'0' + n.min(9),
599            KeypadKey::Decimal => b'.',
600            KeypadKey::Enter => b'\r',
601            KeypadKey::Add => b'+',
602            KeypadKey::Subtract => b'-',
603            KeypadKey::Multiply => b'*',
604            KeypadKey::Divide => b'/',
605            KeypadKey::Equal => b'=',
606        };
607        vec![c]
608    }
609}
610
611/// Keys encoded as `CSI <n> ~` (Insert/Delete/PageUp/PageDown and F5+), with an
612/// optional `;<mod>` parameter.
613fn tilde_key(n: u8, mods: Modifiers) -> Vec<u8> {
614    let mut v = vec![ESC, b'['];
615    v.extend_from_slice(n.to_string().as_bytes());
616    if let Some(param) = mods.csi_param() {
617        v.push(b';');
618        v.extend_from_slice(param.to_string().as_bytes());
619    }
620    v.push(b'~');
621    v
622}
623
624/// Function keys. F1–F4 are SS3 `P/Q/R/S` (CSI `1;<mod>` form when modified);
625/// F5–F12 are tilde keys `15/17/18/19/20/21/23/24 ~`.
626fn function_key(n: u8, mods: Modifiers) -> Option<Vec<u8>> {
627    match n {
628        1..=4 => {
629            let letter = b'P' + (n - 1); // P, Q, R, S
630            match mods.csi_param() {
631                Some(param) => {
632                    let mut v = vec![ESC, b'[', b'1', b';'];
633                    v.extend_from_slice(param.to_string().as_bytes());
634                    v.push(letter);
635                    Some(v)
636                }
637                None => Some(vec![ESC, b'O', letter]),
638            }
639        }
640        5 => Some(tilde_key(15, mods)),
641        6 => Some(tilde_key(17, mods)),
642        7 => Some(tilde_key(18, mods)),
643        8 => Some(tilde_key(19, mods)),
644        9 => Some(tilde_key(20, mods)),
645        10 => Some(tilde_key(21, mods)),
646        11 => Some(tilde_key(23, mods)),
647        12 => Some(tilde_key(24, mods)),
648        _ => None,
649    }
650}
651
652/// Whether a button is one of the wheel directions (the 64-base wheel group).
653fn is_wheel(button: Option<MouseButton>) -> bool {
654    matches!(
655        button,
656        Some(
657            MouseButton::WheelUp
658                | MouseButton::WheelDown
659                | MouseButton::WheelLeft
660                | MouseButton::WheelRight
661        )
662    )
663}
664
665/// The event's category as a single [`MouseEvents`] bit — what the tracking mode
666/// must *want* for this event to report. Wheel releases are dropped before this
667/// (see `encode_mouse`), so a `Release` here is always a real button-up.
668fn event_category(ev: &MouseEvent) -> MouseEvents {
669    match ev.action {
670        MouseAction::Press if is_wheel(ev.button) => MouseEvents::WHEEL,
671        MouseAction::Press => MouseEvents::DOWN,
672        MouseAction::Release => MouseEvents::UP,
673        MouseAction::Motion if ev.button.is_some() => MouseEvents::DRAG,
674        MouseAction::Motion => MouseEvents::MOVE,
675    }
676}
677
678/// Encode a mouse event, given the active tracking mode and encoding. Returns
679/// `None` when reporting is off or the event is filtered out by the mode (e.g.
680/// a bare move under `?1000`).
681pub fn encode_mouse(ev: &MouseEvent, proto: MouseProtocol, enc: MouseEncoding) -> Option<Vec<u8>> {
682    // A wheel turn is a single press-like event; a release on a wheel button is
683    // not a real report (it would leak a bogus SGR `m` / an identity-less X10
684    // release), so drop it — independent of the tracking mode.
685    if ev.action == MouseAction::Release && is_wheel(ev.button) {
686        return None;
687    }
688    // The tracking mode gates which event categories report at all. This is the
689    // single source `MouseProtocol::wanted_events` — the same mask the frame
690    // carries for the consumer's routing (#129) — so the encode-time gate and the
691    // wire mask cannot drift. (Off wants nothing → None; X10 wants only DOWN, so
692    // its press-only/no-wheel restriction falls out here too.)
693    if !proto.wanted_events().contains(event_category(ev)) {
694        return None;
695    }
696    // X10 (?9) additionally carries no modifier bits in the button byte; the
697    // strip is applied at `mod_bits` below.
698    let x10 = proto == MouseProtocol::X10;
699
700    // Low button bits + wheel base.
701    let button_bits = match ev.button {
702        Some(MouseButton::Left) => 0,
703        Some(MouseButton::Middle) => 1,
704        Some(MouseButton::Right) => 2,
705        Some(MouseButton::WheelUp) => 64,
706        Some(MouseButton::WheelDown) => 65,
707        Some(MouseButton::WheelLeft) => 66,
708        Some(MouseButton::WheelRight) => 67,
709        Some(MouseButton::Back) => 128,
710        Some(MouseButton::Forward) => 129,
711        // Any other button by its X11 number, via the xterm bit translation:
712        // low 2 bits as-is, +64 for the wheel group, +128 for the extra group.
713        Some(MouseButton::Other(n)) => {
714            let n = n as usize;
715            (n & 3) | (if n & 4 != 0 { 64 } else { 0 }) | (if n & 8 != 0 { 128 } else { 0 })
716        }
717        None => 3, // motion with no button: the "no button" code
718    };
719    let motion = if ev.action == MouseAction::Motion {
720        32
721    } else {
722        0
723    };
724    // X10 carries no modifier bits; the others pack shift 4 / alt 8 / ctrl 16.
725    let mod_bits = if x10 {
726        0
727    } else {
728        (if ev.mods.contains(Modifiers::SHIFT) {
729            4
730        } else {
731            0
732        }) + (if ev.mods.contains(Modifiers::ALT) {
733            8
734        } else {
735            0
736        }) + (if ev.mods.contains(Modifiers::CTRL) {
737            16
738        } else {
739            0
740        })
741    };
742
743    let col1 = ev.col + 1;
744    let row1 = ev.row + 1;
745
746    match enc {
747        MouseEncoding::Sgr | MouseEncoding::SgrPixels => {
748            // SGR framing; `?1016` swaps cell coords for the consumer's pixels.
749            // SGR keeps the button identity on release; the terminator says which.
750            let cb = button_bits + motion + mod_bits;
751            let (x, y) = if enc == MouseEncoding::SgrPixels {
752                (ev.px + 1, ev.py + 1)
753            } else {
754                (col1, row1)
755            };
756            let final_byte = if ev.action == MouseAction::Release {
757                b'm'
758            } else {
759                b'M'
760            };
761            let mut v = vec![ESC, b'[', b'<'];
762            v.extend_from_slice(cb.to_string().as_bytes());
763            v.push(b';');
764            v.extend_from_slice(x.to_string().as_bytes());
765            v.push(b';');
766            v.extend_from_slice(y.to_string().as_bytes());
767            v.push(final_byte);
768            Some(v)
769        }
770        MouseEncoding::Default => {
771            // X10: release loses button identity (button bits = 3); all values +32.
772            let base = if ev.action == MouseAction::Release {
773                3
774            } else {
775                button_bits
776            };
777            let cb = base + motion + mod_bits + 32;
778            let cx = (col1 + 32).min(255) as u8;
779            let cy = (row1 + 32).min(255) as u8;
780            Some(vec![ESC, b'[', b'M', cb as u8, cx, cy])
781        }
782        MouseEncoding::Urxvt => {
783            // Default's Cb semantics (release → button 3, +32 base) but as decimal
784            // params and always terminated by `M`.
785            let base = if ev.action == MouseAction::Release {
786                3
787            } else {
788                button_bits
789            };
790            let cb = base + motion + mod_bits + 32;
791            let mut v = vec![ESC, b'['];
792            v.extend_from_slice(cb.to_string().as_bytes());
793            v.push(b';');
794            v.extend_from_slice(col1.to_string().as_bytes());
795            v.push(b';');
796            v.extend_from_slice(row1.to_string().as_bytes());
797            v.push(b'M');
798            Some(v)
799        }
800        MouseEncoding::Utf8 => {
801            // Default's CSI M framing, but each value UTF-8-encoded so it can
802            // exceed one byte (the 223-column fix that predates SGR).
803            let base = if ev.action == MouseAction::Release {
804                3
805            } else {
806                button_bits
807            };
808            let mut v = vec![ESC, b'[', b'M'];
809            push_utf8(&mut v, base + motion + mod_bits + 32);
810            push_utf8(&mut v, col1 + 32);
811            push_utf8(&mut v, row1 + 32);
812            Some(v)
813        }
814    }
815}
816
817/// Append `val` UTF-8-encoded (a single code point) — the ?1005 coordinate
818/// packing. Out-of-range values fall back to the replacement character.
819fn push_utf8(out: &mut Vec<u8>, val: usize) {
820    let c = char::from_u32(val as u32).unwrap_or('\u{fffd}');
821    let mut buf = [0u8; 4];
822    out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
823}
824
825/// Wrap pasted text in bracketed-paste markers when the mode is on, else return
826/// it raw. The markers let the app treat the payload as literal text, never as
827/// typed control sequences.
828pub fn encode_paste(text: &str, bracketed: bool) -> Vec<u8> {
829    if !bracketed {
830        return text.as_bytes().to_vec();
831    }
832    let mut v = Vec::with_capacity(text.len() + 12);
833    v.extend_from_slice(b"\x1b[200~");
834    v.extend_from_slice(text.as_bytes());
835    v.extend_from_slice(b"\x1b[201~");
836    v
837}
838
839/// Focus in/out report (`CSI I` / `CSI O`), or `None` when focus reporting
840/// (`?1004`) is off.
841pub fn encode_focus(focused: bool, enabled: bool) -> Option<Vec<u8>> {
842    if !enabled {
843        return None;
844    }
845    Some(vec![ESC, b'[', if focused { b'I' } else { b'O' }])
846}