use crate::{
altscreen::AltScreen,
cell::Cell,
line::Line,
scrollback::Scrollback,
term::{self, AsTermInput, OriginMode, Pos, ScrollRegion},
};
use tracing::warn;
#[derive(Debug)]
pub struct Screen {
grid: Grid,
pub size: crate::Size,
pub cursor: Pos,
pub saved_cursor: SavedCursor,
}
impl Screen {
pub fn scrollback(mut scrollback_lines: usize, size: crate::Size) -> Self {
if scrollback_lines < size.height {
scrollback_lines = size.height;
}
Screen {
grid: Grid::Scrollback(Scrollback::new(scrollback_lines)),
size: size,
cursor: Pos { row: 0, col: 0 },
saved_cursor: SavedCursor::new(Pos { row: 0, col: 0 }),
}
}
pub fn alt(size: crate::Size) -> Self {
Screen {
grid: Grid::AltScreen(AltScreen::new(size)),
size: size,
cursor: Pos { row: 0, col: 0 },
saved_cursor: SavedCursor::new(Pos { row: 0, col: 0 }),
}
}
pub fn scrollback_lines(&self) -> Option<usize> {
if let Grid::Scrollback(scrollback) = &self.grid {
Some(scrollback.scrollback_lines())
} else {
None
}
}
pub fn set_scrollback_lines(&mut self, scrollback_lines: usize) {
if let Grid::Scrollback(scrollback) = &mut self.grid {
scrollback.set_scrollback_lines(self.size, scrollback_lines);
} else {
warn!("attempt to set scrollback lines on non-scrollback screen");
}
}
pub fn set_scroll_region(&mut self, scroll_region: ScrollRegion) {
match &mut self.grid {
Grid::Scrollback(scrollback) => scrollback.scroll_region = scroll_region,
Grid::AltScreen(altscreen) => altscreen.scroll_region = scroll_region,
}
}
pub fn set_origin_mode(&mut self, origin_mode: OriginMode) {
match &mut self.grid {
Grid::Scrollback(s) => s.origin_mode = origin_mode,
Grid::AltScreen(alt) => alt.origin_mode = origin_mode,
}
}
pub fn set_cursor(&mut self, pos: Pos) {
match self.grid.origin_mode() {
OriginMode::Term => {
self.cursor.row = pos.row.saturating_sub(1);
self.cursor.col = pos.col.saturating_sub(1);
}
OriginMode::ScrollRegion => match self.grid.scroll_region() {
ScrollRegion::TrackSize => {
self.cursor.row = pos.row.saturating_sub(1);
self.cursor.col = pos.col.saturating_sub(1);
}
ScrollRegion::Window { top, .. } => {
self.cursor.row = pos.row.saturating_sub(1) + top;
self.cursor.col = pos.col.saturating_sub(1);
}
},
}
}
pub fn dump_contents_into(&self, buf: &mut Vec<u8>, dump_region: crate::ContentRegion) {
match &self.grid {
Grid::Scrollback(scrollback) => {
scrollback.dump_contents_into(buf, self.size, dump_region)
}
Grid::AltScreen(altscreen) => altscreen.term_input_into(buf),
}
term::ControlCodes::cursor_position(
(self.cursor.row + 1) as u16,
(self.cursor.col + 1) as u16,
)
.term_input_into(buf);
if matches!(self.grid.origin_mode(), OriginMode::ScrollRegion) {
term::control_codes().enable_scroll_region_origin_mode.term_input_into(buf);
}
}
pub fn resize(&mut self, new_size: crate::Size) {
match &mut self.grid {
Grid::Scrollback(scrollback) => scrollback.reflow(new_size.width),
Grid::AltScreen(altscreen) => altscreen.resize(new_size),
}
self.size = new_size;
self.cursor.clamp_to(self.size);
self.saved_cursor.pos.clamp_to(self.size);
}
pub fn clamp(&mut self) {
match &self.grid {
Grid::Scrollback(scrollback) => {
scrollback.clamp_to_scroll_region(&mut self.cursor, &self.size)
}
Grid::AltScreen(altscreen) => {
altscreen.clamp_to_scroll_region(&mut self.cursor, &self.size)
}
}
}
pub fn snap_to_bottom(&mut self) {
if let Grid::Scrollback(scrollback) = &mut self.grid {
scrollback.snap_to_bottom();
}
}
pub fn write_at_cursor(&mut self, cell: Cell) -> anyhow::Result<()> {
self.cursor = match &mut self.grid {
Grid::Scrollback(scrollback) => {
scrollback.write_at_cursor(self.size, self.cursor, cell)?
}
Grid::AltScreen(altscreen) => {
altscreen.write_at_cursor(self.size, self.cursor, cell)?
}
};
Ok(())
}
pub fn erase_to_end(&mut self) {
match &mut self.grid {
Grid::Scrollback(s) => s.erase_to_end(self.size, self.cursor),
Grid::AltScreen(alt) => alt.erase_to_end(self.cursor),
}
}
pub fn erase_from_start(&mut self) {
match &mut self.grid {
Grid::Scrollback(s) => s.erase_from_start(self.size, self.cursor),
Grid::AltScreen(alt) => alt.erase_from_start(self.cursor),
}
}
pub fn erase(&mut self, include_scrollback: bool) {
match &mut self.grid {
Grid::Scrollback(s) => s.erase(self.size, include_scrollback),
Grid::AltScreen(alt) => alt.erase(),
}
}
pub fn get_line_mut(&mut self) -> Option<&mut Line> {
match &mut self.grid {
Grid::Scrollback(s) => s.get_line_mut(self.size, self.cursor.row),
Grid::AltScreen(alt) => Some(alt.get_line_mut(self.cursor.row)),
}
}
pub fn scroll_up(&mut self, n: usize) {
match &mut self.grid {
Grid::Scrollback(s) => s.scroll_up(n),
_ => {}
}
}
pub fn scroll_down(&mut self, n: usize) {
match &mut self.grid {
Grid::Scrollback(s) => s.scroll_down(n),
_ => {}
}
}
pub fn insert_lines(&mut self, n: usize) {
match &mut self.grid {
Grid::Scrollback(s) => s.insert_lines(&self.cursor, &self.size, n),
Grid::AltScreen(alt) => alt.insert_lines(&self.cursor, n),
}
}
pub fn delete_lines(&mut self, n: usize) {
match &mut self.grid {
Grid::Scrollback(s) => s.delete_lines(&self.cursor, &self.size, n),
Grid::AltScreen(alt) => alt.delete_lines(&self.cursor, n),
}
}
}
impl std::fmt::Display for Screen {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for _ in 0..self.size.width {
write!(f, "-")?;
}
writeln!(f, "")?;
match &self.grid {
Grid::Scrollback(s) => write!(f, "{}", s)?,
Grid::AltScreen(alt) => write!(f, "{}", alt)?,
}
for _ in 0..self.size.width {
write!(f, "-")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct SavedCursor {
pub pos: Pos,
pub attrs: term::Attrs,
}
impl SavedCursor {
pub fn new(pos: Pos) -> Self {
SavedCursor { pos, attrs: term::Attrs::default() }
}
}
#[derive(Debug)]
enum Grid {
Scrollback(Scrollback),
AltScreen(AltScreen),
}
impl Grid {
fn origin_mode(&self) -> OriginMode {
match self {
Grid::Scrollback(s) => s.origin_mode,
Grid::AltScreen(alt) => alt.origin_mode,
}
}
fn scroll_region(&self) -> &ScrollRegion {
match self {
Grid::Scrollback(s) => &s.scroll_region,
Grid::AltScreen(alt) => &alt.scroll_region,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::term::Attrs;
use crate::Size;
#[test]
fn altscreen_resize_grow_height() {
let mut screen = Screen::alt(Size { width: 10, height: 5 });
screen.resize(Size { width: 10, height: 10 });
match &screen.grid {
Grid::AltScreen(alt) => {
assert_eq!(alt.buf.len(), 10);
}
_ => panic!("wrong grid type"),
}
assert_eq!(screen.size.height, 10);
}
#[test]
fn altscreen_resize_shrink_height() {
let mut screen = Screen::alt(Size { width: 10, height: 10 });
screen.resize(Size { width: 10, height: 5 });
match &screen.grid {
Grid::AltScreen(alt) => {
assert_eq!(alt.buf.len(), 5);
}
_ => panic!("wrong grid type"),
}
assert_eq!(screen.size.height, 5);
}
#[test]
fn altscreen_resize_shrink_width() {
let mut screen = Screen::alt(Size { width: 10, height: 5 });
match &mut screen.grid {
Grid::AltScreen(alt) => {
alt.buf[0].set_cell(10, 9, crate::cell::Cell::new('a', Attrs::default())).unwrap();
assert_eq!(alt.buf[0].cells.len(), 10);
}
_ => panic!("wrong grid type"),
}
screen.resize(Size { width: 5, height: 5 });
assert_eq!(screen.size.width, 5);
match &screen.grid {
Grid::AltScreen(alt) => {
assert_eq!(alt.buf[0].cells.len(), 5);
}
_ => panic!("wrong grid type"),
}
}
#[test]
fn altscreen_cursor_clamping() {
let mut screen = Screen::alt(Size { width: 10, height: 10 });
screen.cursor = Pos { row: 9, col: 9 };
screen.saved_cursor.pos = Pos { row: 8, col: 8 };
screen.resize(Size { width: 5, height: 5 });
assert_eq!(screen.cursor.row, 4);
assert_eq!(screen.cursor.col, 4);
assert_eq!(screen.saved_cursor.pos.row, 4);
assert_eq!(screen.saved_cursor.pos.col, 4);
}
fn get_screen_cell(screen: &Screen, row: usize, col: usize) -> Option<Cell> {
match &screen.grid {
Grid::Scrollback(sb) => sb
.get_line(screen.size, row)
.and_then(|l| l.get_cell(screen.size.width, col))
.cloned(),
_ => None,
}
}
#[test]
fn scrollback_grid_new() {
let size = Size { width: 10, height: 5 };
let screen = Screen::scrollback(5, size);
assert_eq!(screen.size, size);
match &screen.grid {
Grid::Scrollback(sb) => assert!(sb.buf.is_empty()),
_ => panic!("wrong grid type"),
}
}
#[test]
fn scrollback_push_simple() -> anyhow::Result<()> {
let size = Size { width: 5, height: 2 };
let mut screen = Screen::scrollback(5, size);
let c = Cell::new('x', term::Attrs::default());
screen.write_at_cursor(c.clone())?;
let pos = Pos { row: 0, col: 0 };
assert_eq!(
get_screen_cell(&screen, pos.row, pos.col),
Some(c),
"Scrollback:\n{:?}",
screen.grid
);
Ok(())
}
#[test]
fn scrollback_push_wrapping() -> anyhow::Result<()> {
let size = Size { width: 2, height: 5 };
let mut screen = Screen::scrollback(5, size);
screen.write_at_cursor(Cell::new('1', term::Attrs::default()))?;
screen.write_at_cursor(Cell::new('2', term::Attrs::default()))?;
screen.write_at_cursor(Cell::new('3', term::Attrs::default()))?;
assert_eq!(
get_screen_cell(&screen, 0, 0),
Some(Cell::new('1', term::Attrs::default())),
"Scrollback:\n{:?}",
screen.grid,
);
assert_eq!(
get_screen_cell(&screen, 0, 1),
Some(Cell::new('2', term::Attrs::default())),
"Scrollback:\n{:?}",
screen.grid,
);
assert_eq!(
get_screen_cell(&screen, 1, 0),
Some(Cell::new('3', term::Attrs::default())),
"Scrollback:\n{:?}",
screen.grid,
);
Ok(())
}
#[test]
fn scrollback_indexing() -> anyhow::Result<()> {
let size = Size { width: 10, height: 3 };
let mut screen = Screen::scrollback(3, size);
for _ in 0..10 {
screen.write_at_cursor(Cell::new('X', term::Attrs::default()))?;
}
let c_top = Cell::new('T', term::Attrs::default());
let c_mid = Cell::new('M', term::Attrs::default());
let c_bot = Cell::new('B', term::Attrs::default());
for _ in 0..10 {
screen.write_at_cursor(c_top.clone())?;
}
for _ in 0..10 {
screen.write_at_cursor(c_mid.clone())?;
}
for _ in 0..10 {
screen.write_at_cursor(c_bot.clone())?;
}
for r in 0..3 {
for c in 0..10 {
let expected = match r {
0 => &c_top,
1 => &c_mid,
2 => &c_bot,
_ => unreachable!(),
};
assert_eq!(get_screen_cell(&screen, r, c), Some(expected.clone()));
}
}
Ok(())
}
#[test]
fn scrollback_resize_narrower() -> anyhow::Result<()> {
let size = Size { width: 10, height: 5 };
let mut screen = Screen::scrollback(20, size);
for i in 0..10 {
screen.write_at_cursor(Cell::new(
char::from_digit(i, 10).unwrap(),
term::Attrs::default(),
))?;
}
let new_size = Size { width: 5, height: 5 };
screen.resize(new_size);
assert_eq!(
get_screen_cell(&screen, 1, 0),
Some(Cell::new('5', term::Attrs::default())),
"Scrollback:\n{:?}",
screen.grid
);
assert_eq!(
get_screen_cell(&screen, 0, 0),
Some(Cell::new('0', term::Attrs::default())),
"Scrollback:\n{:?}",
screen.grid
);
Ok(())
}
#[test]
fn scrollback_resize_wider() -> anyhow::Result<()> {
let size = Size { width: 5, height: 5 };
let mut screen = Screen::scrollback(30, size);
for i in 0..10 {
screen.write_at_cursor(Cell::new(
char::from_digit(i, 10).unwrap(),
term::Attrs::default(),
))?;
}
assert_eq!(
get_screen_cell(&screen, 1, 0),
Some(Cell::new('5', term::Attrs::default())),
"Scrollback:\n{:?}",
screen.grid
);
let new_size = Size { width: 10, height: 5 };
screen.resize(new_size);
assert_eq!(
get_screen_cell(&screen, 0, 0),
Some(Cell::new('0', term::Attrs::default())),
"Scrollback:\n{:?}",
screen.grid
);
assert_eq!(
get_screen_cell(&screen, 0, 9),
Some(Cell::new('9', term::Attrs::default())),
"Scrollback:\n{:?}",
screen.grid
);
Ok(())
}
#[test]
fn scrollback_reflow_roundtrip() -> anyhow::Result<()> {
let shapes = vec![
(10, 20), (5, 10), (10, 10), ];
for (start_w, end_w) in shapes {
let start_size = Size { width: start_w, height: 10 };
let mut screen = Screen::scrollback(100, start_size);
let count = 30;
for i in 0..count {
screen.write_at_cursor(Cell::new(
char::from_u32(65 + i % 26).unwrap(),
term::Attrs::default(),
))?;
}
screen.resize(Size { width: end_w, height: 10 });
screen.resize(start_size);
let mut expected_screen = Screen::scrollback(100, start_size);
for i in 0..count {
expected_screen.write_at_cursor(Cell::new(
char::from_u32(65 + i % 26).unwrap(),
term::Attrs::default(),
))?;
}
match (&screen.grid, &expected_screen.grid) {
(Grid::Scrollback(actual), Grid::Scrollback(expected)) => {
assert_eq!(
actual, expected,
"Scrollback state mismatch after roundtrip resize {} -> {} -> {}",
start_w, end_w, start_w
);
}
_ => panic!("wrong grid type"),
}
}
Ok(())
}
}