use std::{
fmt::{self, Display},
io,
};
use bitflags::bitflags;
use crossterm::style::{Color, Stylize};
use smallvec::SmallVec;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthChar;
const ZWJ: char = '\u{200D}';
#[derive(Default, Clone, Debug, Hash)]
pub struct Frame(Vec<Vec<Cell>>);
type Rows = usize;
type Cols = usize;
impl Frame {
pub fn take(&mut self) -> Self {
let (rows, cols) = self.dims().unwrap_or((0, 0));
let new_cells = Self::with_capacity(cols, rows).0;
Frame(std::mem::replace(&mut self.0, new_cells))
}
pub fn height(&self) -> usize {
self.0.len()
}
pub fn width(&self) -> usize {
self.0.first().map(|row| row.len()).unwrap_or(0)
}
pub fn into_cells(self) -> Vec<Vec<Cell>> {
self.0
}
pub fn cells(&self) -> &[Vec<Cell>] {
&self.0
}
pub(crate) fn cells_mut(&mut self) -> &mut Vec<Vec<Cell>> {
&mut self.0
}
pub fn get_cell(&self, row: usize, col: usize) -> Option<&Cell> {
self.0.get(row).and_then(|r| r.get(col))
}
pub fn get_cell_mut(&mut self, row: usize, col: usize) -> Option<&mut Cell> {
self.0.get_mut(row).and_then(|r| r.get_mut(col))
}
pub fn with_capacity(cols: usize, rows: usize) -> Self {
Frame(vec![vec![Cell::default(); cols]; rows])
}
pub fn from_cells(cells: Vec<Vec<Cell>>) -> Self {
let len = cells.first().map(|row| row.len());
if let Some(len) = len {
assert!(
cells.iter().all(|row| row.len() == len),
"all rows in a Frame must have equal length",
)
}
Frame(cells)
}
pub fn from_terminal() -> Self {
let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));
let mut builder = FrameBuilder::new(cols as usize, rows as usize);
builder.feed_bytes(b"\x1b[?25l"); builder.feed_bytes(b"\x1b[2J"); builder.feed_bytes(b"\x1b[H"); let mut frame = builder.build();
frame.resize(cols as usize, rows as usize);
frame
}
pub fn from_command(mut command: std::process::Command) -> io::Result<Self> {
let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));
let output = command.env("COLUMNS", cols.to_string()).output()?;
let mut builder = FrameBuilder::new(cols as usize, rows as usize);
builder.feed_bytes(&output.stdout);
let mut frame = builder.build();
frame.resize(cols as usize, rows as usize);
Ok(frame)
}
pub fn dims(&self) -> Option<(Rows, Cols)> {
let rows = self.0.len();
if rows == 0 {
return None;
}
let cols = self.0[0].len();
Some((rows, cols))
}
pub fn resize(&mut self, w: usize, h: usize) {
for row in &mut self.0 {
row.resize(w, Cell::default());
}
self.0.resize(h, vec![Cell::default(); w]);
}
}
pub struct FrameBuilder {
frame: Frame,
row: usize,
rows: usize,
col: usize,
cols: usize,
last_pos: Option<(usize, usize)>,
pending_zwj: bool,
current_fg: Color,
current_bg: Color,
current_flags: CellFlags,
parser: vte::Parser,
}
impl FrameBuilder {
pub fn new(cols: usize, rows: usize) -> Self {
Self {
frame: Frame::from_cells(vec![vec![Cell::default(); cols]; rows]),
row: 0,
rows,
col: 0,
cols,
last_pos: None,
pending_zwj: false,
current_fg: Color::Reset,
current_bg: Color::Reset,
current_flags: CellFlags::empty(),
parser: vte::Parser::new(),
}
}
pub fn feed_bytes(&mut self, bytes: &[u8]) {
let mut parser = std::mem::take(&mut self.parser);
parser.advance(self, bytes);
self.parser = parser;
}
pub fn feed_str(&mut self, s: &str) {
self.feed_bytes(s.as_bytes());
}
pub fn build(self) -> Frame {
self.frame
}
}
impl vte::Perform for FrameBuilder {
fn print(&mut self, c: char) {
if (c == ZWJ || self.pending_zwj)
&& let Some((row, col)) = self.last_pos
&& let Some(last_cell) = self.frame.get_cell_mut(row, col)
{
last_cell.push_char(c);
self.pending_zwj = c == ZWJ;
return;
}
self.pending_zwj = false;
if self.col >= self.cols {
self.col = 0;
self.row += 1;
}
if self.row >= self.rows {
self
.frame
.cells_mut()
.push(vec![Cell::default(); self.cols]);
self.rows += 1;
}
let cell = Cell::new(c, self.current_fg, self.current_bg, self.current_flags);
let Some(frame_cell) = self.frame.get_cell_mut(self.row, self.col) else {
return;
};
*frame_cell = cell;
self.last_pos = Some((self.row, self.col));
let width = UnicodeWidthChar::width(c).unwrap_or(1);
if width > 1
&& let Some(next) = self.frame.get_cell_mut(self.row, self.col + 1)
{
next.flags |= CellFlags::WIDE_CONTINUATION;
}
self.col += width;
}
fn execute(&mut self, byte: u8) {
match byte {
b'\n' => {
self.row += 1;
self.col = 0;
if self.row >= self.rows {
self
.frame
.cells_mut()
.push(vec![Cell::default(); self.cols]);
self.rows += 1;
}
}
b'\r' => {
self.col = 0;
}
_ => {}
}
}
fn csi_dispatch(
&mut self,
params: &vte::Params,
_intermediates: &[u8],
_ignore: bool,
action: char,
) {
if action != 'm' {
return;
}
let params: Vec<u16> = params.iter().flat_map(|p| p.iter().copied()).collect();
let mut i = 0;
while i < params.len() {
let Some(param) = params.get(i) else {
continue;
};
match param {
0 => {
self.current_fg = Color::Reset;
self.current_bg = Color::Reset;
self.current_flags = CellFlags::empty();
}
1 => self.current_flags.insert(CellFlags::BOLD),
2 => self.current_flags.insert(CellFlags::DIM),
3 => self.current_flags.insert(CellFlags::ITALIC),
4 => self.current_flags.insert(CellFlags::UNDERLINE),
7 => self.current_flags.insert(CellFlags::INVERSE),
8 => self.current_flags.insert(CellFlags::HIDDEN),
9 => self.current_flags.insert(CellFlags::STRIKETHROUGH),
30..=37 => self.current_fg = Color::AnsiValue((params[i] - 30) as u8),
38 | 48 => {
let is_bg = *param == 48;
i += 1;
let Some(param2) = params.get(i) else {
continue;
};
match param2 {
5 => {
i += 1;
let Some(param3) = params.get(i) else {
continue;
};
let color = Color::AnsiValue(*param3 as u8);
if is_bg {
self.current_bg = color;
} else {
self.current_fg = color;
}
}
2 => {
i += 1;
let Some(param3) = params.get(i) else {
continue;
};
i += 1;
let Some(param4) = params.get(i) else {
continue;
};
i += 1;
let Some(param5) = params.get(i) else {
continue;
};
let color = Color::Rgb {
r: *param3 as u8,
g: *param4 as u8,
b: *param5 as u8,
};
if is_bg {
self.current_bg = color;
} else {
self.current_fg = color;
}
}
_ => {}
}
}
39 => self.current_fg = Color::Reset,
40..=47 => self.current_bg = Color::AnsiValue((params[i] - 40) as u8),
49 => self.current_bg = Color::Reset,
90..=97 => self.current_fg = Color::AnsiValue((params[i] - 90 + 8) as u8),
100..=107 => self.current_bg = Color::AnsiValue((params[i] - 100 + 8) as u8),
_ => { }
}
i += 1;
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Grapheme(SmallVec<[char; 4]>);
impl Grapheme {
pub fn chars(&self) -> &[char] {
&self.0
}
pub fn width(&self) -> usize {
self.0.iter().map(|c| c.width().unwrap_or(0)).sum()
}
pub fn is_lf(&self) -> bool {
self.is_char('\n')
}
pub fn is_char(&self, c: char) -> bool {
self.0.len() == 1 && self.0[0] == c
}
pub fn as_char(&self) -> Option<char> {
if self.0.len() == 1 {
Some(self.0[0])
} else {
None
}
}
pub fn push_char(&mut self, c: char) {
self.0.push(c);
}
pub fn is_whitespace(&self) -> bool {
self.0.iter().all(|c| c.is_whitespace())
}
}
impl From<char> for Grapheme {
fn from(value: char) -> Self {
let mut new = SmallVec::<[char; 4]>::new();
new.push(value);
Self(new)
}
}
impl From<&str> for Grapheme {
fn from(value: &str) -> Self {
assert_eq!(value.graphemes(true).count(), 1);
let mut new = SmallVec::<[char; 4]>::new();
for char in value.chars() {
new.push(char);
}
Self(new)
}
}
impl From<String> for Grapheme {
fn from(value: String) -> Self {
Into::<Self>::into(value.as_str())
}
}
impl From<&String> for Grapheme {
fn from(value: &String) -> Self {
Into::<Self>::into(value.as_str())
}
}
impl Display for Grapheme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for ch in &self.0 {
write!(f, "{ch}")?;
}
Ok(())
}
}
pub fn to_graphemes(s: impl ToString) -> Vec<Grapheme> {
let s = s.to_string();
s.graphemes(true).map(Grapheme::from).collect()
}
bitflags! {
#[derive(Default,Clone,Copy,Debug,PartialEq,Eq,Hash)]
pub struct CellFlags: u32 {
const BOLD = 0b000000001;
const ITALIC = 0b000000010;
const UNDERLINE = 0b000000100;
const INVERSE = 0b000001000;
const HIDDEN = 0b000010000;
const STRIKETHROUGH = 0b000100000;
const DIM = 0b001000000;
const BLINK = 0b010000000;
const WIDE_CONTINUATION = 0b100000000;
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Cell {
ch: Grapheme,
fg: Color,
bg: Color,
flags: CellFlags,
}
impl Default for Cell {
fn default() -> Self {
Self::new(' ', Color::Reset, Color::Reset, CellFlags::empty())
}
}
impl Cell {
pub fn new(ch: impl Into<Grapheme>, fg: Color, bg: Color, flags: CellFlags) -> Self {
Self {
ch: ch.into(),
fg,
bg,
flags,
}
}
pub fn ch(&self) -> &Grapheme {
&self.ch
}
pub fn fg(&self) -> Color {
self.fg
}
pub fn bg(&self) -> Color {
self.bg
}
pub fn flags(&self) -> CellFlags {
self.flags
}
pub fn is_empty(&self) -> bool {
self.ch.is_whitespace() && self.bg == Color::Reset
}
pub fn with_bg(mut self, bg: Color) -> Self {
self.bg = bg;
self
}
pub fn with_fg(mut self, fg: Color) -> Self {
self.fg = fg;
self
}
pub fn with_flags(mut self, flags: CellFlags) -> Self {
self.flags = flags;
self
}
pub fn with_char(mut self, ch: char) -> Self {
self.ch = ch.into();
self
}
pub fn set_bg(&mut self, bg: Color) {
self.bg = bg;
}
pub fn set_fg(&mut self, fg: Color) {
self.fg = fg;
}
pub fn set_flags(&mut self, flags: CellFlags) {
self.flags = flags;
}
pub fn set_char(&mut self, ch: char) {
self.ch = ch.into();
}
pub fn push_char(&mut self, ch: char) {
self.ch.push_char(ch);
}
}
impl Display for Cell {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut styled = crossterm::style::style(&self.ch).with(self.fg).on(self.bg);
if self.flags.contains(CellFlags::BOLD) {
styled = styled.bold();
}
if self.flags.contains(CellFlags::ITALIC) {
styled = styled.italic();
}
if self.flags.contains(CellFlags::UNDERLINE) {
styled = styled.underlined();
}
if self.flags.contains(CellFlags::INVERSE) {
styled = styled.reverse();
}
if self.flags.contains(CellFlags::HIDDEN) {
styled = styled.hidden();
}
if self.flags.contains(CellFlags::STRIKETHROUGH) {
styled = styled.crossed_out();
}
if self.flags.contains(CellFlags::DIM) {
styled = styled.dim();
}
if self.flags.contains(CellFlags::BLINK) {
styled = styled.slow_blink();
}
write!(f, "{styled}")
}
}
impl From<char> for Cell {
fn from(value: char) -> Self {
Self::new(value, Color::Reset, Color::Reset, CellFlags::empty())
}
}