use alacritty_terminal::event::VoidListener;
use alacritty_terminal::grid::Dimensions;
use alacritty_terminal::index::{Column, Line};
use alacritty_terminal::term::{Config, Term};
use alacritty_terminal::vte::ansi::Processor;
use crate::readiness::ScreenView;
pub const DEFAULT_ROWS: u16 = 24;
pub const DEFAULT_COLS: u16 = 80;
#[derive(Debug, Clone, Copy)]
pub(crate) struct GridSize {
pub rows: usize,
pub cols: usize,
}
impl Dimensions for GridSize {
fn total_lines(&self) -> usize {
self.rows
}
fn screen_lines(&self) -> usize {
self.rows
}
fn columns(&self) -> usize {
self.cols
}
}
pub(crate) fn visible_only_config() -> Config {
Config {
scrolling_history: 0,
..Config::default()
}
}
pub struct TerminalGrid {
term: Term<VoidListener>,
processor: Processor,
osc: crate::osc::OscCapture,
rows: u16,
cols: u16,
}
impl TerminalGrid {
pub fn new(rows: u16, cols: u16) -> Self {
let rows = rows.max(1);
let cols = cols.max(1);
let size = GridSize {
rows: rows as usize,
cols: cols as usize,
};
TerminalGrid {
term: Term::new(visible_only_config(), &size, VoidListener),
processor: Processor::new(),
osc: crate::osc::OscCapture::new(),
rows,
cols,
}
}
pub fn with_default_size() -> Self {
Self::new(DEFAULT_ROWS, DEFAULT_COLS)
}
pub fn feed(&mut self, bytes: &[u8]) {
self.processor.advance(&mut self.term, bytes);
self.osc.feed(bytes);
}
pub fn resize(&mut self, rows: u16, cols: u16) {
let rows = rows.max(1);
let cols = cols.max(1);
self.term.resize(GridSize {
rows: rows as usize,
cols: cols as usize,
});
self.rows = rows;
self.cols = cols;
}
pub fn snapshot(&self) -> OwnedScreen {
let grid = self.term.grid();
let cursor_point = grid.cursor.point;
let cursor_row = cursor_point.line.0.max(0) as usize;
let cursor_col = cursor_point.column.0;
let mut rows: Vec<String> = Vec::with_capacity(self.rows as usize);
for row_idx in 0..(self.rows as usize) {
let line = Line(row_idx as i32);
let mut row = String::with_capacity(self.cols as usize);
for col_idx in 0..(self.cols as usize) {
row.push(grid[line][Column(col_idx)].c);
}
while row.ends_with(' ') {
row.pop();
}
rows.push(row);
}
while rows.last().map(|r| r.is_empty()).unwrap_or(false) {
rows.pop();
}
let text = rows.join("\n");
OwnedScreen {
text,
cursor_row,
cursor_col,
osc_title: self.osc.title().map(str::to_string),
osc_progress: self.osc.progress().map(str::to_string),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OwnedScreen {
pub text: String,
pub cursor_row: usize,
pub cursor_col: usize,
pub osc_title: Option<String>,
pub osc_progress: Option<String>,
}
impl OwnedScreen {
pub fn view(&self) -> ScreenView<'_> {
ScreenView {
visible_text: &self.text,
cursor_row: self.cursor_row,
cursor_col: self.cursor_col,
osc_title: self.osc_title.as_deref(),
osc_progress: self.osc_progress.as_deref(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plain_text_renders_and_cursor_advances() {
let mut grid = TerminalGrid::with_default_size();
grid.feed(b"hello");
let owned = grid.snapshot();
assert!(owned.text.starts_with("hello"));
assert_eq!(owned.cursor_row, 0);
assert_eq!(owned.cursor_col, 5);
}
#[test]
fn ansi_escapes_are_interpreted_not_echoed() {
let mut grid = TerminalGrid::new(4, 20);
grid.feed(b"AAA\x1b[1;1H\x1b[2KB");
let owned = grid.snapshot();
assert!(
!owned.text.contains('\x1b'),
"raw escape leaked into visible text: {:?}",
owned.text
);
assert!(
owned.text.starts_with('B'),
"cursor-home + clear should leave 'B' at the top-left, got {:?}",
owned.text
);
}
#[test]
fn partial_escape_split_across_feeds_is_buffered() {
let mut grid = TerminalGrid::new(4, 20);
grid.feed(b"X\x1b[1");
grid.feed(b";1HY");
let owned = grid.snapshot();
assert!(!owned.text.contains('\x1b'));
assert!(owned.text.starts_with('Y'), "got {:?}", owned.text);
}
#[test]
fn resize_does_not_panic_and_view_roundtrips() {
let mut grid = TerminalGrid::with_default_size();
grid.resize(0, 0); grid.feed(b"q");
let owned = grid.snapshot();
let view = owned.view();
assert_eq!(view.visible_text, owned.text);
assert_eq!(view.cursor_row, owned.cursor_row);
}
#[test]
fn osc_title_exposed_on_snapshot_and_reassembled_across_feeds() {
let mut grid = TerminalGrid::with_default_size();
grid.feed(b"\x1b]2;\xe2\xa0\x8b Compil");
grid.feed(b"ing\x07hello");
let owned = grid.snapshot();
assert_eq!(owned.osc_title.as_deref(), Some("\u{280b} Compiling"));
assert!(
owned.text.starts_with("hello"),
"OSC bytes must not leak into the grid, got {:?}",
owned.text
);
let view = owned.view();
assert_eq!(view.osc_title, Some("\u{280b} Compiling"));
}
#[test]
fn prompt_glyph_survives_to_visible_tail() {
let mut grid = TerminalGrid::new(3, 20);
grid.feed("\u{276f} ".as_bytes()); let owned = grid.snapshot();
assert!(
owned
.text
.lines()
.next()
.unwrap()
.trim_end()
.ends_with('\u{276f}'),
"prompt glyph should be the visible tail, got {:?}",
owned.text
);
}
}