pub(crate) mod cell;
pub(crate) mod grid;
pub(crate) mod performer;
pub(crate) mod render;
pub(crate) mod style;
pub mod traits;
use std::collections::VecDeque;
use vte::Parser;
pub use cell::{Cell, Grapheme, Row};
use grid::Grid;
pub use grid::{sanitize_dimensions, CursorShape, TerminalSize, MAX_DIMENSION};
pub use grid::{ActiveCharset, Charset, MouseEncoding, MouseModes, TerminalModes};
use performer::ScreenPerformer;
pub use render::{AnsiRenderer, DirtyTracker};
pub use style::{Color, Style, StyleId, UnderlineStyle};
pub use traits::TerminalEmulator;
#[derive(Copy, Clone)]
pub(super) struct SavedCursor {
pub(super) x: u16,
pub(super) y: u16,
pub(super) style: Style,
pub(super) g0_charset: grid::Charset,
pub(super) g1_charset: grid::Charset,
pub(super) active_charset: grid::ActiveCharset,
pub(super) autowrap_mode: bool,
pub(super) origin_mode: bool,
pub(super) wrap_pending: bool,
}
const MAX_PENDING: usize = 1024;
const MAX_QUEUED_NOTIFICATIONS: usize = 50;
pub(super) struct ScreenState {
pub(super) current_style: Style,
pub(super) in_alt_screen: bool,
pub(super) saved_grid: Option<grid::SavedGrid>,
pub(super) saved_cursor_state: Option<SavedCursor>,
pub(super) saved_modes: Option<grid::TerminalModes>,
pub(super) saved_scroll_region: Option<(u16, u16)>,
pub(super) pending_responses: Vec<Vec<u8>>,
pub(super) pending_passthrough: Vec<Vec<u8>>,
pub(super) queued_notifications: VecDeque<Vec<u8>>,
pub(super) title: String,
pub(super) title_stack: Vec<String>,
pub(super) last_printed_char: char,
}
impl ScreenState {
pub fn push_response(&mut self, data: Vec<u8>) {
if self.pending_responses.len() < MAX_PENDING {
self.pending_responses.push(data);
} else {
#[cfg(feature = "tracing")]
tracing::debug!("pending_responses full, dropping response");
}
}
pub fn push_passthrough(&mut self, data: Vec<u8>) {
if self.pending_passthrough.len() < MAX_PENDING {
self.pending_passthrough.push(data);
}
}
pub fn push_notification(&mut self, data: Vec<u8>) {
if self.queued_notifications.len() >= MAX_QUEUED_NOTIFICATIONS {
self.queued_notifications.pop_front();
}
self.queued_notifications.push_back(data);
}
}
impl Default for ScreenState {
fn default() -> Self {
Self {
current_style: Style::default(),
in_alt_screen: false,
saved_grid: None,
saved_cursor_state: None,
saved_modes: None,
saved_scroll_region: None,
pending_responses: Vec::new(),
pending_passthrough: Vec::new(),
queued_notifications: VecDeque::new(),
title: String::new(),
title_stack: Vec::new(),
last_printed_char: ' ',
}
}
}
pub struct Screen {
pub(super) grid: Grid,
pub(super) state: ScreenState,
parser: Parser,
}
impl Screen {
pub fn new(cols: u16, rows: u16, scrollback_limit: usize) -> Self {
Self {
grid: Grid::new(cols, rows, scrollback_limit),
state: ScreenState::default(),
parser: Parser::new(),
}
}
#[cfg(test)]
pub(crate) fn grid(&self) -> &Grid {
&self.grid
}
#[cfg(test)]
pub(crate) fn current_style(&self) -> style::Style {
self.state.current_style
}
pub fn rows(&self) -> u16 {
self.grid.rows()
}
pub fn in_alt_screen(&self) -> bool {
self.state.in_alt_screen
}
pub fn process(&mut self, bytes: &[u8]) {
let mut performer = ScreenPerformer {
grid: &mut self.grid,
state: &mut self.state,
};
for &byte in bytes {
self.parser.advance(&mut performer, byte);
}
}
pub fn take_responses(&mut self) -> Vec<Vec<u8>> {
std::mem::take(&mut self.state.pending_responses)
}
pub fn take_passthrough(&mut self) -> Vec<Vec<u8>> {
std::mem::take(&mut self.state.pending_passthrough)
}
pub fn take_queued_notifications(&mut self) -> Vec<Vec<u8>> {
self.state.queued_notifications.drain(..).collect()
}
pub fn take_pending_scrollback(&mut self) -> Vec<Row> {
let start = self.grid.pending_start();
let count = self.grid.pending_scrollback_count();
self.grid.set_pending_start(self.grid.scrollback_len());
self.grid
.scrollback_rows()
.skip(start)
.take(count)
.cloned()
.collect()
}
pub fn discard_pending_scrollback(&mut self) {
self.grid.set_pending_start(self.grid.scrollback_len());
}
pub fn get_history(&self) -> Vec<Vec<u8>> {
self.grid
.scrollback_rows()
.map(|row| render::render_line(row, self.grid.style_table()))
.collect()
}
#[cfg(test)]
pub(crate) fn cell_style(&self, row: usize, col: usize) -> style::Style {
self.grid
.style_table()
.get(self.grid.visible_row(row)[col].style_id)
}
#[cfg(test)]
pub(crate) fn cell_char(&self, row: usize, col: usize) -> char {
self.grid.visible_row(row)[col].c
}
#[cfg(test)]
pub(crate) fn cell_width(&self, row: usize, col: usize) -> u8 {
self.grid.visible_row(row)[col].width
}
#[cfg(test)]
pub fn compact_styles(&mut self) {
compact_styles(&mut self.grid, self.state.saved_grid.as_ref());
}
pub fn resize(&mut self, cols: u16, rows: u16) {
let old_rows = self.grid.rows();
if !self.state.in_alt_screen && rows > old_rows {
let grow = (rows - old_rows) as usize;
let restore_count = grow.min(self.grid.scrollback_len());
self.grid.restore_scrollback(restore_count);
self.grid.set_cursor_y_unclamped(
self.grid
.cursor_y()
.saturating_add(u16::try_from(restore_count).unwrap_or(u16::MAX)),
);
}
self.grid.resize(cols, rows);
}
pub fn cols(&self) -> u16 {
self.grid.cols()
}
pub fn visible_row(&self, y: u16) -> &cell::Row {
self.grid.visible_row(y as usize)
}
pub fn visible_rows(&self) -> impl Iterator<Item = &cell::Row> {
self.grid.visible_rows()
}
pub fn scrollback_row(&self, i: usize) -> &cell::Row {
self.grid.scrollback_row(i)
}
pub fn scrollback_rows(&self) -> impl Iterator<Item = &cell::Row> {
self.grid.scrollback_rows()
}
pub fn scrollback_len(&self) -> usize {
self.grid.scrollback_len()
}
pub fn cursor_position(&self) -> (u16, u16) {
self.grid.cursor_pos()
}
pub fn cursor_visible(&self) -> bool {
self.grid.cursor_visible()
}
pub fn cursor_shape(&self) -> grid::CursorShape {
self.grid.modes().cursor_shape
}
pub fn resolve_style(&self, id: style::StyleId) -> style::Style {
self.grid.style_table().get(id)
}
pub fn scroll_region(&self) -> (u16, u16) {
self.grid.scroll_region()
}
pub fn modes(&self) -> &grid::TerminalModes {
self.grid.modes()
}
pub fn title(&self) -> &str {
&self.state.title
}
}
impl traits::TerminalEmulator for Screen {
fn process(&mut self, bytes: &[u8]) {
self.process(bytes);
}
fn resize(&mut self, cols: u16, rows: u16) {
self.resize(cols, rows);
}
fn cols(&self) -> u16 {
Screen::cols(self)
}
fn rows(&self) -> u16 {
self.grid.rows()
}
fn visible_row(&self, y: u16) -> &cell::Row {
Screen::visible_row(self, y)
}
fn scrollback_row(&self, i: usize) -> &cell::Row {
Screen::scrollback_row(self, i)
}
fn scrollback_len(&self) -> usize {
Screen::scrollback_len(self)
}
fn cursor_position(&self) -> (u16, u16) {
Screen::cursor_position(self)
}
fn cursor_visible(&self) -> bool {
Screen::cursor_visible(self)
}
fn resolve_style(&self, id: style::StyleId) -> style::Style {
Screen::resolve_style(self, id)
}
fn in_alt_screen(&self) -> bool {
Screen::in_alt_screen(self)
}
fn take_responses(&mut self) -> Vec<Vec<u8>> {
Screen::take_responses(self)
}
fn title(&self) -> &str {
Screen::title(self)
}
fn cursor_shape(&self) -> grid::CursorShape {
Screen::cursor_shape(self)
}
fn scroll_region(&self) -> (u16, u16) {
Screen::scroll_region(self)
}
fn modes(&self) -> &grid::TerminalModes {
Screen::modes(self)
}
fn take_passthrough(&mut self) -> Vec<Vec<u8>> {
Screen::take_passthrough(self)
}
fn take_queued_notifications(&mut self) -> Vec<Vec<u8>> {
Screen::take_queued_notifications(self)
}
fn take_pending_scrollback(&mut self) -> Vec<cell::Row> {
Screen::take_pending_scrollback(self)
}
}
pub(crate) fn compact_styles(grid: &mut Grid, saved_grid: Option<&grid::SavedGrid>) {
let cap = grid.style_table().capacity();
if cap <= 1 {
return;
}
let mut live = vec![false; cap];
live[0] = true;
for row in grid.scrollback_rows().chain(grid.visible_rows()) {
for cell in row.iter() {
let id = cell.style_id.index();
if id < cap {
live[id] = true;
}
}
}
if let Some(saved) = saved_grid {
for row in saved.visible_rows() {
for cell in row.iter() {
let id = cell.style_id.index();
if id < cap {
live[id] = true;
}
}
}
}
grid.style_table_mut().reclaim(&live);
}
#[cfg(test)]
mod tests_traits {
use super::traits::TerminalEmulator;
use super::*;
#[test]
fn screen_implements_terminal_emulator() {
let mut screen = Screen::new(80, 24, 100);
TerminalEmulator::process(&mut screen, b"Hello");
let rows: Vec<&cell::Row> = TerminalEmulator::visible_rows(&screen).collect();
assert_eq!(rows.len(), 24);
assert_eq!(rows[0][0].c, 'H');
assert_eq!(rows[0][4].c, 'o');
assert_eq!(TerminalEmulator::cols(&screen), 80);
assert_eq!(TerminalEmulator::rows(&screen), 24);
assert_eq!(TerminalEmulator::cursor_position(&screen), (5, 0));
assert!(TerminalEmulator::cursor_visible(&screen));
let style = TerminalEmulator::resolve_style(&screen, rows[0][0].style_id);
assert!(style.is_default());
assert!(!TerminalEmulator::in_alt_screen(&screen));
assert_eq!(TerminalEmulator::title(&screen), "");
assert_eq!(TerminalEmulator::scrollback_len(&screen), 0);
assert_eq!(TerminalEmulator::scrollback_rows(&screen).count(), 0);
assert!(TerminalEmulator::take_responses(&mut screen).is_empty());
}
#[test]
fn screen_as_dyn_terminal_emulator() {
let mut screen = Screen::new(40, 10, 50);
let emu: &mut dyn TerminalEmulator = &mut screen;
emu.process(b"test");
assert_eq!(emu.cols(), 40);
assert_eq!(emu.rows(), 10);
let rows: Vec<_> = emu.visible_rows().collect();
assert_eq!(rows[0][0].c, 't');
}
#[test]
fn trait_index_access_matches_iterators() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"a\r\nb\r\nc\r\nd\r\ne"); let emu: &dyn TerminalEmulator = &screen;
let via_iter: Vec<String> = emu.visible_rows().map(|r| r.text()).collect();
let via_index: Vec<String> = (0..emu.rows())
.map(|y| emu.visible_row(y).text())
.collect();
assert_eq!(via_iter, via_index);
assert!(emu.scrollback_len() > 0);
let sb_iter: Vec<String> = emu.scrollback_rows().map(|r| r.text()).collect();
let sb_index: Vec<String> = (0..emu.scrollback_len())
.map(|i| emu.scrollback_row(i).text())
.collect();
assert_eq!(sb_iter, sb_index);
assert_eq!(sb_index[0], "a"); }
#[test]
fn ansi_renderer_clears_title_when_empty() {
use super::render::AnsiRenderer;
let mut screen = Screen::new(10, 3, 0);
screen.process(b"\x1b]2;Hello\x07");
assert_eq!(screen.title(), "Hello");
let mut renderer = AnsiRenderer::new();
let output = renderer.render(&screen, true);
let text = String::from_utf8_lossy(&output);
assert!(
text.contains("\x1b]2;Hello\x07"),
"first render should contain title OSC"
);
screen.process(b"\x1b]2;\x07");
assert_eq!(screen.title(), "");
let output = renderer.render(&screen, true);
let text = String::from_utf8_lossy(&output);
assert!(
text.contains("\x1b]2;\x07"),
"render should emit title-clearing OSC when title becomes empty, \
got: {text}"
);
}
}
#[cfg(test)]
pub(super) mod test_helpers {
use super::*;
pub fn strip_ansi(bytes: &[u8]) -> String {
let s = String::from_utf8_lossy(bytes);
let mut out = String::new();
let mut in_esc = false;
for ch in s.chars() {
if in_esc {
if ch.is_ascii_alphabetic() {
in_esc = false;
}
continue;
}
if ch == '\x1b' {
in_esc = true;
continue;
}
if ch >= ' ' {
out.push(ch);
}
}
out.trim_end().to_string()
}
pub fn screen_lines(screen: &Screen) -> Vec<String> {
screen
.grid
.visible_rows()
.map(|row| {
let s: String = row.iter().map(|c| c.c).collect();
s.trim_end().to_string()
})
.collect()
}
pub fn history_texts(screen: &Screen) -> Vec<String> {
screen.get_history().iter().map(|b| strip_ansi(b)).collect()
}
}
#[cfg(test)]
mod history_boundary_tests;
#[cfg(test)]
mod tests_large_updates;
#[cfg(test)]
mod tests_live_scrollback;
#[cfg(test)]
mod tests_progress_bar_scrollback;
#[cfg(test)]
mod tests_reattach;
#[cfg(test)]
mod tests_reconnect_scrollback;
#[cfg(test)]
mod tests_resize;
#[cfg(test)]
mod tests_screen;