Something went wrong. Try again.
A performant and featureful re-implementation of Kakoune's TUI
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256use crate::protocol::types::Face;
use super::frame::Frame;
#[derive(Debug, Clone, PartialEq, Eq)]pub enum WriteInstruction { MoveCursor { row: usize, col: usize }, SetFace(Face), Text(String), ShowCursor { row: usize, col: usize }, HideCursor,}
pub fn diff(previous: &Frame, next: &Frame) -> Vec<WriteInstruction> { // A resized grid has no cell-by-cell correspondence, so repaint every next-frame cell. let dimensions_changed = previous.width != next.width || previous.height != next.height; let mut instructions = Vec::new();
for row in 0..next.height { let mut column = 0; while column < next.width { let Some(next_cell) = next.cell(row, column) else { break; }; let changed = dimensions_changed || previous .cell(row, column) .is_none_or(|previous_cell| previous_cell != next_cell); if !changed { column += 1; continue; }
// Merge only adjacent changed cells with the same face; unchanged gaps stay separate. let face = next_cell.face; let run_start = column; let mut text = String::new(); while column < next.width { let Some(next_cell) = next.cell(row, column) else { break; }; let changed = dimensions_changed || previous .cell(row, column) .is_none_or(|previous_cell| previous_cell != next_cell); if !changed || next_cell.face != face { break; } next_cell.append_text(&mut text); column += 1; } if text.is_empty() { // A changed wide-glyph trailing cell must still clear old terminal content. text.push(' '); }
instructions.push(WriteInstruction::MoveCursor { row, col: run_start, }); instructions.push(WriteInstruction::SetFace(face)); instructions.push(WriteInstruction::Text(text)); } }
// Text runs move the terminal cursor; restore the editor cursor after repainting. let cells_changed = !instructions.is_empty(); match next.cursor { Some((row, col)) if previous.cursor != next.cursor || cells_changed => { instructions.push(WriteInstruction::ShowCursor { row, col }); } None if previous.cursor.is_some() => instructions.push(WriteInstruction::HideCursor), _ => {} }
instructions}
#[cfg(test)]mod tests { use super::*; use crate::protocol::types::{Color, Coord, DisplayAtom, NamedColor}; use crate::render::frame::{Cell, build_base_frame};
fn blank(width: usize, height: usize) -> Frame { Frame::blank(width, height, &Face::default()) }
fn with_cells(width: usize, cells: &[(usize, &str, Face)]) -> Frame { let mut frame = blank(width, 1); for (column, text, face) in cells { frame.set_cell(0, *column, Cell::new(text, *face)); } frame }
#[test] fn unchanged_frames_produce_no_instructions() { let frame = with_cells(3, &[(0, "a", Face::default())]); assert!(diff(&frame, &frame).is_empty()); }
#[test] fn changed_cell_produces_one_write_run() { let previous = blank(3, 1); let next = with_cells(3, &[(1, "x", Face::default())]); assert_eq!( diff(&previous, &next), vec![ WriteInstruction::MoveCursor { row: 0, col: 1 }, WriteInstruction::SetFace(Face::default()), WriteInstruction::Text("x".into()), ] ); }
#[test] fn adjacent_changed_cells_with_the_same_face_are_merged() { let previous = blank(4, 1); let next = with_cells(4, &[(1, "x", Face::default()), (2, "y", Face::default())]); assert_eq!( diff(&previous, &next), vec![ WriteInstruction::MoveCursor { row: 0, col: 1 }, WriteInstruction::SetFace(Face::default()), WriteInstruction::Text("xy".into()), ] ); }
#[test] fn unchanged_gaps_are_not_bridged() { let previous = blank(4, 1); let next = with_cells(4, &[(0, "x", Face::default()), (2, "y", Face::default())]); let instructions = diff(&previous, &next);
assert_eq!( instructions, vec![ WriteInstruction::MoveCursor { row: 0, col: 0 }, WriteInstruction::SetFace(Face::default()), WriteInstruction::Text("x".into()), WriteInstruction::MoveCursor { row: 0, col: 2 }, WriteInstruction::SetFace(Face::default()), WriteInstruction::Text("y".into()), ] ); }
#[test] fn changed_cells_with_different_faces_form_separate_runs() { let previous = blank(3, 1); let red = Face { fg: Color::Named(NamedColor::Red), ..Face::default() }; let blue = Face { fg: Color::Named(NamedColor::Blue), ..Face::default() }; let next = with_cells(3, &[(0, "r", red), (1, "b", blue)]);
assert_eq!( diff(&previous, &next), vec![ WriteInstruction::MoveCursor { row: 0, col: 0 }, WriteInstruction::SetFace(red), WriteInstruction::Text("r".into()), WriteInstruction::MoveCursor { row: 0, col: 1 }, WriteInstruction::SetFace(blue), WriteInstruction::Text("b".into()), ] ); }
#[test] fn dimension_change_forces_a_full_repaint() { let previous = blank(1, 1); let next = with_cells(3, &[(1, "x", Face::default())]); assert_eq!( diff(&previous, &next), vec![ WriteInstruction::MoveCursor { row: 0, col: 0 }, WriteInstruction::SetFace(Face::default()), WriteInstruction::Text(" x ".into()), ] ); }
#[test] fn resized_draw_repaints_rows_that_were_padding_before_resize() { let default_face = Face::default(); let lines = |count| { (0..count) .map(|line| { vec![DisplayAtom { face: default_face, contents: format!("{line:02}\n"), }] }) .collect::<Vec<_>>() }; let previous_lines = lines(24); let next_lines = lines(52); let previous = build_base_frame( 2, 52, &previous_lines, Coord { line: 0, column: 0 }, &default_face, &Face::default(), 0, ); let next = build_base_frame( 2, 52, &next_lines, Coord { line: 0, column: 0 }, &default_face, &Face::default(), 0, );
assert_eq!(previous.cell(51, 0).unwrap().text(), "~"); assert_eq!(next.cell(51, 0).unwrap().text(), "5"); assert!(diff(&previous, &next).iter().any(|instruction| { matches!(instruction, WriteInstruction::MoveCursor { row: 51, .. }) })); }
#[test] fn cursor_changes_are_reported_after_cell_writes() { let previous = blank(2, 1); let mut next = with_cells(2, &[(0, "x", Face::default())]); next.cursor = Some((0, 1));
assert_eq!( diff(&previous, &next), vec![ WriteInstruction::MoveCursor { row: 0, col: 0 }, WriteInstruction::SetFace(Face::default()), WriteInstruction::Text("x".into()), WriteInstruction::ShowCursor { row: 0, col: 1 }, ] ); }
#[test] fn cursor_visibility_transitions_are_reported() { let mut previous = blank(2, 1); previous.cursor = Some((0, 1)); let next = blank(2, 1); assert_eq!(diff(&previous, &next), vec![WriteInstruction::HideCursor]); }}