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 vs16_widens_preceding_char() {
    let mut screen = make_screen(24, 80);
    // ❄ (U+2744) is width 1 by unicode-width, but ❄️ (U+2744 + U+FE0F)
    // should be treated as width 2 in emoji presentation
    screen.text('\u{2744}');
    assert!(
        !screen.cell(0, 0).unwrap().is_wide(),
        "bare snowflake (no VS16) should still render width 1"
    );
    assert_eq!(screen.cursor(), Position { row: 0, col: 1 });

    screen.text('\u{fe0f}');
    assert!(
        screen.cell(0, 0).unwrap().is_wide(),
        "VS16 should retroactively widen the preceding snowflake"
    );
    assert!(
        screen.cell(0, 1).unwrap().is_wide_continuation(),
        "col 1 must become the wide continuation slot once VS16 widens col 0"
    );
    assert_eq!(screen.cursor(), Position { row: 0, col: 2 });
    assert!(
        screen.cell(0, 0).unwrap().contents().contains('\u{fe0f}'),
        "VS16 selector must be retained in the cell contents for downstream rendering"
    );
}

#[test]
fn vs16_emoji_then_text() {
    let mut screen = make_screen(24, 80);
    screen.text('\u{2744}');
    screen.text('\u{fe0f}');
    screen.text('X');
    assert_eq!(screen.cursor(), Position { row: 0, col: 3 });
    assert_eq!(screen.cell(0, 2).unwrap().contents(), "X");
}

#[test]
fn vs16_via_parser() {
    // Test VS16 handling through the full vte parser path
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    // ❄ (U+2744) = 0xE2 0x9D 0x84, VS16 (U+FE0F) = 0xEF 0xB8 0x8F
    process(&mut parser, "\u{fe0f}X".as_bytes());
    let screen = parser.screen();
    let c0 = screen.cell(0, 0).unwrap();
    let c1 = screen.cell(0, 1).unwrap();
    assert!(c0.is_wide(), "snowflake+VS16 should be wide");
    assert!(c1.is_wide_continuation());
    assert_eq!(screen.cell(0, 2).unwrap().contents(), "X");
    assert_eq!(screen.cursor(), Position { row: 0, col: 3 });
}

#[test]
fn starship_prompt_with_emoji() {
    // Real starship prompt from user's config with nix_shell ❄️ symbol.
    // This tests the full escape sequence flow including SGR colors
    // interleaved with emoji + VS16.
    // Note: \n is replaced with \r\n because in a real PTY the kernel's
    // onlcr setting translates \n → \r\n before the terminal sees it.
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    let prompt = b"\x1b[J\x1b[1;36mtastty\x1b[0m on \x1b[1;35m\xee\x82\xa0 main\x1b[0m \x1b[1;31m[!]\x1b[0m via \x1b[1;34m\xe2\x9d\x84\xef\xb8\x8f (tastty-env)\x1b[0m \r\n\x1b[1;32m\xe2\x9d\xaf\x1b[0m ";
    process(&mut parser, prompt);
    let screen = parser.screen();

    // Row 0: "tastty on  main [!] via ❄️ (tastty-env) "
    // Check the ❄️ occupies 2 cells
    let mut found_snowflake = false;
    for col in 0..80 {
        let cell = screen.cell(0, col).unwrap();
        if cell.has_contents() && cell.contents().starts_with('\u{2744}') {
            assert!(cell.is_wide(), "snowflake+VS16 at col {col} should be wide");
            let next = screen.cell(0, col + 1).unwrap();
            assert!(
                next.is_wide_continuation(),
                "col {} should be wide continuation",
                col + 1
            );
            found_snowflake = true;
            break;
        }
    }
    assert!(found_snowflake, "should find snowflake in prompt");

    // Row 1: "❯ " - the prompt char
    let r1c0 = screen.cell(1, 0).unwrap();
    assert_eq!(r1c0.contents(), "\u{276f}");

    // Now simulate ctrl+l redraw: erase display + home + re-render prompt
    process(&mut parser, b"\x1b[2J\x1b[H");
    process(&mut parser, prompt);
    let screen = parser.screen();

    // After redraw, the prompt should be identical with no ghost characters
    let mut found_snowflake = false;
    for col in 0..80 {
        let cell = screen.cell(0, col).unwrap();
        if cell.has_contents() && cell.contents().starts_with('\u{2744}') {
            assert!(
                cell.is_wide(),
                "after redraw: snowflake at col {col} should be wide"
            );
            let next = screen.cell(0, col + 1).unwrap();
            assert!(
                next.is_wide_continuation(),
                "after redraw: col {} should be wide continuation",
                col + 1
            );
            found_snowflake = true;
            break;
        }
    }
    assert!(found_snowflake, "should find snowflake after redraw");

    // Verify no stray content at end of row 0 (the ghost char issue)
    let last_content_col = (0..80u16)
        .rev()
        .find(|&col| screen.cell(0, col).unwrap().has_contents())
        .unwrap();
    let last_cell = screen.cell(0, last_content_col).unwrap();
    // The last visible char should be a space (after "tastty-env)"), not a ghost artifact
    assert!(
        last_cell.contents() == " "
            || last_cell.contents() == ")"
            || last_cell.is_wide_continuation(),
        "last content at col {last_content_col} should not be a ghost: {:?}",
        last_cell.contents()
    );
}

#[test]
fn nerd_font_git_branch_width() {
    //  (U+E0A0) is a Nerd Font private-use character.
    // unicode-width reports it as None (ambiguous), so our text() treats it
    // as width 1. Terminals with Nerd Fonts also render it as width 1.
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, "\u{e0a0}X".as_bytes());
    let screen = parser.screen();
    let c0 = screen.cell(0, 0).unwrap();
    let c1 = screen.cell(0, 1).unwrap();
    assert_eq!(c0.contents(), "\u{e0a0}");
    assert!(!c0.is_wide());
    assert_eq!(c1.contents(), "X");
}