tastty-core 0.1.0

Sans-IO core of the tastty terminal session library: VT parser, screen buffer, and byte encoders.
use super::*;

fn dirty_indices(screen: &Screen) -> Vec<u16> {
    screen.dirty_rows().map(|(idx, _)| idx).collect()
}

#[test]
fn fresh_screen_has_no_dirty_rows() {
    let screen = make_screen(3, 5);
    assert!(dirty_indices(&screen).is_empty());
}

#[test]
fn cell_write_marks_only_target_row_dirty() {
    let mut screen = make_screen(4, 8);
    screen.cup((3, 5));
    // Isolate the cell-write from the preceding cursor move, which
    // now also marks the cursor's old and new rows dirty.
    screen.clear_dirty();
    screen.text('A');
    assert_eq!(dirty_indices(&screen), vec![2]);
}

#[test]
fn dirty_rows_yields_dirty_state_variant() {
    let mut screen = make_screen(2, 3);
    screen.text('X');
    let collected: Vec<_> = screen.dirty_rows().collect();
    assert_eq!(collected, vec![(0, DirtyState::Dirty)]);
}

#[test]
fn erase_row_marks_only_that_row_dirty() {
    let mut screen = make_screen(3, 5);
    // Move to row 1, write, then erase that line.
    screen.cup((2, 1));
    screen.text('A');
    screen.clear_dirty();
    screen.el(2); // erase entire current line
    assert_eq!(dirty_indices(&screen), vec![1]);
}

#[test]
fn clear_dirty_resets_every_row() {
    let mut screen = make_screen(3, 5);
    screen.text('A');
    screen.cup((2, 1));
    screen.text('B');
    assert!(!dirty_indices(&screen).is_empty());
    screen.clear_dirty();
    assert!(dirty_indices(&screen).is_empty());
}

#[test]
fn resize_marks_all_rows_dirty() {
    // Existing rows resize() (marks them dirty); newly grown rows
    // are inserted as fresh dirty rows. After any size change every
    // row is dirty, so embedders unconditionally redraw -- the
    // simpler contract documented in `DirtyState`.
    let mut screen = make_screen(2, 4);
    screen.clear_dirty();
    screen.set_size(TerminalSize { rows: 4, cols: 6 });
    assert_eq!(
        dirty_indices(&screen),
        vec![0, 1, 2, 3],
        "after a resize, every row (existing + grown) must be marked dirty for a full redraw",
    );
}

#[test]
fn alt_screen_swap_marks_every_row_dirty() {
    let mut screen = make_screen(3, 5);
    // Dirty the primary grid, then clear -- proves we are not
    // tracking residual primary-grid state through the swap.
    screen.text('A');
    screen.clear_dirty();
    screen.enter_alternate_grid();
    assert_eq!(
        dirty_indices(&screen),
        vec![0, 1, 2],
        "entering the alt grid must mark every row dirty (the visible buffer flipped wholesale)",
    );
    screen.clear_dirty();
    screen.exit_alternate_grid();
    assert_eq!(
        dirty_indices(&screen),
        vec![0, 1, 2],
        "exiting the alt grid must mark every row dirty (the visible buffer flipped back)",
    );
}

#[test]
fn scroll_marks_scrolled_region_dirty() {
    // A scroll-up shifts every row in the scroll region into a
    // different visible index, so every row in the region must
    // report dirty even though some rows kept their own content.
    let mut screen = make_screen(3, 5);
    screen.cup((1, 1));
    screen.text('A');
    screen.cup((2, 1));
    screen.text('B');
    screen.cup((3, 1));
    screen.text('C');
    screen.clear_dirty();
    // Cursor is on the last row; lf() runs row_inc_scroll(1)
    // which scrolls the entire grid (default scroll region).
    screen.lf();
    assert_eq!(dirty_indices(&screen), vec![0, 1, 2]);
}

#[test]
fn dirty_rows_indices_ascending() {
    let mut screen = make_screen(5, 5);
    screen.cup((4, 1));
    screen.text('B');
    screen.cup((2, 1));
    screen.text('A');
    let indices = dirty_indices(&screen);
    assert!(
        indices.windows(2).all(|w| w[0] < w[1]),
        "dirty_rows() must yield strictly ascending indices, got {indices:?}",
    );
    // Initial cursor at row 0; first cup marks {0, 3}, write B
    // keeps {0, 3}; second cup marks {3, 1}; write A keeps {1, 3}.
    // Union after all four operations: {0, 1, 3}.
    assert_eq!(indices, vec![0, 1, 3]);
}

#[test]
fn cursor_move_marks_old_and_new_rows_dirty() {
    let mut screen = make_screen(8, 5);
    screen.cup((3, 1));
    screen.clear_dirty();
    screen.cup((5, 1));
    assert_eq!(dirty_indices(&screen), vec![2, 4]);
}

#[test]
fn cursor_col_only_move_marks_current_row_dirty() {
    let mut screen = make_screen(5, 10);
    screen.cup((3, 1));
    screen.clear_dirty();
    screen.cuf(5);
    assert_eq!(dirty_indices(&screen), vec![2]);
}

#[test]
fn cursor_no_op_move_still_marks_row() {
    let mut screen = make_screen(3, 5);
    screen.cup((1, 1));
    screen.clear_dirty();
    // cha(1) moves the cursor to col 0; it is already at col 0,
    // so position is unchanged but the row must still be marked.
    screen.cha(1);
    assert_eq!(dirty_indices(&screen), vec![0]);
}

#[test]
fn linefeed_marks_old_and_new_rows() {
    // lf() routes through row_inc_scroll, which already marks the
    // entire scroll region dirty as part of its scroll contract;
    // we only need to confirm that old + new cursor rows are part
    // of the marked set.
    let mut screen = make_screen(5, 5);
    screen.cup((1, 1));
    screen.clear_dirty();
    screen.lf();
    let indices = dirty_indices(&screen);
    assert!(indices.contains(&0), "expected old row 0 in {indices:?}");
    assert!(indices.contains(&1), "expected new row 1 in {indices:?}");
}

#[test]
fn cud_marks_old_and_new_rows_without_full_region() {
    // CUD (row_inc_clamp) does not delegate through scroll_up, so
    // a non-scrolling vertical move marks exactly old + new.
    let mut screen = make_screen(8, 5);
    screen.cup((3, 1));
    screen.clear_dirty();
    screen.cud(2);
    assert_eq!(dirty_indices(&screen), vec![2, 4]);
}

#[test]
fn cup_then_clear_dirty_makes_dirty_rows_empty() {
    let mut screen = make_screen(5, 5);
    screen.cup((4, 1));
    screen.clear_dirty();
    assert_eq!(screen.dirty_rows().count(), 0);
}

#[test]
fn cursor_save_restore_marks_target_row() {
    let mut screen = make_screen(8, 5);
    screen.cup((3, 1));
    screen.decsc();
    screen.cup((5, 1));
    screen.clear_dirty();
    screen.decrc();
    assert_eq!(dirty_indices(&screen), vec![2, 4]);
}