feat(core): deterministic ANSI snapshot of the grid for reattach repaint
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
pub mod grid;
|
pub mod grid;
|
||||||
// snapshot module added in Task 12
|
pub mod snapshot;
|
||||||
// pub mod snapshot;
|
|
||||||
|
|
||||||
pub use grid::GridSurface;
|
pub use grid::GridSurface;
|
||||||
// pub use snapshot::Snapshot;
|
pub use snapshot::Snapshot;
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
use alacritty_terminal::index::Point;
|
||||||
|
use alacritty_terminal::term::cell::Flags;
|
||||||
|
use alacritty_terminal::vte::ansi::Color;
|
||||||
|
use crate::grid::GridSurface;
|
||||||
|
|
||||||
|
/// Serializable snapshot returned by `attach`.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct Snapshot {
|
||||||
|
/// ANSI byte dump suitable for `xterm.write()`.
|
||||||
|
pub ansi: String,
|
||||||
|
pub cols: u16,
|
||||||
|
pub rows: u16,
|
||||||
|
/// 1-based cursor position.
|
||||||
|
pub cursor_row: u16,
|
||||||
|
pub cursor_col: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sgr_for_color(c: Color, foreground: bool) -> String {
|
||||||
|
let base = if foreground { 38 } else { 48 };
|
||||||
|
match c {
|
||||||
|
Color::Named(named) => {
|
||||||
|
// Map common named colors to SGR; default fg/bg reset for the rest.
|
||||||
|
use alacritty_terminal::vte::ansi::NamedColor;
|
||||||
|
let code = match named {
|
||||||
|
NamedColor::Black => Some(if foreground { 30 } else { 40 }),
|
||||||
|
NamedColor::Red => Some(if foreground { 31 } else { 41 }),
|
||||||
|
NamedColor::Green => Some(if foreground { 32 } else { 42 }),
|
||||||
|
NamedColor::Yellow => Some(if foreground { 33 } else { 43 }),
|
||||||
|
NamedColor::Blue => Some(if foreground { 34 } else { 44 }),
|
||||||
|
NamedColor::Magenta => Some(if foreground { 35 } else { 45 }),
|
||||||
|
NamedColor::Cyan => Some(if foreground { 36 } else { 46 }),
|
||||||
|
NamedColor::White => Some(if foreground { 37 } else { 47 }),
|
||||||
|
NamedColor::BrightBlack => Some(if foreground { 90 } else { 100 }),
|
||||||
|
NamedColor::BrightRed => Some(if foreground { 91 } else { 101 }),
|
||||||
|
NamedColor::BrightGreen => Some(if foreground { 92 } else { 102 }),
|
||||||
|
NamedColor::BrightYellow => Some(if foreground { 93 } else { 103 }),
|
||||||
|
NamedColor::BrightBlue => Some(if foreground { 94 } else { 104 }),
|
||||||
|
NamedColor::BrightMagenta => Some(if foreground { 95 } else { 105 }),
|
||||||
|
NamedColor::BrightCyan => Some(if foreground { 96 } else { 106 }),
|
||||||
|
NamedColor::BrightWhite => Some(if foreground { 97 } else { 107 }),
|
||||||
|
_ => None, // Foreground/Background/Cursor etc. → use reset.
|
||||||
|
};
|
||||||
|
match code {
|
||||||
|
Some(n) => format!("{n}"),
|
||||||
|
None => format!("{}", if foreground { 39 } else { 49 }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Color::Indexed(i) => format!("{base};5;{i}"),
|
||||||
|
Color::Spec(rgb) => format!("{base};2;{};{};{}", rgb.r, rgb.g, rgb.b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize the visible grid into an ANSI dump.
|
||||||
|
pub fn snapshot_ansi(g: &GridSurface) -> Snapshot {
|
||||||
|
let size = g.size();
|
||||||
|
let term = g.term();
|
||||||
|
let grid = term.grid();
|
||||||
|
|
||||||
|
let mut out = String::new();
|
||||||
|
out.push_str("\x1b[2J\x1b[H"); // clear + home
|
||||||
|
|
||||||
|
let cols = size.cols;
|
||||||
|
let lines = size.lines;
|
||||||
|
|
||||||
|
// Track the last emitted attributes to avoid redundant SGR sequences.
|
||||||
|
let mut last: Option<(Color, Color, Flags)> = None;
|
||||||
|
|
||||||
|
for line in 0..lines {
|
||||||
|
for col in 0..cols {
|
||||||
|
let point = Point::new(alacritty_terminal::index::Line(line as i32), alacritty_terminal::index::Column(col));
|
||||||
|
let cell = &grid[point];
|
||||||
|
let cur = (cell.fg, cell.bg, cell.flags);
|
||||||
|
if last != Some(cur) {
|
||||||
|
let mut codes: Vec<String> = vec!["0".into()]; // reset, then re-apply
|
||||||
|
if cell.flags.contains(Flags::BOLD) { codes.push("1".into()); }
|
||||||
|
if cell.flags.contains(Flags::DIM) { codes.push("2".into()); }
|
||||||
|
if cell.flags.contains(Flags::ITALIC) { codes.push("3".into()); }
|
||||||
|
if cell.flags.contains(Flags::UNDERLINE) { codes.push("4".into()); }
|
||||||
|
if cell.flags.contains(Flags::INVERSE) { codes.push("7".into()); }
|
||||||
|
codes.push(sgr_for_color(cell.fg, true));
|
||||||
|
codes.push(sgr_for_color(cell.bg, false));
|
||||||
|
out.push_str(&format!("\x1b[{}m", codes.join(";")));
|
||||||
|
last = Some(cur);
|
||||||
|
}
|
||||||
|
out.push(cell.c);
|
||||||
|
}
|
||||||
|
out.push_str("\r\n");
|
||||||
|
}
|
||||||
|
out.push_str("\x1b[0m"); // reset attributes at end
|
||||||
|
|
||||||
|
let cursor = grid.cursor.point;
|
||||||
|
let cursor_row = (cursor.line.0 as i64 + 1).clamp(1, lines as i64) as u16;
|
||||||
|
let cursor_col = (cursor.column.0 as i64 + 1).clamp(1, cols as i64) as u16;
|
||||||
|
out.push_str(&format!("\x1b[{cursor_row};{cursor_col}H"));
|
||||||
|
|
||||||
|
Snapshot {
|
||||||
|
ansi: out,
|
||||||
|
cols: cols as u16,
|
||||||
|
rows: lines as u16,
|
||||||
|
cursor_row,
|
||||||
|
cursor_col,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn snapshot_contains_fed_text_and_is_deterministic() {
|
||||||
|
let mut g = GridSurface::new(10, 3);
|
||||||
|
g.feed(b"hi");
|
||||||
|
let a = snapshot_ansi(&g);
|
||||||
|
let b = snapshot_ansi(&g);
|
||||||
|
assert_eq!(a.ansi, b.ansi, "snapshot must be deterministic");
|
||||||
|
assert!(a.ansi.contains("hi"));
|
||||||
|
assert!(a.ansi.starts_with("\x1b[2J\x1b[H"));
|
||||||
|
assert_eq!(a.cols, 10);
|
||||||
|
assert_eq!(a.rows, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cursor_is_one_based_after_input() {
|
||||||
|
let mut g = GridSurface::new(10, 3);
|
||||||
|
g.feed(b"abc");
|
||||||
|
let s = snapshot_ansi(&g);
|
||||||
|
// After 'abc' the cursor sits at column 4 (1-based) on row 1.
|
||||||
|
assert_eq!(s.cursor_row, 1);
|
||||||
|
assert_eq!(s.cursor_col, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user