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}