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 kitty_flag_stack_push_pop() {
    let mut stack = KittyFlagStack::default();
    assert_eq!(stack.current(), 0, "fresh stack must report flags=0");

    stack.push(3);
    assert_eq!(stack.current(), 3, "push(3) should make 3 the active level");

    stack.push(5);
    assert_eq!(
        stack.current(),
        5,
        "push(5) on top of 3 should make 5 the active level (not OR them)",
    );

    stack.pop(1);
    assert_eq!(
        stack.current(),
        3,
        "pop(1) should reveal the 3 underneath, not clear to 0",
    );

    stack.pop(1);
    assert_eq!(
        stack.current(),
        0,
        "pop(1) past the bottom returns to default 0"
    );
}

#[test]
fn kitty_flag_stack_pop_empty() {
    let mut stack = KittyFlagStack::default();
    stack.pop(5);
    assert_eq!(stack.current(), 0);
}

#[test]
fn kitty_flag_stack_pop_multiple() {
    let mut stack = KittyFlagStack::default();
    stack.push(1);
    stack.push(3);
    stack.push(5);
    stack.pop(2);
    assert_eq!(stack.current(), 1);
}

#[test]
fn kitty_flag_stack_overflow_clamped() {
    let mut stack = KittyFlagStack::default();
    for i in 0..8 {
        stack.push(i + 1);
    }
    assert_eq!(stack.current(), 8);
    // 9th push is ignored
    stack.push(99);
    assert_eq!(
        stack.current(),
        8,
        "push past stack capacity must be silently dropped, not overwrite the top",
    );
    // Pop still works correctly
    stack.pop(1);
    assert_eq!(
        stack.current(),
        7,
        "after a dropped overflow push, pop(1) must reveal the previous valid level (7), not 99",
    );
}

#[test]
fn kitty_flag_stack_set_assign() {
    let mut stack = KittyFlagStack::default();
    stack.push(1);
    stack.set(3, 1); // assign
    assert_eq!(stack.current(), 3);
}

#[test]
fn kitty_flag_stack_set_or() {
    let mut stack = KittyFlagStack::default();
    stack.push(1);
    stack.set(2, 2); // OR
    assert_eq!(stack.current(), 3); // 1 | 2
}

#[test]
fn kitty_flag_stack_set_and_not() {
    let mut stack = KittyFlagStack::default();
    stack.push(3);
    stack.set(1, 3); // AND-NOT
    assert_eq!(stack.current(), 2); // 3 & !1
}

#[test]
fn kitty_flag_stack_set_empty_pushes() {
    let mut stack = KittyFlagStack::default();
    stack.set(5, 1);
    assert_eq!(stack.current(), 5);
}

#[test]
fn kitty_csi_push() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[>3u");
    assert_eq!(parser.screen().kitty_keyboard_flags(), 3);
}

#[test]
fn kitty_csi_pop() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[>1u\x1b[>5u");
    assert_eq!(parser.screen().kitty_keyboard_flags(), 5);
    process(&mut parser, b"\x1b[<u");
    assert_eq!(parser.screen().kitty_keyboard_flags(), 1);
}

#[test]
fn kitty_csi_pop_count() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[>1u\x1b[>5u");
    process(&mut parser, b"\x1b[<2u");
    assert_eq!(parser.screen().kitty_keyboard_flags(), 0);
}

#[test]
fn kitty_csi_set_assign() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[>1u");
    process(&mut parser, b"\x1b[=3;1u"); // assign
    assert_eq!(parser.screen().kitty_keyboard_flags(), 3);
}

#[test]
fn kitty_csi_set_or() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[>1u");
    process(&mut parser, b"\x1b[=2;2u"); // OR
    assert_eq!(parser.screen().kitty_keyboard_flags(), 3);
}

#[test]
fn kitty_csi_set_and_not() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[>3u");
    process(&mut parser, b"\x1b[=1;3u"); // AND-NOT
    assert_eq!(parser.screen().kitty_keyboard_flags(), 2);
}

#[test]
fn kitty_csi_set_default_mode() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[>0u");
    process(&mut parser, b"\x1b[=3u"); // default mode is assign
    assert_eq!(parser.screen().kitty_keyboard_flags(), 3);
}

#[test]
fn kitty_csi_set_empty_stack() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[=5u");
    assert_eq!(parser.screen().kitty_keyboard_flags(), 5);
}

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

    process(&mut parser, b"\x1b[>3u");
    process(&mut parser, b"\x1b[?u");
    assert_eq!(drain_replies(&mut parser), b"\x1b[?3u");
}

#[test]
fn kitty_ris_clears_flags() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[>5u");
    process(&mut parser, b"\x1bc"); // RIS
    assert_eq!(parser.screen().kitty_keyboard_flags(), 0);
}

#[test]
fn kitty_alternate_screen_isolation() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[>3u");
    assert_eq!(parser.screen().kitty_keyboard_flags(), 3);

    // Enter alternate screen
    process(&mut parser, b"\x1b[?1049h");
    assert_eq!(
        parser.screen().kitty_keyboard_flags(),
        0,
        "alt screen must start with a fresh Kitty keyboard flag stack, not inherit primary's flags",
    );

    // Push on alternate
    process(&mut parser, b"\x1b[>7u");
    assert_eq!(parser.screen().kitty_keyboard_flags(), 7);

    // Exit alternate, primary flags restored
    process(&mut parser, b"\x1b[?1049l");
    assert_eq!(
        parser.screen().kitty_keyboard_flags(),
        3,
        "leaving alt screen must restore primary's Kitty keyboard flags, not bleed alt's stack back",
    );
}

#[test]
fn kitty_event_on_push() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[>3u");
    let events = parser.screen_mut().drain_events();
    assert_eq!(events, vec![ScreenEvent::KittyFlagsChanged(3)]);
}

#[test]
fn kitty_event_on_pop() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[>3u");
    parser.screen_mut().drain_events(); // drain the push event
    process(&mut parser, b"\x1b[<u");
    let events = parser.screen_mut().drain_events();
    assert_eq!(events, vec![ScreenEvent::KittyFlagsChanged(0)]);
}

#[test]
fn kitty_no_event_on_noop() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    // Pop empty stack, no change
    process(&mut parser, b"\x1b[<u");
    let events = parser.screen_mut().drain_events();
    assert!(events.is_empty());
}

#[test]
fn kitty_event_on_ris() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[>5u");
    parser.screen_mut().drain_events();
    process(&mut parser, b"\x1bc"); // RIS
    let events = parser.screen_mut().drain_events();
    assert!(events.contains(&ScreenEvent::KittyFlagsChanged(0)));
}

#[test]
fn kitty_event_on_alt_screen_switch() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[>3u");
    parser.screen_mut().drain_events();

    // Entering alt screen with flags=0 should emit event
    process(&mut parser, b"\x1b[?1049h");
    let events = parser.screen_mut().drain_events();
    assert_eq!(events, vec![ScreenEvent::KittyFlagsChanged(0)]);
}

#[test]
fn kitty_keyboard_query_emits_event() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[?u");
    let events = parser.screen_mut().drain_events();
    assert!(
        events
            .iter()
            .any(|e| matches!(e, ScreenEvent::KittyKeyboardQuery { .. }))
    );
}