fux 0.12.0

A minimal trusted Bevy terminal multiplexer
use super::*;
use crate::testing::*;
use fux_vt::Parser;

#[test]
fn unicode_selection_normalizes_wide_continuations_and_preserves_combining_marks() -> Outcome {
    let mut parser = Parser::new(3, 12, 20)?;
    parser.process("A界e\u{301}Z".as_bytes())?;
    let grid = Grid::capture(parser.screen(), 0)?;
    assert_eq!(grid.text((0, 2), (0, 3))?, "界e\u{301}");
    assert_eq!(grid.text((0, 3), (0, 2))?, "界e\u{301}");
    assert_eq!(grid.text((0, 0), (0, 4))?, "A界e\u{301}Z");
    assert_eq!(grid.text((0, 1), (0, 1))?, "");
    Ok(())
}
#[test]
fn wrapped_rows_join_and_hard_breaks_remain_newlines() -> Outcome {
    let mut parser = Parser::new(4, 5, 20)?;
    parser.process(b"abcdefgh\r\nijk")?;
    let grid = Grid::capture(parser.screen(), 0)?;
    assert_eq!(grid.text((0, 0), (2, 2))?, "abcdefgh\nijk");
    assert_eq!(grid.text((1, 0), (1, 4))?, "fgh");
    Ok(())
}
#[test]
fn history_capture_is_immutable_and_clamps_to_retained_rows() -> Outcome {
    let mut parser = Parser::new(3, 8, 2)?;
    parser.process(b"one\r\ntwo\r\nthree\r\nfour\r\nfive")?;
    let mark = parser.screen().mark();
    let grid = Grid::capture(parser.screen(), usize::MAX)?;
    assert_eq!(grid.offset, 2);
    assert_eq!(parser.screen().mark(), mark);
    assert_eq!(
        parser
            .screen()
            .window(0, 3, 8)
            .text((0, 0), (2, 7), 24, 100)?,
        "three\nfour\nfive"
    );
    assert_eq!(grid.text((0, 0), (2, 7))?, "one\ntwo\nthree");
    Ok(())
}
#[test]
fn clipped_viewports_never_select_a_backing_only_row_or_half_a_wide_glyph() -> Outcome {
    let mut parser = Parser::new(2, 2, 0)?;
    parser.process("".as_bytes())?;
    let grid = Grid::capture(parser.screen(), 0)?.clip((1, 1))?;
    assert_eq!(grid.size, (1, 1));
    assert_eq!(grid.text((0, 0), (1, 1))?, "");
    assert!(Grid::capture(parser.screen(), 0)?.clip((0, 1)).is_err());
    Ok(())
}
#[test]
fn clipboard_policy_and_encoded_limit_are_explicit() -> Outcome {
    let mut settings = Settings::default();
    assert!(
        validate_clipboard(&settings, "x")
            .err()
            .need()?
            .contains("disabled")
    );
    settings.clipboard = ClipboardPolicy::WriteOnly;
    assert!(validate_clipboard(&settings, &"a".repeat(MAX_COPY_BYTES)).is_ok());
    assert!(validate_clipboard(&settings, &"a".repeat(MAX_COPY_BYTES + 1)).is_err());
    let mut parser = Parser::new(1, 40_000, 0)?;
    parser.process(
        "A\u{1d185}\u{1d185}\u{1d185}\u{1d185}\u{1d185}"
            .repeat(40_000)
            .as_bytes(),
    )?;
    assert!(
        Grid::capture(parser.screen(), 0)?
            .text((0, 0), (0, 39_999))
            .is_err()
    );
    Ok(())
}
fn selection(parser: &Parser, a: (u16, u16), b: (u16, u16)) -> Result<Selection, String> {
    let grid = Grid::capture(parser.screen(), 0)?;
    Ok(Selection {
        leaf: Entity::PLACEHOLDER,
        cursor: b,
        anchor: grid.identity(a),
        grid,
        revision: 0,
        instance: 0,
        dragging: false,
        mouse_origin: false,
    })
}
fn renew(selection: &mut Selection, parser: &Parser) -> Result<bool, String> {
    let (grid, cursor, invalid) =
        selection.renew(parser.screen(), selection.grid.offset, selection.grid.size)?;
    selection.grid = grid;
    selection.cursor = cursor;
    if invalid {
        selection.anchor = None;
    }
    Ok(invalid)
}
fn selected(selection: &Selection) -> Result<String, String> {
    selection.grid.text(
        selection.grid.position(selection.anchor.need()?).need()?,
        selection.cursor,
    )
}
#[test]
fn retained_selection_follows_scrolling_and_ignores_unrelated_output() -> Outcome {
    let mut parser = Parser::new(6, 12, 4)?;
    parser.process(b"AAA\r\nBBB\r\nCCC\r\nDDD\r\nEEE\r\nREADY")?;
    let mut selection = selection(&parser, (1, 0), (3, 2))?;
    parser.process(b"\x1b[6;1Hother")?;
    assert!(!renew(&mut selection, &parser)?);
    parser.process(b"\r\nnext\r\nlast")?;
    assert!(!renew(&mut selection, &parser)?);
    assert_eq!(selection.grid.offset, 2);
    assert_eq!(selected(&selection)?, "BBB\nCCC\nDDD");
    Ok(())
}
#[test]
fn span_versions_allow_unrelated_cells_and_styles_but_reject_overwritten_text() -> Outcome {
    let mut parser = Parser::new(3, 12, 2)?;
    parser.process(b"ABCdef")?;
    let mut selection = selection(&parser, (0, 0), (0, 2))?;
    parser.process(b"\x1b[1;10HX\x1b[H\x1b[31mABC")?;
    assert!(!renew(&mut selection, &parser)?);
    assert_eq!(selected(&selection)?, "ABC");
    parser.process(b"\x1b[1;2HZ")?;
    assert!(renew(&mut selection, &parser)?);
    Ok(())
}
#[test]
fn interior_row_loss_invalidates_even_when_both_endpoints_survive() -> Outcome {
    let mut parser = Parser::new(5, 12, 2)?;
    parser.process(b"AAA\r\nBBB\r\nCCC\r\nDDD\r\nEEE")?;
    let mut selection = selection(&parser, (0, 0), (4, 2))?;
    let first = *selection.grid.ids.first().need()?;
    let last = *selection.grid.ids.last().need()?;
    parser.process(b"\x1b[2;4r\x1b[3;1H\x1b[L")?;
    assert!(parser.screen().row_by_id(first).is_some());
    assert!(parser.screen().row_by_id(last).is_some());
    assert!(renew(&mut selection, &parser)?);
    Ok(())
}
#[test]
fn partial_scrolling_preserves_contiguous_retained_selected_rows() -> Outcome {
    let mut parser = Parser::new(5, 12, 2)?;
    parser.process(b"AAA\r\nBBB\r\nCCC\r\nDDD\r\nEEE")?;
    let mut selection = selection(&parser, (2, 0), (3, 2))?;
    parser.process(b"\x1b[2;5r\x1b[S")?;
    assert!(!renew(&mut selection, &parser)?);
    assert_eq!(selected(&selection)?, "CCC\nDDD");
    assert_eq!(selection.cursor, (2, 2));
    Ok(())
}
#[test]
fn eviction_of_unselected_top_is_safe_but_loss_of_required_rows_is_not() -> Outcome {
    let mut parser = Parser::new(3, 12, 2)?;
    parser.process(b"AAA\r\nBBB\r\nCCC")?;
    let mut safe = selection(&parser, (2, 0), (2, 2))?;
    let mut lost = selection(&parser, (0, 0), (0, 2))?;
    parser.process(b"\r\nDDD\r\nEEE\r\nFFF")?;
    assert!(!renew(&mut safe, &parser)?);
    assert_eq!(selected(&safe)?, "CCC");
    assert!(renew(&mut lost, &parser)?);
    Ok(())
}
#[test]
fn parser_local_row_ids_cannot_cross_terminal_instances() -> Outcome {
    for replace_same_entity in [false, true] {
        let mut old = Parser::new(2, 5, 2)?;
        let mut new = Parser::new(2, 5, 2)?;
        old.process(b"AAA")?;
        new.process(b"BBB")?;
        let mut selection = selection(&old, (0, 0), (0, 2))?;
        let mut world = World::new();
        let root = world.spawn(Workspace).id();
        let tab = world.spawn((Tab, ChildOf(root))).id();
        let terminal = Terminal::for_test(old);
        selection.instance = terminal.instance();
        let pane = world.spawn(terminal).id();
        let leaf = world.spawn((PaneView { pane }, ChildOf(tab))).id();
        selection.leaf = leaf;
        let id = world
            .spawn((
                Viewer {
                    rows: 3,
                    cols: 5,
                    zoom: false,
                    scrollback: 0,
                    notice: None,
                },
                Viewing(root),
                OnTab(tab),
                Focused(leaf),
                selection,
            ))
            .id();
        refresh_visible(&mut world, id, (2, 5));
        assert!(world.get::<Selection>(id).is_some());
        if replace_same_entity {
            world.entity_mut(pane).insert(Terminal::for_test(new));
        } else {
            let other = world.spawn(Terminal::for_test(new)).id();
            world.entity_mut(leaf).insert(PaneView { pane: other });
        }
        refresh_visible(&mut world, id, (2, 5));
        assert!(
            world.get::<Selection>(id).is_none(),
            "parser-local row IDs aliased a replacement terminal"
        );
        assert!(
            world
                .get::<Viewer>(id)
                .need()?
                .notice
                .as_ref()
                .need()?
                .text
                .contains("selection cleared")
        );
    }
    Ok(())
}

#[test]
fn pane_removal_ends_copy_mode_with_a_notice() -> Outcome {
    let mut parser = Parser::new(3, 12, 2)?;
    parser.process(b"ABC")?;
    let mut world = World::new();
    let pane = world.spawn_empty().id();
    let leaf = world.spawn(PaneView { pane }).id();
    let mut selection = selection(&parser, (0, 0), (0, 2))?;
    selection.leaf = leaf;
    let id = world
        .spawn((
            Viewer {
                rows: 4,
                cols: 12,
                zoom: false,
                scrollback: 0,
                notice: None,
            },
            selection,
        ))
        .id();
    world.despawn(leaf);
    refresh_visible(&mut world, id, (3, 12));
    assert!(world.get::<Selection>(id).is_none());
    assert!(
        world
            .get::<Viewer>(id)
            .need()?
            .notice
            .as_ref()
            .need()?
            .text
            .contains("selection cleared: pane removed")
    );
    Ok(())
}

#[test]
fn browsing_resize_clipping_reset_and_buffer_switches_clear_anchors() -> Outcome {
    for bytes in [b"\x1bc".as_slice(), b"\x1b[?47h", b"\x1b[?1049h"] {
        let mut parser = Parser::new(3, 12, 2)?;
        parser.process(b"ABC")?;
        let mut selection = selection(&parser, (0, 0), (0, 2))?;
        parser.process(bytes)?;
        assert!(renew(&mut selection, &parser)?);
    }
    let mut parser = Parser::new(3, 12, 2)?;
    parser.process(b"OLD\r\nABC\r\nDEF\r\nGHI")?;
    let selection = selection(&parser, (0, 0), (0, 2))?;
    assert!(selection.renew(parser.screen(), 1, (3, 12))?.2);
    assert!(selection.renew(parser.screen(), 0, (2, 10))?.2);
    parser.resize(4, 12)?;
    assert!(selection.renew(parser.screen(), 0, (3, 12))?.2);
    Ok(())
}