Skip to main content

tui_lipan/widgets/terminal/
events.rs

1use std::sync::Arc;
2
3use crate::core::event::{KeyCode, KeyEvent, KeyMods, MouseButton, MouseEvent, MouseKind};
4use crate::style::Span;
5use crate::utils::spans::{line_text, line_width, slice_columns};
6use crate::utils::{GridSelection, SelectionEnd};
7
8/// Terminal input event source.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum TerminalInputKind {
11    /// Keyboard input encoded for the PTY.
12    Key,
13    /// Clipboard paste input encoded for the PTY.
14    Paste,
15    /// Focus-in notification.
16    FocusIn,
17    /// Focus-out notification.
18    FocusOut,
19}
20
21/// Terminal input event emitted by the framework.
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct TerminalInputEvent {
24    /// Event source.
25    pub kind: TerminalInputKind,
26    /// Original key event (if applicable).
27    pub key: Option<KeyEvent>,
28    /// Encoded bytes suitable for PTY stdin.
29    pub bytes: Arc<[u8]>,
30}
31
32/// Extract a terminal selection from styled rendered lines.
33///
34/// Terminal selection columns are **display columns**, not character indices. Wide characters and
35/// zero-width combining characters therefore use the same column accounting as the renderer.
36/// The selection endpoint is exclusive, matching mouse drag coordinates.
37pub fn terminal_selection_text(lines: &[Vec<Span>], selection: &GridSelection) -> String {
38    terminal_selection_text_with(lines, selection, SelectionEnd::Exclusive, false)
39}
40
41/// Extract a terminal selection with an explicit endpoint and row-trimming policy.
42pub(crate) fn terminal_selection_text_with(
43    lines: &[Vec<Span>],
44    selection: &GridSelection,
45    endpoint: SelectionEnd,
46    trim_row_end: bool,
47) -> String {
48    if selection.is_empty() && matches!(endpoint, SelectionEnd::Exclusive) {
49        return String::new();
50    }
51
52    let (start, end) = selection.normalized();
53    let mut result = String::new();
54    for row in start.row..=end.row {
55        let Some(line) = lines.get(row) else { continue };
56        let width = line_width(line);
57        let col_start = if row == start.row { start.col } else { 0 };
58        let col_end = if row == end.row {
59            end.col
60                .saturating_add(matches!(endpoint, SelectionEnd::Inclusive) as usize)
61        } else {
62            width
63        };
64        let mut text = line_text(&slice_columns(line, col_start, col_end));
65        if trim_row_end {
66            text.truncate(text.trim_end().len());
67        }
68        result.push_str(&text);
69        if row < end.row {
70            result.push('\n');
71        }
72    }
73    result
74}
75
76/// The [Kitty keyboard protocol] enhancement flags a child program has pushed with `CSI > <flags> u`.
77///
78/// A terminal must not send the protocol's `CSI u` encodings until the child has asked for them,
79/// so these gate [`key_event_to_bytes`]. Only `disambiguate_escape_codes` changes what this encoder
80/// emits today; the rest are surfaced so hosts can see what the child negotiated.
81///
82/// [Kitty keyboard protocol]: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
83#[cfg_attr(
84    feature = "terminal-serde",
85    derive(serde::Serialize, serde::Deserialize)
86)]
87#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
88pub struct KittyKeyboardFlags {
89    /// Bit 1. Keys with no unambiguous legacy encoding are reported as `CSI <codepoint>;<mod> u`,
90    /// and a lone `Esc` becomes `CSI 27 u` so it cannot be confused with an escape sequence.
91    pub disambiguate_escape_codes: bool,
92    /// Bit 2. Key release and repeat events are reported. Not emitted: `KeyEvent` carries no kind.
93    pub report_event_types: bool,
94    /// Bit 4. Shifted and base-layout key codes accompany each report. Not emitted.
95    pub report_alternate_keys: bool,
96    /// Bit 8. Every key, including plain text, is reported as an escape code. Not emitted.
97    pub report_all_keys_as_escape_codes: bool,
98    /// Bit 16. The text a key would produce accompanies each report. Not emitted.
99    pub report_associated_text: bool,
100}
101
102impl KittyKeyboardFlags {
103    /// Whether the child has negotiated any part of the protocol.
104    pub fn any(&self) -> bool {
105        self.disambiguate_escape_codes
106            || self.report_event_types
107            || self.report_alternate_keys
108            || self.report_all_keys_as_escape_codes
109            || self.report_associated_text
110    }
111}
112
113/// Input-affecting modes the child program has turned on.
114///
115/// The child requests these with `CSI ? <n> h` / `CSI ? <n> l` (DEC private modes) or `CSI > <n> u`
116/// (the Kitty keyboard protocol), and they change what bytes a key press or a paste must produce.
117/// `TerminalScreen` tracks them and publishes them on
118/// [`TerminalRenderSnapshot`](crate::widgets::TerminalRenderSnapshot), the same way it publishes
119/// [`MouseModeState`]. Pass [`TerminalKeyModes::default()`] when no child has spoken yet.
120#[cfg_attr(
121    feature = "terminal-serde",
122    derive(serde::Serialize, serde::Deserialize)
123)]
124#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
125pub struct TerminalKeyModes {
126    /// DECCKM (`CSI ? 1 h`): unmodified cursor keys are introduced by `SS3` (`ESC O`) instead of
127    /// `CSI` (`ESC [`). Modified cursor keys always stay on the `CSI` form.
128    pub app_cursor: bool,
129    /// Bracketed paste (`CSI ? 2004 h`): pasted text is wrapped in `CSI 200~` / `CSI 201~` so the
130    /// child can tell it apart from typing. When off, pasting the wrapper would insert its literal
131    /// bytes into the child's input.
132    pub bracketed_paste: bool,
133    /// Kitty keyboard protocol flags the child pushed with `CSI > <flags> u`.
134    pub kitty_keyboard: KittyKeyboardFlags,
135}
136
137/// How a focused terminal handles a direct `Ctrl+V` shortcut before app commands.
138#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
139pub enum TerminalPasteShortcutBehavior {
140    /// Forward `Ctrl+V` to the child unchanged.
141    #[default]
142    Forward,
143    /// Paste plain text locally, but forward `Ctrl+V` when the clipboard contains files, an image,
144    /// or another non-text format so the child can inspect the system clipboard itself.
145    Performable,
146}
147
148/// Encode a framework `KeyEvent` into terminal bytes.
149///
150/// This covers common printable keys and ANSI control sequences. `modes` carries the modes the
151/// child has negotiated; see [`TerminalKeyModes`]. Returns `None` when the key has no encoding the
152/// child would understand, which leaves the caller free to route it elsewhere.
153///
154/// Chords like `Ctrl+1` have no legacy encoding at all and can only be delivered once the child has
155/// negotiated the Kitty keyboard protocol; until then they return `None` rather than being
156/// flattened onto some other key's bytes.
157///
158/// Note that in the legacy encoding `Ctrl+Shift+C` produces `0x03` (SIGINT) exactly like `Ctrl+C`,
159/// because a control code has no shift bit. The `Terminal` widget never reaches this path for that
160/// chord: the clipboard preflight consumes it first. Direct callers must do the same. Under the
161/// Kitty protocol the two are distinct (`CSI 99;6u` versus `CSI 99;5u`).
162pub fn key_event_to_bytes(key: KeyEvent, modes: TerminalKeyModes) -> Option<Vec<u8>> {
163    // Super has no representation in any encoding we speak. Forwarding the unmodified key would
164    // type a character the user never asked for (Super+C inserting a literal `c`), so drop it and
165    // let the chord bubble to the app instead.
166    if key.mods.super_key {
167        return None;
168    }
169
170    // Only once the child has negotiated the protocol. Sending `CSI u` unsolicited would hand a
171    // legacy child a sequence it cannot parse.
172    if modes.kitty_keyboard.disambiguate_escape_codes
173        && let Some(bytes) = kitty_csi_u_bytes(key.code, key.mods)
174    {
175        return Some(bytes);
176    }
177
178    if let Some(bytes) = modified_special_key_bytes(key.code, key.mods) {
179        return Some(bytes);
180    }
181
182    // Ctrl+Backspace has no native PTY encoding, so a terminal has to pick a sequence for it.
183    // Emit `ESC DEL` - readline's `backward-kill-word` and the same bytes as Alt+Backspace - so
184    // "delete the previous word" works out of the box in shells and line editors, instead of
185    // collapsing to a bare Backspace (`DEL`) that only deletes a single character. A child that
186    // negotiated the Kitty protocol gets `CSI 127;5 u` above instead.
187    if key.mods.ctrl && key.code == KeyCode::Backspace {
188        return Some(vec![0x1b, 0x7f]);
189    }
190
191    let mut bytes = match key.code {
192        KeyCode::Char(ch) => {
193            if key.mods.ctrl {
194                vec![ctrl_char(ch)?]
195            } else {
196                ch.to_string().into_bytes()
197            }
198        }
199        KeyCode::Enter => vec![b'\r'],
200        KeyCode::Tab => vec![b'\t'],
201        KeyCode::BackTab => b"\x1b[Z".to_vec(),
202        KeyCode::Backspace => vec![0x7f],
203        KeyCode::Esc => vec![0x1b],
204        KeyCode::Up => cursor_key_bytes(b'A', modes),
205        KeyCode::Down => cursor_key_bytes(b'B', modes),
206        KeyCode::Right => cursor_key_bytes(b'C', modes),
207        KeyCode::Left => cursor_key_bytes(b'D', modes),
208        KeyCode::Home => cursor_key_bytes(b'H', modes),
209        KeyCode::End => cursor_key_bytes(b'F', modes),
210        KeyCode::PageUp => b"\x1b[5~".to_vec(),
211        KeyCode::PageDown => b"\x1b[6~".to_vec(),
212        KeyCode::Insert => b"\x1b[2~".to_vec(),
213        KeyCode::Delete => b"\x1b[3~".to_vec(),
214        KeyCode::F(n) => format!("\x1b[{}~", f_key_number(n)?).into_bytes(),
215    };
216
217    if key.mods.alt {
218        let mut alt_prefixed = Vec::with_capacity(bytes.len() + 1);
219        alt_prefixed.push(0x1b);
220        alt_prefixed.extend(bytes);
221        bytes = alt_prefixed;
222    }
223
224    Some(bytes)
225}
226
227/// An unmodified cursor key, introduced by `SS3` when the child has set DECCKM and by `CSI`
228/// otherwise. ncurses emits `smkx` (`ESC [ ? 1 h ESC =`) on startup and then matches arrows
229/// against terminfo's `kcuu1=\EOA`, so a child in application mode expects `ESC O A`.
230fn cursor_key_bytes(final_byte: u8, modes: TerminalKeyModes) -> Vec<u8> {
231    let introducer: &[u8] = if modes.app_cursor { b"\x1bO" } else { b"\x1b[" };
232    let mut bytes = Vec::with_capacity(3);
233    bytes.extend_from_slice(introducer);
234    bytes.push(final_byte);
235    bytes
236}
237
238/// The xterm modifier parameter for a modified special key: `1 + shift + 2·alt + 4·ctrl`, so
239/// Shift=2, Alt=3, Ctrl=5, Ctrl+Shift=6, and so on. Super has no bit here; `key_event_to_bytes`
240/// drops Super-modified keys before reaching this point.
241fn xterm_modifier_param(mods: KeyMods) -> u8 {
242    1 + u8::from(mods.shift) + 2 * u8::from(mods.alt) + 4 * u8::from(mods.ctrl)
243}
244
245/// The parameter number for a function key in the `CSI <num> ~` scheme (F1→11 … F20→34),
246/// matching the unmodified encoding. `None` for out-of-range function keys.
247fn f_key_number(n: u8) -> Option<u8> {
248    Some(match n {
249        1 => 11,
250        2 => 12,
251        3 => 13,
252        4 => 14,
253        5 => 15,
254        6 => 17,
255        7 => 18,
256        8 => 19,
257        9 => 20,
258        10 => 21,
259        11 => 23,
260        12 => 24,
261        13 => 25,
262        14 => 26,
263        15 => 28,
264        16 => 29,
265        17 => 31,
266        18 => 32,
267        19 => 33,
268        20 => 34,
269        _ => return None,
270    })
271}
272
273/// Encode a chord in the Kitty keyboard protocol's `CSI <codepoint> ; <mod> u` form.
274///
275/// Only reached once the child has set `disambiguate_escape_codes`. This is what lets a chord like
276/// `Ctrl+1` reach the child at all: it has no legacy encoding, so without the protocol it can only
277/// be dropped. `Ctrl+Enter` and `Shift+Enter` likewise become distinguishable from a bare `Enter`.
278///
279/// Returns `None` for the keys that keep their legacy encoding at this protocol level: plain text,
280/// the arrows, `Home`/`End`, the tilde keys, and the function keys. Those already have unambiguous
281/// sequences, and Kitty only escapes them under `report_all_keys_as_escape_codes`.
282fn kitty_csi_u_bytes(code: KeyCode, mods: KeyMods) -> Option<Vec<u8>> {
283    let (codepoint, mods) = match code {
284        // A text-producing key still produces text under Shift alone. Ctrl and Alt are what
285        // promote it to the escape form, because a control code cannot express which key it was.
286        KeyCode::Char(ch) if mods.ctrl || mods.alt => (kitty_char_codepoint(ch), mods),
287        // The whole point of `disambiguate_escape_codes`: a lone Esc must not look like the start
288        // of an escape sequence.
289        KeyCode::Esc => (27, mods),
290        KeyCode::Enter if !mods.is_empty() => (13, mods),
291        KeyCode::Tab if mods.ctrl || mods.alt => (9, mods),
292        // BackTab *is* Shift+Tab, so put the shift back into the parameter even when the backend
293        // reported the chord without it.
294        KeyCode::BackTab => (
295            9,
296            KeyMods {
297                shift: true,
298                ..mods
299            },
300        ),
301        KeyCode::Backspace if mods.ctrl || mods.alt => (127, mods),
302        _ => return None,
303    };
304
305    let m = xterm_modifier_param(mods);
306    let seq = if m == 1 {
307        format!("\x1b[{codepoint}u")
308    } else {
309        format!("\x1b[{codepoint};{m}u")
310    };
311    Some(seq.into_bytes())
312}
313
314/// The codepoint Kitty reports for a character key: the key as engraved, without Shift applied.
315/// `Ctrl+Shift+C` therefore reports `c` (99) with the shift bit set in the modifier parameter.
316fn kitty_char_codepoint(ch: char) -> u32 {
317    ch.to_lowercase().next().unwrap_or(ch) as u32
318}
319
320/// Whether Shift is the only modifier on a key that a terminal emulator conventionally handles
321/// itself: Shift+Insert pastes, Shift+PageUp/PageDown page the scrollback.
322///
323/// This widget forwards those keys to the child rather than consuming them (its scrollback is
324/// driven by the wheel and `on_scroll_to`), so encoding them as `CSI <num> ; 2 ~` would hand the
325/// child a sequence it does not recognize and turn the key into a no-op. Keep the unmodified form
326/// so the child still pages and pastes.
327fn shift_reserved_by_emulator(code: KeyCode, mods: KeyMods) -> bool {
328    mods.shift
329        && !mods.ctrl
330        && !mods.alt
331        && matches!(code, KeyCode::Insert | KeyCode::PageUp | KeyCode::PageDown)
332}
333
334/// Encode a cursor, navigation, or function key that carries a modifier into its xterm
335/// parameterized CSI form: `CSI 1 ; <mod> <letter>` for the arrows and Home/End, `CSI <num> ;
336/// <mod> ~` for the tilde keys. Without this a modified key like Ctrl+Left would collapse to a
337/// bare Left and lose word-wise motion in TUIs (readline, editors).
338///
339/// Returns `None`, leaving the caller to fall back on the plain encoding, when the key has no
340/// parameterized form (`Char`, `Enter`, …), when Alt is the only modifier (that keeps its
341/// historical ESC-prefix encoding), and for the Shift-only keys an emulator normally reserves.
342fn modified_special_key_bytes(code: KeyCode, mods: KeyMods) -> Option<Vec<u8>> {
343    if (!mods.ctrl && !mods.shift) || shift_reserved_by_emulator(code, mods) {
344        return None;
345    }
346
347    let m = xterm_modifier_param(mods);
348    let seq = match code {
349        KeyCode::Up => format!("\x1b[1;{m}A"),
350        KeyCode::Down => format!("\x1b[1;{m}B"),
351        KeyCode::Right => format!("\x1b[1;{m}C"),
352        KeyCode::Left => format!("\x1b[1;{m}D"),
353        KeyCode::Home => format!("\x1b[1;{m}H"),
354        KeyCode::End => format!("\x1b[1;{m}F"),
355        KeyCode::Insert => format!("\x1b[2;{m}~"),
356        KeyCode::Delete => format!("\x1b[3;{m}~"),
357        KeyCode::PageUp => format!("\x1b[5;{m}~"),
358        KeyCode::PageDown => format!("\x1b[6;{m}~"),
359        KeyCode::F(n) => format!("\x1b[{};{m}~", f_key_number(n)?),
360        _ => return None,
361    };
362    Some(seq.into_bytes())
363}
364
365/// The C0 control code a `Ctrl+<char>` chord produces, or `None` when the chord has no control
366/// code (`Ctrl+1`, `Ctrl+;`, …) and should be left for the app to handle.
367fn ctrl_char(ch: char) -> Option<u8> {
368    if ch.is_ascii_alphabetic() {
369        return Some((ch.to_ascii_uppercase() as u8) - b'@');
370    }
371
372    Some(match ch {
373        ' ' | '@' => 0x00,
374        '[' => 0x1b,
375        '\\' => 0x1c,
376        ']' => 0x1d,
377        '^' => 0x1e,
378        // US. Readline binds it to `undo`, and `/` reaches it without Shift on most layouts.
379        '_' | '/' => 0x1f,
380        '?' => 0x7f,
381        // xterm's digit aliases, for the control codes whose named key needs Shift.
382        '2' => 0x00,
383        '3' => 0x1b,
384        '4' => 0x1c,
385        '5' => 0x1d,
386        '6' => 0x1e,
387        '7' => 0x1f,
388        '8' => 0x7f,
389        _ => return None,
390    })
391}
392
393/// Mouse reporting mode requested by PTY application.
394#[cfg_attr(
395    feature = "terminal-serde",
396    derive(serde::Serialize, serde::Deserialize)
397)]
398#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
399pub enum MouseMode {
400    /// No mouse reporting.
401    #[default]
402    None,
403    /// X10 compatibility mode (1000) - button press only.
404    X10,
405    /// Normal tracking (1002) - button press/release.
406    Normal,
407    /// Any-event tracking (1003) - all motion.
408    AnyEvent,
409}
410
411/// Mouse protocol encoding.
412#[cfg_attr(
413    feature = "terminal-serde",
414    derive(serde::Serialize, serde::Deserialize)
415)]
416#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
417pub enum MouseEncoding {
418    /// Default X10 encoding (coordinates limited to 223).
419    #[default]
420    X10,
421    /// SGR extended encoding (1006) - no coordinate limits.
422    Sgr,
423    /// UTF-8 extended encoding (1005) - no coordinate limits.
424    Utf8,
425}
426
427/// Combined mouse mode state.
428#[cfg_attr(
429    feature = "terminal-serde",
430    derive(serde::Serialize, serde::Deserialize)
431)]
432#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
433pub struct MouseModeState {
434    /// Mouse reporting mode to enable in the PTY.
435    pub mode: MouseMode,
436    /// Wire encoding used for mouse reports.
437    pub encoding: MouseEncoding,
438    /// Whether focus reporting is enabled (CSI ? 1004 h).
439    pub focus_events_enabled: bool,
440}
441
442/// Encode a MouseEvent to bytes for PTY (SGR 1006 format).
443pub fn mouse_event_to_bytes(
444    event: MouseEvent,
445    encoding: MouseEncoding,
446    viewport_offset: (u16, u16),
447) -> Option<Vec<u8>> {
448    let (button_code, is_release) = match event.kind {
449        MouseKind::Down(btn) => (button_to_code(btn), false),
450        MouseKind::Up(btn) => (button_to_code(btn), true),
451        MouseKind::Drag(btn) => (button_to_code(btn).saturating_add(32), false),
452        MouseKind::ScrollUp => (64, false),
453        MouseKind::ScrollDown => (65, false),
454        // Motion without a pressed button: code 3 ("no button") + 32 (motion
455        // flag). Callers gate this on any-event tracking (1003) being active.
456        MouseKind::Moved => (35, false),
457    };
458
459    let mut cb = button_code;
460    if event.mods.shift {
461        cb = cb.saturating_add(4);
462    }
463    if event.mods.alt {
464        cb = cb.saturating_add(8);
465    }
466    if event.mods.ctrl {
467        cb = cb.saturating_add(16);
468    }
469
470    let cx = event.x.saturating_sub(viewport_offset.0).saturating_add(1);
471    let cy = event.y.saturating_sub(viewport_offset.1).saturating_add(1);
472
473    match encoding {
474        MouseEncoding::Sgr => {
475            let suffix = if is_release { 'm' } else { 'M' };
476            Some(format!("\x1b[<{};{};{}{}", cb, cx, cy, suffix).into_bytes())
477        }
478        MouseEncoding::X10 => {
479            if cx > 223 || cy > 223 {
480                return None;
481            }
482            let cb = cb.saturating_add(32);
483            let cx = cx.saturating_add(32) as u8;
484            let cy = cy.saturating_add(32) as u8;
485            Some(vec![0x1b, b'[', b'M', cb, cx, cy])
486        }
487        MouseEncoding::Utf8 => {
488            let mut out = Vec::with_capacity(6);
489            out.extend_from_slice(b"\x1b[M");
490            out.push(cb.saturating_add(32));
491            push_utf8_coord(&mut out, cx.saturating_add(32))?;
492            push_utf8_coord(&mut out, cy.saturating_add(32))?;
493            Some(out)
494        }
495    }
496}
497
498#[cfg(all(test, feature = "terminal-serde"))]
499mod terminal_serde_tests {
500    use super::*;
501
502    #[test]
503    fn mouse_mode_state_round_trips() {
504        let state = MouseModeState {
505            mode: MouseMode::AnyEvent,
506            encoding: MouseEncoding::Sgr,
507            focus_events_enabled: true,
508        };
509        let json = serde_json::to_string(&state).unwrap();
510        assert_eq!(
511            serde_json::from_str::<MouseModeState>(&json).unwrap(),
512            state
513        );
514    }
515}
516
517fn push_utf8_coord(out: &mut Vec<u8>, value: u16) -> Option<()> {
518    let mut buffer = [0u8; 4];
519    let ch = char::from_u32(u32::from(value))?;
520    let encoded = ch.encode_utf8(&mut buffer);
521    out.extend_from_slice(encoded.as_bytes());
522    Some(())
523}
524
525fn button_to_code(btn: MouseButton) -> u8 {
526    match btn {
527        MouseButton::Left => 0,
528        MouseButton::Middle => 1,
529        MouseButton::Right => 2,
530    }
531}
532
533/// Focus-in escape sequence.
534pub fn focus_in_sequence() -> &'static [u8] {
535    b"\x1b[I"
536}
537
538/// Focus-out escape sequence.
539pub fn focus_out_sequence() -> &'static [u8] {
540    b"\x1b[O"
541}
542
543/// Focus-related sequences (focus-in, focus-out).
544pub fn focus_sequences() -> (&'static [u8], &'static [u8]) {
545    (focus_in_sequence(), focus_out_sequence())
546}
547
548/// Encode pasted text for the child's stdin.
549///
550/// Wraps the text in the bracketed-paste sequences only when the child has enabled the mode
551/// (`CSI ? 2004 h`). A child that has not asked for bracketed paste does not strip the wrapper, so
552/// sending it unconditionally would insert the literal bytes `ESC [ 200 ~` into its input.
553pub fn encode_paste(text: &str, modes: TerminalKeyModes) -> Vec<u8> {
554    if !modes.bracketed_paste {
555        return text.as_bytes().to_vec();
556    }
557
558    let (start, end) = paste_sequences();
559    let mut out = Vec::with_capacity(text.len() + start.len() + end.len());
560    out.extend_from_slice(start);
561    out.extend_from_slice(text.as_bytes());
562    out.extend_from_slice(end);
563    out
564}
565
566/// All paste-related sequences.
567pub fn paste_sequences() -> (&'static [u8], &'static [u8]) {
568    (b"\x1b[200~", b"\x1b[201~")
569}
570
571#[cfg(test)]
572mod selection_tests {
573    use super::*;
574
575    #[test]
576    fn terminal_selection_text_uses_display_columns_for_wide_characters() {
577        use crate::utils::GridPos;
578
579        let lines = vec![vec![Span::new("a界🙂b")]];
580        let mut cjk = GridSelection::new(GridPos { row: 0, col: 1 });
581        cjk.extend_to(GridPos { row: 0, col: 3 });
582        assert_eq!(terminal_selection_text(&lines, &cjk), "界");
583
584        let mut emoji = GridSelection::new(GridPos { row: 0, col: 3 });
585        emoji.extend_to(GridPos { row: 0, col: 5 });
586        assert_eq!(terminal_selection_text(&lines, &emoji), "🙂");
587    }
588}