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 cursor_style_default() {
    let parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    assert_eq!(parser.screen().cursor_style(), CursorStyle::Default);
}

#[test]
fn cursor_style_all_values() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    let cases = [
        (0, CursorStyle::Default),
        (1, CursorStyle::BlinkingBlock),
        (2, CursorStyle::SteadyBlock),
        (3, CursorStyle::BlinkingUnderline),
        (4, CursorStyle::SteadyUnderline),
        (5, CursorStyle::BlinkingBar),
        (6, CursorStyle::SteadyBar),
    ];
    for (param, expected) in cases {
        let seq = format!("\x1b[{param} q");
        process(&mut parser, seq.as_bytes());
        assert_eq!(
            parser.screen().cursor_style(),
            expected,
            "DECSCUSR param {param} did not map to the documented cursor style",
        );
    }
}

#[test]
fn cursor_style_invalid_ignored() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[2 q");
    assert_eq!(parser.screen().cursor_style(), CursorStyle::SteadyBlock);
    process(&mut parser, b"\x1b[99 q");
    assert_eq!(
        parser.screen().cursor_style(),
        CursorStyle::SteadyBlock,
        "invalid DECSCUSR param 99 must leave the previous style untouched",
    );
}

#[test]
fn cursor_style_reset_by_ris() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[5 q");
    assert_eq!(parser.screen().cursor_style(), CursorStyle::BlinkingBar);
    process(&mut parser, b"\x1bc");
    assert_eq!(parser.screen().cursor_style(), CursorStyle::Default);
}

fn cursor_style_events(parser: &mut crate::Parser) -> Vec<CursorStyle> {
    parser
        .screen_mut()
        .drain_events()
        .into_iter()
        .filter_map(|e| match e {
            ScreenEvent::CursorStyleChanged(s) => Some(s),
            _ => None,
        })
        .collect()
}

#[test]
fn cursor_style_changed_fires_on_decscusr() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    assert!(cursor_style_events(&mut parser).is_empty());

    process(&mut parser, b"\x1b[5 q");
    assert_eq!(
        cursor_style_events(&mut parser),
        vec![CursorStyle::BlinkingBar]
    );
    assert_eq!(parser.screen().cursor_style(), CursorStyle::BlinkingBar);

    assert!(cursor_style_events(&mut parser).is_empty());
}

#[test]
fn cursor_style_changed_not_fired_for_same_style() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[2 q");
    assert_eq!(
        cursor_style_events(&mut parser),
        vec![CursorStyle::SteadyBlock]
    );

    process(&mut parser, b"\x1b[2 q");
    assert!(cursor_style_events(&mut parser).is_empty());
}

#[test]
fn cursor_style_changed_survives_intermediate_content() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[5 q");
    assert_eq!(
        cursor_style_events(&mut parser),
        vec![CursorStyle::BlinkingBar]
    );

    process(&mut parser, b"hello");
    assert!(cursor_style_events(&mut parser).is_empty());

    process(&mut parser, b"\x1b[2 q");
    assert_eq!(
        cursor_style_events(&mut parser),
        vec![CursorStyle::SteadyBlock]
    );
    assert_eq!(parser.screen().cursor_style(), CursorStyle::SteadyBlock);
}

#[test]
fn cursor_style_changed_emits_per_change_within_chunk() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    // Two style changes in same process() call emit two separate events
    process(&mut parser, b"\x1b[5 q\x1b[2 q");
    assert_eq!(
        cursor_style_events(&mut parser),
        vec![CursorStyle::BlinkingBar, CursorStyle::SteadyBlock]
    );
    assert_eq!(parser.screen().cursor_style(), CursorStyle::SteadyBlock);
}

#[test]
fn cursor_style_changed_reset_by_ris() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[5 q");
    let _ = cursor_style_events(&mut parser);

    // RIS resets cursor to Default, a change from BlinkingBar
    process(&mut parser, b"\x1bc");
    assert_eq!(cursor_style_events(&mut parser), vec![CursorStyle::Default]);
    assert_eq!(parser.screen().cursor_style(), CursorStyle::Default);
}

#[test]
fn cursor_style_shape_collapses_blink_pairs() {
    assert_eq!(CursorStyle::Default.shape(), CursorShape::Default);
    assert_eq!(
        CursorStyle::BlinkingBlock.shape(),
        CursorShape::Block,
        "BlinkingBlock and SteadyBlock must share the Block shape",
    );
    assert_eq!(CursorStyle::SteadyBlock.shape(), CursorShape::Block);
    assert_eq!(
        CursorStyle::BlinkingUnderline.shape(),
        CursorShape::Underline,
        "BlinkingUnderline and SteadyUnderline must share the Underline shape",
    );
    assert_eq!(CursorStyle::SteadyUnderline.shape(), CursorShape::Underline);
    assert_eq!(
        CursorStyle::BlinkingBar.shape(),
        CursorShape::Bar,
        "BlinkingBar and SteadyBar must share the Bar shape",
    );
    assert_eq!(CursorStyle::SteadyBar.shape(), CursorShape::Bar);
}

#[test]
fn cursor_style_blink_only_for_blinking_variants() {
    assert!(
        !CursorStyle::Default.blink(),
        "Default must not advertise blink"
    );
    assert!(
        CursorStyle::BlinkingBlock.blink(),
        "BlinkingBlock must report blink=true",
    );
    assert!(
        !CursorStyle::SteadyBlock.blink(),
        "SteadyBlock must report blink=false (steady is the no-blink half of the pair)",
    );
    assert!(CursorStyle::BlinkingUnderline.blink());
    assert!(
        !CursorStyle::SteadyUnderline.blink(),
        "SteadyUnderline must report blink=false",
    );
    assert!(CursorStyle::BlinkingBar.blink());
    assert!(
        !CursorStyle::SteadyBar.blink(),
        "SteadyBar must report blink=false",
    );
}