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