use super::grid::{to_crossterm_color, CellUpdate, Style};
use crossterm::style::{
Attribute, Color, ResetColor, SetAttribute, SetBackgroundColor, SetForegroundColor,
};
use crossterm::{cursor, QueueableCommand};
use std::io::Write;
pub fn flush_diff<'a, W: Write>(
w: &mut W,
updates: impl Iterator<Item = CellUpdate<'a>>,
) -> std::io::Result<()> {
let mut current = Style::default();
let mut cursor_x: u16 = u16::MAX;
let mut cursor_y: u16 = u16::MAX;
for update in updates {
if update.y != cursor_y || update.x != cursor_x {
w.queue(cursor::MoveTo(update.x, update.y))?;
}
if update.cell.style != current {
emit_style_diff(w, ¤t, &update.cell.style)?;
current = update.cell.style;
}
let mut buf = [0u8; 4];
let s = update.cell.symbol.encode_utf8(&mut buf);
w.write_all(s.as_bytes())?;
cursor_x = update.x + 1;
cursor_y = update.y;
}
if cursor_x != u16::MAX {
w.queue(SetAttribute(Attribute::Reset))?;
w.queue(ResetColor)?;
}
Ok(())
}
fn emit_style_diff<W: Write>(w: &mut W, from: &Style, to: &Style) -> std::io::Result<()> {
let need_unbold = from.bold && !to.bold;
let need_undim = from.dim && !to.dim;
let need_unitalic = from.italic && !to.italic;
let need_uncrossed = from.crossedout && !to.crossedout;
let need_ununderline = from.underline && !to.underline;
let unsets = need_unbold as u8
+ need_undim as u8
+ need_unitalic as u8
+ need_uncrossed as u8
+ need_ununderline as u8;
let intensity_conflict = (need_unbold && to.dim) || (need_undim && to.bold);
if unsets >= 2 || intensity_conflict {
w.queue(SetAttribute(Attribute::Reset))?;
w.queue(ResetColor)?;
if let Some(fg) = to.fg {
w.queue(SetForegroundColor(to_crossterm_color(fg)))?;
}
if let Some(bg) = to.bg {
w.queue(SetBackgroundColor(to_crossterm_color(bg)))?;
}
if to.bold {
w.queue(SetAttribute(Attribute::Bold))?;
}
if to.dim {
w.queue(SetAttribute(Attribute::Dim))?;
}
if to.italic {
w.queue(SetAttribute(Attribute::Italic))?;
}
if to.crossedout {
w.queue(SetAttribute(Attribute::CrossedOut))?;
}
if to.underline {
w.queue(SetAttribute(Attribute::Underlined))?;
}
return Ok(());
}
if need_unbold || need_undim {
w.queue(SetAttribute(Attribute::NormalIntensity))?;
if need_unbold && to.dim {
w.queue(SetAttribute(Attribute::Dim))?;
}
if need_undim && to.bold {
w.queue(SetAttribute(Attribute::Bold))?;
}
}
if need_unitalic {
w.queue(SetAttribute(Attribute::NoItalic))?;
}
if need_uncrossed {
w.queue(SetAttribute(Attribute::NotCrossedOut))?;
}
if need_ununderline {
w.queue(SetAttribute(Attribute::NoUnderline))?;
}
if !from.bold && to.bold {
w.queue(SetAttribute(Attribute::Bold))?;
}
if !from.dim && to.dim {
w.queue(SetAttribute(Attribute::Dim))?;
}
if !from.italic && to.italic {
w.queue(SetAttribute(Attribute::Italic))?;
}
if !from.crossedout && to.crossedout {
w.queue(SetAttribute(Attribute::CrossedOut))?;
}
if !from.underline && to.underline {
w.queue(SetAttribute(Attribute::Underlined))?;
}
if from.fg != to.fg {
if let Some(fg) = to.fg {
w.queue(SetForegroundColor(to_crossterm_color(fg)))?;
} else {
w.queue(SetForegroundColor(Color::Reset))?;
}
}
if from.bg != to.bg {
if let Some(bg) = to.bg {
w.queue(SetBackgroundColor(to_crossterm_color(bg)))?;
} else {
w.queue(SetBackgroundColor(Color::Reset))?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::grid::Grid;
use smelt_style::style::Color;
#[test]
fn flush_empty_diff_produces_no_output() {
let a = Grid::new(5, 3);
let b = Grid::new(5, 3);
let mut out = Vec::new();
flush_diff(&mut out, a.diff(&b)).unwrap();
assert!(out.is_empty());
}
fn flush_to_string(curr: &Grid, prev: &Grid) -> String {
let mut out = Vec::new();
flush_diff(&mut out, curr.diff(prev)).unwrap();
String::from_utf8(out).unwrap()
}
#[test]
fn flush_single_cell_moves_cursor_and_writes_char() {
let prev = Grid::new(5, 3);
let mut curr = Grid::new(5, 3);
curr.set(2, 1, 'X', Style::default());
let s = flush_to_string(&curr, &prev);
assert!(
s.starts_with("\x1b[2;3H"),
"expected MoveTo before char, got {s:?}"
);
assert!(s.contains("\x1b[2;3HX"));
assert!(s.ends_with("\x1b[0m"));
}
#[test]
fn flush_styled_cell_emits_an_sgr_before_the_char() {
let prev = Grid::new(5, 1);
let mut curr = Grid::new(5, 1);
curr.set(0, 0, 'A', Style::new().fg(Color::Red));
let s = flush_to_string(&curr, &prev);
let a_pos = s.find('A').expect("A in output");
let before_a = &s[..a_pos];
assert!(
before_a.contains("\x1b[") && before_a.matches("\x1b[").count() >= 2,
"expected at least one SGR escape before A (after the MoveTo), got {before_a:?}"
);
}
#[test]
fn flush_emits_different_bytes_for_different_fg_colors() {
let render = |c: Color| {
let prev = Grid::new(3, 1);
let mut curr = Grid::new(3, 1);
curr.set(0, 0, 'A', Style::new().fg(c));
flush_to_string(&curr, &prev)
};
assert_ne!(render(Color::Red), render(Color::Blue));
assert_ne!(render(Color::Green), render(Color::Yellow));
}
#[test]
fn flush_resets_style_at_end() {
let prev = Grid::new(3, 1);
let mut curr = Grid::new(3, 1);
curr.set(0, 0, 'A', Style::new().bold());
let mut out = Vec::new();
flush_diff(&mut out, curr.diff(&prev)).unwrap();
let s = String::from_utf8(out).unwrap();
assert!(s.ends_with("\x1b[0m"));
}
#[test]
fn flush_emits_ansi_palette_color_code() {
let prev = Grid::new(3, 1);
let mut curr = Grid::new(3, 1);
curr.set(0, 0, 'A', Style::new().fg(Color::AnsiValue(208)));
let s = flush_to_string(&curr, &prev);
assert!(s.contains("\x1b[38;5;208m"), "got {s:?}");
}
#[test]
fn flush_emits_rgb_color_code() {
let prev = Grid::new(3, 1);
let mut curr = Grid::new(3, 1);
curr.set(
0,
0,
'A',
Style::new().fg(Color::Rgb {
r: 10,
g: 20,
b: 30,
}),
);
let s = flush_to_string(&curr, &prev);
assert!(s.contains("\x1b[38;2;10;20;30m"), "got {s:?}");
}
#[test]
fn flush_emits_distinct_sgr_for_fg_vs_bg() {
let render = |s: Style| {
let prev = Grid::new(3, 1);
let mut curr = Grid::new(3, 1);
curr.set(0, 0, 'A', s);
flush_to_string(&curr, &prev)
};
let fg_only = render(Style::new().fg(Color::Blue));
let bg_only = render(Style::new().bg(Color::Blue));
assert_ne!(fg_only, bg_only);
}
#[test]
fn flush_emits_bold_attribute() {
let prev = Grid::new(3, 1);
let mut curr = Grid::new(3, 1);
curr.set(0, 0, 'A', Style::new().bold());
let s = flush_to_string(&curr, &prev);
assert!(s.contains("\x1b[1m"), "got {s:?}");
}
#[test]
fn flush_emits_dim_attribute() {
let prev = Grid::new(3, 1);
let mut curr = Grid::new(3, 1);
curr.set(0, 0, 'A', Style::new().dim());
let s = flush_to_string(&curr, &prev);
assert!(s.contains("\x1b[2m"), "got {s:?}");
}
#[test]
fn flush_emits_italic_attribute() {
let prev = Grid::new(3, 1);
let mut curr = Grid::new(3, 1);
curr.set(0, 0, 'A', Style::new().italic());
let s = flush_to_string(&curr, &prev);
assert!(s.contains("\x1b[3m"), "got {s:?}");
}
#[test]
fn flush_emits_underline_attribute() {
let prev = Grid::new(3, 1);
let mut curr = Grid::new(3, 1);
curr.set(0, 0, 'A', Style::new().underline());
let s = flush_to_string(&curr, &prev);
assert!(s.contains("\x1b[4m"), "got {s:?}");
}
#[test]
fn flush_emits_crossedout_attribute() {
let prev = Grid::new(3, 1);
let mut curr = Grid::new(3, 1);
curr.set(0, 0, 'A', Style::new().crossedout());
let s = flush_to_string(&curr, &prev);
assert!(s.contains("\x1b[9m"), "got {s:?}");
}
#[test]
fn flush_re_emits_sgr_when_style_changes_mid_run() {
let prev = Grid::new(3, 1);
let mut curr = Grid::new(3, 1);
curr.set(0, 0, 'A', Style::default());
curr.set(1, 0, 'B', Style::new().fg(Color::Red));
let s = flush_to_string(&curr, &prev);
let a_pos = s.find('A').expect("A in output");
let b_pos = s.find('B').expect("B in output");
let between = &s[a_pos + 1..b_pos];
assert!(
between.contains("\x1b["),
"expected an SGR escape between unstyled A and styled B, got {between:?}"
);
}
#[test]
fn flush_noncontiguous_cells_emit_moveto_between() {
let prev = Grid::new(10, 2);
let mut curr = Grid::new(10, 2);
curr.set(0, 0, 'A', Style::default());
curr.set(5, 1, 'B', Style::default());
let s = flush_to_string(&curr, &prev);
assert!(s.contains("\x1b[1;1HA"), "got {s:?}");
assert!(s.contains("\x1b[2;6HB"), "got {s:?}");
}
#[test]
fn flush_contiguous_cells_share_one_moveto() {
let prev = Grid::new(10, 1);
let mut curr = Grid::new(10, 1);
curr.set(0, 0, 'A', Style::default());
curr.set(1, 0, 'B', Style::default());
curr.set(2, 0, 'C', Style::default());
let s = flush_to_string(&curr, &prev);
assert!(s.contains("\x1b[1;1HABC"), "got {s:?}");
}
}