malevich 1.18.2

Terminal plotting: a small grammar of marks, honest axes, millions of points
Documentation
use super::super::{Canvas, Charset, Color, ColorMode, PlotRect, PointShape};
use super::Surface;

const ONE_CELL: PlotRect = PlotRect {
    gutter: 0,
    top: 0,
    columns: 1,
    rows: 1,
};

fn patch(surface: &mut Surface, row: usize, sample: Option<(f64, Color)>) {
    Canvas::patch(surface, 0, row, ONE_CELL, sample);
}

const fn assert_send_sync<T: Send + Sync>() {}
const _: () = assert_send_sync::<Surface>();
const _: () = assert_send_sync::<Color>();
const _: () = assert_send_sync::<Charset>();

#[test]
fn a_dot_lights_one_braille_subpixel() {
    let mut surface = Surface::new(2, 1, Charset::Braille);
    surface.dot(0.0, 0.0, Color::Default);
    assert_eq!(surface.to_plain(), "\u{2801}");
}

#[test]
fn point_shapes_stay_distinct_in_colorless_cell_output() {
    let mut surface = Surface::new(3, 1, Charset::Braille);
    Canvas::point(&mut surface, 0.0, 0.0, PointShape::Dot, Color::Default);
    Canvas::point(&mut surface, 2.0, 0.0, PointShape::Plus, Color::Default);
    Canvas::point(&mut surface, 4.0, 0.0, PointShape::Cross, Color::Default);
    assert_eq!(surface.to_plain(), "\u{2801}+x");
}

#[test]
fn a_bottom_row_line_renders_as_lower_dots() {
    let mut surface = Surface::new(2, 1, Charset::Braille);
    surface.line((0.0, 3.0), (3.0, 3.0), Color::Default);
    assert_eq!(surface.to_plain(), "\u{28C0}\u{28C0}");
}

#[test]
fn a_left_column_line_renders_as_a_wall() {
    let mut surface = Surface::new(1, 1, Charset::Braille);
    surface.line((0.0, 0.0), (0.0, 3.0), Color::Default);
    assert_eq!(surface.to_plain(), "\u{2847}");
}

#[test]
fn a_diagonal_descends_across_cells() {
    let mut surface = Surface::new(2, 1, Charset::Braille);
    surface.line((0.0, 3.0), (3.0, 0.0), Color::Default);
    assert_eq!(surface.to_plain(), "\u{2860}\u{280A}");
}

#[test]
fn segments_clip_to_the_surface() {
    let mut surface = Surface::new(2, 1, Charset::Braille);
    surface.line((-100.0, 2.0), (100.0, 2.0), Color::Default);
    assert_eq!(surface.to_plain(), "\u{2824}\u{2824}");
}

#[test]
fn fully_outside_segments_draw_nothing() {
    let mut surface = Surface::new(2, 1, Charset::Braille);
    surface.line((-5.0, -5.0), (-1.0, -1.0), Color::Default);
    surface.set(99, 0, Color::Default);
    surface.set(-1, 0, Color::Default);
    assert_eq!(surface.to_plain(), "");
}

#[test]
fn non_finite_coordinates_draw_nothing() {
    let mut surface = Surface::new(2, 1, Charset::Braille);
    surface.line((f64::NAN, 0.0), (3.0, 3.0), Color::Default);
    surface.dot(f64::INFINITY, 0.0, Color::Default);
    assert_eq!(surface.to_plain(), "");
}

#[test]
fn text_wins_over_pixels_in_shared_cells() {
    let mut surface = Surface::new(3, 1, Charset::Braille);
    surface.line((0.0, 0.0), (5.0, 0.0), Color::Default);
    surface.text(0, 0, "ab", Color::Default);
    assert_eq!(surface.to_plain(), "ab\u{2809}");
}

#[test]
fn text_clips_at_the_edges() {
    let mut surface = Surface::new(3, 1, Charset::Ascii);
    surface.text(-1, 0, "abcde", Color::Default);
    assert_eq!(surface.to_plain(), "bcd");
    let mut surface = Surface::new(3, 1, Charset::Ascii);
    surface.text(0, -1, "abc", Color::Default);
    surface.text(0, 1, "abc", Color::Default);
    assert_eq!(surface.to_plain(), "");
}

#[test]
fn wide_glyphs_occupy_two_cells_and_keep_alignment() {
    let mut surface = Surface::new(5, 1, Charset::Ascii);
    surface.text(0, 0, "\u{65E5}\u{672C}", Color::Default);
    surface.text(4, 0, "x", Color::Default);
    assert_eq!(surface.to_plain(), "\u{65E5}\u{672C}x");
}

#[test]
fn a_wide_glyph_straddling_the_edge_is_dropped_whole() {
    let mut surface = Surface::new(3, 1, Charset::Ascii);
    surface.text(0, 0, "\u{65E5}\u{672C}", Color::Default);
    assert_eq!(surface.to_plain(), "\u{65E5}");
}

#[test]
fn overwriting_half_a_wide_glyph_blanks_the_other_half() {
    let mut surface = Surface::new(4, 1, Charset::Ascii);
    surface.text(0, 0, "\u{65E5}", Color::Default);
    surface.text(1, 0, "a", Color::Default);
    assert_eq!(surface.to_plain(), " a");

    let mut surface = Surface::new(4, 1, Charset::Ascii);
    surface.text(0, 0, "\u{65E5}", Color::Default);
    surface.text(0, 0, "b", Color::Default);
    assert_eq!(surface.to_plain(), "b");
}

#[test]
fn combining_marks_are_dropped_at_the_cell_grid() {
    let mut surface = Surface::new(4, 1, Charset::Ascii);
    surface.text(0, 0, "e\u{0301}x", Color::Default);
    assert_eq!(surface.to_plain(), "ex");
}

#[test]
fn plain_output_trims_trailing_spaces_and_keeps_leading_ones() {
    let mut surface = Surface::new(4, 2, Charset::Ascii);
    surface.set(0, 0, Color::Default);
    surface.set(1, 1, Color::Default);
    assert_eq!(surface.to_plain(), "*\n *");
}

#[test]
fn empty_surfaces_encode_to_nothing() {
    assert_eq!(Surface::new(0, 0, Charset::Braille).to_plain(), "");
    assert_eq!(
        Surface::new(0, 0, Charset::Braille).encode(ColorMode::Ansi16),
        ""
    );
    assert_eq!(
        Surface::new(3, 0, Charset::Ascii).encode(ColorMode::Ansi16),
        ""
    );
}

#[test]
fn oversized_surfaces_are_fallible_and_the_convenience_constructor_stays_safe() {
    let error =
        Surface::try_new(usize::MAX, 0, Charset::Braille).expect_err("oversized surface must fail");
    assert!(matches!(error, crate::Error::DimensionTooLarge { .. }));

    let surface = Surface::new(usize::MAX, 0, Charset::Braille);
    assert_eq!(surface.size(), (0, 0));
    assert_eq!(surface.to_plain(), "");
}

#[test]
fn uncolored_surfaces_encode_identically_in_both_encoders() {
    let mut surface = Surface::new(4, 2, Charset::Braille);
    surface.line((0.0, 0.0), (7.0, 7.0), Color::Default);
    assert_eq!(surface.encode(ColorMode::Ansi16), surface.to_plain());
}

#[test]
fn ansi_runs_emit_one_code_per_color_change() {
    let mut surface = Surface::new(3, 1, Charset::Ascii);
    surface.set(0, 0, Color::Red);
    surface.set(1, 0, Color::Red);
    surface.set(2, 0, Color::Blue);
    assert_eq!(
        surface.encode(ColorMode::Ansi16),
        "\x1b[31m**\x1b[34m*\x1b[0m"
    );
}

#[test]
fn the_last_write_owns_a_shared_cell_color() {
    let mut surface = Surface::new(2, 1, Charset::Ascii);
    surface.set(0, 0, Color::Red);
    surface.set(1, 0, Color::Red);
    surface.set(1, 0, Color::Blue);
    assert_eq!(
        surface.encode(ColorMode::Ansi16),
        "\x1b[31m*\x1b[34m*\x1b[0m"
    );
}

#[test]
fn colored_rows_end_with_a_reset_even_after_trimming() {
    let mut surface = Surface::new(4, 1, Charset::Ascii);
    surface.set(0, 0, Color::Green);
    let encoded = surface.encode(ColorMode::Ansi16);
    assert_eq!(encoded, "\x1b[32m*\x1b[0m");
}

#[test]
fn two_vertical_samples_snapshot_every_string_color_tier() {
    let mut surface = Surface::new(1, 1, Charset::Quadrants);
    patch(&mut surface, 0, Some((0.0, Color::Rgb(255, 0, 0))));
    patch(&mut surface, 1, Some((1.0, Color::Rgb(0, 0, 255))));

    let snapshots = [
        (ColorMode::Plain, "\u{2593}"),
        (ColorMode::Ansi16, "\x1b[91;44m\u{2580}\x1b[0m"),
        (ColorMode::Ansi256, "\x1b[38;5;196;48;5;21m\u{2580}\x1b[0m"),
        (
            ColorMode::TrueColor,
            "\x1b[38;2;255;0;0;48;2;0;0;255m\u{2580}\x1b[0m",
        ),
    ];
    for (mode, expected) in snapshots {
        assert_eq!(surface.encode(mode), expected, "{mode:?}");
    }
}

#[test]
fn same_color_and_missing_half_cell_patches_have_stable_fallbacks() {
    let mut solid = Surface::new(1, 1, Charset::Quadrants);
    patch(&mut solid, 0, Some((0.0, Color::Red)));
    patch(&mut solid, 1, Some((1.0, Color::Red)));
    assert_eq!(solid.to_plain(), "\u{2593}");
    assert_eq!(solid.encode(ColorMode::Ansi16), "\x1b[31m\u{2588}\x1b[0m");

    let mut top = Surface::new(1, 1, Charset::Quadrants);
    patch(&mut top, 0, Some((0.25, Color::Red)));
    patch(&mut top, 1, None);
    assert_eq!(top.to_plain(), "\u{2592}");
    assert_eq!(top.encode(ColorMode::Ansi16), "\x1b[31m\u{2580}\x1b[0m");

    let mut bottom = Surface::new(1, 1, Charset::Quadrants);
    patch(&mut bottom, 0, None);
    patch(&mut bottom, 1, Some((0.75, Color::Blue)));
    assert_eq!(bottom.to_plain(), "\u{2588}");
    assert_eq!(bottom.encode(ColorMode::Ansi16), "\x1b[34m\u{2584}\x1b[0m");
}

#[test]
fn foreground_and_background_reset_together_at_run_boundaries() {
    let rect = PlotRect {
        columns: 2,
        ..ONE_CELL
    };
    let mut surface = Surface::new(2, 1, Charset::Quadrants);
    Canvas::patch(&mut surface, 0, 0, rect, Some((0.0, Color::Red)));
    Canvas::patch(&mut surface, 0, 1, rect, Some((1.0, Color::Blue)));
    surface.text(1, 0, "x", Color::Green);
    assert_eq!(
        surface.encode(ColorMode::Ansi16),
        "\x1b[31;44m\u{2580}\x1b[32;49mx\x1b[0m"
    );

    surface.text(0, 0, "y", Color::Yellow);
    assert_eq!(
        surface.encode(ColorMode::Ansi16),
        "\x1b[33my\x1b[32mx\x1b[0m"
    );
}

#[test]
fn patch_pair_overwrites_are_atomic_and_identical_styles_share_one_sgr_run() {
    let rect = PlotRect {
        columns: 2,
        ..ONE_CELL
    };
    let mut surface = Surface::new(2, 1, Charset::Quadrants);
    for column in 0..2 {
        Canvas::patch(&mut surface, column, 0, rect, Some((0.0, Color::Red)));
        Canvas::patch(&mut surface, column, 1, rect, Some((1.0, Color::Blue)));
    }
    assert_eq!(
        surface.encode(ColorMode::Ansi16),
        "\x1b[31;44m\u{2580}\u{2580}\x1b[0m"
    );

    Canvas::patch(&mut surface, 0, 0, rect, None);
    Canvas::patch(&mut surface, 0, 1, rect, None);
    assert_eq!(
        surface.encode(ColorMode::Ansi16),
        "\x1b[31;44m\u{2580}\u{2580}\x1b[0m"
    );

    Canvas::patch(&mut surface, 0, 0, rect, Some((0.25, Color::Green)));
    Canvas::patch(&mut surface, 0, 1, rect, Some((0.75, Color::Yellow)));
    assert_eq!(
        surface.encode(ColorMode::Ansi16),
        "\x1b[32;43m\u{2580}\x1b[31;44m\u{2580}\x1b[0m"
    );
}

#[cfg(feature = "evcxr")]
#[test]
fn html_escapes_every_glyph_that_can_open_markup() {
    let mut surface = Surface::new(3, 1, Charset::Ascii);
    surface.text(0, 0, "<&>", Color::Default);
    assert_eq!(surface.encode_html(), "&lt;&amp;&gt;");
}

#[cfg(feature = "evcxr")]
#[test]
fn html_collapses_concrete_rgb_runs() {
    let mut surface = Surface::new(4, 1, Charset::Ascii);
    surface.set(0, 0, Color::Red);
    surface.set(1, 0, Color::Rgb(205, 0, 0));
    surface.set(2, 0, Color::Blue);
    assert_eq!(
        surface.encode_html(),
        "<span style=\"color:#cd0000\">**</span><span style=\"color:#0000ee\">*</span>"
    );
}

#[cfg(feature = "evcxr")]
#[test]
fn html_preserves_both_colors_of_a_half_block() {
    let mut surface = Surface::new(1, 1, Charset::Quadrants);
    patch(&mut surface, 0, Some((0.0, Color::Red)));
    patch(&mut surface, 1, Some((1.0, Color::Blue)));
    assert_eq!(
        surface.encode_html(),
        "<span style=\"color:#cd0000;background-color:#0000ee\">\u{2580}</span>"
    );
}

#[cfg(feature = "evcxr")]
#[test]
fn default_html_color_inherits_without_a_span() {
    let mut surface = Surface::new(3, 1, Charset::Ascii);
    surface.set(0, 0, Color::Red);
    surface.set(1, 0, Color::Default);
    surface.set(2, 0, Color::Red);
    let html = surface.encode_html();
    assert_eq!(
        html,
        "<span style=\"color:#cd0000\">*</span>*<span style=\"color:#cd0000\">*</span>"
    );
    assert!(!html.contains("#808080"));
}

#[cfg(feature = "evcxr")]
#[test]
fn html_spaces_extend_runs_and_trailing_spaces_are_trimmed() {
    let mut surface = Surface::new(6, 2, Charset::Ascii);
    surface.text(0, 0, "x", Color::Green);
    surface.text(2, 0, "x", Color::Green);
    surface.text(1, 1, "y", Color::Blue);
    assert_eq!(
        surface.encode_html(),
        "<span style=\"color:#00cd00\">x x</span>\n <span style=\"color:#0000ee\">y</span>"
    );
}