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