use std::fmt;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Color {
#[default]
Default,
Indexed(u8),
Rgb(u8, u8, u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Style {
pub fg: Color,
pub bg: Color,
pub bold: bool,
pub dim: bool,
pub italic: bool,
pub underline: bool,
pub reverse: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cell {
contents: String,
style: Style,
wide: bool,
wide_continuation: bool,
}
impl Cell {
pub(crate) fn new(contents: String, style: Style, wide: bool, wide_continuation: bool) -> Self {
Self {
contents,
style,
wide,
wide_continuation,
}
}
#[must_use]
pub fn contents(&self) -> &str {
&self.contents
}
#[must_use]
pub fn style(&self) -> &Style {
&self.style
}
#[must_use]
pub fn is_wide(&self) -> bool {
self.wide
}
#[must_use]
pub fn is_wide_continuation(&self) -> bool {
self.wide_continuation
}
}
#[derive(Clone)]
pub struct Screen {
cols: u16,
rows: u16,
cursor_row: u16,
cursor_col: u16,
cursor_visible: bool,
cells: Arc<[Cell]>,
}
impl Screen {
pub(crate) fn from_parts(
cols: u16,
rows: u16,
cursor_row: u16,
cursor_col: u16,
cursor_visible: bool,
cells: Vec<Cell>,
) -> Self {
debug_assert_eq!(cells.len(), usize::from(cols) * usize::from(rows));
Self {
cols,
rows,
cursor_row,
cursor_col,
cursor_visible,
cells: cells.into(),
}
}
#[must_use]
pub fn cols(&self) -> u16 {
self.cols
}
#[must_use]
pub fn rows(&self) -> u16 {
self.rows
}
#[must_use]
pub fn size(&self) -> (u16, u16) {
(self.cols, self.rows)
}
#[must_use]
pub fn cursor(&self) -> (u16, u16, bool) {
(self.cursor_row, self.cursor_col, self.cursor_visible)
}
#[must_use]
pub fn cell(&self, row: u16, col: u16) -> Option<&Cell> {
if row >= self.rows || col >= self.cols {
return None;
}
self.cells
.get(usize::from(row) * usize::from(self.cols) + usize::from(col))
}
#[must_use]
pub fn row_text(&self, row: u16) -> String {
let mut out = String::with_capacity(usize::from(self.cols));
for col in 0..self.cols {
let Some(cell) = self.cell(row, col) else {
return out;
};
if cell.is_wide_continuation() {
continue;
}
if cell.contents().is_empty() {
out.push(' ');
} else {
out.push_str(cell.contents());
}
}
out
}
#[must_use]
pub fn text(&self) -> String {
let mut out = String::new();
for row in 0..self.rows {
if row > 0 {
out.push('\n');
}
let line = self.row_text(row);
out.push_str(line.trim_end());
}
out
}
#[must_use]
pub fn contains(&self, needle: &str) -> bool {
self.text().contains(needle)
}
#[must_use]
pub fn find(&self, needle: &str) -> Option<(u16, u16)> {
if needle.is_empty() {
return Some((0, 0));
}
for row in 0..self.rows {
let text = self.row_text(row);
let Some(byte_off) = text.find(needle) else {
continue;
};
let mut acc = 0usize;
for col in 0..self.cols {
let cell = self.cell(row, col)?;
let len = if cell.is_wide_continuation() {
0
} else if cell.contents().is_empty() {
1 } else {
cell.contents().len()
};
if len > 0 && byte_off < acc + len {
return Some((row, col));
}
acc += len;
}
}
None
}
}
impl fmt::Debug for Screen {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Screen({self})")
}
}
impl fmt::Display for Screen {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "size: {}x{} cursor: ", self.cols, self.rows)?;
if self.cursor_visible {
write!(f, "{},{}", self.cursor_row, self.cursor_col)?;
} else {
write!(f, "hidden")?;
}
write!(f, "\n{}", self.text())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn screen(cols: u16, rows: u16, lines: &[&str]) -> Screen {
use unicode_width::UnicodeWidthChar;
let mut cells: Vec<Cell> = Vec::new();
for r in 0..usize::from(rows) {
let mut row_cells: Vec<Cell> = Vec::new();
if let Some(line) = lines.get(r) {
for ch in line.chars() {
let wide = ch.width().unwrap_or(1) == 2;
let style = Style {
bold: ch == '*',
..Style::default()
};
row_cells.push(Cell::new(ch.to_string(), style, wide, false));
if wide {
row_cells.push(Cell::new(String::new(), Style::default(), false, true));
}
}
}
assert!(row_cells.len() <= usize::from(cols), "test line too long");
while row_cells.len() < usize::from(cols) {
row_cells.push(Cell::new(String::new(), Style::default(), false, false));
}
cells.extend(row_cells);
}
Screen::from_parts(cols, rows, 1, 2, true, cells)
}
#[test]
fn row_text_pads_blanks_and_skips_continuations() {
let s = screen(10, 2, &["ab", "汉x"]);
assert_eq!(s.row_text(0), "ab ");
assert_eq!(s.row_text(1), "汉x ");
assert_eq!(s.row_text(9), "");
}
#[test]
fn text_strips_trailing_whitespace_per_line() {
let s = screen(10, 3, &["ab", "", "c"]);
assert_eq!(s.text(), "ab\n\nc");
}
#[test]
fn contains_matches_across_rows() {
let s = screen(10, 2, &["hello", "world"]);
assert!(s.contains("hello"));
assert!(s.contains("hello\nworld"));
assert!(!s.contains("hello world"));
}
#[test]
fn find_reports_wide_aware_columns() {
let s = screen(10, 2, &["abc", "汉字x"]);
assert_eq!(s.find("bc"), Some((0, 1)));
assert_eq!(s.find("x"), Some((1, 4)));
assert_eq!(s.find("字"), Some((1, 2)));
assert_eq!(s.find("missing"), None);
}
#[test]
fn cell_and_cursor_accessors() {
let s = screen(10, 2, &["a*"]);
assert_eq!(s.cell(0, 0).unwrap().contents(), "a");
assert!(s.cell(0, 1).unwrap().style().bold);
assert!(s.cell(2, 0).is_none());
assert!(s.cell(0, 10).is_none());
assert_eq!(s.cursor(), (1, 2, true));
assert_eq!(s.size(), (10, 2));
assert_eq!((s.cols(), s.rows()), (10, 2));
}
#[test]
fn display_format_matches_spec() {
let s = screen(10, 2, &["hi"]);
assert_eq!(format!("{s}"), "size: 10x2 cursor: 1,2\nhi\n");
}
}