use std::fmt;
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct GridSize {
pub width: u16,
pub height: u16,
}
impl GridSize {
pub const ZERO: Self = Self {
width: 0,
height: 0,
};
#[must_use]
pub const fn new(width: u16, height: u16) -> Self {
Self { width, height }
}
#[must_use]
pub const fn area(self) -> usize {
self.width as usize * self.height as usize
}
#[must_use]
pub const fn contains(self, position: CellPosition) -> bool {
position.x < self.width && position.y < self.height
}
}
impl From<(u16, u16)> for GridSize {
fn from((width, height): (u16, u16)) -> Self {
Self::new(width, height)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct CellPosition {
pub x: u16,
pub y: u16,
}
impl CellPosition {
pub const ORIGIN: Self = Self { x: 0, y: 0 };
#[must_use]
pub const fn new(x: u16, y: u16) -> Self {
Self { x, y }
}
}
impl From<(u16, u16)> for CellPosition {
fn from((x, y): (u16, u16)) -> Self {
Self::new(x, y)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum TerminalColor {
#[default]
Default,
Indexed(u8),
Rgb(u8, u8, u8),
}
impl TerminalColor {
pub const BLACK: Self = Self::Indexed(0);
pub const RED: Self = Self::Indexed(1);
pub const GREEN: Self = Self::Indexed(2);
pub const YELLOW: Self = Self::Indexed(3);
pub const BLUE: Self = Self::Indexed(4);
pub const MAGENTA: Self = Self::Indexed(5);
pub const CYAN: Self = Self::Indexed(6);
pub const GRAY: Self = Self::Indexed(7);
pub const DARK_GRAY: Self = Self::Indexed(8);
pub const LIGHT_RED: Self = Self::Indexed(9);
pub const LIGHT_GREEN: Self = Self::Indexed(10);
pub const LIGHT_YELLOW: Self = Self::Indexed(11);
pub const LIGHT_BLUE: Self = Self::Indexed(12);
pub const LIGHT_MAGENTA: Self = Self::Indexed(13);
pub const LIGHT_CYAN: Self = Self::Indexed(14);
pub const WHITE: Self = Self::Indexed(15);
}
#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
pub struct StyleFlags(u16);
impl StyleFlags {
pub const NONE: Self = Self(0);
pub const BOLD: Self = Self(1 << 0);
pub const DIM: Self = Self(1 << 1);
pub const ITALIC: Self = Self(1 << 2);
pub const UNDERLINED: Self = Self(1 << 3);
pub const SLOW_BLINK: Self = Self(1 << 4);
pub const RAPID_BLINK: Self = Self(1 << 5);
pub const REVERSED: Self = Self(1 << 6);
pub const HIDDEN: Self = Self(1 << 7);
pub const CROSSED_OUT: Self = Self(1 << 8);
const ALL_NAMED: [(Self, &'static str); 9] = [
(Self::BOLD, "BOLD"),
(Self::DIM, "DIM"),
(Self::ITALIC, "ITALIC"),
(Self::UNDERLINED, "UNDERLINED"),
(Self::SLOW_BLINK, "SLOW_BLINK"),
(Self::RAPID_BLINK, "RAPID_BLINK"),
(Self::REVERSED, "REVERSED"),
(Self::HIDDEN, "HIDDEN"),
(Self::CROSSED_OUT, "CROSSED_OUT"),
];
#[must_use]
pub const fn from_bits(bits: u16) -> Self {
Self(bits & 0x1ff)
}
#[must_use]
pub const fn bits(self) -> u16 {
self.0
}
#[must_use]
pub const fn contains(self, other: Self) -> bool {
self.0 & other.0 == other.0
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.0 == 0
}
#[must_use]
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
#[must_use]
pub const fn difference(self, other: Self) -> Self {
Self(self.0 & !other.0)
}
pub const fn insert(&mut self, other: Self) {
self.0 |= other.0;
}
pub const fn remove(&mut self, other: Self) {
self.0 &= !other.0;
}
pub const fn set(&mut self, other: Self, enabled: bool) {
if enabled {
self.insert(other);
} else {
self.remove(other);
}
}
}
impl std::ops::BitOr for StyleFlags {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
self.union(rhs)
}
}
impl std::ops::BitOrAssign for StyleFlags {
fn bitor_assign(&mut self, rhs: Self) {
self.insert(rhs);
}
}
impl fmt::Debug for StyleFlags {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_empty() {
return f.write_str("StyleFlags(NONE)");
}
f.write_str("StyleFlags(")?;
let mut first = true;
for (flag, name) in Self::ALL_NAMED {
if self.contains(flag) {
if !first {
f.write_str(" | ")?;
}
first = false;
f.write_str(name)?;
}
}
f.write_str(")")
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct TerminalStyle {
pub foreground: TerminalColor,
pub background: TerminalColor,
pub underline: TerminalColor,
pub flags: StyleFlags,
}
impl TerminalStyle {
pub const DEFAULT: Self = Self {
foreground: TerminalColor::Default,
background: TerminalColor::Default,
underline: TerminalColor::Default,
flags: StyleFlags::NONE,
};
#[must_use]
pub const fn new() -> Self {
Self::DEFAULT
}
#[must_use]
pub const fn fg(mut self, color: TerminalColor) -> Self {
self.foreground = color;
self
}
#[must_use]
pub const fn bg(mut self, color: TerminalColor) -> Self {
self.background = color;
self
}
#[must_use]
pub const fn underline_color(mut self, color: TerminalColor) -> Self {
self.underline = color;
self
}
#[must_use]
pub const fn with(mut self, flags: StyleFlags) -> Self {
self.flags = self.flags.union(flags);
self
}
#[must_use]
pub const fn has(self, flags: StyleFlags) -> bool {
self.flags.contains(flags)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum CellOccupancy {
#[default]
Single,
Wide {
columns: u16,
},
Continuation,
}
impl CellOccupancy {
#[must_use]
pub const fn spanning(columns: u16) -> Self {
if columns <= 1 {
Self::Single
} else {
Self::Wide { columns }
}
}
#[must_use]
pub const fn columns(self) -> u16 {
match self {
Self::Single | Self::Continuation => 1,
Self::Wide { columns } => columns,
}
}
}
const INLINE_SYMBOL_BYTES: usize = 22;
const ASCII_SYMBOLS: [&str; 128] = [
"\u{0}", "\u{1}", "\u{2}", "\u{3}", "\u{4}", "\u{5}", "\u{6}", "\u{7}", "\u{8}", "\u{9}",
"\u{a}", "\u{b}", "\u{c}", "\u{d}", "\u{e}", "\u{f}", "\u{10}", "\u{11}", "\u{12}", "\u{13}",
"\u{14}", "\u{15}", "\u{16}", "\u{17}", "\u{18}", "\u{19}", "\u{1a}", "\u{1b}", "\u{1c}",
"\u{1d}", "\u{1e}", "\u{1f}", " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",",
"-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?",
"@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R",
"S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", "`", "a", "b", "c", "d", "e",
"f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x",
"y", "z", "{", "|", "}", "~", "\u{7f}",
];
#[derive(Clone, Eq, Hash, PartialEq)]
pub struct CellSymbol(SymbolRepr);
#[derive(Clone, Eq, Hash, PartialEq)]
enum SymbolRepr {
Ascii(u8),
Inline {
len: u8,
bytes: [u8; INLINE_SYMBOL_BYTES],
},
Heap(Box<str>),
}
impl CellSymbol {
pub const SPACE: Self = Self(SymbolRepr::Ascii(b' '));
#[must_use]
pub fn new(symbol: &str) -> Self {
let source = symbol.as_bytes();
if let [byte] = source
&& byte.is_ascii()
{
Self(SymbolRepr::Ascii(*byte))
} else if source.len() <= INLINE_SYMBOL_BYTES {
let mut bytes = [0; INLINE_SYMBOL_BYTES];
bytes[..source.len()].copy_from_slice(source);
Self(SymbolRepr::Inline {
len: source.len() as u8,
bytes,
})
} else {
Self(SymbolRepr::Heap(symbol.into()))
}
}
#[must_use]
pub fn as_str(&self) -> &str {
match &self.0 {
SymbolRepr::Ascii(byte) => ASCII_SYMBOLS[usize::from(*byte & 0x7f)],
SymbolRepr::Inline { len, bytes } => {
std::str::from_utf8(&bytes[..usize::from(*len)]).unwrap_or("")
}
SymbolRepr::Heap(text) => text,
}
}
}
impl Default for CellSymbol {
fn default() -> Self {
Self::SPACE
}
}
impl fmt::Debug for CellSymbol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(self.as_str(), f)
}
}
impl fmt::Display for CellSymbol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl From<&str> for CellSymbol {
fn from(symbol: &str) -> Self {
Self::new(symbol)
}
}
impl From<char> for CellSymbol {
fn from(symbol: char) -> Self {
let mut buffer = [0; 4];
Self::new(symbol.encode_utf8(&mut buffer))
}
}
impl std::ops::Deref for CellSymbol {
type Target = str;
fn deref(&self) -> &str {
self.as_str()
}
}
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
pub struct TerminalCell {
pub symbol: CellSymbol,
pub style: TerminalStyle,
occupancy: CellOccupancy,
}
impl TerminalCell {
pub const EMPTY: Self = Self {
symbol: CellSymbol::SPACE,
style: TerminalStyle::DEFAULT,
occupancy: CellOccupancy::Single,
};
#[must_use]
pub fn new(symbol: &str) -> Self {
Self {
symbol: CellSymbol::new(symbol),
style: TerminalStyle::DEFAULT,
occupancy: CellOccupancy::Single,
}
}
#[must_use]
pub fn wide(symbol: &str, columns: u16) -> Self {
Self {
symbol: CellSymbol::new(symbol),
style: TerminalStyle::DEFAULT,
occupancy: CellOccupancy::spanning(columns),
}
}
#[must_use]
pub const fn continuation_of(anchor: &Self) -> Self {
Self {
symbol: CellSymbol::SPACE,
style: anchor.style,
occupancy: CellOccupancy::Continuation,
}
}
#[must_use]
pub const fn with_style(mut self, style: TerminalStyle) -> Self {
self.style = style;
self
}
#[must_use]
pub const fn occupancy(&self) -> CellOccupancy {
self.occupancy
}
#[must_use]
pub fn symbol(&self) -> &str {
self.symbol.as_str()
}
#[must_use]
pub const fn is_continuation(&self) -> bool {
matches!(self.occupancy, CellOccupancy::Continuation)
}
#[must_use]
pub const fn columns(&self) -> u16 {
self.occupancy.columns()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TerminalSnapshot {
pub(crate) size: GridSize,
pub(crate) cells: Vec<TerminalCell>,
pub(crate) cursor_position: CellPosition,
pub(crate) cursor_visible: bool,
pub(crate) revision: u64,
}
impl TerminalSnapshot {
#[must_use]
pub fn empty(size: GridSize) -> Self {
Self {
size,
cells: vec![TerminalCell::EMPTY; size.area()],
cursor_position: CellPosition::ORIGIN,
cursor_visible: false,
revision: 0,
}
}
#[must_use]
pub const fn size(&self) -> GridSize {
self.size
}
#[must_use]
pub fn cells(&self) -> &[TerminalCell] {
&self.cells
}
#[must_use]
pub fn row(&self, row: u16) -> &[TerminalCell] {
if row >= self.size.height {
return &[];
}
let width = usize::from(self.size.width);
let start = usize::from(row) * width;
&self.cells[start..start + width]
}
#[must_use]
pub fn cell(&self, x: u16, y: u16) -> Option<&TerminalCell> {
if !self.size.contains(CellPosition::new(x, y)) {
return None;
}
self.cells
.get(usize::from(y) * usize::from(self.size.width) + usize::from(x))
}
#[must_use]
pub const fn cursor_position(&self) -> CellPosition {
self.cursor_position
}
#[must_use]
pub const fn cursor_visible(&self) -> bool {
self.cursor_visible
}
#[must_use]
pub const fn revision(&self) -> u64 {
self.revision
}
}
impl From<&str> for TerminalCell {
fn from(symbol: &str) -> Self {
Self::new(symbol)
}
}
impl From<char> for TerminalCell {
fn from(symbol: char) -> Self {
Self {
symbol: symbol.into(),
style: TerminalStyle::DEFAULT,
occupancy: CellOccupancy::Single,
}
}
}
impl std::ops::Index<(u16, u16)> for TerminalSnapshot {
type Output = TerminalCell;
fn index(&self, (x, y): (u16, u16)) -> &TerminalCell {
self.cell(x, y)
.unwrap_or_else(|| panic!("cell ({x}, {y}) is outside {:?}", self.size))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn short_symbols_are_stored_inline_and_long_ones_on_the_heap() {
assert!(matches!(CellSymbol::new("A").0, SymbolRepr::Ascii(b'A')));
assert_eq!(CellSymbol::new("A").as_str(), "A");
assert_eq!(CellSymbol::new("\u{7f}").as_str(), "\u{7f}");
assert_eq!(CellSymbol::new("\"").as_str(), "\"");
assert_eq!(CellSymbol::new("\\").as_str(), "\\");
for byte in 0..128_u8 {
assert_eq!(ASCII_SYMBOLS[usize::from(byte)].as_bytes(), [byte]);
}
assert!(matches!(
CellSymbol::new("e\u{301}").0,
SymbolRepr::Inline { .. }
));
assert!(matches!(CellSymbol::new("👍🏽").0, SymbolRepr::Inline { .. }));
let long = "👨\u{200d}👩\u{200d}👧\u{200d}👦";
let symbol = CellSymbol::new(long);
assert!(matches!(symbol.0, SymbolRepr::Heap(_)));
assert_eq!(&*symbol, long);
assert_eq!(symbol.as_str(), long);
assert_eq!(CellSymbol::from('界').as_str(), "界");
assert_eq!(CellSymbol::default(), CellSymbol::SPACE);
}
#[test]
fn cell_types_stay_compact() {
assert_eq!(std::mem::size_of::<CellSymbol>(), 24);
assert_eq!(std::mem::size_of::<TerminalStyle>(), 14);
assert!(std::mem::size_of::<TerminalCell>() <= 48);
}
#[test]
fn style_flags_are_a_bit_set() {
let mut flags = StyleFlags::BOLD | StyleFlags::ITALIC;
assert!(flags.contains(StyleFlags::BOLD));
assert!(!flags.contains(StyleFlags::DIM));
flags.remove(StyleFlags::BOLD);
assert_eq!(flags, StyleFlags::ITALIC);
flags.set(StyleFlags::HIDDEN, true);
assert_eq!(format!("{flags:?}"), "StyleFlags(ITALIC | HIDDEN)");
assert_eq!(StyleFlags::from_bits(0xffff).bits(), 0x1ff);
}
#[test]
fn occupancy_spanning_normalizes_narrow_widths() {
assert_eq!(CellOccupancy::spanning(0), CellOccupancy::Single);
assert_eq!(CellOccupancy::spanning(1), CellOccupancy::Single);
assert_eq!(CellOccupancy::spanning(2).columns(), 2);
assert_eq!(CellOccupancy::Continuation.columns(), 1);
let anchor =
TerminalCell::wide("界", 2).with_style(TerminalStyle::new().bg(TerminalColor::RED));
let continuation = TerminalCell::continuation_of(&anchor);
assert!(continuation.is_continuation());
assert_eq!(continuation.style, anchor.style);
assert_eq!(continuation.symbol(), " ");
}
#[test]
fn snapshot_indexing_follows_row_major_order() {
let mut snapshot = TerminalSnapshot::empty(GridSize::new(3, 2));
snapshot.cells[4] = TerminalCell::new("X");
assert_eq!(snapshot[(1, 1)].symbol(), "X");
assert_eq!(snapshot.row(1)[1].symbol(), "X");
assert!(snapshot.cell(3, 0).is_none());
assert!(snapshot.row(2).is_empty());
}
}