use super::cursor::Cursor;
use super::grid::{Cell, EraseMode, Grid};
use super::sgr::Sgr;
#[derive(Debug)]
pub(super) struct Performer {
grid: Grid,
cursor: Cursor,
sgr: Sgr,
evicted: Vec<String>,
}
impl Performer {
pub(super) fn new(rows: usize, cols: usize) -> Self {
Self {
grid: Grid::new(rows, cols),
cursor: Cursor::default(),
sgr: Sgr::default(),
evicted: Vec::new(),
}
}
pub(super) fn take_evicted(&mut self) -> Vec<String> {
std::mem::take(&mut self.evicted)
}
pub(super) fn render(&self) -> Vec<String> {
self.grid.render()
}
fn blank_cell(&self) -> Cell {
Cell::blank(self.sgr.erased())
}
fn line_feed(&mut self) {
let blank = self.blank_cell();
if let Some(row) = self.grid.line_feed(&mut self.cursor, &blank) {
self.evicted.push(row);
}
}
fn param(params: &vte::Params, index: usize, default: usize) -> usize {
params
.iter()
.nth(index)
.and_then(<[u16]>::first)
.map(|&value| value as usize)
.filter(|&value| value != 0)
.unwrap_or(default)
}
}
impl vte::Perform for Performer {
fn print(&mut self, c: char) {
let cols = self.grid.cols();
if self.cursor.col >= cols {
self.grid.set(
self.cursor.row,
cols - 1,
Cell {
ch: '…',
sgr: self.sgr,
},
);
return;
}
self.grid.set(
self.cursor.row,
self.cursor.col,
Cell {
ch: c,
sgr: self.sgr,
},
);
self.cursor.col += 1;
}
fn execute(&mut self, byte: u8) {
self.cursor.clear_pending_wrap(self.grid.cols());
match byte {
b'\n' => {
self.cursor.carriage_return();
self.line_feed();
}
b'\r' => self.cursor.carriage_return(),
b'\t' => self.cursor.tab(self.grid.cols()),
0x08 => self.cursor.backspace(),
_ => {}
}
}
fn csi_dispatch(
&mut self,
params: &vte::Params,
_intermediates: &[u8],
_ignore: bool,
action: char,
) {
let rows = self.grid.rows();
let cols = self.grid.cols();
self.cursor.clear_pending_wrap(cols);
match action {
'A' => self.cursor.up(Self::param(params, 0, 1)),
'B' => self.cursor.down(Self::param(params, 0, 1), rows),
'C' => self.cursor.right(Self::param(params, 0, 1), cols),
'D' => self.cursor.left(Self::param(params, 0, 1)),
'H' | 'f' => {
let row = Self::param(params, 0, 1);
let col = Self::param(params, 1, 1);
self.cursor.to(row, col, rows, cols);
}
'K' => {
if let Some(mode) = EraseMode::from_param(Self::param(params, 0, 0)) {
self.grid.erase_line(&self.cursor, mode, &self.blank_cell());
}
}
'J' => {
if let Some(mode) = EraseMode::from_param(Self::param(params, 0, 0)) {
self.grid
.erase_display(&self.cursor, mode, &self.blank_cell());
}
}
'S' => {
let evicted = self
.grid
.scroll_up(Self::param(params, 0, 1), &self.blank_cell());
self.evicted.extend(evicted);
}
'T' => self
.grid
.scroll_down(Self::param(params, 0, 1), &self.blank_cell()),
'm' => self.sgr.apply(params),
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::Performer;
fn feed(performer: &mut Performer, bytes: &[u8]) {
let mut parser = vte::Parser::new();
parser.advance(performer, bytes);
}
#[test]
fn print_truncates_at_width_without_wrapping() {
let mut performer = Performer::new(2, 3);
feed(&mut performer, b"abcd");
assert_eq!(performer.render(), vec!["ab…".to_string(), String::new()]);
assert!(performer.take_evicted().is_empty());
}
#[test]
fn truncated_row_preserves_active_sgr() {
let mut performer = Performer::new(1, 3);
feed(&mut performer, b"\x1b[31mabcd");
assert_eq!(performer.render(), vec!["\x1b[0;31mab…\x1b[0m".to_string()]);
}
#[test]
fn carriage_return_overwrites_in_place() {
let mut performer = Performer::new(1, 8);
feed(&mut performer, b"50%\r100%");
assert_eq!(performer.render(), vec!["100%".to_string()]);
}
#[test]
fn cursor_up_and_erase_line_redraw_in_place() {
let mut performer = Performer::new(3, 6);
feed(&mut performer, b"one\r\ntwo\r\n");
feed(&mut performer, b"\x1b[2A\x1b[2Kzero");
assert_eq!(
performer.render(),
vec!["zero".to_string(), "two".to_string(), String::new()]
);
}
#[test]
fn line_feed_at_bottom_evicts_top_row() {
let mut performer = Performer::new(2, 4);
feed(&mut performer, b"a\r\nb\r\nc");
assert_eq!(performer.take_evicted(), vec!["a".to_string()]);
assert_eq!(performer.render(), vec!["b".to_string(), "c".to_string()]);
}
#[test]
fn control_sequences_never_leak_into_content() {
let mut performer = Performer::new(3, 10);
feed(&mut performer, b"building\r\n\x1b[1A\r\x1b[2K\x1b[1Bdone");
let rendered = performer.render();
for line in &rendered {
assert!(
!line.contains('\x1b'),
"escape leaked into content: {line:?}"
);
}
assert!(
!rendered
.iter()
.any(|line| line.contains("[2K") || line.contains("[1A"))
);
}
#[test]
fn bare_line_feed_returns_to_column_zero() {
let mut performer = Performer::new(2, 6);
feed(&mut performer, b"one\ntwo");
assert_eq!(
performer.render(),
vec!["one".to_string(), "two".to_string()]
);
}
#[test]
fn cursor_move_from_pending_wrap_does_not_scroll() {
let mut performer = Performer::new(2, 3);
feed(&mut performer, b"x\r\nabc\x1b[BY");
assert_eq!(
performer.take_evicted(),
Vec::<String>::new(),
"explicit move from pending wrap must not evict rows"
);
assert_eq!(performer.render(), vec!["x".to_string(), "abY".to_string()]);
}
#[test]
fn scroll_backfill_preserves_active_background() {
let mut performer = Performer::new(2, 3);
feed(&mut performer, b"\x1b[42mab\r\ncd\n");
assert_eq!(
performer.render(),
vec![
"\x1b[0;42mcd\x1b[0m".to_string(),
"\x1b[0;42m \x1b[0m".to_string()
]
);
}
#[test]
fn erase_preserves_active_background() {
let mut performer = Performer::new(1, 4);
feed(&mut performer, b"\x1b[41mAB\x1b[2K");
let rendered = performer.render();
assert_eq!(rendered, vec!["\x1b[0;41m \x1b[0m".to_string()]);
}
#[test]
fn osc_sequences_are_ignored() {
let mut performer = Performer::new(1, 10);
feed(&mut performer, b"\x1b]0;title\x07hi");
assert_eq!(performer.render(), vec!["hi".to_string()]);
}
}