pub struct SerialPane {
parser: vt100::Parser,
scrollback_rows: usize,
}
impl SerialPane {
pub const DEFAULT_SCROLLBACK_ROWS: usize = 10_000;
#[must_use]
pub fn new(rows: u16, cols: u16) -> Self {
Self::with_scrollback(rows, cols, Self::DEFAULT_SCROLLBACK_ROWS)
}
#[must_use]
pub fn with_scrollback(rows: u16, cols: u16, scrollback_rows: usize) -> Self {
Self {
parser: vt100::Parser::new(rows, cols, scrollback_rows),
scrollback_rows,
}
}
pub fn ingest(&mut self, bytes: &[u8]) {
self.parser.process(bytes);
}
#[must_use]
pub fn screen(&self) -> &vt100::Screen {
self.parser.screen()
}
pub fn resize(&mut self, rows: u16, cols: u16) {
self.parser.screen_mut().set_size(rows, cols);
}
#[must_use]
pub const fn scrollback_rows(&self) -> usize {
self.scrollback_rows
}
#[must_use]
pub fn scrollback_offset(&self) -> usize {
self.parser.screen().scrollback()
}
#[must_use]
pub fn is_scrolled(&self) -> bool {
self.scrollback_offset() > 0
}
pub fn scroll_up(&mut self, lines: usize) {
let target = self
.scrollback_offset()
.saturating_add(lines)
.min(self.scrollback_rows);
self.parser.screen_mut().set_scrollback(target);
}
pub fn scroll_down(&mut self, lines: usize) {
let target = self.scrollback_offset().saturating_sub(lines);
self.parser.screen_mut().set_scrollback(target);
}
pub fn scroll_to_top(&mut self) {
self.parser
.screen_mut()
.set_scrollback(self.scrollback_rows);
}
pub fn scroll_to_bottom(&mut self) {
self.parser.screen_mut().set_scrollback(0);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serial_pane_ingests_bytes_into_vt100() {
let mut pane = SerialPane::new(24, 80);
pane.ingest(b"hello\r\nworld");
let screen = pane.screen();
assert_eq!(screen.cell(0, 0).unwrap().contents(), "h");
assert_eq!(screen.cell(1, 0).unwrap().contents(), "w");
}
#[test]
fn serial_pane_resize_updates_size() {
let mut pane = SerialPane::new(24, 80);
for _ in 0..30 {
pane.ingest(b"line\r\n");
}
pane.resize(40, 80);
assert_eq!(pane.screen().size(), (40, 80));
}
#[test]
fn serial_pane_default_scrollback_is_ten_thousand() {
let _pane = SerialPane::new(24, 80);
assert_eq!(SerialPane::DEFAULT_SCROLLBACK_ROWS, 10_000);
}
#[test]
fn serial_pane_custom_scrollback() {
let pane = SerialPane::with_scrollback(24, 80, 500);
assert_eq!(pane.scrollback_rows(), 500);
}
#[test]
fn scroll_up_increments_offset() {
let mut pane = SerialPane::new(24, 80);
for i in 0..40 {
pane.ingest(format!("row {i}\r\n").as_bytes());
}
assert_eq!(pane.scrollback_offset(), 0);
assert!(!pane.is_scrolled());
pane.scroll_up(5);
assert_eq!(pane.scrollback_offset(), 5);
assert!(pane.is_scrolled());
}
#[test]
fn scroll_down_decrements_offset() {
let mut pane = SerialPane::new(24, 80);
for i in 0..40 {
pane.ingest(format!("row {i}\r\n").as_bytes());
}
pane.scroll_up(10);
pane.scroll_down(4);
assert_eq!(pane.scrollback_offset(), 6);
}
#[test]
fn scroll_to_bottom_resets_offset() {
let mut pane = SerialPane::new(24, 80);
for i in 0..40 {
pane.ingest(format!("row {i}\r\n").as_bytes());
}
pane.scroll_up(15);
assert!(pane.is_scrolled());
pane.scroll_to_bottom();
assert_eq!(pane.scrollback_offset(), 0);
assert!(!pane.is_scrolled());
}
#[test]
fn scroll_up_clamps_to_scrollback_capacity() {
let mut pane = SerialPane::new(24, 80);
for _ in 0..5 {
pane.ingest(b"x\r\n");
}
pane.scroll_up(usize::MAX / 2);
assert!(pane.scrollback_offset() <= SerialPane::DEFAULT_SCROLLBACK_ROWS);
}
#[test]
fn scroll_down_saturates_at_zero() {
let mut pane = SerialPane::new(24, 80);
for i in 0..40 {
pane.ingest(format!("row {i}\r\n").as_bytes());
}
pane.scroll_down(100);
assert_eq!(pane.scrollback_offset(), 0);
}
#[test]
fn scroll_to_top_jumps_to_oldest() {
let mut pane = SerialPane::new(24, 80);
for i in 0..40 {
pane.ingest(format!("row {i}\r\n").as_bytes());
}
pane.scroll_to_top();
assert!(pane.is_scrolled());
assert!(pane.scrollback_offset() <= SerialPane::DEFAULT_SCROLLBACK_ROWS);
}
#[test]
fn serial_pane_ingest_handles_ansi_escape_sequences() {
let mut pane = SerialPane::new(24, 80);
pane.ingest(b"\x1b[31mX\x1b[0m");
let cell = pane.screen().cell(0, 0).unwrap();
assert_eq!(cell.contents(), "X");
let fgcolor = cell.fgcolor();
assert!(
!matches!(fgcolor, vt100::Color::Default),
"expected coloured fg, got {fgcolor:?}",
);
}
}