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