use std::collections::VecDeque;
use crate::{
cell::Cell,
line::{self, Line},
log,
term::{self, AsTermInput, OriginMode, Pos, ScrollRegion},
};
use anyhow::{anyhow, Context};
#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) struct AltScreen {
pub buf: VecDeque<Line>,
pub scroll_region: ScrollRegion,
pub origin_mode: OriginMode,
logger: log::Context,
}
impl AltScreen {
pub fn new(size: crate::Size) -> Self {
let mut buf = VecDeque::new();
for _ in 0..size.height {
buf.push_back(Line::new());
}
AltScreen {
buf,
scroll_region: ScrollRegion::default(),
origin_mode: OriginMode::default(),
logger: log::Context::None,
}
}
pub fn set_logger(&mut self, logger: log::Context) {
self.logger = logger;
}
pub fn write_at_cursor(
&mut self,
size: crate::Size,
mut cursor: Pos,
cell: Cell,
) -> anyhow::Result<Pos> {
if size.width < 1 {
return Err(anyhow!("cannot write to zero width terminal grid"));
}
let cell_width = cell.width() as usize;
let Some(line) = self.buf.get_mut(cursor.row) else {
return Err(anyhow!("row {} out of bounds (height={})", cursor.row, self.buf.len()));
};
line.set_cell(size.width, cursor.col, cell).context("setting cell in alt screen")?;
cursor.col += cell_width;
if cursor.col >= size.width {
cursor.row += 1;
cursor.col = 0;
if cursor.row >= size.height {
self.scroll_down(1);
}
}
cursor.clamp_to(size);
Ok(cursor)
}
fn scroll_region_rows(&self) -> (usize, usize) {
match self.scroll_region {
ScrollRegion::TrackSize => (0, self.buf.len()),
ScrollRegion::Window { top, bottom } => {
(std::cmp::min(top, self.buf.len()), std::cmp::min(bottom, self.buf.len()))
}
}
}
pub fn scroll_down(&mut self, rows: usize) {
if let ScrollRegion::TrackSize = self.scroll_region {
for _ in 0..rows {
self.buf.pop_front();
self.buf.push_back(Line::new());
}
return;
}
let (top, bottom) = self.scroll_region_rows();
if top >= bottom {
return;
}
if rows > bottom - top {
for i in top..bottom {
self.buf[i] = Line::new();
}
return;
}
let to_shuffle = (bottom - top) - rows;
for i in 0..to_shuffle {
self.buf[top + i] = self.buf[top + rows + i].clone();
}
for i in 0..rows {
self.buf[top + to_shuffle + i] = Line::new();
}
}
pub fn clamp_to_scroll_region(&self, cursor: &mut Pos, size: &crate::Size) {
match self.origin_mode {
OriginMode::Term => cursor.clamp_to(size),
OriginMode::ScrollRegion => cursor.clamp_to(self.scroll_region.as_region(size)),
}
}
pub fn resize(&mut self, new_size: crate::Size) {
for line in self.buf.iter_mut() {
line.truncate(new_size.width);
}
let old_height = self.buf.len();
if new_size.height > old_height {
for _ in 0..(new_size.height - old_height) {
self.buf.push_back(Line::new());
}
} else if new_size.height < old_height {
for _ in 0..(old_height - new_size.height) {
self.buf.pop_back();
}
}
}
pub fn get_line_mut(&mut self, row: usize) -> Option<&mut Line> {
self.buf.get_mut(row)
}
pub fn erase_to_end(&mut self, cursor: Pos) {
if let Some(line) = self.buf.get_mut(cursor.row) {
line.truncate(cursor.col);
}
let end = match (self.origin_mode, &self.scroll_region) {
(OriginMode::ScrollRegion, ScrollRegion::Window { bottom, .. }) => *bottom,
_ => self.buf.len(),
};
for i in (cursor.row + 1)..std::cmp::min(end, self.buf.len()) {
self.buf[i].truncate(0);
}
}
pub fn erase_from_start(&mut self, cursor: Pos) {
let start = match (self.origin_mode, &self.scroll_region) {
(OriginMode::ScrollRegion, ScrollRegion::Window { top, .. }) => *top,
_ => 0,
};
for i in start..std::cmp::min(cursor.row, self.buf.len()) {
self.buf[i].truncate(0);
}
if let Some(line) = self.buf.get_mut(cursor.row) {
line.erase(line::Section::StartTo(cursor.col));
}
}
pub fn erase(&mut self) {
let (start, end) = match (self.origin_mode, &self.scroll_region) {
(OriginMode::ScrollRegion, ScrollRegion::Window { top, bottom }) => (*top, *bottom),
_ => (0, self.buf.len()),
};
for i in start..std::cmp::min(end, self.buf.len()) {
self.buf[i].truncate(0);
}
}
pub fn insert_lines(&mut self, cursor: &Pos, n: usize) {
let (top, bottom) = self.scroll_region_rows();
if cursor.row < top || bottom <= cursor.row {
return;
}
let lines_to_insert = std::cmp::min(n, bottom - cursor.row);
let shuffle_lines = (bottom - top) - lines_to_insert - (cursor.row - top);
for i in 0..shuffle_lines {
let bottom_offset = bottom - 1 - i;
self.buf[bottom_offset] =
std::mem::replace(&mut self.buf[bottom_offset - lines_to_insert], Line::new());
}
for i in shuffle_lines..lines_to_insert {
self.buf[cursor.row + i] = Line::new();
}
}
pub fn delete_lines(&mut self, cursor: &Pos, n: usize) {
let (top, bottom) = self.scroll_region_rows();
if cursor.row < top || bottom <= cursor.row {
return;
}
let lines_to_delete = std::cmp::min(n, bottom - cursor.row);
let shuffle_lines = (bottom - top) - lines_to_delete - (cursor.row - top);
for i in 0..shuffle_lines {
self.buf[cursor.row + i] =
std::mem::replace(&mut self.buf[cursor.row + lines_to_delete + i], Line::new());
}
for i in shuffle_lines..lines_to_delete {
self.buf[cursor.row + i] = Line::new();
}
}
}
impl std::fmt::Display for AltScreen {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for line in self.buf.iter() {
write!(f, "{}", line)?;
}
Ok(())
}
}
impl AsTermInput for AltScreen {
fn term_input_into(&self, buf: &mut Vec<u8>) {
for (i, line) in self.buf.iter().enumerate() {
line.term_input_into(buf);
if i != self.buf.len() - 1 {
term::Crlf::default().term_input_into(buf);
}
}
self.scroll_region.term_input_into(buf);
}
}
#[cfg(test)]
mod tests {
use super::*;
const SIZE: crate::Size = crate::Size { width: 5, height: 3 };
fn alt_screen() -> AltScreen {
let mut alt = AltScreen::new(SIZE);
for (i, c) in ['a', 'b', 'c'].iter().enumerate() {
alt.buf[i].set_cell(SIZE.width, 0, Cell::new(*c, term::Attrs::default())).unwrap();
}
alt
}
fn first_col(alt: &AltScreen) -> String {
alt.buf
.iter()
.map(|line| match line.cells.first() {
Some(cell) if !cell.is_empty() => cell.to_string(),
_ => String::from("."),
})
.collect()
}
#[test]
fn scroll_down_with_region_past_end_of_buf() {
let mut alt = alt_screen();
alt.scroll_region = ScrollRegion::Window { top: 0, bottom: SIZE.height + 6 };
alt.scroll_down(1);
assert_eq!(first_col(&alt), "bc.");
}
#[test]
fn erase_with_region_past_end_of_buf() {
let mut alt = alt_screen();
alt.origin_mode = OriginMode::ScrollRegion;
alt.scroll_region = ScrollRegion::Window { top: 0, bottom: SIZE.height + 6 };
alt.erase();
assert_eq!(first_col(&alt), "...");
}
#[test]
fn write_at_cursor_past_last_row() {
let mut alt = alt_screen();
let cursor = Pos { row: SIZE.height, col: 0 };
let cell = Cell::new('x', term::Attrs::default());
assert!(alt.write_at_cursor(SIZE, cursor, cell).is_err());
assert_eq!(first_col(&alt), "abc");
}
#[test]
fn insert_lines_past_last_row() {
let mut alt = alt_screen();
alt.insert_lines(&Pos { row: SIZE.height + 2, col: 0 }, 1);
assert_eq!(first_col(&alt), "abc");
}
#[test]
fn delete_lines_past_last_row() {
let mut alt = alt_screen();
alt.delete_lines(&Pos { row: SIZE.height + 2, col: 0 }, 1);
assert_eq!(first_col(&alt), "abc");
}
#[test]
fn erase_to_end_past_last_row() {
let mut alt = alt_screen();
alt.erase_to_end(Pos { row: SIZE.height + 2, col: 0 });
assert_eq!(first_col(&alt), "abc");
}
#[test]
fn erase_from_start_past_last_row() {
let mut alt = alt_screen();
alt.erase_from_start(Pos { row: SIZE.height + 2, col: 0 });
assert_eq!(first_col(&alt), "...");
}
}