use std::fmt;
use super::{same_cursor, Cell, Screen, Style};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScreenDiff {
before_size: (u16, u16),
after_size: (u16, u16),
before_cursor: (u16, u16, bool),
after_cursor: (u16, u16, bool),
cells: Vec<(u16, u16, Cell, Cell)>,
unchanged_rows: u16,
style_changes: Vec<(u16, String, String)>,
rows: Vec<(u16, String, String, Vec<u16>)>,
}
impl Screen {
#[must_use]
pub fn diff(&self, other: &Screen) -> ScreenDiff {
let cols = self.cols().min(other.cols());
let rows = self.rows().min(other.rows());
let mut cells = Vec::new();
let mut rows_out = Vec::new();
let mut style_changes = Vec::new();
let mut unchanged_rows = 0;
for row in 0..rows {
let mut changed_cols = Vec::new();
for col in 0..cols {
let (Some(a), Some(b)) = (self.cell(row, col), other.cell(row, col)) else {
continue;
};
if !same_picture(a, b) {
changed_cols.push(col);
cells.push((row, col, a.clone(), b.clone()));
}
}
if changed_cols.is_empty() {
unchanged_rows += 1;
continue;
}
rows_out.push((
row,
self.row_text(row).trim_end().to_owned(),
other.row_text(row).trim_end().to_owned(),
changed_cols,
));
let before = row_styles(self, row);
let after = row_styles(other, row);
if before != after {
style_changes.push((row, before, after));
}
}
ScreenDiff {
before_size: self.size(),
after_size: other.size(),
before_cursor: self.cursor(),
after_cursor: other.cursor(),
cells,
unchanged_rows,
style_changes,
rows: rows_out,
}
}
}
fn same_picture(a: &Cell, b: &Cell) -> bool {
let blank = |cell: &Cell| matches!(cell.contents(), "" | " ");
a.style() == b.style()
&& a.is_wide() == b.is_wide()
&& a.is_wide_continuation() == b.is_wide_continuation()
&& (a.contents() == b.contents() || (blank(a) && blank(b)))
}
pub(super) fn row_styles(screen: &Screen, row: u16) -> String {
let mut spans: Vec<String> = Vec::new();
let mut run: Option<(u16, u16, Style)> = None;
let flush = |run: Option<(u16, u16, Style)>, spans: &mut Vec<String>| {
if let Some((start, end, style)) = run {
if !style.is_default() {
let range = if start == end {
format!("{start}")
} else {
format!("{start}-{end}")
};
spans.push(format!("{range} {}", style.tokens()));
}
}
};
for col in 0..screen.cols() {
let style = screen
.cell(row, col)
.map_or_else(Style::default, |cell| *cell.style());
match &mut run {
Some((_, end, current)) if *current == style => *end = col,
_ => {
flush(run.take(), &mut spans);
run = Some((col, col, style));
}
}
}
flush(run, &mut spans);
if spans.is_empty() {
"(none)".to_owned()
} else {
spans.join("; ")
}
}
impl ScreenDiff {
#[must_use]
pub fn is_empty(&self) -> bool {
self.cells.is_empty()
&& self.before_size == self.after_size
&& same_cursor(self.before_cursor, self.after_cursor)
}
pub fn cells(&self) -> impl Iterator<Item = (u16, u16, &Cell, &Cell)> {
self.cells.iter().map(|(r, c, a, b)| (*r, *c, a, b))
}
pub fn changed_rows(&self) -> impl Iterator<Item = u16> + '_ {
self.rows.iter().map(|(row, ..)| *row)
}
pub fn style_changes(&self) -> impl Iterator<Item = (u16, &str, &str)> {
self.style_changes
.iter()
.map(|(row, before, after)| (*row, before.as_str(), after.as_str()))
}
}
fn arrow<T: PartialEq + fmt::Display>(a: T, b: T) -> String {
if a == b {
a.to_string()
} else {
format!("{a} → {b}")
}
}
fn cursor_text((row, col, visible): (u16, u16, bool)) -> String {
if visible {
format!("{row},{col}")
} else {
"hidden".to_owned()
}
}
impl fmt::Display for ScreenDiff {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_empty() {
return write!(f, "no difference");
}
writeln!(
f,
"size: {} cursor: {}",
arrow(
format!("{}x{}", self.before_size.0, self.before_size.1),
format!("{}x{}", self.after_size.0, self.after_size.1)
),
arrow(
cursor_text(self.before_cursor),
cursor_text(self.after_cursor)
)
)?;
if self.before_size != self.after_size {
let cols = self.before_size.0.min(self.after_size.0);
let rows = self.before_size.1.min(self.after_size.1);
writeln!(
f,
"compared over the {cols}x{rows} overlap; the rest is clipped"
)?;
}
let width = usize::from(self.before_size.0.min(self.after_size.0));
for (row, before, after, changed) in &self.rows {
writeln!(f, "{row:>3} │{before:<width$}│{after}")?;
let mut marks = vec![' '; width];
for &col in changed {
if let Some(mark) = marks.get_mut(usize::from(col)) {
*mark = '^';
}
}
let marks: String = marks.into_iter().collect();
let marks = marks.trim_end();
writeln!(f, " │{marks:<width$}│{marks}")?;
}
if self.unchanged_rows > 0 {
writeln!(f, "… {} rows unchanged", self.unchanged_rows)?;
}
for (row, before, after) in &self.style_changes {
writeln!(f, "styles: {row}: {before} → {after}")?;
}
Ok(())
}
}