tastty-core 0.1.0

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

#[test]
fn xtwinops_22_23_t_push_pop_both_round_trips() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b]2;outer\x07");
    process(&mut parser, b"\x1b]1;outer-icon\x07");
    let _ = parser.screen_mut().drain_events();

    process(&mut parser, b"\x1b[22;0t");
    // Push must not mutate the live title or fire TitleChanged.
    assert_eq!(parser.screen().title(), "outer");
    assert_eq!(parser.screen().icon_name(), "outer-icon");
    assert!(parser.screen_mut().drain_events().is_empty());

    process(&mut parser, b"\x1b]0;inner\x07");
    let _ = parser.screen_mut().drain_events();
    assert_eq!(parser.screen().title(), "inner");
    assert_eq!(parser.screen().icon_name(), "inner");

    process(&mut parser, b"\x1b[23;0t");
    assert_eq!(parser.screen().title(), "outer");
    assert_eq!(parser.screen().icon_name(), "outer-icon");
    let events = parser.screen_mut().drain_events();
    assert!(events.contains(&ScreenEvent::TitleChanged));
}

#[test]
fn xtwinops_22_23_t_default_zero_param() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b]2;outer\x07");
    let _ = parser.screen_mut().drain_events();
    process(&mut parser, b"\x1b[22t");
    process(&mut parser, b"\x1b]2;inner\x07");
    let _ = parser.screen_mut().drain_events();
    process(&mut parser, b"\x1b[23t");
    assert_eq!(parser.screen().title(), "outer");
}

#[test]
fn xtwinops_22_23_t_selective_icon_only() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b]2;window\x07");
    process(&mut parser, b"\x1b]1;icon-outer\x07");
    let _ = parser.screen_mut().drain_events();

    process(&mut parser, b"\x1b[22;1t");
    process(&mut parser, b"\x1b]1;icon-inner\x07");
    process(&mut parser, b"\x1b]2;window-replaced\x07");
    let _ = parser.screen_mut().drain_events();

    // Icon-only pop must leave the window title alone.
    process(&mut parser, b"\x1b[23;1t");
    assert_eq!(parser.screen().icon_name(), "icon-outer");
    assert_eq!(parser.screen().title(), "window-replaced");
}

#[test]
fn xtwinops_22_23_t_selective_title_only() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b]2;window-outer\x07");
    process(&mut parser, b"\x1b]1;icon\x07");
    let _ = parser.screen_mut().drain_events();

    process(&mut parser, b"\x1b[22;2t");
    process(&mut parser, b"\x1b]2;window-inner\x07");
    process(&mut parser, b"\x1b]1;icon-replaced\x07");
    let _ = parser.screen_mut().drain_events();

    process(&mut parser, b"\x1b[23;2t");
    assert_eq!(parser.screen().title(), "window-outer");
    assert_eq!(parser.screen().icon_name(), "icon-replaced");
}

#[test]
fn xtwinops_22_t_caps_at_ten_with_fifo_drop() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    // Push 11 distinct titles. The first ("t0") must be evicted.
    for i in 0..11 {
        let osc = format!("\x1b]2;t{i}\x07");
        process(&mut parser, osc.as_bytes());
        process(&mut parser, b"\x1b[22;2t");
    }
    let _ = parser.screen_mut().drain_events();

    // Pop 10 times; each pop restores the most recently pushed entry.
    // After 10 pops, the live title is "t1" (the entry that was at
    // index 0 after the FIFO drop). One more pop is a no-op.
    for _ in 0..10 {
        process(&mut parser, b"\x1b[23;2t");
    }
    assert_eq!(parser.screen().title(), "t1");
    let title_before = parser.screen().title().to_string();
    let _ = parser.screen_mut().drain_events();
    process(&mut parser, b"\x1b[23;2t");
    assert_eq!(parser.screen().title(), title_before);
    let events = parser.screen_mut().drain_events();
    assert!(!events.contains(&ScreenEvent::TitleChanged));
}

#[test]
fn xtwinops_23_t_pop_on_empty_stack_is_noop() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b]2;only\x07");
    let _ = parser.screen_mut().drain_events();
    process(&mut parser, b"\x1b[23;0t");
    assert_eq!(parser.screen().title(), "only");
    let events = parser.screen_mut().drain_events();
    assert!(!events.contains(&ScreenEvent::TitleChanged));
}

#[test]
fn xtwinops_22_23_t_persists_across_alt_screen_toggle() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b]2;outer\x07");
    let _ = parser.screen_mut().drain_events();
    process(&mut parser, b"\x1b[22;0t");
    // Enter alternate screen, mutate title there, then leave.
    process(&mut parser, b"\x1b[?1049h");
    process(&mut parser, b"\x1b]2;alt\x07");
    process(&mut parser, b"\x1b[?1049l");
    // The popped value is the one captured before alt-screen entry.
    process(&mut parser, b"\x1b[23;0t");
    assert_eq!(parser.screen().title(), "outer");
}

#[test]
fn xtwinops_22_23_t_ris_clears_stack() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b]2;outer\x07");
    process(&mut parser, b"\x1b[22;0t");
    process(&mut parser, b"\x1bc"); // RIS
    let _ = parser.screen_mut().drain_events();
    // Stack must be empty after RIS, so a subsequent pop is a no-op
    // and does not magically restore the pre-RIS title.
    process(&mut parser, b"\x1b[23;0t");
    assert_eq!(parser.screen().title(), "");
    let events = parser.screen_mut().drain_events();
    assert!(!events.contains(&ScreenEvent::TitleChanged));
}

#[test]
fn xtwinops_text_area_cells() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[18t");
    assert_eq!(drain_replies(&mut parser), b"\x1b[8;24;80t");
}

#[test]
fn xtwinops_text_area_cells_custom_size() {
    let mut parser = crate::Parser::new(
        TerminalSize {
            rows: 40,
            cols: 132,
        },
        0,
    );
    process(&mut parser, b"\x1b[18t");
    assert_eq!(drain_replies(&mut parser), b"\x1b[8;40;132t");
}

#[test]
fn xtwinops_cell_size_pixels() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    parser.screen_mut().set_pixel_cell_size(CellPixelSize {
        width: 8,
        height: 16,
    });
    process(&mut parser, b"\x1b[16t");
    assert_eq!(drain_replies(&mut parser), b"\x1b[6;16;8t");
}

#[test]
fn xtwinops_cell_size_pixels_unknown() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    // No pixel dimensions set, should not respond
    process(&mut parser, b"\x1b[16t");
    assert!(drain_replies(&mut parser).is_empty());
}

#[test]
fn xtwinops_text_area_pixels() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    parser.screen_mut().set_pixel_cell_size(CellPixelSize {
        width: 8,
        height: 16,
    });
    process(&mut parser, b"\x1b[14t");
    // 24*16=384 height, 80*8=640 width
    assert_eq!(drain_replies(&mut parser), b"\x1b[4;384;640t");
}

#[test]
fn xtwinops_text_area_pixels_unknown() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[14t");
    assert!(drain_replies(&mut parser).is_empty());
}

#[test]
fn xtwinops_text_area_pixels_no_reply_when_height_is_zero() {
    // A half-configured pixel cell (width known, height not yet
    // measured) is indistinguishable from "pixel size unknown" --
    // multiplying rows by zero would emit a nonsense `4;0;Wt`
    // reply. Both dimensions must be set before the report fires.
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    parser.screen_mut().set_pixel_cell_size(CellPixelSize {
        width: 12,
        height: 0,
    });
    process(&mut parser, b"\x1b[14t");
    assert!(drain_replies(&mut parser).is_empty());
}

#[test]
fn xtwinops_cell_size_pixels_no_reply_when_height_is_zero() {
    // Same guard for CSI 16 t: report only when both pixel
    // dimensions are known.
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    parser.screen_mut().set_pixel_cell_size(CellPixelSize {
        width: 12,
        height: 0,
    });
    process(&mut parser, b"\x1b[16t");
    assert!(drain_replies(&mut parser).is_empty());
}