use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Color {
Named(NamedColor),
Indexed(u8),
Rgb(u8, u8, u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NamedColor {
Black = 0,
Red = 1,
Green = 2,
Yellow = 3,
Blue = 4,
Magenta = 5,
Cyan = 6,
White = 7,
BrightBlack = 8,
BrightRed = 9,
BrightGreen = 10,
BrightYellow = 11,
BrightBlue = 12,
BrightMagenta = 13,
BrightCyan = 14,
BrightWhite = 15,
}
impl Default for Color {
fn default() -> Self {
Color::Named(NamedColor::White)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CellAttributes {
pub bold: bool,
pub dim: bool,
pub italic: bool,
pub underline: bool,
pub blink: bool,
pub reverse: bool,
pub hidden: bool,
pub strikethrough: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TerminalCell {
pub c: char,
pub fg: Color,
pub bg: Color,
pub attrs: CellAttributes,
}
impl Default for TerminalCell {
fn default() -> Self {
Self {
c: ' ',
fg: Color::Named(NamedColor::White),
bg: Color::Named(NamedColor::Black),
attrs: CellAttributes::default(),
}
}
}
impl TerminalCell {
pub fn reset(&mut self) {
self.c = ' ';
self.fg = Color::Named(NamedColor::White);
self.bg = Color::Named(NamedColor::Black);
self.attrs = CellAttributes::default();
}
}
#[derive(Debug, Clone, Copy)]
pub struct Cursor {
pub x: usize,
pub y: usize,
pub visible: bool,
pub shape: CursorShape,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CursorShape {
Block,
Underline,
Bar,
}
impl Default for Cursor {
fn default() -> Self {
Self {
x: 0,
y: 0,
visible: true,
shape: CursorShape::Block,
}
}
}
pub struct TerminalGrid {
rows: Vec<Vec<TerminalCell>>,
scrollback: Vec<Vec<TerminalCell>>,
max_scrollback: usize,
cols: usize,
rows_count: usize,
pub cursor: Cursor,
pub current_attrs: CellAttributes,
pub current_fg: Color,
pub current_bg: Color,
scroll_region_top: usize,
scroll_region_bottom: usize,
saved_cursor: Option<Cursor>,
alt_screen: Option<Vec<Vec<TerminalCell>>>,
tab_stops: Vec<bool>,
}
impl TerminalGrid {
pub fn new(cols: usize, rows: usize, max_scrollback: usize) -> Self {
let mut tab_stops = vec![false; cols];
for i in (0..cols).step_by(8) {
tab_stops[i] = true;
}
Self {
rows: vec![vec![TerminalCell::default(); cols]; rows],
scrollback: Vec::new(),
max_scrollback,
cols,
rows_count: rows,
cursor: Cursor::default(),
current_attrs: CellAttributes::default(),
current_fg: Color::Named(NamedColor::White),
current_bg: Color::Named(NamedColor::Black),
scroll_region_top: 0,
scroll_region_bottom: rows.saturating_sub(1),
saved_cursor: None,
alt_screen: None,
tab_stops,
}
}
pub fn cols(&self) -> usize {
self.cols
}
pub fn rows(&self) -> usize {
self.rows_count
}
#[allow(dead_code)]
pub fn scrollback_len(&self) -> usize {
self.scrollback.len()
}
pub fn resize(&mut self, new_cols: usize, new_rows: usize) {
for row in &mut self.rows {
row.resize(new_cols, TerminalCell::default());
}
match new_rows.cmp(&self.rows_count) {
std::cmp::Ordering::Greater => {
self.rows
.resize(new_rows, vec![TerminalCell::default(); new_cols]);
}
std::cmp::Ordering::Less => {
self.rows.truncate(new_rows);
}
std::cmp::Ordering::Equal => {}
}
self.tab_stops.resize(new_cols, false);
for i in (0..new_cols).step_by(8) {
self.tab_stops[i] = true;
}
self.cols = new_cols;
self.rows_count = new_rows;
self.scroll_region_bottom = new_rows.saturating_sub(1);
self.cursor.x = self.cursor.x.min(new_cols.saturating_sub(1));
self.cursor.y = self.cursor.y.min(new_rows.saturating_sub(1));
}
pub fn get_cell(&self, x: usize, y: usize) -> Option<&TerminalCell> {
self.rows.get(y)?.get(x)
}
pub fn get_cell_mut(&mut self, x: usize, y: usize) -> Option<&mut TerminalCell> {
self.rows.get_mut(y)?.get_mut(x)
}
#[allow(dead_code)]
pub fn get_scrollback_line(&self, idx: usize) -> Option<&Vec<TerminalCell>> {
self.scrollback.get(idx)
}
pub fn put_char(&mut self, c: char) {
if self.cursor.y >= self.rows_count {
return;
}
match c {
'\n' => self.linefeed(),
'\r' => self.carriage_return(),
'\t' => self.tab(),
'\x08' => self.backspace(),
c if c.is_control() => {
}
c => {
if self.cursor.x < self.cols {
let fg = self.current_fg;
let bg = self.current_bg;
let attrs = self.current_attrs;
if let Some(cell) = self.get_cell_mut(self.cursor.x, self.cursor.y) {
cell.c = c;
cell.fg = fg;
cell.bg = bg;
cell.attrs = attrs;
}
self.cursor.x += 1;
if self.cursor.x >= self.cols {
self.cursor.x = 0;
self.linefeed();
}
}
}
}
}
fn linefeed(&mut self) {
if self.cursor.y == 0 && self.cursor.x == 0 {
if let Some(row) = self.rows.first() {
let is_empty = row.iter().all(|cell| cell.c == ' ');
if is_empty {
return;
}
}
}
if self.cursor.y == self.scroll_region_bottom {
self.scroll_up(1);
} else if self.cursor.y < self.rows_count - 1 {
self.cursor.y += 1;
}
}
fn carriage_return(&mut self) {
self.cursor.x = 0;
}
fn tab(&mut self) {
for x in (self.cursor.x + 1)..self.cols {
if self.tab_stops[x] {
self.cursor.x = x;
return;
}
}
self.cursor.x = self.cols.saturating_sub(1);
}
fn backspace(&mut self) {
if self.cursor.x > 0 {
self.cursor.x -= 1;
}
}
pub fn scroll_up(&mut self, n: usize) {
for _ in 0..n {
if self.scroll_region_top < self.rows_count {
let line = self.rows.remove(self.scroll_region_top);
self.scrollback.push(line);
if self.scrollback.len() > self.max_scrollback {
self.scrollback.remove(0);
}
let insert_pos = self.scroll_region_bottom.min(self.rows_count - 1);
self.rows
.insert(insert_pos, vec![TerminalCell::default(); self.cols]);
}
}
}
pub fn scroll_down(&mut self, n: usize) {
for _ in 0..n {
if self.scroll_region_bottom < self.rows_count {
self.rows.remove(self.scroll_region_bottom);
self.rows.insert(
self.scroll_region_top,
vec![TerminalCell::default(); self.cols],
);
}
}
}
pub fn clear_screen(&mut self) {
for row in &mut self.rows {
for cell in row {
cell.reset();
}
}
}
pub fn clear_line(&mut self) {
if let Some(row) = self.rows.get_mut(self.cursor.y) {
for cell in row {
cell.reset();
}
}
}
pub fn erase_to_eol(&mut self) {
if let Some(row) = self.rows.get_mut(self.cursor.y) {
for x in self.cursor.x..self.cols {
if let Some(cell) = row.get_mut(x) {
cell.reset();
}
}
}
}
pub fn erase_to_eos(&mut self) {
self.erase_to_eol();
for y in (self.cursor.y + 1)..self.rows_count {
if let Some(row) = self.rows.get_mut(y) {
for cell in row {
cell.reset();
}
}
}
}
pub fn goto(&mut self, x: usize, y: usize) {
self.cursor.x = x.min(self.cols.saturating_sub(1));
self.cursor.y = y.min(self.rows_count.saturating_sub(1));
}
pub fn move_cursor(&mut self, dx: isize, dy: isize) {
let new_x = (self.cursor.x as isize + dx).max(0) as usize;
let new_y = (self.cursor.y as isize + dy).max(0) as usize;
self.goto(new_x, new_y);
}
pub fn save_cursor(&mut self) {
self.saved_cursor = Some(self.cursor);
}
pub fn restore_cursor(&mut self) {
if let Some(saved) = self.saved_cursor {
self.cursor = saved;
}
}
pub fn use_alt_screen(&mut self) {
if self.alt_screen.is_none() {
self.alt_screen = Some(self.rows.clone());
self.clear_screen();
self.cursor = Cursor::default();
}
}
pub fn use_main_screen(&mut self) {
if let Some(main_screen) = self.alt_screen.take() {
self.rows = main_screen;
}
}
pub fn set_scroll_region(&mut self, top: usize, bottom: usize) {
self.scroll_region_top = top.min(self.rows_count.saturating_sub(1));
self.scroll_region_bottom = bottom.min(self.rows_count.saturating_sub(1));
}
#[allow(dead_code)]
pub fn reset_scroll_region(&mut self) {
self.scroll_region_top = 0;
self.scroll_region_bottom = self.rows_count.saturating_sub(1);
}
}
impl fmt::Debug for TerminalGrid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TerminalGrid")
.field("cols", &self.cols)
.field("rows", &self.rows_count)
.field("scrollback_lines", &self.scrollback.len())
.field("cursor", &self.cursor)
.finish()
}
}