panasyn 0.1.0

A lightweight GPU-accelerated terminal emulator for macOS and Linux.
use std::collections::VecDeque;

use super::Cell;

/// Ring buffer of historical rows scrolled off the top of the terminal grid.
pub struct Scrollback {
    rows: VecDeque<Vec<Cell>>,
    max_lines: usize,
}

impl Scrollback {
    pub fn new() -> Self {
        Self::new_scrollback(10_000)
    }

    pub fn new_scrollback(max_lines: usize) -> Self {
        Self {
            rows: VecDeque::with_capacity(max_lines.min(1000)),
            max_lines,
        }
    }

    /// Push a grid row into the scrollback. Drops oldest row if at capacity.
    pub fn push(&mut self, row: &[Cell]) {
        if self.max_lines == 0 {
            return;
        }
        let cloned: Vec<Cell> = row.to_vec();
        self.push_owned(cloned);
    }

    pub fn push_owned(&mut self, row: Vec<Cell>) {
        let _ = self.push_owned_recycling(row);
    }

    pub fn push_owned_recycling(&mut self, row: Vec<Cell>) -> Option<Vec<Cell>> {
        if self.max_lines == 0 {
            return Some(row);
        }
        let recycled = (self.rows.len() >= self.max_lines)
            .then(|| self.rows.pop_front())
            .flatten();
        self.rows.push_back(row);
        recycled
    }

    /// Number of rows currently stored.
    pub fn len(&self) -> usize {
        self.rows.len()
    }

    /// Change retained history length, dropping oldest rows if needed.
    pub fn set_max_lines(&mut self, max_lines: usize) {
        self.max_lines = max_lines;
        while self.rows.len() > self.max_lines {
            self.rows.pop_front();
        }
    }

    /// Return a slice of cells at `index` (oldest = 0, newest = len-1).
    pub fn row_cells(&self, index: usize) -> Option<&[Cell]> {
        self.rows.get(index).map(|r| r.as_slice())
    }

    /// Return the text of row at `index` (oldest = 0, newest = len-1).
    pub fn row_text(&self, index: usize, cols: usize) -> String {
        if let Some(row) = self.rows.get(index) {
            let mut s = String::with_capacity(cols);
            for cell in row {
                cell.push_text(&mut s);
            }
            s
        } else {
            " ".repeat(cols)
        }
    }

    /// Clear all scrollback.
    pub fn clear(&mut self) {
        self.rows.clear();
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn row(text: &str) -> Vec<Cell> {
        text.chars().map(Cell::new).collect()
    }

    #[test]
    fn set_max_lines_trims_oldest_rows() {
        let mut scrollback = Scrollback::new_scrollback(4);
        for value in ["one", "two", "three", "four"] {
            scrollback.push_owned(row(value));
        }

        scrollback.set_max_lines(2);

        assert_eq!(scrollback.len(), 2);
        assert_eq!(scrollback.row_text(0, 0), "three");
        assert_eq!(scrollback.row_text(1, 0), "four");
    }

    #[test]
    fn set_max_lines_zero_drops_all_rows_and_recycles_future_rows() {
        let mut scrollback = Scrollback::new_scrollback(2);
        scrollback.push_owned(row("one"));

        scrollback.set_max_lines(0);
        let recycled = scrollback.push_owned_recycling(row("two"));

        assert_eq!(scrollback.len(), 0);
        assert!(recycled.is_some());
    }
}