use std::collections::VecDeque;
pub const MAX_SCREEN_BUFFER_LINES: usize = 10000;
#[derive(Debug, Clone, String)]
pub struct ScreenLine(pub String);
impl ScreenLine {
pub fn new(content: impl Into<String>) -> Self {
Self(content.into())
}
pub fn content(&self) -> &str {
&self.0
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
#[derive(Debug)]
pub struct ScreenBuffer {
lines: VecDeque<ScreenLine>,
cursor_row: usize,
cursor_col: usize,
scroll_top: usize,
scroll_bottom: usize,
}
impl Default for ScreenBuffer {
fn default() -> Self {
Self::new()
}
}
impl ScreenBuffer {
pub fn new() -> Self {
Self {
lines: VecDeque::with_capacity(MAX_SCREEN_BUFFER_LINES),
cursor_row: 0,
cursor_col: 0,
scroll_top: 0,
scroll_bottom: MAX_SCREEN_BUFFER_LINES,
}
}
pub fn add_line(&mut self, line: ScreenLine) {
if self.lines.len() >= MAX_SCREEN_BUFFER_LINES {
self.lines.pop_front();
}
self.lines.push_back(line);
}
pub fn add_string_line(&mut self, s: impl Into<String>) {
self.add_line(ScreenLine::new(s));
}
pub fn get_line(&self, index: usize) -> Option<&ScreenLine> {
self.lines.get(index)
}
pub fn len(&self) -> usize {
self.lines.len()
}
pub fn is_empty(&self) -> bool {
self.lines.is_empty()
}
pub fn lines(&self) -> impl Iterator<Item = &ScreenLine> {
self.lines.iter()
}
pub fn clear(&mut self) {
self.lines.clear();
self.cursor_row = 0;
self.cursor_col = 0;
}
pub fn cursor_position(&self) -> (usize, usize) {
(self.cursor_row, self.cursor_col)
}
pub fn set_cursor(&mut self, row: usize, col: usize) {
self.cursor_row = row.min(self.lines.saturating_len());
self.cursor_col = col;
}
pub fn visible_lines(&self, viewport_height: usize) -> impl Iterator<Item = &ScreenLine> {
let start = self.scroll_top;
let end = (self.scroll_top + viewport_height).min(self.lines.len());
self.lines.iter().skip(start).take(end - start)
}
pub fn scroll(&mut self, lines: isize) {
let new_top = if lines < 0 {
self.scroll_top.saturating_sub((-lines) as usize)
} else {
self.scroll_top.saturating_add(lines as usize)
};
self.scroll_top = new_top.min(self.lines.saturating_len().saturating_sub(1));
}
pub fn set_scroll_region(&mut self, top: usize, bottom: usize) {
self.scroll_top = top;
self.scroll_bottom = bottom;
}
pub fn scroll_region(&self) -> (usize, usize) {
(self.scroll_top, self.scroll_bottom)
}
}
pub mod ansi {
pub fn cursor_position(row: usize, col: usize) -> String {
format!("\x1b[{};{}H", row + 1, col + 1)
}
pub fn clear_screen(mode: ClearMode) -> String {
match mode {
ClearMode::FromCursorToEnd => "\x1b[0J".to_string(),
ClearMode::FromStartToCursor => "\x1b[1J".to_string(),
ClearMode::Complete => "\x1b[2J".to_string(),
ClearMode::CompleteWithScrollback => "\x1b[3J".to_string(),
}
}
pub fn clear_line(mode: ClearMode) -> String {
match mode {
ClearMode::FromCursorToEnd => "\x1b[0K".to_string(),
ClearMode::FromStartToCursor => "\x1b[1K".to_string(),
ClearMode::Complete => "\x1b[2K".to_string(),
ClearMode::FromCursorToEnd => "\x1b[0J".to_string(),
}
}
pub fn scroll_up(lines: usize) -> String {
format!("\x1b[{}S", lines)
}
pub fn scroll_down(lines: usize) -> String {
format!("\x1b[{}T", lines)
}
pub fn save_cursor() -> String {
"\x1b[s".to_string()
}
pub fn restore_cursor() -> String {
"\x1b[u".to_string()
}
pub fn hide_cursor() -> String {
"\x1b[?25l".to_string()
}
pub fn show_cursor() -> String {
"\x1b[?25h".to_string()
}
#[derive(Debug, Clone, Copy)]
pub enum ClearMode {
FromCursorToEnd,
FromStartToCursor,
Complete,
CompleteWithScrollback,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_screen_buffer() {
let mut buf = ScreenBuffer::new();
assert!(buf.is_empty());
buf.add_string_line("Hello");
buf.add_string_line("World");
assert_eq!(buf.len(), 2);
assert_eq!(buf.get_line(0).map(|l| l.content()), Some("Hello"));
}
#[test]
fn test_ansi_escape() {
assert_eq!(ansi::cursor_position(0, 0), "\x1b[1;1H");
assert_eq!(ansi::clear_screen(ansi::ClearMode::Complete), "\x1b[2J");
}
}