fux 0.12.0

A minimal trusted Bevy terminal multiplexer
use super::*;
use crate::testing::*;
use bevy_ecs::prelude::Entity;

fn viewer(rows: u16, cols: u16) -> Viewer {
    Viewer {
        rows,
        cols,
        zoom: false,
        scrollback: 0,
        notice: None,
    }
}

#[test]
fn truncation_is_cell_sized_sanitized_and_keeps_the_requested_end() -> crate::testing::Outcome {
    for text in ["็•Œeฬ/path/to/long-name", "abc", "\x1b[31m\r\nhi", "๐Ÿฆ€็•Œ", ""] {
        for cols in 0..32 {
            for tail in [false, true] {
                let shown = fit(text, cols, tail);
                assert!(width(&shown) <= cols, "{shown:?} in {cols}");
                assert!(!shown.chars().any(char::is_control));
            }
        }
    }
    assert_eq!(fit("/long/name", 5, true), "โ€ฆname");
    assert_eq!(fit("notice sentence", 7, false), "noticeโ€ฆ");
    assert_eq!(fit("็•Œ็•Œ", 3, false), "็•Œโ€ฆ");
    Ok(())
}

#[test]
fn every_binding_is_reachable_at_every_short_height() -> crate::testing::Outcome {
    let settings = Settings::default();
    for rows in 2..40 {
        let v = viewer(rows, 80);
        let mut seen = std::collections::BTreeSet::new();
        for offset in 0..=help_limit(&settings, rows) {
            let mut out = String::new();
            let bounds = panel(&mut out, &v, &settings, offset).need()?;
            assert_eq!(bounds.max.y, u32::from(rows - 1));
            assert_eq!(bounds.max.x, 80);
            let mut parser = fux_vt::Parser::new(rows, 80, 0)?;
            parser.process(out.as_bytes())?;
            let contents = parser.screen().contents();
            for binding in &settings.bindings {
                let label = match &binding.action {
                    BindingAction::Known(action) => action.label().to_owned(),
                    BindingAction::Custom(name) => name.replace('_', " "),
                };
                if contents.contains(&label) {
                    seen.insert(binding.action.to_string());
                }
            }
            assert_eq!(
                parser.screen().cell(rows - 1, 79).need()?.bgcolor(),
                fux_vt::Color::Default
            );
        }
        assert_eq!(seen.len(), settings.bindings.len(), "{rows} rows: {seen:?}");
    }
    Ok(())
}

#[test]
fn tiny_unicode_command_selection_is_visible_even_when_disabled() -> crate::testing::Outcome {
    let settings = Settings {
        bindings: vec![
            crate::assets::Binding {
                key: "็•Œ".into(),
                action: BindingAction::Custom("custom_็•Œeฬ".into()),
            },
            crate::assets::Binding {
                key: "x".into(),
                action: BindingAction::Known(crate::actions::Action::Close),
            },
        ],
        ..Default::default()
    };
    for rows in 0..=4 {
        for cols in 0..=2 {
            for selected in 0..2 {
                let v = viewer(rows, cols);
                let mut out = String::new();
                let bounds = panel_context(&mut out, &v, &settings, selected, |_| true);
                if rows < 2 || cols == 0 {
                    assert!(bounds.is_none());
                    assert!(out.is_empty());
                    continue;
                }
                let mut parser = fux_vt::Parser::new(rows.max(1), cols.max(1), 0)?;
                parser.process(out.as_bytes())?;
                assert!((0..rows - 1).any(|y| (0..cols).any(|x| {
                    parser
                        .screen()
                        .cell(y, x)
                        .is_some_and(|c| c.inverse() && c.dim() && !c.bold())
                })));
            }
        }
    }
    Ok(())
}

#[test]
fn panel_is_content_sized_above_a_full_width_bar_and_resets_styles() -> crate::testing::Outcome {
    let settings = Settings {
        bindings: vec![crate::assets::Binding {
            key: "k".into(),
            action: BindingAction::Custom("known_action".into()),
        }],
        ..Default::default()
    };
    let v = viewer(12, 40);
    let mut out = "\x1b[31;44;7m".to_owned();
    tab_bar(
        &mut out,
        &v,
        (Entity::PLACEHOLDER, "workspace"),
        (None, &[]),
        "7: pane",
    );
    let bounds = panel(&mut out, &v, &settings, 0).need()?;
    assert_eq!(bounds.height(), 3);
    assert_eq!(bounds.width(), u32::from(width("k  known action") + 2));
    let mut parser = fux_vt::Parser::new(12, 40, 0)?;
    parser.process(out.as_bytes())?;
    let screen = parser.screen();
    for x in 0..40 {
        assert_eq!(screen.cell(11, x).need()?.bgcolor(), fux_vt::Color::Idx(8));
        assert!(!screen.cell(11, x).need()?.inverse());
    }
    let (x, y) = (bounds.min.x as u16, bounds.min.y as u16);
    assert!(screen.cell(y, x + 1).need()?.bold());
    assert!(screen.cell(y + 1, x + 1).need()?.bold());
    assert!(!screen.cell(y + 2, x + 1).need()?.bold());
    assert_eq!(
        screen.cell(y, x - 1).need()?.bgcolor(),
        fux_vt::Color::Default
    );
    assert_eq!(screen.bgcolor(), fux_vt::Color::Default);
    assert!(!screen.inverse());
    Ok(())
}

#[test]
fn overflowing_unicode_tab_bar_keeps_active_cells_and_pick_bounds_inside_viewport()
-> crate::testing::Outcome {
    let tabs: Vec<_> = (0..12)
        .map(|i| (Entity::from_bits(i + 1), format!("็•Œeฬ-tab-{i}")))
        .collect();
    for cols in 0..100 {
        for rows in 0..3 {
            let v = viewer(rows, cols);
            let tab = Some(tabs.get(9).need()?.0);
            let mut out = String::new();
            let hits = tab_bar(
                &mut out,
                &v,
                (Entity::PLACEHOLDER, "workspace"),
                (tab, &tabs),
                "process",
            );
            if rows == 0 || cols == 0 {
                assert!(out.is_empty());
                assert!(hits.is_empty());
                continue;
            }
            assert!(
                hits.iter()
                    .any(|(id, bounds)| Some(*id) == tab && bounds.width() > 0),
                "{rows}x{cols}"
            );
            for (_, bounds) in &hits {
                assert!(bounds.max.x <= u32::from(cols));
                assert_eq!(bounds.max.y, u32::from(rows));
                assert_eq!(bounds.height(), 1);
                assert_eq!(bounds.min.y, u32::from(rows - 1));
            }
            let mut parser = fux_vt::Parser::new(rows.max(1), cols.max(1), 0)?;
            parser.process(out.as_bytes())?;
            let active = hits.iter().find(|(id, _)| Some(*id) == tab).need()?.1;
            assert!(
                parser
                    .screen()
                    .cell(rows - 1, active.min.x as u16)
                    .need()?
                    .inverse()
            );
            for x in 0..cols {
                // A wide glyph's attributes belong to its leading cell.
                if parser
                    .screen()
                    .cell(rows - 1, x)
                    .need()?
                    .is_wide_continuation()
                {
                    continue;
                }
                assert_eq!(
                    parser.screen().cell(rows - 1, x).need()?.bgcolor(),
                    fux_vt::Color::Idx(8),
                    "{rows}x{cols} cell {x}: {out:?}"
                );
            }
        }
    }
    Ok(())
}

#[test]
fn squeezed_labels_keep_their_first_glyph_even_when_it_is_wide() -> crate::testing::Outcome {
    let tabs = vec![(Entity::from_bits(1), "็•Œ139".to_owned())];
    for cols in 8..40u16 {
        let v = viewer(3, cols);
        let mut out = String::new();
        let hits = tab_bar(
            &mut out,
            &v,
            (Entity::PLACEHOLDER, "ๅฎ‡167"),
            (Some(Entity::from_bits(1)), &tabs),
            "process",
        );
        let mut parser = fux_vt::Parser::new(3, cols, 0)?;
        parser.process(out.as_bytes())?;
        let bar = parser.screen().contents();
        let bar = bar.lines().last().need()?.to_owned();
        // Three cells hold a wide glyph and an ellipsis; padding must not
        // spend them first.
        let tab = hits
            .iter()
            .find(|(id, _)| *id == Entity::from_bits(1))
            .need()?;
        if tab.1.width() >= 3 {
            assert!(bar.contains('็•Œ'), "{cols}: {bar:?}");
        }
        if hits.first().need()?.1.width() >= 3 {
            assert!(bar.contains('ๅฎ‡'), "{cols}: {bar:?}");
        }
        if cols >= 40 {
            assert!(bar.contains(" ็•Œ139 "), "{cols}: {bar:?}");
        }
    }
    Ok(())
}