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 (#23).
20 ///
21 /// **No `#[non_exhaustive]` (#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, #74)
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 (#83).
73///
74/// **`#[non_exhaustive]` (#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).** 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, #83).
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 (#23).
125///
126/// **Deliberately exhaustive (#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).** 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), 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).** `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'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).** 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`.
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 (#129) — 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): 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
322/// Mouse coordinate encoding — *how* a report is framed (default X10 vs DEC
323/// `?1006` SGR).
324#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
325pub enum MouseEncoding {
326 /// X10 `CSI M Cb Cx Cy`, each value offset by 32 — breaks past column 223.
327 #[default]
328 Default,
329 /// `?1006` SGR `CSI < Cb ; Cx ; Cy M|m` — coords unbounded, release distinct.
330 Sgr,
331 /// `?1015` urxvt `CSI Cb ; Cx ; Cy M` — the Default byte semantics (Cb with
332 /// the +32 base, release loses button identity) as decimal params, always
333 /// terminated by `M`. Unbounded coords, no separate release form.
334 Urxvt,
335 /// `?1005` UTF-8 — the Default `CSI M Cb Cx Cy` framing but each value
336 /// UTF-8-encoded, so values past 127 become multi-byte (extends the range).
337 Utf8,
338 /// `?1016` SGR-pixels — the SGR framing but the coordinates are pixels
339 /// (from `MouseEvent::px`/`py`) instead of cells.
340 SgrPixels,
341}
342
343const ESC: u8 = 0x1b;
344
345/// Bit 0 of the kitty progressive-enhancement flags: disambiguate escape codes.
346const KITTY_DISAMBIGUATE: u8 = 0b1;
347
348/// Encode a key event to bytes, given the four pieces of mode state that decide it:
349/// DECCKM (application cursor keys), application keypad, the kitty keyboard-protocol
350/// flags, and `modifyOtherKeys` level 2. Returns `None` only for keys with no defined
351/// encoding. The order the last two are asked in is load-bearing — `docs/map/territory/input-encoding.md`.
352pub fn encode_key(
353 ev: &KeyEvent,
354 app_cursor: bool,
355 app_keypad: bool,
356 kitty_flags: u8,
357 modify_other_keys_2: bool,
358) -> Option<Vec<u8>> {
359 // Under the kitty protocol, events legacy cannot express (a modifier on a
360 // text key, a release/repeat) take the `CSI unicode ; mods : event u` form;
361 // everything else falls through to legacy (#23).
362 if kitty_flags != 0
363 && let Some(bytes) = kitty_encode(ev, kitty_flags)
364 {
365 return Some(bytes);
366 }
367 // Deliberately *after* kitty and inside legacy, not beside it (#890).
368 if modify_other_keys_2 && let Some(bytes) = modify_other_key(ev.key, ev.mods) {
369 return Some(bytes);
370 }
371 match ev.key {
372 Key::Char(c) => Some(encode_char(c, ev.mods)),
373 Key::Keypad(k) => Some(keypad_key(k, app_keypad)),
374 Key::Up => Some(cursor_key(b'A', ev.mods, app_cursor)),
375 Key::Down => Some(cursor_key(b'B', ev.mods, app_cursor)),
376 Key::Right => Some(cursor_key(b'C', ev.mods, app_cursor)),
377 Key::Left => Some(cursor_key(b'D', ev.mods, app_cursor)),
378 Key::Home => Some(cursor_key(b'H', ev.mods, app_cursor)),
379 Key::End => Some(cursor_key(b'F', ev.mods, app_cursor)),
380 Key::Insert => Some(tilde_key(2, ev.mods)),
381 Key::Delete => Some(tilde_key(3, ev.mods)),
382 Key::PageUp => Some(tilde_key(5, ev.mods)),
383 Key::PageDown => Some(tilde_key(6, ev.mods)),
384 Key::Enter => Some(vec![b'\r']),
385 Key::Backspace => Some(vec![0x7f]), // DEL, the PC-keyboard convention
386 Key::Escape => Some(vec![ESC]),
387 Key::Tab => {
388 if ev.mods.contains(Modifiers::SHIFT) {
389 Some(vec![ESC, b'[', b'Z']) // back-tab (CBT)
390 } else {
391 Some(vec![b'\t'])
392 }
393 }
394 Key::F(n) => function_key(n, ev.mods),
395 }
396}
397
398/// Bit 1 of the kitty flags: report event types (repeat / release).
399const KITTY_REPORT_EVENTS: u8 = 0b10;
400/// Bit 2 of the kitty flags: report alternate (shifted / base-layout) keys.
401const KITTY_ALTERNATE_KEYS: u8 = 0b100;
402/// Bit 3 of the kitty flags: report all keys (incl. printable) as escape codes.
403const KITTY_ALL_AS_ESCAPE: u8 = 0b1000;
404/// Bit 4 of the kitty flags: report the text a key produced.
405const KITTY_ASSOCIATED_TEXT: u8 = 0b10000;
406
407/// Kitty `CSI unicode ; mods : event u` encoding. Returns `None` to fall through
408/// to legacy when this event needs no kitty form (a plain press of an
409/// unmodified key under disambiguate, etc.). The functional-key codepoint table
410/// and the remaining flags grow this in later slices.
411fn kitty_encode(ev: &KeyEvent, flags: u8) -> Option<Vec<u8>> {
412 // Event sub-parameter — only reported when the report-events flag is on, and
413 // a plain press is the omitted default.
414 let event = if flags & KITTY_REPORT_EVENTS != 0 {
415 match ev.action {
416 KeyAction::Press => None,
417 KeyAction::Repeat => Some(2),
418 KeyAction::Release => Some(3),
419 }
420 } else {
421 None
422 };
423 let modified = ev.mods.kitty_param();
424 let disambiguate = flags & KITTY_DISAMBIGUATE != 0;
425
426 // Functional keys (arrows / nav / F-keys) keep their legacy escape form but
427 // gain the kitty `;mods:event` parameter when modified or evented; an
428 // unmodified press stays legacy.
429 if let Some((number, terminator)) = functional_key(ev.key) {
430 if event.is_none() && modified.is_none() {
431 return None; // legacy form
432 }
433 return Some(kitty_seq(number, modified, event, terminator));
434 }
435
436 // Codepoint keys. Escape is ambiguous (introduces sequences) → disambiguated
437 // even unmodified. Enter/Tab/Backspace are the documented *exceptions*: legacy
438 // unless modified or carrying a non-press event.
439 let codepoint = match ev.key {
440 Key::Escape => 27,
441 Key::Enter => 13,
442 Key::Tab => 9,
443 Key::Backspace => 127,
444 Key::Char(c) => c as u32,
445 _ => return None,
446 };
447 // Escape disambiguates even unmodified. All-as-escape sends *every* key in
448 // CSI u form — by here, functional keys are already handled, so the rest
449 // (Esc/Enter/Tab/Backspace/Char) all qualify. Otherwise a modifier or a
450 // non-press event is needed.
451 let all_as_escape = flags & KITTY_ALL_AS_ESCAPE != 0;
452 let always = (disambiguate && ev.key == Key::Escape) || all_as_escape;
453 if !always && event.is_none() && !(disambiguate && modified.is_some()) {
454 return None;
455 }
456 Some(kitty_csi_u(ev, codepoint, modified, event, flags))
457}
458
459/// The `CSI u` codepoint form, including the alternate-keys and associated-text
460/// sub-fields when their flags are active:
461/// `CSI codepoint[:shifted[:base]] [; mods[:event] [; text]] u`.
462fn kitty_csi_u(
463 ev: &KeyEvent,
464 codepoint: u32,
465 modified: Option<u8>,
466 event: Option<u8>,
467 flags: u8,
468) -> Vec<u8> {
469 let mut s = format!("\x1b[{codepoint}");
470
471 // Alternate keys (bit 2): codepoint : shifted : base.
472 if flags & KITTY_ALTERNATE_KEYS != 0 && (ev.shifted_key.is_some() || ev.base_key.is_some()) {
473 s.push(':');
474 if let Some(sh) = ev.shifted_key {
475 s.push_str(&(sh as u32).to_string());
476 }
477 if let Some(b) = ev.base_key {
478 s.push(':');
479 s.push_str(&(b as u32).to_string());
480 }
481 }
482
483 // The text sub-parameter (bit 4) forces the modifier field to be present.
484 let text = if flags & KITTY_ASSOCIATED_TEXT != 0 {
485 ev.text
486 } else {
487 None
488 };
489 if modified.is_some() || event.is_some() || text.is_some() {
490 s.push(';');
491 s.push_str(&modified.unwrap_or(1).to_string());
492 if let Some(e) = event {
493 s.push(':');
494 s.push_str(&e.to_string());
495 }
496 }
497 if let Some(txt) = text {
498 s.push(';');
499 s.push_str(&(txt as u32).to_string());
500 }
501
502 s.push('u');
503 s.into_bytes()
504}
505
506/// A functional key's legacy CSI form: `(leading number, terminator)` — e.g. Up
507/// is `(1, b'A')` → `CSI 1 A`, Delete is `(3, b'~')` → `CSI 3 ~`. `None` for keys
508/// that take the `CSI u` codepoint form instead.
509fn functional_key(key: Key) -> Option<(u32, u8)> {
510 Some(match key {
511 Key::Up => (1, b'A'),
512 Key::Down => (1, b'B'),
513 Key::Right => (1, b'C'),
514 Key::Left => (1, b'D'),
515 Key::Home => (1, b'H'),
516 Key::End => (1, b'F'),
517 Key::Insert => (2, b'~'),
518 Key::Delete => (3, b'~'),
519 Key::PageUp => (5, b'~'),
520 Key::PageDown => (6, b'~'),
521 Key::F(1) => (1, b'P'),
522 Key::F(2) => (1, b'Q'),
523 Key::F(3) => (1, b'R'),
524 Key::F(4) => (1, b'S'),
525 Key::F(5) => (15, b'~'),
526 Key::F(6) => (17, b'~'),
527 Key::F(7) => (18, b'~'),
528 Key::F(8) => (19, b'~'),
529 Key::F(9) => (20, b'~'),
530 Key::F(10) => (21, b'~'),
531 Key::F(11) => (23, b'~'),
532 Key::F(12) => (24, b'~'),
533 _ => return None,
534 })
535}
536
537/// Build `CSI <number> [; <param> [: <event>]] <terminator>` — the shared shape
538/// of both the `CSI u` codepoint form and the functional-key legacy form. The
539/// `;param` is emitted when modified or evented (param defaults to 1).
540fn kitty_seq(number: u32, modified: Option<u8>, event: Option<u8>, terminator: u8) -> Vec<u8> {
541 let mut s = format!("\x1b[{number}");
542 if modified.is_some() || event.is_some() {
543 s.push(';');
544 s.push_str(&modified.unwrap_or(1).to_string());
545 if let Some(e) = event {
546 s.push(':');
547 s.push_str(&e.to_string());
548 }
549 }
550 let mut v = s.into_bytes();
551 v.push(terminator);
552 v
553}
554
555/// A modified character under `modifyOtherKeys` level 2 (#890): `CSI 27 ; <1+mods> ;
556/// <codepoint> ~`. `None` leaves the key to the ordinary legacy encoding.
557///
558/// Three things here are deliberate and none is obvious from the code: the emitted shape
559/// is not the `u` form the reference also offers, one of its three qualifying clauses is
560/// dropped, and named keys are in scope while `Delete` is not. All three, with what they
561/// were measured against, are in `docs/map/territory/input-encoding.md` and
562/// `docs/agents/reference-facts.md` (#890).
563fn modify_other_key(key: Key, mods: Modifiers) -> Option<Vec<u8>> {
564 let code = match key {
565 Key::Char(c) => {
566 char_qualifies(c, mods)?;
567 c as u32
568 }
569 // Tab / Enter / Escape: any expressible modifier (`input.c:720-724`).
570 Key::Tab => {
571 // Shift alone is excluded on purpose, so back-tab keeps `CSI Z` (`:715-718`).
572 mods.difference(Modifiers::SHIFT).csi_param()?;
573 9
574 }
575 Key::Enter => {
576 mods.csi_param()?;
577 13
578 }
579 Key::Escape => {
580 mods.csi_param()?;
581 27
582 }
583 // Backspace: a modifier that is **not** Control, so `Ctrl+Backspace` keeps its
584 // legacy byte (`input.c:706-710`). `127` rather than `8` is deliberate — note.
585 Key::Backspace => {
586 mods.difference(Modifiers::CTRL).csi_param()?;
587 127
588 }
589 // **`Delete` and every other named key are deliberately out**, not forgotten — they
590 // already have an unambiguous modified form. Grounds in the note; the references
591 // disagree here.
592 _ => return None,
593 };
594 let param = mods.csi_param()?;
595 Some(format!("\x1b[27;{};{}~", param, code).into_bytes())
596}
597
598/// Whether a *character* key qualifies under level 2.
599///
600/// **Ask the question of the parameter, not of the raw bits** — a gate on the bitflags
601/// admits a chord `csi_param` cannot then describe. Why that is a defect rather than a
602/// nicety, and the modifier it was reached through, are in
603/// `docs/map/territory/input-encoding.md`.
604fn char_qualifies(c: char, mods: Modifiers) -> Option<()> {
605 if mods.difference(Modifiers::SHIFT).csi_param().is_none()
606 && !(mods == Modifiers::SHIFT && c == ' ')
607 {
608 return None;
609 }
610 Some(())
611}
612
613/// A printable character with modifiers. Ctrl folds an ASCII letter to its
614/// control code; Alt (meta-sends-escape) prefixes ESC.
615fn encode_char(c: char, mods: Modifiers) -> Vec<u8> {
616 let mut out = Vec::new();
617 if mods.contains(Modifiers::ALT) {
618 out.push(ESC);
619 }
620 if mods.contains(Modifiers::CTRL) {
621 // Ctrl+letter → 0x01..=0x1a; Ctrl+@/[/\/]/^/_ → 0x00..0x1f.
622 let code = match c {
623 'a'..='z' => Some((c as u8 - b'a') + 1),
624 'A'..='Z' => Some((c as u8 - b'A') + 1),
625 '@' => Some(0),
626 '[' => Some(0x1b),
627 '\\' => Some(0x1c),
628 ']' => Some(0x1d),
629 '^' => Some(0x1e),
630 '_' => Some(0x1f),
631 ' ' => Some(0),
632 _ => None,
633 };
634 if let Some(b) = code {
635 out.push(b);
636 return out;
637 }
638 }
639 let mut buf = [0u8; 4];
640 out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
641 out
642}
643
644/// Cursor keys and Home/End. Unmodified: SS3 under DECCKM, else CSI. Modified:
645/// always the CSI `1;<mod>` form regardless of DECCKM (xterm rule).
646fn cursor_key(final_byte: u8, mods: Modifiers, app_cursor: bool) -> Vec<u8> {
647 match mods.csi_param() {
648 Some(param) => {
649 let mut v = vec![ESC, b'['];
650 v.extend_from_slice(b"1;");
651 v.extend_from_slice(param.to_string().as_bytes());
652 v.push(final_byte);
653 v
654 }
655 None if app_cursor => vec![ESC, b'O', final_byte],
656 None => vec![ESC, b'[', final_byte],
657 }
658}
659
660/// A numeric-keypad key (#83). In application-keypad mode it is the classic
661/// VT100/VT220 `SS3` sequence (`ESC O <final>`); in numeric mode it is the
662/// literal character. Sequences verified against the xterm ctlseqs DEC
663/// application-keypad table.
664fn keypad_key(k: KeypadKey, app_keypad: bool) -> Vec<u8> {
665 if app_keypad {
666 let final_byte = match k {
667 KeypadKey::Digit(n) => b'p' + n.min(9), // p=0 .. y=9
668 KeypadKey::Decimal => b'n',
669 KeypadKey::Enter => b'M',
670 KeypadKey::Add => b'k',
671 KeypadKey::Subtract => b'm',
672 KeypadKey::Multiply => b'j',
673 KeypadKey::Divide => b'o',
674 KeypadKey::Equal => b'X',
675 };
676 vec![ESC, b'O', final_byte]
677 } else {
678 let c = match k {
679 KeypadKey::Digit(n) => b'0' + n.min(9),
680 KeypadKey::Decimal => b'.',
681 KeypadKey::Enter => b'\r',
682 KeypadKey::Add => b'+',
683 KeypadKey::Subtract => b'-',
684 KeypadKey::Multiply => b'*',
685 KeypadKey::Divide => b'/',
686 KeypadKey::Equal => b'=',
687 };
688 vec![c]
689 }
690}
691
692/// Keys encoded as `CSI <n> ~` (Insert/Delete/PageUp/PageDown and F5+), with an
693/// optional `;<mod>` parameter.
694fn tilde_key(n: u8, mods: Modifiers) -> Vec<u8> {
695 let mut v = vec![ESC, b'['];
696 v.extend_from_slice(n.to_string().as_bytes());
697 if let Some(param) = mods.csi_param() {
698 v.push(b';');
699 v.extend_from_slice(param.to_string().as_bytes());
700 }
701 v.push(b'~');
702 v
703}
704
705/// Function keys. F1–F4 are SS3 `P/Q/R/S` (CSI `1;<mod>` form when modified);
706/// F5–F12 are tilde keys `15/17/18/19/20/21/23/24 ~`.
707fn function_key(n: u8, mods: Modifiers) -> Option<Vec<u8>> {
708 match n {
709 1..=4 => {
710 let letter = b'P' + (n - 1); // P, Q, R, S
711 match mods.csi_param() {
712 Some(param) => {
713 let mut v = vec![ESC, b'[', b'1', b';'];
714 v.extend_from_slice(param.to_string().as_bytes());
715 v.push(letter);
716 Some(v)
717 }
718 None => Some(vec![ESC, b'O', letter]),
719 }
720 }
721 5 => Some(tilde_key(15, mods)),
722 6 => Some(tilde_key(17, mods)),
723 7 => Some(tilde_key(18, mods)),
724 8 => Some(tilde_key(19, mods)),
725 9 => Some(tilde_key(20, mods)),
726 10 => Some(tilde_key(21, mods)),
727 11 => Some(tilde_key(23, mods)),
728 12 => Some(tilde_key(24, mods)),
729 _ => None,
730 }
731}
732
733/// Whether a button is one of the wheel directions (the 64-base wheel group).
734fn is_wheel(button: Option<MouseButton>) -> bool {
735 matches!(
736 button,
737 Some(
738 MouseButton::WheelUp
739 | MouseButton::WheelDown
740 | MouseButton::WheelLeft
741 | MouseButton::WheelRight
742 )
743 )
744}
745
746/// The event's category as a single [`MouseEvents`] bit — what the tracking mode
747/// must *want* for this event to report. Wheel releases are dropped before this
748/// (see `encode_mouse`), so a `Release` here is always a real button-up.
749fn event_category(ev: &MouseEvent) -> MouseEvents {
750 match ev.action {
751 MouseAction::Press if is_wheel(ev.button) => MouseEvents::WHEEL,
752 MouseAction::Press => MouseEvents::DOWN,
753 MouseAction::Release => MouseEvents::UP,
754 MouseAction::Motion if ev.button.is_some() => MouseEvents::DRAG,
755 MouseAction::Motion => MouseEvents::MOVE,
756 }
757}
758
759/// Encode a mouse event, given the active tracking mode and encoding. Returns
760/// `None` when reporting is off or the event is filtered out by the mode (e.g.
761/// a bare move under `?1000`).
762pub fn encode_mouse(ev: &MouseEvent, proto: MouseProtocol, enc: MouseEncoding) -> Option<Vec<u8>> {
763 // A wheel turn is a single press-like event; a release on a wheel button is
764 // not a real report (it would leak a bogus SGR `m` / an identity-less X10
765 // release), so drop it — independent of the tracking mode.
766 if ev.action == MouseAction::Release && is_wheel(ev.button) {
767 return None;
768 }
769 // The tracking mode gates which event categories report at all. This is the
770 // single source `MouseProtocol::wanted_events` — the same mask the frame
771 // carries for the consumer's routing (#129) — so the encode-time gate and the
772 // wire mask cannot drift. (Off wants nothing → None; X10 wants only DOWN, so
773 // its press-only/no-wheel restriction falls out here too.)
774 if !proto.wanted_events().contains(event_category(ev)) {
775 return None;
776 }
777 // X10 (?9) additionally carries no modifier bits in the button byte; the
778 // strip is applied at `mod_bits` below.
779 let x10 = proto == MouseProtocol::X10;
780
781 // Low button bits + wheel base.
782 let button_bits = match ev.button {
783 Some(MouseButton::Left) => 0,
784 Some(MouseButton::Middle) => 1,
785 Some(MouseButton::Right) => 2,
786 Some(MouseButton::WheelUp) => 64,
787 Some(MouseButton::WheelDown) => 65,
788 Some(MouseButton::WheelLeft) => 66,
789 Some(MouseButton::WheelRight) => 67,
790 Some(MouseButton::Back) => 128,
791 Some(MouseButton::Forward) => 129,
792 // Any other button by its X11 number, via the xterm bit translation:
793 // low 2 bits as-is, +64 for the wheel group, +128 for the extra group.
794 Some(MouseButton::Other(n)) => {
795 let n = n as usize;
796 (n & 3) | (if n & 4 != 0 { 64 } else { 0 }) | (if n & 8 != 0 { 128 } else { 0 })
797 }
798 None => 3, // motion with no button: the "no button" code
799 };
800 let motion = if ev.action == MouseAction::Motion {
801 32
802 } else {
803 0
804 };
805 // X10 carries no modifier bits; the others pack shift 4 / alt 8 / ctrl 16.
806 let mod_bits = if x10 {
807 0
808 } else {
809 (if ev.mods.contains(Modifiers::SHIFT) {
810 4
811 } else {
812 0
813 }) + (if ev.mods.contains(Modifiers::ALT) {
814 8
815 } else {
816 0
817 }) + (if ev.mods.contains(Modifiers::CTRL) {
818 16
819 } else {
820 0
821 })
822 };
823
824 let col1 = ev.col + 1;
825 let row1 = ev.row + 1;
826
827 match enc {
828 MouseEncoding::Sgr | MouseEncoding::SgrPixels => {
829 // SGR framing; `?1016` swaps cell coords for the consumer's pixels.
830 // SGR keeps the button identity on release; the terminator says which.
831 let cb = button_bits + motion + mod_bits;
832 let (x, y) = if enc == MouseEncoding::SgrPixels {
833 (ev.px + 1, ev.py + 1)
834 } else {
835 (col1, row1)
836 };
837 let final_byte = if ev.action == MouseAction::Release {
838 b'm'
839 } else {
840 b'M'
841 };
842 let mut v = vec![ESC, b'[', b'<'];
843 v.extend_from_slice(cb.to_string().as_bytes());
844 v.push(b';');
845 v.extend_from_slice(x.to_string().as_bytes());
846 v.push(b';');
847 v.extend_from_slice(y.to_string().as_bytes());
848 v.push(final_byte);
849 Some(v)
850 }
851 MouseEncoding::Default => {
852 // X10: release loses button identity (button bits = 3); all values +32.
853 let base = if ev.action == MouseAction::Release {
854 3
855 } else {
856 button_bits
857 };
858 let cb = base + motion + mod_bits + 32;
859 let cx = (col1 + 32).min(255) as u8;
860 let cy = (row1 + 32).min(255) as u8;
861 Some(vec![ESC, b'[', b'M', cb as u8, cx, cy])
862 }
863 MouseEncoding::Urxvt => {
864 // Default's Cb semantics (release → button 3, +32 base) but as decimal
865 // params and always terminated by `M`.
866 let base = if ev.action == MouseAction::Release {
867 3
868 } else {
869 button_bits
870 };
871 let cb = base + motion + mod_bits + 32;
872 let mut v = vec![ESC, b'['];
873 v.extend_from_slice(cb.to_string().as_bytes());
874 v.push(b';');
875 v.extend_from_slice(col1.to_string().as_bytes());
876 v.push(b';');
877 v.extend_from_slice(row1.to_string().as_bytes());
878 v.push(b'M');
879 Some(v)
880 }
881 MouseEncoding::Utf8 => {
882 // Default's CSI M framing, but each value UTF-8-encoded so it can
883 // exceed one byte (the 223-column fix that predates SGR).
884 let base = if ev.action == MouseAction::Release {
885 3
886 } else {
887 button_bits
888 };
889 let mut v = vec![ESC, b'[', b'M'];
890 push_utf8(&mut v, base + motion + mod_bits + 32);
891 push_utf8(&mut v, col1 + 32);
892 push_utf8(&mut v, row1 + 32);
893 Some(v)
894 }
895 }
896}
897
898/// Append `val` UTF-8-encoded (a single code point) — the ?1005 coordinate
899/// packing. Out-of-range values fall back to the replacement character.
900fn push_utf8(out: &mut Vec<u8>, val: usize) {
901 let c = char::from_u32(val as u32).unwrap_or('\u{fffd}');
902 let mut buf = [0u8; 4];
903 out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
904}
905
906/// Wrap pasted text in bracketed-paste markers when the mode is on, else return
907/// it raw. The markers let the app treat the payload as literal text, never as
908/// typed control sequences.
909pub fn encode_paste(text: &str, bracketed: bool) -> Vec<u8> {
910 if !bracketed {
911 return text.as_bytes().to_vec();
912 }
913 let mut v = Vec::with_capacity(text.len() + 12);
914 v.extend_from_slice(b"\x1b[200~");
915 v.extend_from_slice(text.as_bytes());
916 v.extend_from_slice(b"\x1b[201~");
917 v
918}
919
920/// Focus in/out report (`CSI I` / `CSI O`), or `None` when focus reporting
921/// (`?1004`) is off.
922pub fn encode_focus(focused: bool, enabled: bool) -> Option<Vec<u8>> {
923 if !enabled {
924 return None;
925 }
926 Some(vec![ESC, b'[', if focused { b'I' } else { b'O' }])
927}