mod buffer;
#[cfg(feature = "terminal")]
mod copy_mode;
mod events;
#[cfg(feature = "terminal-images")]
mod graphics;
mod layout;
mod mod_private;
mod node;
mod osc;
mod pty;
mod reconcile;
mod screen;
mod scrollback_ledger;
#[cfg(feature = "terminal")]
mod selection;
pub use buffer::TerminalBuffer;
#[cfg(feature = "terminal")]
pub use copy_mode::{CopyModeAction, CopyModeGrid, TerminalCopyMode};
pub use events::{
KittyKeyboardFlags, MouseEncoding, MouseMode, MouseModeState, TerminalInputEvent,
TerminalInputKind, TerminalKeyModes, TerminalPasteShortcutBehavior, encode_paste,
focus_sequences, key_event_to_bytes, mouse_event_to_bytes, paste_sequences,
terminal_selection_text,
};
#[cfg(feature = "terminal-images")]
pub use graphics::{TerminalImage, TerminalImageCrop, TerminalImagePlacement};
pub use mod_private::Terminal;
pub use osc::{
TerminalCommandPhase, TerminalSemanticEvent, TerminalSemanticState, TerminalWorkingDirectory,
TerminalWorkingDirectorySource,
};
#[cfg(unix)]
pub use pty::TerminalPtyHandoff;
pub use pty::{TerminalPty, TerminalPtyConfig, TerminalPtyError, TerminalPtyEvent};
pub use screen::{
SemanticMark, SemanticMarkKind, TerminalCellSize, TerminalColorPalette, TerminalDecoration,
TerminalRenderSnapshot, TerminalScreen, TerminalScreenHandle, TerminalViewport,
};
#[cfg(feature = "terminal")]
pub use selection::{
ScrollbackLineage, TerminalPos, TerminalSelection, TerminalSelectionEvent, absolute_line,
from_viewport, to_viewport, viewport_row,
};
pub(crate) use layout::{measure_terminal, terminal_content_layout, terminal_mouse_content_rect};
pub(crate) use node::{TerminalNode, apply_terminal_selection_input};
pub(crate) use reconcile::reconcile_terminal;
#[cfg(feature = "terminal")]
pub(crate) fn terminal_node_selection_text(
node: &TerminalNode,
sel: &TerminalSelection,
endpoint: crate::utils::SelectionEnd,
trim_row_end: bool,
) -> String {
if let Some(screen) = node.screen.as_ref() {
return screen.selection_display_text(sel, endpoint, trim_row_end);
}
let Some(projected) = to_viewport(
sel,
node.scrollback_offset,
node.total_scrollback_rows,
node.viewport_rows,
) else {
return String::new();
};
events::terminal_selection_text_with(&node.lines, &projected, endpoint, trim_row_end)
}
use crate::callback::{Callback, KeyHandler};
use crate::core::element::{Element, ElementKind};
use crate::style::{
BorderStyle, CaretShape, Color, Length, Padding, ScrollbarConfig, ScrollbarVariant, Span,
Style, StyleSlot,
};
use crate::widgets::ScrollEvent;
use std::sync::Arc;
impl Default for Terminal {
fn default() -> Self {
Self {
content: Arc::from(""),
cursor_row: 0,
cursor_col: 0,
show_cursor: true,
cursor_shape: CaretShape::Block,
cursor_blinking: true,
caret_color: None,
color_lines: None,
color_cache_key: 0,
screen: None,
decorations: Arc::from([] as [TerminalDecoration; 0]),
scrollback_offset: 0,
total_scrollback_rows: 0,
mouse_mode: MouseModeState::default(),
key_modes: TerminalKeyModes::default(),
#[cfg(feature = "terminal-images")]
images: Arc::from([]),
paste_shortcut_behavior: TerminalPasteShortcutBehavior::Forward,
selection: None,
selection_controlled: false,
selection_style: StyleSlot::Inherit,
on_selection: None,
on_resize: None,
on_mouse_forward: None,
scroll_wheel: true,
on_scroll: None,
on_scroll_to: None,
style: Style::default(),
hover_style: StyleSlot::Inherit,
focus_style: StyleSlot::Inherit,
focus_content_style: Style::default(),
border: false,
border_style: BorderStyle::default(),
padding: Padding::default(),
scrollbar: true,
scrollbar_variant: ScrollbarVariant::default(),
scrollbar_gap: 0,
scrollbar_thumb: None,
scrollbar_thumb_style: None,
scrollbar_thumb_focus_style: None,
scrollbar_track_style: None,
h_scrollbar: true,
h_scrollbar_variant: ScrollbarVariant::default(),
width: Length::Flex(1),
height: Length::Flex(1),
focusable: true,
tab_stop: true,
on_focus: None,
on_blur: None,
on_key: None,
on_input: None,
}
}
}
impl Terminal {
pub fn new() -> Self {
Self::default()
}
pub fn content(mut self, content: impl Into<Arc<str>>) -> Self {
self.content = content.into();
self
}
pub fn cursor(mut self, cursor: usize) -> Self {
let (row, col) = byte_to_row_col(self.content.as_ref(), cursor);
self.cursor_row = row;
self.cursor_col = col;
self
}
pub fn cursor_position(mut self, row: u16, col: u16) -> Self {
self.cursor_row = row;
self.cursor_col = col;
self
}
pub fn show_cursor(mut self, show_cursor: bool) -> Self {
self.show_cursor = show_cursor;
self
}
pub fn cursor_shape(mut self, shape: CaretShape) -> Self {
self.cursor_shape = shape;
self
}
pub fn cursor_blinking(mut self, blinking: bool) -> Self {
self.cursor_blinking = blinking;
self
}
pub fn caret_color(mut self, color: Color) -> Self {
self.caret_color = Some(color);
self
}
pub fn color_lines(mut self, color_lines: Arc<[Vec<Span>]>, cache_key: u64) -> Self {
self.color_lines = Some(color_lines);
self.color_cache_key = cache_key;
self
}
pub fn screen(mut self, screen: impl Into<TerminalScreenHandle>) -> Self {
self.screen = Some(screen.into());
self
}
pub fn decorations(mut self, decorations: impl Into<Arc<[TerminalDecoration]>>) -> Self {
self.decorations = decorations.into();
self
}
pub fn snapshot(mut self, snapshot: TerminalRenderSnapshot) -> Self {
self.content = snapshot.text;
self.cursor_row = snapshot.cursor_row;
self.cursor_col = snapshot.cursor_col;
self.show_cursor = snapshot.cursor_visible;
self.cursor_shape = snapshot.cursor_shape;
self.cursor_blinking = snapshot.cursor_blinking;
self.color_lines = Some(snapshot.color_lines);
self.color_cache_key = snapshot.sequence;
self.scrollback_offset = snapshot.scrollback_offset;
self.total_scrollback_rows = snapshot.total_scrollback_rows;
self.mouse_mode = snapshot.mouse_mode;
self.key_modes = snapshot.key_modes;
#[cfg(feature = "terminal-images")]
{
self.images = snapshot.images;
}
self
}
pub fn key_modes(mut self, key_modes: TerminalKeyModes) -> Self {
self.key_modes = key_modes;
self
}
pub fn paste_shortcut_behavior(mut self, behavior: TerminalPasteShortcutBehavior) -> Self {
self.paste_shortcut_behavior = behavior;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn hover_style(mut self, style: Style) -> Self {
self.hover_style = StyleSlot::Replace(style);
self
}
pub fn extend_hover_style(mut self, style: Style) -> Self {
self.hover_style = StyleSlot::Extend(style);
self
}
pub fn inherit_hover_style(mut self) -> Self {
self.hover_style = StyleSlot::Inherit;
self
}
pub fn hover_style_slot(mut self, slot: StyleSlot) -> Self {
self.hover_style = slot;
self
}
pub fn focus_style(mut self, style: Style) -> Self {
self.focus_style = StyleSlot::Replace(style);
self
}
pub fn extend_focus_style(mut self, style: Style) -> Self {
self.focus_style = StyleSlot::Extend(style);
self
}
pub fn inherit_focus_style(mut self) -> Self {
self.focus_style = StyleSlot::Inherit;
self
}
pub fn focus_style_slot(mut self, slot: StyleSlot) -> Self {
self.focus_style = slot;
self
}
pub fn focus_content_style(mut self, style: Style) -> Self {
self.focus_content_style = style;
self
}
pub fn border(mut self, border: bool) -> Self {
self.border = border;
self
}
pub fn border_style(mut self, border_style: BorderStyle) -> Self {
self.border_style = border_style;
self
}
pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
self.padding = padding.into();
self
}
pub fn scrollbar(mut self, scrollbar: bool) -> Self {
self.scrollbar = scrollbar;
self
}
pub fn scrollbar_config(mut self, config: ScrollbarConfig) -> Self {
self.scrollbar_variant = config.variant;
self.scrollbar_gap = config.gap;
self.scrollbar_thumb = config.thumb;
self.scrollbar_thumb_style = config.thumb_style;
self.scrollbar_thumb_focus_style = config.thumb_focus_style;
self.scrollbar_track_style = config.track_style;
self
}
pub fn h_scrollbar(mut self, h_scrollbar: bool) -> Self {
self.h_scrollbar = h_scrollbar;
self
}
pub fn h_scrollbar_variant(mut self, style: ScrollbarVariant) -> Self {
self.h_scrollbar_variant = style;
self
}
pub fn scroll_wheel(mut self, scroll_wheel: bool) -> Self {
self.scroll_wheel = scroll_wheel;
self
}
pub fn on_scroll(mut self, cb: Callback<ScrollEvent>) -> Self {
self.on_scroll = Some(cb);
self
}
pub fn on_scroll_to(mut self, cb: Callback<usize>) -> Self {
self.on_scroll_to = Some(cb);
self
}
pub fn selection_style(mut self, style: Style) -> Self {
self.selection_style = StyleSlot::Replace(style);
self
}
pub fn extend_selection_style(mut self, style: Style) -> Self {
self.selection_style = StyleSlot::Extend(style);
self
}
pub fn inherit_selection_style(mut self) -> Self {
self.selection_style = StyleSlot::Inherit;
self
}
pub fn selection_style_slot(mut self, slot: StyleSlot) -> Self {
self.selection_style = slot;
self
}
pub fn selection(mut self, selection: Option<TerminalSelection>) -> Self {
self.selection = selection;
self.selection_controlled = true;
self
}
pub fn on_selection(mut self, cb: Callback<TerminalSelectionEvent>) -> Self {
self.on_selection = Some(cb);
self
}
pub fn on_resize(mut self, cb: Callback<TerminalViewport>) -> Self {
self.on_resize = Some(cb);
self
}
pub fn on_mouse_forward(mut self, cb: Callback<Vec<u8>>) -> Self {
self.on_mouse_forward = Some(cb);
self
}
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
pub fn focusable(mut self, focusable: bool) -> Self {
self.focusable = focusable;
self
}
pub fn tab_stop(mut self, tab_stop: bool) -> Self {
self.tab_stop = tab_stop;
self
}
pub fn on_focus(mut self, cb: Callback<()>) -> Self {
self.on_focus = Some(cb);
self
}
pub fn on_blur(mut self, cb: Callback<()>) -> Self {
self.on_blur = Some(cb);
self
}
pub fn on_key(mut self, handler: KeyHandler) -> Self {
self.on_key = Some(handler);
self
}
pub fn on_input(mut self, cb: Callback<TerminalInputEvent>) -> Self {
self.on_input = Some(cb);
self
}
}
impl From<Terminal> for Element {
fn from(mut terminal: Terminal) -> Self {
let on_input = terminal.on_input.clone();
let fallback_on_key = terminal.on_key.clone();
let key_modes = terminal.key_modes;
terminal.on_key = if on_input.is_some() || fallback_on_key.is_some() {
Some(KeyHandler::new(move |key| {
let mut handled = false;
if let Some(on_input) = on_input.as_ref()
&& let Some(bytes) = key_event_to_bytes(key, key_modes)
{
on_input.emit(TerminalInputEvent {
kind: TerminalInputKind::Key,
key: Some(key),
bytes: bytes.into(),
});
handled = true;
}
if let Some(handler) = fallback_on_key.as_ref() {
handled = handler.handle(key) || handled;
}
handled
}))
} else {
None
};
Element::new(ElementKind::Terminal(terminal))
}
}
fn byte_to_row_col(value: &str, cursor: usize) -> (u16, u16) {
let cursor = cursor.min(value.len());
let mut row = 0u16;
let mut col = 0u16;
let mut seen = 0usize;
for ch in value.chars() {
if seen >= cursor {
break;
}
seen = seen.saturating_add(ch.len_utf8());
if ch == '\n' {
row = row.saturating_add(1);
col = 0;
} else {
col = col.saturating_add(1);
}
}
(row, col)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::event::{KeyCode, KeyEvent, KeyMods, MouseButton, MouseEvent, MouseKind};
fn enc(code: KeyCode, mods: KeyMods) -> Option<Vec<u8>> {
key_event_to_bytes(KeyEvent { code, mods }, TerminalKeyModes::default())
}
fn kitty(code: KeyCode, mods: KeyMods) -> Option<Vec<u8>> {
let modes = TerminalKeyModes {
kitty_keyboard: KittyKeyboardFlags {
disambiguate_escape_codes: true,
..KittyKeyboardFlags::default()
},
..TerminalKeyModes::default()
};
key_event_to_bytes(KeyEvent { code, mods }, modes)
}
const CTRL_SHIFT: KeyMods = KeyMods {
ctrl: true,
shift: true,
alt: false,
super_key: false,
};
const CTRL_ALT: KeyMods = KeyMods {
ctrl: true,
alt: true,
shift: false,
super_key: false,
};
const ALT_SHIFT: KeyMods = KeyMods {
alt: true,
shift: true,
ctrl: false,
super_key: false,
};
#[test]
fn ctrl_mapping_works() {
assert_eq!(enc(KeyCode::Char('c'), KeyMods::CTRL), Some(vec![3]));
}
#[test]
fn alt_prefixes_escape() {
assert_eq!(
enc(KeyCode::Char('x'), KeyMods::ALT),
Some(vec![0x1b, b'x'])
);
}
#[test]
fn ctrl_arrows_encode_xterm_modifier_param() {
for (code, expected) in [
(KeyCode::Left, "\x1b[1;5D"),
(KeyCode::Right, "\x1b[1;5C"),
(KeyCode::Up, "\x1b[1;5A"),
(KeyCode::Down, "\x1b[1;5B"),
(KeyCode::Home, "\x1b[1;5H"),
(KeyCode::End, "\x1b[1;5F"),
] {
assert_eq!(
enc(code, KeyMods::CTRL),
Some(expected.as_bytes().to_vec()),
"ctrl {code:?}"
);
}
}
#[test]
fn shift_and_combined_modifiers_encode_params() {
assert_eq!(
enc(KeyCode::Left, KeyMods::SHIFT),
Some(b"\x1b[1;2D".to_vec())
);
assert_eq!(enc(KeyCode::Right, CTRL_SHIFT), Some(b"\x1b[1;6C".to_vec()));
assert_eq!(enc(KeyCode::Up, CTRL_ALT), Some(b"\x1b[1;7A".to_vec()));
assert_eq!(enc(KeyCode::Left, ALT_SHIFT), Some(b"\x1b[1;4D".to_vec()));
}
#[test]
fn shift_only_emulator_reserved_keys_keep_plain_form() {
for (code, expected) in [
(KeyCode::Insert, "\x1b[2~"),
(KeyCode::PageUp, "\x1b[5~"),
(KeyCode::PageDown, "\x1b[6~"),
] {
assert_eq!(
enc(code, KeyMods::SHIFT),
Some(expected.as_bytes().to_vec()),
"shift {code:?}"
);
}
assert_eq!(
enc(KeyCode::Delete, KeyMods::SHIFT),
Some(b"\x1b[3;2~".to_vec())
);
assert_eq!(
enc(KeyCode::PageUp, CTRL_SHIFT),
Some(b"\x1b[5;6~".to_vec())
);
}
#[test]
fn ctrl_tilde_keys_encode_params() {
assert_eq!(
enc(KeyCode::Delete, KeyMods::CTRL),
Some(b"\x1b[3;5~".to_vec())
);
assert_eq!(
enc(KeyCode::PageUp, KeyMods::CTRL),
Some(b"\x1b[5;5~".to_vec())
);
assert_eq!(
enc(KeyCode::F(5), KeyMods::SHIFT),
Some(b"\x1b[15;2~".to_vec())
);
}
#[test]
fn high_function_keys_encode_through_f20() {
assert_eq!(
enc(KeyCode::F(13), KeyMods::NONE),
Some(b"\x1b[25~".to_vec())
);
assert_eq!(
enc(KeyCode::F(15), KeyMods::NONE),
Some(b"\x1b[28~".to_vec())
);
assert_eq!(
enc(KeyCode::F(20), KeyMods::CTRL),
Some(b"\x1b[34;5~".to_vec())
);
assert_eq!(enc(KeyCode::F(21), KeyMods::NONE), None);
}
#[test]
fn ctrl_backspace_sends_backward_kill_word() {
assert_eq!(
enc(KeyCode::Backspace, KeyMods::CTRL),
Some(vec![0x1b, 0x7f])
);
assert_eq!(enc(KeyCode::Backspace, KeyMods::NONE), Some(vec![0x7f]));
assert_eq!(
enc(KeyCode::Backspace, KeyMods::ALT),
Some(vec![0x1b, 0x7f])
);
}
#[test]
fn legacy_children_keep_legacy_enter_tab_and_lose_ctrl_digits() {
assert_eq!(enc(KeyCode::Enter, KeyMods::NONE), Some(vec![b'\r']));
assert_eq!(enc(KeyCode::Enter, KeyMods::CTRL), Some(vec![b'\r']));
assert_eq!(enc(KeyCode::Enter, CTRL_SHIFT), Some(vec![b'\r']));
assert_eq!(enc(KeyCode::Tab, KeyMods::NONE), Some(vec![b'\t']));
assert_eq!(enc(KeyCode::Tab, KeyMods::CTRL), Some(vec![b'\t']));
assert_eq!(
enc(KeyCode::BackTab, KeyMods::SHIFT),
Some(b"\x1b[Z".to_vec())
);
assert_eq!(enc(KeyCode::Esc, KeyMods::NONE), Some(vec![0x1b]));
assert_eq!(enc(KeyCode::Enter, KeyMods::ALT), Some(vec![0x1b, b'\r']));
assert_eq!(enc(KeyCode::Char('1'), KeyMods::CTRL), None);
}
#[test]
fn kitty_children_get_csi_u_for_chords_with_no_legacy_encoding() {
for (ch, expected) in [
('1', "\x1b[49;5u"),
('2', "\x1b[50;5u"),
('9', "\x1b[57;5u"),
] {
assert_eq!(
kitty(KeyCode::Char(ch), KeyMods::CTRL),
Some(expected.as_bytes().to_vec())
);
}
assert_eq!(
kitty(KeyCode::Enter, KeyMods::CTRL),
Some(b"\x1b[13;5u".to_vec())
);
assert_eq!(
kitty(KeyCode::Enter, KeyMods::SHIFT),
Some(b"\x1b[13;2u".to_vec())
);
assert_eq!(
kitty(KeyCode::Tab, KeyMods::CTRL),
Some(b"\x1b[9;5u".to_vec())
);
assert_eq!(
kitty(KeyCode::Backspace, KeyMods::CTRL),
Some(b"\x1b[127;5u".to_vec())
);
assert_eq!(
kitty(KeyCode::BackTab, KeyMods::NONE),
Some(b"\x1b[9;2u".to_vec())
);
assert_eq!(
kitty(KeyCode::Esc, KeyMods::NONE),
Some(b"\x1b[27u".to_vec())
);
assert_eq!(
kitty(KeyCode::Char('c'), KeyMods::CTRL),
Some(b"\x1b[99;5u".to_vec())
);
assert_eq!(
kitty(KeyCode::Char('C'), CTRL_SHIFT),
Some(b"\x1b[99;6u".to_vec())
);
assert_eq!(
kitty(KeyCode::Char('x'), KeyMods::ALT),
Some(b"\x1b[120;3u".to_vec())
);
}
#[test]
fn kitty_children_keep_legacy_bytes_for_text_and_functional_keys() {
assert_eq!(
kitty(KeyCode::Char('a'), KeyMods::NONE),
Some(b"a".to_vec())
);
assert_eq!(
kitty(KeyCode::Char('A'), KeyMods::SHIFT),
Some(b"A".to_vec())
);
assert_eq!(kitty(KeyCode::Enter, KeyMods::NONE), Some(vec![b'\r']));
assert_eq!(kitty(KeyCode::Tab, KeyMods::NONE), Some(vec![b'\t']));
assert_eq!(kitty(KeyCode::Backspace, KeyMods::NONE), Some(vec![0x7f]));
assert_eq!(
kitty(KeyCode::Left, KeyMods::NONE),
Some(b"\x1b[D".to_vec())
);
assert_eq!(
kitty(KeyCode::Left, KeyMods::CTRL),
Some(b"\x1b[1;5D".to_vec())
);
assert_eq!(
kitty(KeyCode::PageUp, KeyMods::CTRL),
Some(b"\x1b[5;5~".to_vec())
);
assert_eq!(
kitty(KeyCode::F(5), KeyMods::NONE),
Some(b"\x1b[15~".to_vec())
);
assert_eq!(
kitty(
KeyCode::Char('1'),
KeyMods {
super_key: true,
..KeyMods::default()
}
),
None
);
}
#[test]
fn ctrl_punctuation_maps_to_control_codes() {
assert_eq!(enc(KeyCode::Char('/'), KeyMods::CTRL), Some(vec![0x1f]));
assert_eq!(enc(KeyCode::Char('_'), KeyMods::CTRL), Some(vec![0x1f]));
assert_eq!(enc(KeyCode::Char('?'), KeyMods::CTRL), Some(vec![0x7f]));
assert_eq!(enc(KeyCode::Char('@'), KeyMods::CTRL), Some(vec![0x00]));
assert_eq!(enc(KeyCode::Char(' '), KeyMods::CTRL), Some(vec![0x00]));
assert_eq!(enc(KeyCode::Char('2'), KeyMods::CTRL), Some(vec![0x00]));
assert_eq!(enc(KeyCode::Char('8'), KeyMods::CTRL), Some(vec![0x7f]));
assert_eq!(enc(KeyCode::Char('1'), KeyMods::CTRL), None);
}
#[test]
fn super_modified_keys_are_dropped() {
let sup = KeyMods {
super_key: true,
..KeyMods::default()
};
assert_eq!(enc(KeyCode::Char('c'), sup), None);
assert_eq!(enc(KeyCode::Left, sup), None);
assert_eq!(
enc(
KeyCode::Char('v'),
KeyMods {
super_key: true,
ctrl: true,
..KeyMods::default()
}
),
None
);
}
#[test]
fn plain_arrows_are_unmodified() {
assert_eq!(enc(KeyCode::Left, KeyMods::NONE), Some(b"\x1b[D".to_vec()));
assert_eq!(
enc(KeyCode::Left, KeyMods::ALT),
Some(b"\x1b\x1b[D".to_vec())
);
}
#[test]
fn app_cursor_mode_switches_unmodified_arrows_to_ss3() {
let modes = TerminalKeyModes {
app_cursor: true,
..TerminalKeyModes::default()
};
let app = |code| {
key_event_to_bytes(
KeyEvent {
code,
mods: KeyMods::NONE,
},
modes,
)
};
assert_eq!(app(KeyCode::Up), Some(b"\x1bOA".to_vec()));
assert_eq!(app(KeyCode::Down), Some(b"\x1bOB".to_vec()));
assert_eq!(app(KeyCode::Right), Some(b"\x1bOC".to_vec()));
assert_eq!(app(KeyCode::Left), Some(b"\x1bOD".to_vec()));
assert_eq!(app(KeyCode::Home), Some(b"\x1bOH".to_vec()));
assert_eq!(app(KeyCode::End), Some(b"\x1bOF".to_vec()));
assert_eq!(
key_event_to_bytes(
KeyEvent {
code: KeyCode::Left,
mods: KeyMods::CTRL
},
modes
),
Some(b"\x1b[1;5D".to_vec())
);
assert_eq!(
key_event_to_bytes(
KeyEvent {
code: KeyCode::PageUp,
mods: KeyMods::NONE
},
modes
),
Some(b"\x1b[5~".to_vec())
);
}
#[test]
fn paste_is_bracketed_only_when_the_child_asked_for_it() {
let off = TerminalKeyModes::default();
assert_eq!(encode_paste("hi", off), b"hi".to_vec());
let on = TerminalKeyModes {
bracketed_paste: true,
..TerminalKeyModes::default()
};
assert_eq!(encode_paste("hi", on), b"\x1b[200~hi\x1b[201~".to_vec());
}
#[test]
fn terminal_buffer_trims_lines() {
let mut buffer = TerminalBuffer::new(2);
buffer.push_text("a\nb\nc\n");
let snapshot = buffer.snapshot();
assert_eq!(snapshot.as_ref(), "b\nc");
}
#[test]
fn terminal_screen_applies_vt_sequences() {
let mut screen = TerminalScreen::new(4, 20, 128);
screen.process_bytes(b"\x1b[31mhello\x1b[0m\nworld");
let snapshot = screen.snapshot();
let mut lines = snapshot.lines().map(str::trim);
assert_eq!(lines.next(), Some("hello"));
assert_eq!(lines.next(), Some("world"));
}
#[test]
fn mouse_event_to_bytes_sgr_encodes_coordinates() {
let event = MouseEvent {
x: 2,
y: 3,
kind: MouseKind::Down(MouseButton::Left),
mods: KeyMods::default(),
};
use super::events::MouseEncoding;
let bytes = mouse_event_to_bytes(event, MouseEncoding::Sgr, (0, 0)).expect("mouse bytes");
assert_eq!(String::from_utf8(bytes).unwrap(), "\u{1b}[<0;3;4M");
}
#[test]
fn mouse_event_to_bytes_sgr_encodes_plain_motion() {
let event = MouseEvent {
x: 26,
y: 5,
kind: MouseKind::Moved,
mods: KeyMods::default(),
};
use super::events::MouseEncoding;
let bytes = mouse_event_to_bytes(event, MouseEncoding::Sgr, (0, 0)).expect("mouse bytes");
assert_eq!(String::from_utf8(bytes).unwrap(), "\u{1b}[<35;27;6M");
}
#[test]
fn grid_selection_extracts_text() {
use crate::utils::{GridPos, GridSelection};
let selection = GridSelection {
anchor: GridPos { row: 0, col: 1 },
cursor: GridPos { row: 1, col: 2 },
};
let lines = vec!["abcd", "efgh", "ijkl"];
assert_eq!(selection.extract_text(&lines), "bcd\nef");
}
}