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
216/// Mouse coordinate encoding — *how* a report is framed (default X10 vs DEC
217/// `?1006` SGR).
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
219pub enum MouseEncoding {
220 /// X10 `CSI M Cb Cx Cy`, each value offset by 32 — breaks past column 223.
221 #[default]
222 Default,
223 /// `?1006` SGR `CSI < Cb ; Cx ; Cy M|m` — coords unbounded, release distinct.
224 Sgr,
225 /// `?1015` urxvt `CSI Cb ; Cx ; Cy M` — the Default byte semantics (Cb with
226 /// the +32 base, release loses button identity) as decimal params, always
227 /// terminated by `M`. Unbounded coords, no separate release form.
228 Urxvt,
229 /// `?1005` UTF-8 — the Default `CSI M Cb Cx Cy` framing but each value
230 /// UTF-8-encoded, so values past 127 become multi-byte (extends the range).
231 Utf8,
232 /// `?1016` SGR-pixels — the SGR framing but the coordinates are pixels
233 /// (from `MouseEvent::px`/`py`) instead of cells.
234 SgrPixels,
235}
236
237const ESC: u8 = 0x1b;
238
239/// Bit 0 of the kitty progressive-enhancement flags: disambiguate escape codes.
240const KITTY_DISAMBIGUATE: u8 = 0b1;
241
242/// Encode a key event to bytes, given whether DECCKM (application cursor keys)
243/// is active and the kitty keyboard-protocol flags. Returns `None` only for keys
244/// with no defined encoding.
245pub fn encode_key(
246 ev: &KeyEvent,
247 app_cursor: bool,
248 app_keypad: bool,
249 kitty_flags: u8,
250) -> Option<Vec<u8>> {
251 // Under the kitty protocol, events legacy cannot express (a modifier on a
252 // text key, a release/repeat) take the `CSI unicode ; mods : event u` form;
253 // everything else falls through to legacy (#23).
254 if kitty_flags != 0
255 && let Some(bytes) = kitty_encode(ev, kitty_flags)
256 {
257 return Some(bytes);
258 }
259 match ev.key {
260 Key::Char(c) => Some(encode_char(c, ev.mods)),
261 Key::Keypad(k) => Some(keypad_key(k, app_keypad)),
262 Key::Up => Some(cursor_key(b'A', ev.mods, app_cursor)),
263 Key::Down => Some(cursor_key(b'B', ev.mods, app_cursor)),
264 Key::Right => Some(cursor_key(b'C', ev.mods, app_cursor)),
265 Key::Left => Some(cursor_key(b'D', ev.mods, app_cursor)),
266 Key::Home => Some(cursor_key(b'H', ev.mods, app_cursor)),
267 Key::End => Some(cursor_key(b'F', ev.mods, app_cursor)),
268 Key::Insert => Some(tilde_key(2, ev.mods)),
269 Key::Delete => Some(tilde_key(3, ev.mods)),
270 Key::PageUp => Some(tilde_key(5, ev.mods)),
271 Key::PageDown => Some(tilde_key(6, ev.mods)),
272 Key::Enter => Some(vec![b'\r']),
273 Key::Backspace => Some(vec![0x7f]), // DEL, the PC-keyboard convention
274 Key::Escape => Some(vec![ESC]),
275 Key::Tab => {
276 if ev.mods.contains(Modifiers::SHIFT) {
277 Some(vec![ESC, b'[', b'Z']) // back-tab (CBT)
278 } else {
279 Some(vec![b'\t'])
280 }
281 }
282 Key::F(n) => function_key(n, ev.mods),
283 }
284}
285
286/// Bit 1 of the kitty flags: report event types (repeat / release).
287const KITTY_REPORT_EVENTS: u8 = 0b10;
288/// Bit 2 of the kitty flags: report alternate (shifted / base-layout) keys.
289const KITTY_ALTERNATE_KEYS: u8 = 0b100;
290/// Bit 3 of the kitty flags: report all keys (incl. printable) as escape codes.
291const KITTY_ALL_AS_ESCAPE: u8 = 0b1000;
292/// Bit 4 of the kitty flags: report the text a key produced.
293const KITTY_ASSOCIATED_TEXT: u8 = 0b10000;
294
295/// Kitty `CSI unicode ; mods : event u` encoding. Returns `None` to fall through
296/// to legacy when this event needs no kitty form (a plain press of an
297/// unmodified key under disambiguate, etc.). The functional-key codepoint table
298/// and the remaining flags grow this in later slices.
299fn kitty_encode(ev: &KeyEvent, flags: u8) -> Option<Vec<u8>> {
300 // Event sub-parameter — only reported when the report-events flag is on, and
301 // a plain press is the omitted default.
302 let event = if flags & KITTY_REPORT_EVENTS != 0 {
303 match ev.action {
304 KeyAction::Press => None,
305 KeyAction::Repeat => Some(2),
306 KeyAction::Release => Some(3),
307 }
308 } else {
309 None
310 };
311 let modified = ev.mods.kitty_param();
312 let disambiguate = flags & KITTY_DISAMBIGUATE != 0;
313
314 // Functional keys (arrows / nav / F-keys) keep their legacy escape form but
315 // gain the kitty `;mods:event` parameter when modified or evented; an
316 // unmodified press stays legacy.
317 if let Some((number, terminator)) = functional_key(ev.key) {
318 if event.is_none() && modified.is_none() {
319 return None; // legacy form
320 }
321 return Some(kitty_seq(number, modified, event, terminator));
322 }
323
324 // Codepoint keys. Escape is ambiguous (introduces sequences) → disambiguated
325 // even unmodified. Enter/Tab/Backspace are the documented *exceptions*: legacy
326 // unless modified or carrying a non-press event.
327 let codepoint = match ev.key {
328 Key::Escape => 27,
329 Key::Enter => 13,
330 Key::Tab => 9,
331 Key::Backspace => 127,
332 Key::Char(c) => c as u32,
333 _ => return None,
334 };
335 // Escape disambiguates even unmodified. All-as-escape sends *every* key in
336 // CSI u form — by here, functional keys are already handled, so the rest
337 // (Esc/Enter/Tab/Backspace/Char) all qualify. Otherwise a modifier or a
338 // non-press event is needed.
339 let all_as_escape = flags & KITTY_ALL_AS_ESCAPE != 0;
340 let always = (disambiguate && ev.key == Key::Escape) || all_as_escape;
341 if !always && event.is_none() && !(disambiguate && modified.is_some()) {
342 return None;
343 }
344 Some(kitty_csi_u(ev, codepoint, modified, event, flags))
345}
346
347/// The `CSI u` codepoint form, including the alternate-keys and associated-text
348/// sub-fields when their flags are active:
349/// `CSI codepoint[:shifted[:base]] [; mods[:event] [; text]] u`.
350fn kitty_csi_u(
351 ev: &KeyEvent,
352 codepoint: u32,
353 modified: Option<u8>,
354 event: Option<u8>,
355 flags: u8,
356) -> Vec<u8> {
357 let mut s = format!("\x1b[{codepoint}");
358
359 // Alternate keys (bit 2): codepoint : shifted : base.
360 if flags & KITTY_ALTERNATE_KEYS != 0 && (ev.shifted_key.is_some() || ev.base_key.is_some()) {
361 s.push(':');
362 if let Some(sh) = ev.shifted_key {
363 s.push_str(&(sh as u32).to_string());
364 }
365 if let Some(b) = ev.base_key {
366 s.push(':');
367 s.push_str(&(b as u32).to_string());
368 }
369 }
370
371 // The text sub-parameter (bit 4) forces the modifier field to be present.
372 let text = if flags & KITTY_ASSOCIATED_TEXT != 0 {
373 ev.text
374 } else {
375 None
376 };
377 if modified.is_some() || event.is_some() || text.is_some() {
378 s.push(';');
379 s.push_str(&modified.unwrap_or(1).to_string());
380 if let Some(e) = event {
381 s.push(':');
382 s.push_str(&e.to_string());
383 }
384 }
385 if let Some(txt) = text {
386 s.push(';');
387 s.push_str(&(txt as u32).to_string());
388 }
389
390 s.push('u');
391 s.into_bytes()
392}
393
394/// A functional key's legacy CSI form: `(leading number, terminator)` — e.g. Up
395/// is `(1, b'A')` → `CSI 1 A`, Delete is `(3, b'~')` → `CSI 3 ~`. `None` for keys
396/// that take the `CSI u` codepoint form instead.
397fn functional_key(key: Key) -> Option<(u32, u8)> {
398 Some(match key {
399 Key::Up => (1, b'A'),
400 Key::Down => (1, b'B'),
401 Key::Right => (1, b'C'),
402 Key::Left => (1, b'D'),
403 Key::Home => (1, b'H'),
404 Key::End => (1, b'F'),
405 Key::Insert => (2, b'~'),
406 Key::Delete => (3, b'~'),
407 Key::PageUp => (5, b'~'),
408 Key::PageDown => (6, b'~'),
409 Key::F(1) => (1, b'P'),
410 Key::F(2) => (1, b'Q'),
411 Key::F(3) => (1, b'R'),
412 Key::F(4) => (1, b'S'),
413 Key::F(5) => (15, b'~'),
414 Key::F(6) => (17, b'~'),
415 Key::F(7) => (18, b'~'),
416 Key::F(8) => (19, b'~'),
417 Key::F(9) => (20, b'~'),
418 Key::F(10) => (21, b'~'),
419 Key::F(11) => (23, b'~'),
420 Key::F(12) => (24, b'~'),
421 _ => return None,
422 })
423}
424
425/// Build `CSI <number> [; <param> [: <event>]] <terminator>` — the shared shape
426/// of both the `CSI u` codepoint form and the functional-key legacy form. The
427/// `;param` is emitted when modified or evented (param defaults to 1).
428fn kitty_seq(number: u32, modified: Option<u8>, event: Option<u8>, terminator: u8) -> Vec<u8> {
429 let mut s = format!("\x1b[{number}");
430 if modified.is_some() || event.is_some() {
431 s.push(';');
432 s.push_str(&modified.unwrap_or(1).to_string());
433 if let Some(e) = event {
434 s.push(':');
435 s.push_str(&e.to_string());
436 }
437 }
438 let mut v = s.into_bytes();
439 v.push(terminator);
440 v
441}
442
443/// A printable character with modifiers. Ctrl folds an ASCII letter to its
444/// control code; Alt (meta-sends-escape) prefixes ESC.
445fn encode_char(c: char, mods: Modifiers) -> Vec<u8> {
446 let mut out = Vec::new();
447 if mods.contains(Modifiers::ALT) {
448 out.push(ESC);
449 }
450 if mods.contains(Modifiers::CTRL) {
451 // Ctrl+letter → 0x01..=0x1a; Ctrl+@/[/\/]/^/_ → 0x00..0x1f.
452 let code = match c {
453 'a'..='z' => Some((c as u8 - b'a') + 1),
454 'A'..='Z' => Some((c as u8 - b'A') + 1),
455 '@' => Some(0),
456 '[' => Some(0x1b),
457 '\\' => Some(0x1c),
458 ']' => Some(0x1d),
459 '^' => Some(0x1e),
460 '_' => Some(0x1f),
461 ' ' => Some(0),
462 _ => None,
463 };
464 if let Some(b) = code {
465 out.push(b);
466 return out;
467 }
468 }
469 let mut buf = [0u8; 4];
470 out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
471 out
472}
473
474/// Cursor keys and Home/End. Unmodified: SS3 under DECCKM, else CSI. Modified:
475/// always the CSI `1;<mod>` form regardless of DECCKM (xterm rule).
476fn cursor_key(final_byte: u8, mods: Modifiers, app_cursor: bool) -> Vec<u8> {
477 match mods.csi_param() {
478 Some(param) => {
479 let mut v = vec![ESC, b'['];
480 v.extend_from_slice(b"1;");
481 v.extend_from_slice(param.to_string().as_bytes());
482 v.push(final_byte);
483 v
484 }
485 None if app_cursor => vec![ESC, b'O', final_byte],
486 None => vec![ESC, b'[', final_byte],
487 }
488}
489
490/// A numeric-keypad key (#83). In application-keypad mode it is the classic
491/// VT100/VT220 `SS3` sequence (`ESC O <final>`); in numeric mode it is the
492/// literal character. Sequences verified against the xterm ctlseqs DEC
493/// application-keypad table.
494fn keypad_key(k: KeypadKey, app_keypad: bool) -> Vec<u8> {
495 if app_keypad {
496 let final_byte = match k {
497 KeypadKey::Digit(n) => b'p' + n.min(9), // p=0 .. y=9
498 KeypadKey::Decimal => b'n',
499 KeypadKey::Enter => b'M',
500 KeypadKey::Add => b'k',
501 KeypadKey::Subtract => b'm',
502 KeypadKey::Multiply => b'j',
503 KeypadKey::Divide => b'o',
504 KeypadKey::Equal => b'X',
505 };
506 vec![ESC, b'O', final_byte]
507 } else {
508 let c = match k {
509 KeypadKey::Digit(n) => b'0' + n.min(9),
510 KeypadKey::Decimal => b'.',
511 KeypadKey::Enter => b'\r',
512 KeypadKey::Add => b'+',
513 KeypadKey::Subtract => b'-',
514 KeypadKey::Multiply => b'*',
515 KeypadKey::Divide => b'/',
516 KeypadKey::Equal => b'=',
517 };
518 vec![c]
519 }
520}
521
522/// Keys encoded as `CSI <n> ~` (Insert/Delete/PageUp/PageDown and F5+), with an
523/// optional `;<mod>` parameter.
524fn tilde_key(n: u8, mods: Modifiers) -> Vec<u8> {
525 let mut v = vec![ESC, b'['];
526 v.extend_from_slice(n.to_string().as_bytes());
527 if let Some(param) = mods.csi_param() {
528 v.push(b';');
529 v.extend_from_slice(param.to_string().as_bytes());
530 }
531 v.push(b'~');
532 v
533}
534
535/// Function keys. F1–F4 are SS3 `P/Q/R/S` (CSI `1;<mod>` form when modified);
536/// F5–F12 are tilde keys `15/17/18/19/20/21/23/24 ~`.
537fn function_key(n: u8, mods: Modifiers) -> Option<Vec<u8>> {
538 match n {
539 1..=4 => {
540 let letter = b'P' + (n - 1); // P, Q, R, S
541 match mods.csi_param() {
542 Some(param) => {
543 let mut v = vec![ESC, b'[', b'1', b';'];
544 v.extend_from_slice(param.to_string().as_bytes());
545 v.push(letter);
546 Some(v)
547 }
548 None => Some(vec![ESC, b'O', letter]),
549 }
550 }
551 5 => Some(tilde_key(15, mods)),
552 6 => Some(tilde_key(17, mods)),
553 7 => Some(tilde_key(18, mods)),
554 8 => Some(tilde_key(19, mods)),
555 9 => Some(tilde_key(20, mods)),
556 10 => Some(tilde_key(21, mods)),
557 11 => Some(tilde_key(23, mods)),
558 12 => Some(tilde_key(24, mods)),
559 _ => None,
560 }
561}
562
563/// Encode a mouse event, given the active tracking mode and encoding. Returns
564/// `None` when reporting is off or the event is filtered out by the mode (e.g.
565/// a bare move under `?1000`).
566pub fn encode_mouse(ev: &MouseEvent, proto: MouseProtocol, enc: MouseEncoding) -> Option<Vec<u8>> {
567 if proto == MouseProtocol::Off {
568 return None;
569 }
570 // X10 (?9): a button *press* only — no release, no motion, no wheel — and the
571 // button byte carries no modifier bits (xterm.js CoreMouseService X10
572 // `restrict`). The modifier strip is applied at `mod_bits` below.
573 let x10 = proto == MouseProtocol::X10;
574 if x10
575 && (ev.action != MouseAction::Press
576 || matches!(
577 ev.button,
578 Some(
579 MouseButton::WheelUp
580 | MouseButton::WheelDown
581 | MouseButton::WheelLeft
582 | MouseButton::WheelRight
583 )
584 ))
585 {
586 return None;
587 }
588 // A wheel turn is a single press-like event; a release on a wheel button is
589 // not a real report (it would leak a bogus SGR `m` / an identity-less X10
590 // release), so drop it.
591 if ev.action == MouseAction::Release
592 && matches!(
593 ev.button,
594 Some(
595 MouseButton::WheelUp
596 | MouseButton::WheelDown
597 | MouseButton::WheelLeft
598 | MouseButton::WheelRight
599 )
600 )
601 {
602 return None;
603 }
604 // Mode gates which events report at all.
605 match ev.action {
606 MouseAction::Press | MouseAction::Release => {}
607 MouseAction::Motion => match (proto, ev.button) {
608 // Drag (button held) needs ButtonEvent or AnyEvent.
609 (MouseProtocol::ButtonEvent | MouseProtocol::AnyEvent, Some(_)) => {}
610 // Bare motion needs AnyEvent.
611 (MouseProtocol::AnyEvent, None) => {}
612 _ => return None,
613 },
614 }
615
616 // Low button bits + wheel base.
617 let button_bits = match ev.button {
618 Some(MouseButton::Left) => 0,
619 Some(MouseButton::Middle) => 1,
620 Some(MouseButton::Right) => 2,
621 Some(MouseButton::WheelUp) => 64,
622 Some(MouseButton::WheelDown) => 65,
623 Some(MouseButton::WheelLeft) => 66,
624 Some(MouseButton::WheelRight) => 67,
625 Some(MouseButton::Back) => 128,
626 Some(MouseButton::Forward) => 129,
627 // Any other button by its X11 number, via the xterm bit translation:
628 // low 2 bits as-is, +64 for the wheel group, +128 for the extra group.
629 Some(MouseButton::Other(n)) => {
630 let n = n as usize;
631 (n & 3) | (if n & 4 != 0 { 64 } else { 0 }) | (if n & 8 != 0 { 128 } else { 0 })
632 }
633 None => 3, // motion with no button: the "no button" code
634 };
635 let motion = if ev.action == MouseAction::Motion {
636 32
637 } else {
638 0
639 };
640 // X10 carries no modifier bits; the others pack shift 4 / alt 8 / ctrl 16.
641 let mod_bits = if x10 {
642 0
643 } else {
644 (if ev.mods.contains(Modifiers::SHIFT) {
645 4
646 } else {
647 0
648 }) + (if ev.mods.contains(Modifiers::ALT) {
649 8
650 } else {
651 0
652 }) + (if ev.mods.contains(Modifiers::CTRL) {
653 16
654 } else {
655 0
656 })
657 };
658
659 let col1 = ev.col + 1;
660 let row1 = ev.row + 1;
661
662 match enc {
663 MouseEncoding::Sgr | MouseEncoding::SgrPixels => {
664 // SGR framing; `?1016` swaps cell coords for the consumer's pixels.
665 // SGR keeps the button identity on release; the terminator says which.
666 let cb = button_bits + motion + mod_bits;
667 let (x, y) = if enc == MouseEncoding::SgrPixels {
668 (ev.px + 1, ev.py + 1)
669 } else {
670 (col1, row1)
671 };
672 let final_byte = if ev.action == MouseAction::Release {
673 b'm'
674 } else {
675 b'M'
676 };
677 let mut v = vec![ESC, b'[', b'<'];
678 v.extend_from_slice(cb.to_string().as_bytes());
679 v.push(b';');
680 v.extend_from_slice(x.to_string().as_bytes());
681 v.push(b';');
682 v.extend_from_slice(y.to_string().as_bytes());
683 v.push(final_byte);
684 Some(v)
685 }
686 MouseEncoding::Default => {
687 // X10: release loses button identity (button bits = 3); all values +32.
688 let base = if ev.action == MouseAction::Release {
689 3
690 } else {
691 button_bits
692 };
693 let cb = base + motion + mod_bits + 32;
694 let cx = (col1 + 32).min(255) as u8;
695 let cy = (row1 + 32).min(255) as u8;
696 Some(vec![ESC, b'[', b'M', cb as u8, cx, cy])
697 }
698 MouseEncoding::Urxvt => {
699 // Default's Cb semantics (release → button 3, +32 base) but as decimal
700 // params and always terminated by `M`.
701 let base = if ev.action == MouseAction::Release {
702 3
703 } else {
704 button_bits
705 };
706 let cb = base + motion + mod_bits + 32;
707 let mut v = vec![ESC, b'['];
708 v.extend_from_slice(cb.to_string().as_bytes());
709 v.push(b';');
710 v.extend_from_slice(col1.to_string().as_bytes());
711 v.push(b';');
712 v.extend_from_slice(row1.to_string().as_bytes());
713 v.push(b'M');
714 Some(v)
715 }
716 MouseEncoding::Utf8 => {
717 // Default's CSI M framing, but each value UTF-8-encoded so it can
718 // exceed one byte (the 223-column fix that predates SGR).
719 let base = if ev.action == MouseAction::Release {
720 3
721 } else {
722 button_bits
723 };
724 let mut v = vec![ESC, b'[', b'M'];
725 push_utf8(&mut v, base + motion + mod_bits + 32);
726 push_utf8(&mut v, col1 + 32);
727 push_utf8(&mut v, row1 + 32);
728 Some(v)
729 }
730 }
731}
732
733/// Append `val` UTF-8-encoded (a single code point) — the ?1005 coordinate
734/// packing. Out-of-range values fall back to the replacement character.
735fn push_utf8(out: &mut Vec<u8>, val: usize) {
736 let c = char::from_u32(val as u32).unwrap_or('\u{fffd}');
737 let mut buf = [0u8; 4];
738 out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
739}
740
741/// Wrap pasted text in bracketed-paste markers when the mode is on, else return
742/// it raw. The markers let the app treat the payload as literal text, never as
743/// typed control sequences.
744pub fn encode_paste(text: &str, bracketed: bool) -> Vec<u8> {
745 if !bracketed {
746 return text.as_bytes().to_vec();
747 }
748 let mut v = Vec::with_capacity(text.len() + 12);
749 v.extend_from_slice(b"\x1b[200~");
750 v.extend_from_slice(text.as_bytes());
751 v.extend_from_slice(b"\x1b[201~");
752 v
753}
754
755/// Focus in/out report (`CSI I` / `CSI O`), or `None` when focus reporting
756/// (`?1004`) is off.
757pub fn encode_focus(focused: bool, enabled: bool) -> Option<Vec<u8>> {
758 if !enabled {
759 return None;
760 }
761 Some(vec![ESC, b'[', if focused { b'I' } else { b'O' }])
762}