pub mod buffer;
pub mod parser;
pub mod query;
pub use buffer::{
Attributes, Cell, CellChange, ChangeType, Color, Cursor, ScreenBuffer, ScreenDiff,
};
use parser::apply_sgr;
pub use parser::{AnsiParser, AnsiSequence, EraseMode, ParseResult};
pub use query::{Region, ScreenQuery, ScreenQueryExt};
#[derive(Clone)]
pub struct Screen {
buffer: ScreenBuffer,
parser: AnsiParser,
fg: Color,
bg: Color,
attrs: Attributes,
}
impl Screen {
#[must_use]
pub fn new(rows: usize, cols: usize) -> Self {
Self {
buffer: ScreenBuffer::new(rows, cols),
parser: AnsiParser::new(),
fg: Color::Default,
bg: Color::Default,
attrs: Attributes::empty(),
}
}
#[must_use]
pub fn vt100() -> Self {
Self::new(24, 80)
}
#[must_use]
pub const fn rows(&self) -> usize {
self.buffer.rows()
}
#[must_use]
pub const fn cols(&self) -> usize {
self.buffer.cols()
}
#[must_use]
pub const fn buffer(&self) -> &ScreenBuffer {
&self.buffer
}
pub const fn buffer_mut(&mut self) -> &mut ScreenBuffer {
&mut self.buffer
}
#[must_use]
pub const fn cursor(&self) -> &Cursor {
self.buffer.cursor()
}
pub fn process(&mut self, data: &[u8]) {
for byte in data {
if let Some(result) = self.parser.parse(*byte) {
self.apply_result(result);
}
}
}
pub fn process_str(&mut self, s: &str) {
self.process(s.as_bytes());
}
fn apply_result(&mut self, result: ParseResult) {
match result {
ParseResult::Print(c) => {
self.buffer.set_style(self.fg, self.bg, self.attrs);
self.buffer.write_char(c);
}
ParseResult::Control(c) => self.apply_control(c),
ParseResult::Sequence(seq) => self.apply_sequence(seq),
}
}
fn apply_control(&mut self, c: u8) {
match c {
0x07 => {
}
0x08 => {
let cursor = self.buffer.cursor_mut();
if cursor.col > 0 {
cursor.col -= 1;
}
}
0x09 => {
let cols = self.buffer.cols();
let cursor = self.buffer.cursor_mut();
cursor.col = ((cursor.col / 8) + 1) * 8;
if cursor.col >= cols {
cursor.col = cols - 1;
}
}
0x0a..=0x0c => {
let rows = self.buffer.rows();
let cursor_row = self.buffer.cursor().row + 1;
if cursor_row >= rows {
self.buffer.scroll_up(1);
self.buffer.cursor_mut().row = rows - 1;
} else {
self.buffer.cursor_mut().row = cursor_row;
}
self.buffer.cursor_mut().col = 0;
}
0x0d => {
self.buffer.cursor_mut().col = 0;
}
_ => {}
}
}
#[allow(clippy::too_many_lines)] fn apply_sequence(&mut self, seq: AnsiSequence) {
match seq {
AnsiSequence::CursorUp(n) => {
let cursor = self.buffer.cursor_mut();
cursor.row = cursor.row.saturating_sub(n as usize);
}
AnsiSequence::CursorDown(n) => {
let rows = self.buffer.rows();
let cursor = self.buffer.cursor_mut();
cursor.row = (cursor.row + n as usize).min(rows.saturating_sub(1));
}
AnsiSequence::CursorForward(n) => {
let cols = self.buffer.cols();
let cursor = self.buffer.cursor_mut();
cursor.col = (cursor.col + n as usize).min(cols.saturating_sub(1));
}
AnsiSequence::CursorBackward(n) => {
let cursor = self.buffer.cursor_mut();
cursor.col = cursor.col.saturating_sub(n as usize);
}
AnsiSequence::CursorNextLine(n) => {
let rows = self.buffer.rows();
let cursor = self.buffer.cursor_mut();
cursor.row = (cursor.row + n as usize).min(rows.saturating_sub(1));
cursor.col = 0;
}
AnsiSequence::CursorPrevLine(n) => {
let cursor = self.buffer.cursor_mut();
cursor.row = cursor.row.saturating_sub(n as usize);
cursor.col = 0;
}
AnsiSequence::CursorColumn(n) => {
let cols = self.buffer.cols();
let cursor = self.buffer.cursor_mut();
cursor.col = (n.saturating_sub(1) as usize).min(cols.saturating_sub(1));
}
AnsiSequence::CursorRow(n) => {
let rows = self.buffer.rows();
let cursor = self.buffer.cursor_mut();
cursor.row = (n.saturating_sub(1) as usize).min(rows.saturating_sub(1));
}
AnsiSequence::CursorPosition { row, col } => {
self.buffer.goto(
(row.saturating_sub(1)) as usize,
(col.saturating_sub(1)) as usize,
);
}
AnsiSequence::EraseDisplay(mode) => match mode {
EraseMode::ToEnd => self.buffer.clear_to_end(),
EraseMode::ToStart => self.buffer.clear_to_start(),
EraseMode::All => self.buffer.clear(),
},
AnsiSequence::EraseLine(mode) => match mode {
EraseMode::ToEnd => self.buffer.clear_line_to_end(),
EraseMode::ToStart => {
let row = self.buffer.cursor().row;
let col = self.buffer.cursor().col;
for c in 0..=col {
self.buffer.set(row, c, Cell::default());
}
}
EraseMode::All => self.buffer.clear_line(),
},
AnsiSequence::EraseChars(n) => {
let row = self.buffer.cursor().row;
let col = self.buffer.cursor().col;
let cols = self.buffer.cols();
let end = (col + n as usize).min(cols);
for c in col..end {
self.buffer.set(row, c, Cell::default());
}
}
AnsiSequence::SetGraphics(params) => {
apply_sgr(¶ms, &mut self.fg, &mut self.bg, &mut self.attrs);
}
AnsiSequence::ScrollUp(n) => {
self.buffer.scroll_up(n as usize);
}
AnsiSequence::ScrollDown(n) => {
self.buffer.scroll_down(n as usize);
}
AnsiSequence::ReverseIndex => {
let cursor_row = self.buffer.cursor().row;
let (top, _) = (0, self.buffer.rows() - 1); if cursor_row == top {
self.buffer.scroll_down(1);
} else {
self.buffer.cursor_mut().row = cursor_row.saturating_sub(1);
}
}
AnsiSequence::Index => {
let rows = self.buffer.rows();
let cursor_row = self.buffer.cursor().row;
if cursor_row >= rows - 1 {
self.buffer.scroll_up(1);
} else {
self.buffer.cursor_mut().row = cursor_row + 1;
}
}
AnsiSequence::NextLine => {
let rows = self.buffer.rows();
let cursor_row = self.buffer.cursor().row;
if cursor_row >= rows - 1 {
self.buffer.scroll_up(1);
self.buffer.cursor_mut().row = rows - 1;
} else {
self.buffer.cursor_mut().row = cursor_row + 1;
}
self.buffer.cursor_mut().col = 0;
}
AnsiSequence::SaveCursor => {
self.buffer.save_cursor();
}
AnsiSequence::RestoreCursor => {
self.buffer.restore_cursor();
}
AnsiSequence::SetScrollRegion { top, bottom } => {
let top = (top.saturating_sub(1)) as usize;
let bottom = if bottom == 0 {
self.buffer.rows() - 1
} else {
(bottom.saturating_sub(1)) as usize
};
self.buffer.set_scroll_region(top, bottom);
}
AnsiSequence::ShowCursor => {
self.buffer.cursor_mut().visible = true;
}
AnsiSequence::HideCursor => {
self.buffer.cursor_mut().visible = false;
}
AnsiSequence::InsertLines(n) => {
self.buffer.insert_lines(n as usize);
}
AnsiSequence::DeleteLines(n) => {
self.buffer.delete_lines(n as usize);
}
AnsiSequence::InsertChars(n) => {
self.buffer.insert_chars(n as usize);
}
AnsiSequence::DeleteChars(n) => {
self.buffer.delete_chars(n as usize);
}
AnsiSequence::RepeatChar(n) => {
let _ = n;
}
AnsiSequence::Reset => {
self.buffer.clear();
self.buffer.goto(0, 0);
self.fg = Color::Default;
self.bg = Color::Default;
self.attrs = Attributes::empty();
}
AnsiSequence::Unknown(_) => {
}
}
}
#[must_use]
pub fn text(&self) -> String {
self.buffer.text()
}
pub fn clear(&mut self) {
self.buffer.clear();
self.buffer.goto(0, 0);
}
pub fn resize(&mut self, rows: usize, cols: usize) {
self.buffer.resize(rows, cols);
}
#[must_use]
pub const fn query(&self) -> ScreenQuery<'_> {
ScreenQuery::new(&self.buffer)
}
}
impl std::fmt::Debug for Screen {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Screen")
.field("rows", &self.rows())
.field("cols", &self.cols())
.field("cursor", self.cursor())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn screen_basic() {
let mut screen = Screen::new(24, 80);
screen.process_str("Hello, World!");
assert!(screen.query().contains("Hello, World!"));
}
#[test]
fn screen_cursor_movement() {
let mut screen = Screen::new(24, 80);
screen.process_str("Hello\x1b[1;1HWorld");
assert!(screen.query().contains("World"));
}
#[test]
fn screen_clear() {
let mut screen = Screen::new(24, 80);
screen.process_str("Hello\x1b[2J\x1b[HWorld");
assert!(!screen.query().contains("Hello"));
assert!(screen.query().contains("World"));
}
#[test]
fn screen_colors() {
let mut screen = Screen::new(24, 80);
screen.process_str("\x1b[31mRed\x1b[0m Normal");
let cell = screen.buffer().get(0, 0).unwrap();
assert_eq!(cell.char, 'R');
assert_eq!(cell.fg, Color::Red);
}
#[test]
fn screen_scroll() {
let mut screen = Screen::new(3, 10);
screen.process_str("Line 1\n");
screen.process_str("Line 2\n");
screen.process_str("Line 3\n");
screen.process_str("Line 4");
assert!(!screen.query().contains("Line 1"));
assert!(screen.query().contains("Line 4"));
}
#[test]
fn screen_cursor_next_line() {
let mut screen = Screen::new(10, 20);
screen.process_str("Test");
screen.process_str("\x1b[2E"); screen.process_str("Line");
assert_eq!(screen.cursor().row, 2);
assert!(screen.query().contains("Line"));
}
#[test]
fn screen_cursor_prev_line() {
let mut screen = Screen::new(10, 20);
screen.process_str("\x1b[5;10H"); screen.process_str("\x1b[2F"); screen.process_str("X");
assert_eq!(screen.cursor().row, 2);
assert_eq!(screen.cursor().col, 1);
}
#[test]
fn screen_cursor_column() {
let mut screen = Screen::new(10, 20);
screen.process_str("Hello World");
screen.process_str("\x1b[5G"); screen.process_str("X");
assert!(screen.query().contains("HellX World"));
}
#[test]
fn screen_cursor_row() {
let mut screen = Screen::new(10, 20);
screen.process_str("\x1b[5d"); screen.process_str("Test");
assert_eq!(screen.cursor().row, 4); }
#[test]
fn screen_erase_chars() {
let mut screen = Screen::new(1, 20);
screen.process_str("Hello World");
screen.process_str("\x1b[1;1H"); screen.process_str("\x1b[5X");
let text = screen.text();
assert!(text.starts_with(" World") || text.contains("World"));
}
#[test]
fn screen_reverse_index() {
let mut screen = Screen::new(5, 20);
screen.process_str("Line 1\n");
screen.process_str("Line 2\n");
screen.process_str("Line 3");
assert_eq!(screen.cursor().row, 2);
screen.process_str("\x1bM"); assert_eq!(screen.cursor().row, 1);
}
#[test]
fn screen_reverse_index_at_top() {
let mut screen = Screen::new(3, 20);
screen.process_str("Line 1");
screen.process_str("\x1b[1;1H"); screen.process_str("\x1bM");
assert!(screen.buffer().row_text(0).is_empty());
}
#[test]
fn screen_index() {
let mut screen = Screen::new(3, 20);
screen.process_str("Line 1");
screen.process_str("\x1bD");
assert_eq!(screen.cursor().row, 1);
}
#[test]
fn screen_next_line_escape() {
let mut screen = Screen::new(10, 20);
screen.process_str("Hello");
screen.process_str("\x1bE"); screen.process_str("World");
assert_eq!(screen.cursor().row, 1);
assert_eq!(screen.cursor().col, 5);
}
#[test]
fn screen_form_feed() {
let mut screen = Screen::new(10, 20);
screen.process_str("Line 1\x0c"); screen.process_str("Line 2");
assert_eq!(screen.cursor().row, 1);
}
#[test]
fn screen_vertical_tab() {
let mut screen = Screen::new(10, 20);
screen.process_str("Line 1\x0b"); screen.process_str("Line 2");
assert_eq!(screen.cursor().row, 1);
}
}