tv 0.1.1

Terminal User Interface library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
use crate::kitty::{FunctionalKey, KeyEvent, KeyEventType, Modifiers};

/// Keyboard input key.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Key {
    /// A character key with no modifiers (or only Shift that's already
    /// baked into the codepoint).
    Char(char),
    /// Function keys F1..F35.
    F(u8),
    Up,
    Down,
    Left,
    Right,
    Enter,
    Backspace,
    Delete,
    Insert,
    Home,
    End,
    PageUp,
    PageDown,
    Tab,
    BackTab,
    Escape,
    /// Control + character.
    Ctrl(char),
    /// Alt + character.
    Alt(char),
    /// A kitty-enhanced event carrying full modifier/event-type info.
    /// Emitted when the escape sequence contains information that the
    /// basic `Key` variants can't represent (non-press event types,
    /// modifier combinations beyond Ctrl/Alt, alternate keycodes, text).
    Enhanced(KeyEvent),
    /// Bracketed-paste content. Surfaced when bracketed paste is
    /// enabled and the terminal sends `ESC [ 200 ~ <bytes> ESC [ 201 ~`.
    /// The carried `String` is the raw pasted content (UTF-8 lossy on
    /// invalid input). Callers should treat this as a single insertion
    /// rather than per-character keystrokes.
    Paste(String),
    /// Unknown / unparseable input.
    Unknown,
}

impl Key {
    /// Parse an ANSI escape sequence into a `Key`.
    ///
    /// Handles three layers of encoding, in order:
    ///
    /// 1. The bare `ESC` key (single-byte sequence).
    /// 2. Kitty-protocol sequences — any CSI sequence whose terminator is
    ///    `u`, `~`, or one of the function-key letters `A/B/C/D/E/F/H/P/Q/R/S`.
    ///    These may carry modifiers and event types; if the information
    ///    can't fit in a basic `Key`, we return `Key::Enhanced`.
    /// 3. Legacy short forms like `ESC O P` (F1) that kitty never uses.
    pub(crate) fn from_escape_sequence(seq: &[u8]) -> Option<Self> {
        if seq.is_empty() {
            return None;
        }

        // Bare ESC.
        if seq.len() == 1 && seq[0] == 0x1b {
            return Some(Key::Escape);
        }

        // `ESC ESC <...>` — "meta-prefix" form emitted by terminals like
        // macOS Terminal.app (with "Use Option as Meta key" enabled) for
        // Alt+<key> combinations. Parse the inner sequence and mix Alt
        // into the resulting modifiers.
        if seq.len() >= 2 && seq[0] == 0x1b && seq[1] == 0x1b {
            let inner = Self::from_escape_sequence(&seq[1..])?;
            return Some(apply_alt(inner));
        }

        // `ESC <printable>` — emacs-style meta convention. Classic on
        // macOS Terminal's default settings: Option+b → `\x1bb`,
        // Option+f → `\x1bf`. Surface as `Key::Alt(<char>)` so callers
        // can wire Alt+b/Alt+f to word motions.
        if seq.len() == 2 && seq[0] == 0x1b {
            let ch = seq[1];
            if (0x20..=0x7e).contains(&ch) {
                return Some(Key::Alt(ch as char));
            }
        }

        // `ESC O <letter>` — VT100 "application cursor keys" mode, used
        // by some terminals for F1..F4. Kitty doesn't emit these.
        if seq.len() >= 3 && seq[0] == 0x1b && seq[1] == b'O' {
            return match seq[2] {
                b'P' => Some(Key::F(1)),
                b'Q' => Some(Key::F(2)),
                b'R' => Some(Key::F(3)),
                b'S' => Some(Key::F(4)),
                _ => None,
            };
        }

        // Anything else must be a CSI (`ESC [ ...`) sequence.
        if seq.len() < 3 || seq[0] != 0x1b || seq[1] != b'[' {
            return None;
        }

        // Delegate CSI parsing to the kitty parser — it handles both
        // canonical kitty (`u`-terminated) and legacy xterm-style forms
        // (`A/B/C/D/...` and `~`). Then collapse the result to a basic
        // `Key` variant when possible.
        let event = KeyEvent::from_sequence(seq)?;
        Some(key_from_event(event))
    }
}

/// Collapse a `KeyEvent` into the narrowest `Key` variant that losslessly
/// represents it.
///
/// Callers that only care about "what key is this" prefer the basic
/// variants; callers that need modifier/event/text detail get
/// `Key::Enhanced` back untouched.
///
/// The rule depends on whether the key is a functional key or a printable
/// character:
/// - For **printable chars**, Shift is baked into the codepoint
///   (Shift+a → 'A'), so Shift can be discarded when collapsing to
///   `Key::Char`.
/// - For **functional keys** (arrows, Enter, F-keys), Shift carries
///   real meaning — e.g. Shift+arrow drives selection extension — so
///   it must reach the caller as `Key::Enhanced`. The lone exception
///   is `Shift+Tab`, which has its own dedicated `Key::BackTab` variant
///   for tradition.
///
/// Ctrl/Alt always preserve through to dedicated variants when they
/// modify a single printable character.
fn key_from_event(event: KeyEvent) -> Key {
    let only_press = matches!(event.event_type, KeyEventType::Press);
    let no_alts = event.shifted_key.is_none() && event.base_key.is_none();
    let no_text = event.text.is_none();
    // Modifiers we always ignore — set by the OS/firmware, not by user
    // intent.
    let baked_in = Modifiers::CAPS_LOCK | Modifiers::NUM_LOCK;
    // For Char keys, Shift is part of the codepoint already.
    let chars_lossless = (event.modifiers & !(baked_in | Modifiers::SHIFT)).is_empty();
    // For functional keys, only the always-ignored mods are tolerated.
    let fns_lossless = (event.modifiers & !baked_in).is_empty();

    if let Some(fk) = event.functional() {
        if only_press
            && no_alts
            && no_text
            && matches!(fk, FunctionalKey::Tab)
            && event.modifiers == Modifiers::SHIFT
        {
            return Key::BackTab;
        }
        if only_press && no_alts && no_text && fns_lossless {
            if let Some(basic) = functional_to_basic_key(fk) {
                return basic;
            }
        }
        return Key::Enhanced(event);
    }

    // Ctrl+char and Alt+char: only when the codepoint is a printable
    // ASCII character (the range crossterm/ncurses use for these).
    if only_press && no_alts && no_text {
        if let Some(ch) = char::from_u32(event.code) {
            let mods = event.modifiers;
            let pure_ctrl = mods == Modifiers::CTRL;
            let pure_alt = mods == Modifiers::ALT;
            if pure_ctrl && ch.is_ascii() {
                return Key::Ctrl(ch.to_ascii_lowercase());
            }
            if pure_alt && ch.is_ascii() {
                return Key::Alt(ch);
            }
            if chars_lossless {
                return Key::Char(ch);
            }
        }
    }

    Key::Enhanced(event)
}

/// Mix `Alt` into the modifiers of an already-parsed key. Used by the
/// meta-prefix path (`\x1b\x1b[...`) so a pre-parsed basic key like
/// `Key::Left` gets promoted to `Key::Enhanced { Left, ALT }` rather
/// than silently losing the modifier.
fn apply_alt(key: Key) -> Key {
    match key {
        // `\x1b\x1bX` where X is a single byte that parsed as a plain
        // char — the inner parser already handled it as `Key::Char`
        // (not possible via the CSI path, but harmless).
        Key::Char(ch) if ch.is_ascii() => Key::Alt(ch),
        // An already-enhanced event just gets Alt OR'd into its mods.
        Key::Enhanced(mut ev) => {
            ev.modifiers |= Modifiers::ALT;
            Key::Enhanced(ev)
        }
        // A basic functional variant gets promoted to an enhanced event
        // so callers can observe the Alt modifier.
        k => {
            if let Some(code) = basic_key_pua_code(&k) {
                Key::Enhanced(KeyEvent {
                    code,
                    modifiers: Modifiers::ALT,
                    ..Default::default()
                })
            } else {
                // Bare ESC or something we don't know how to enhance —
                // Alt+Esc is rare; surface as an Alt('\x1b') so the
                // modifier isn't silently dropped.
                if matches!(k, Key::Escape) {
                    Key::Alt('\x1b')
                } else {
                    k
                }
            }
        }
    }
}

/// Inverse of [`functional_to_basic_key`]: recover a kitty PUA code for
/// the basic variants that map 1:1 to named functional keys. Used only
/// by [`apply_alt`] to promote `Key::Left` etc. back into a KeyEvent.
fn basic_key_pua_code(k: &Key) -> Option<u32> {
    // Keep these constants in sync with kitty.rs's PUA_* table.
    Some(match k {
        Key::Escape => 57344,
        Key::Enter => 57345,
        Key::Tab => 57346,
        Key::Backspace => 57347,
        Key::Insert => 57348,
        Key::Delete => 57349,
        Key::Left => 57350,
        Key::Right => 57351,
        Key::Up => 57352,
        Key::Down => 57353,
        Key::PageUp => 57354,
        Key::PageDown => 57355,
        Key::Home => 57356,
        Key::End => 57357,
        Key::F(n) if (1..=35).contains(n) => 57364 + (*n as u32) - 1,
        _ => return None,
    })
}

fn functional_to_basic_key(fk: FunctionalKey) -> Option<Key> {
    Some(match fk {
        FunctionalKey::Escape => Key::Escape,
        FunctionalKey::Enter | FunctionalKey::KpEnter => Key::Enter,
        FunctionalKey::Tab => Key::Tab,
        FunctionalKey::Backspace => Key::Backspace,
        FunctionalKey::Insert | FunctionalKey::KpInsert => Key::Insert,
        FunctionalKey::Delete | FunctionalKey::KpDelete => Key::Delete,
        FunctionalKey::Left | FunctionalKey::KpLeft => Key::Left,
        FunctionalKey::Right | FunctionalKey::KpRight => Key::Right,
        FunctionalKey::Up | FunctionalKey::KpUp => Key::Up,
        FunctionalKey::Down | FunctionalKey::KpDown => Key::Down,
        FunctionalKey::PageUp | FunctionalKey::KpPageUp => Key::PageUp,
        FunctionalKey::PageDown | FunctionalKey::KpPageDown => Key::PageDown,
        FunctionalKey::Home | FunctionalKey::KpHome => Key::Home,
        FunctionalKey::End | FunctionalKey::KpEnd => Key::End,
        FunctionalKey::F(n) => Key::F(n),
        _ => return None,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::kitty::KeyEventType;

    #[test]
    fn bare_escape() {
        assert_eq!(Key::from_escape_sequence(&[0x1b]), Some(Key::Escape));
    }

    #[test]
    fn legacy_arrows_no_mods() {
        assert_eq!(Key::from_escape_sequence(b"\x1b[A"), Some(Key::Up));
        assert_eq!(Key::from_escape_sequence(b"\x1b[B"), Some(Key::Down));
        assert_eq!(Key::from_escape_sequence(b"\x1b[C"), Some(Key::Right));
        assert_eq!(Key::from_escape_sequence(b"\x1b[D"), Some(Key::Left));
    }

    #[test]
    fn legacy_home_end() {
        assert_eq!(Key::from_escape_sequence(b"\x1b[H"), Some(Key::Home));
        assert_eq!(Key::from_escape_sequence(b"\x1b[F"), Some(Key::End));
        // Some rxvt-style terminals use `1~` / `4~` instead of H/F.
        assert_eq!(Key::from_escape_sequence(b"\x1b[1~"), Some(Key::Home));
        assert_eq!(Key::from_escape_sequence(b"\x1b[4~"), Some(Key::End));
    }

    #[test]
    fn backtab_via_z_terminator() {
        // xterm-style `\x1b[Z` for Shift+Tab collapses to Key::BackTab.
        assert_eq!(Key::from_escape_sequence(b"\x1b[Z"), Some(Key::BackTab));
    }

    #[test]
    fn backtab_via_kitty_tab_with_shift() {
        // Kitty-protocol form: codepoint 9 (Tab) with Shift modifier
        // collapses to the same semantic BackTab variant.
        assert_eq!(Key::from_escape_sequence(b"\x1b[9;2u"), Some(Key::BackTab));
    }

    #[test]
    fn legacy_tilde_special_keys() {
        assert_eq!(Key::from_escape_sequence(b"\x1b[2~"), Some(Key::Insert));
        assert_eq!(Key::from_escape_sequence(b"\x1b[3~"), Some(Key::Delete));
        assert_eq!(Key::from_escape_sequence(b"\x1b[5~"), Some(Key::PageUp));
        assert_eq!(Key::from_escape_sequence(b"\x1b[6~"), Some(Key::PageDown));
    }

    #[test]
    fn legacy_function_keys_via_ss3() {
        assert_eq!(Key::from_escape_sequence(b"\x1bOP"), Some(Key::F(1)));
        assert_eq!(Key::from_escape_sequence(b"\x1bOQ"), Some(Key::F(2)));
        assert_eq!(Key::from_escape_sequence(b"\x1bOR"), Some(Key::F(3)));
        assert_eq!(Key::from_escape_sequence(b"\x1bOS"), Some(Key::F(4)));
    }

    #[test]
    fn legacy_function_keys_via_tilde() {
        // F5..F12 via classic `\x1b[N~`.
        assert_eq!(Key::from_escape_sequence(b"\x1b[15~"), Some(Key::F(5)));
        assert_eq!(Key::from_escape_sequence(b"\x1b[17~"), Some(Key::F(6)));
        assert_eq!(Key::from_escape_sequence(b"\x1b[24~"), Some(Key::F(12)));
    }

    #[test]
    fn kitty_arrow_with_ctrl_becomes_enhanced() {
        // Ctrl+Up: `\x1b[1;5A`. wire-mods 5 = ctrl only.
        let k = Key::from_escape_sequence(b"\x1b[1;5A").unwrap();
        match k {
            Key::Enhanced(e) => {
                assert!(e.is_ctrl());
                assert!(!e.is_shift());
                assert_eq!(e.functional(), Some(FunctionalKey::Up));
            }
            _ => panic!("expected Key::Enhanced for Ctrl+Up, got {:?}", k),
        }
    }

    #[test]
    fn kitty_arrow_with_shift_becomes_enhanced() {
        // Shift on a functional key is meaningful (selection extension),
        // so it must reach callers as `Key::Enhanced` carrying the
        // Shift modifier rather than collapsing to `Key::Up`.
        let k = Key::from_escape_sequence(b"\x1b[1;2A").unwrap();
        match k {
            Key::Enhanced(e) => {
                assert!(e.is_shift());
                assert_eq!(e.functional(), Some(FunctionalKey::Up));
            }
            _ => panic!("expected Enhanced for Shift+Up, got {:?}", k),
        }
    }

    #[test]
    fn kitty_release_event_becomes_enhanced() {
        // 'a' release: wire-mods = 1 (none), event_type = 3.
        let k = Key::from_escape_sequence(b"\x1b[97;1:3u").unwrap();
        match k {
            Key::Enhanced(e) => assert_eq!(e.event_type, KeyEventType::Release),
            _ => panic!("expected Key::Enhanced for release, got {:?}", k),
        }
    }

    #[test]
    fn kitty_plain_press_collapses_to_char() {
        // Plain 'a' from kitty — no modifiers, no extras.
        let k = Key::from_escape_sequence(b"\x1b[97u").unwrap();
        assert_eq!(k, Key::Char('a'));
    }

    #[test]
    fn kitty_ctrl_char_collapses_to_ctrl_variant() {
        // Ctrl+A (codepoint 97, wire mods = 5).
        let k = Key::from_escape_sequence(b"\x1b[97;5u").unwrap();
        assert_eq!(k, Key::Ctrl('a'));
    }

    #[test]
    fn kitty_alt_char_collapses_to_alt_variant() {
        // Alt+x (codepoint 120, wire mods = 3).
        let k = Key::from_escape_sequence(b"\x1b[120;3u").unwrap();
        assert_eq!(k, Key::Alt('x'));
    }

    #[test]
    fn kitty_ctrl_alt_stays_enhanced() {
        // Ctrl+Alt+x — combination that Key::Ctrl/Alt can't both carry.
        let k = Key::from_escape_sequence(b"\x1b[120;7u").unwrap();
        match k {
            Key::Enhanced(e) => {
                assert!(e.is_ctrl());
                assert!(e.is_alt());
            }
            _ => panic!("expected Enhanced for Ctrl+Alt, got {:?}", k),
        }
    }

    #[test]
    fn kitty_pua_enter_collapses_to_enter() {
        // PUA code for Enter.
        let k = Key::from_escape_sequence(b"\x1b[57345u").unwrap();
        assert_eq!(k, Key::Enter);
    }

    #[test]
    fn kitty_f5_via_pua() {
        let k = Key::from_escape_sequence(b"\x1b[57368u").unwrap();
        assert_eq!(k, Key::F(5));
    }

    #[test]
    fn kitty_alternate_keys_stay_enhanced() {
        // Alternate-key info is richer than Key::Char can carry.
        let k = Key::from_escape_sequence(b"\x1b[97:65;2u").unwrap();
        assert!(matches!(k, Key::Enhanced(_)));
    }

    #[test]
    fn meta_prefix_left_is_alt_left() {
        // macOS Terminal.app "Option as Meta" Alt+Left.
        let k = Key::from_escape_sequence(b"\x1b\x1b[D").unwrap();
        match k {
            Key::Enhanced(e) => {
                assert_eq!(e.functional(), Some(FunctionalKey::Left));
                assert!(e.is_alt());
            }
            _ => panic!("expected Enhanced Alt+Left, got {:?}", k),
        }
    }

    #[test]
    fn meta_prefix_with_modifier_combines_alt_and_ctrl() {
        // `\x1b\x1b[1;5D` — Ctrl already on the inner CSI, meta-prefix
        // adds Alt.
        let k = Key::from_escape_sequence(b"\x1b\x1b[1;5D").unwrap();
        match k {
            Key::Enhanced(e) => {
                assert_eq!(e.functional(), Some(FunctionalKey::Left));
                assert!(e.is_alt());
                assert!(e.is_ctrl());
            }
            _ => panic!("expected Enhanced Alt+Ctrl+Left, got {:?}", k),
        }
    }

    #[test]
    fn emacs_meta_b_is_alt_b() {
        // Default macOS Terminal.app Option+b.
        assert_eq!(Key::from_escape_sequence(b"\x1bb"), Some(Key::Alt('b')));
        assert_eq!(Key::from_escape_sequence(b"\x1bf"), Some(Key::Alt('f')));
    }

    #[test]
    fn rejects_garbage() {
        assert!(Key::from_escape_sequence(b"").is_none());
        // `\x1bX` is now a valid emacs-meta sequence (Alt+X), so the
        // prior "rejects unknown second byte" check is stale — `X` is
        // printable ASCII. We still reject a malformed CSI terminator.
        assert!(Key::from_escape_sequence(b"\x1b[65x").is_none());
    }
}