use std::process::ExitCode;
use monospace_core::{
Buffer, Cell, GlyphCatalog, Offset, OwnerReader, Pos, ShapeId, Size, StampMode, StampSink,
Symbol, SymbolCell, SymbolStyleName, Window, render,
};
use monospace_description::parse;
use monospace_diagram::{Anchor, Delta, Diagram, End};
use monospace_editing::{Command, Session};
const DEMO: &str = include_str!("../assets/demo.json");
const THE_BACK_MOST_AT: Offset = Offset { x: 0, y: 0 };
const THE_HUNG_FROM_AT: Offset = Offset { x: 9, y: 4 };
const THE_ARROW_AT: Offset = Offset { x: 14, y: 3 };
const THE_CROSSING_AT: Offset = Offset { x: 20, y: 2 };
#[cfg(test)]
mod sweep;
const USAGE: &str = "usage: monospace-cli [--size <width>x<height>] [--origin <x>,<y>] [path]\n monospace-cli --symbols";
#[derive(Debug, Clone, PartialEq, Eq)]
enum Mode {
Demonstration,
Description {
path: String,
window: Option<Window>,
},
Symbols,
}
const DEMO_WINDOW: Window = Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 50,
height: 17,
},
};
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
let mode = match parse_args(&args) {
Ok(mode) => mode,
Err(message) => {
eprintln!("{message}");
return ExitCode::FAILURE;
}
};
let (text, window, demonstrates) = match mode {
Mode::Symbols => {
print!("{}", sheet());
return ExitCode::SUCCESS;
}
Mode::Demonstration => (DEMO.to_owned(), DEMO_WINDOW, true),
Mode::Description { path, window } => {
let text = match std::fs::read_to_string(&path) {
Ok(text) => text,
Err(error) => {
eprintln!("{path}: {error}");
return ExitCode::FAILURE;
}
};
(text, window.unwrap_or(DEMO_WINDOW), false)
}
};
let diagram = match parse(&text) {
Ok(diagram) => diagram,
Err(error) => {
eprintln!("{error}");
return ExitCode::FAILURE;
}
};
print!(
"{}",
if demonstrates {
demonstrate(diagram, window)
} else {
render_once(&diagram, window)
}
);
ExitCode::SUCCESS
}
fn parse_args(args: &[String]) -> Result<Mode, String> {
let mut path: Option<String> = None;
let mut origin: Option<Pos> = None;
let mut size: Option<Size> = None;
let mut symbols = false;
let mut rest = args.iter();
while let Some(argument) = rest.next() {
match argument.as_str() {
"--symbols" => {
if symbols {
return Err(format!("`--symbols` was given twice\n{USAGE}"));
}
if let Some(path) = &path {
return Err(format!(
"`{path}` cannot be given beside `--symbols`\n{USAGE}"
));
}
if size.is_some() {
return Err(format!(
"`--size` cannot be given beside `--symbols`\n{USAGE}"
));
}
if origin.is_some() {
return Err(format!(
"`--origin` cannot be given beside `--symbols`\n{USAGE}"
));
}
symbols = true;
}
"--size" => {
if symbols {
return Err(format!(
"`--size` cannot be given beside `--symbols`\n{USAGE}"
));
}
let Some(value) = rest.next() else {
return Err(format!("`--size` was given no size\n{USAGE}"));
};
if size.is_some() {
return Err(format!("`--size` was given twice\n{USAGE}"));
}
size = Some(parse_size(value)?);
}
"--origin" => {
if symbols {
return Err(format!(
"`--origin` cannot be given beside `--symbols`\n{USAGE}"
));
}
let Some(value) = rest.next() else {
return Err(format!("`--origin` was given no origin\n{USAGE}"));
};
if origin.is_some() {
return Err(format!("`--origin` was given twice\n{USAGE}"));
}
origin = Some(parse_origin(value)?);
}
_ if argument.starts_with('-') => {
return Err(format!("`{argument}` is not an option\n{USAGE}"));
}
_ if symbols => {
return Err(format!(
"`{argument}` cannot be given beside `--symbols`\n{USAGE}"
));
}
_ if path.is_some() => {
return Err(format!("`{argument}` is a second path\n{USAGE}"));
}
_ => path = Some(argument.clone()),
}
}
if symbols {
return Ok(Mode::Symbols);
}
let window = size.map(|size| Window {
origin: origin.unwrap_or(Pos { x: 0, y: 0 }),
size,
});
Ok(match path {
Some(path) => Mode::Description { path, window },
None => Mode::Demonstration,
})
}
fn parse_size(text: &str) -> Result<Size, String> {
let said = || format!("`--size` takes `<width>x<height>` and not `{text}`\n{USAGE}");
let (width, height) = text.split_once('x').ok_or_else(said)?;
Ok(Size {
width: width.parse().map_err(|_| said())?,
height: height.parse().map_err(|_| said())?,
})
}
fn parse_origin(text: &str) -> Result<Pos, String> {
let said = || format!("`--origin` takes `<x>,<y>` and not `{text}`\n{USAGE}");
let (x, y) = text.split_once(',').ok_or_else(said)?;
Ok(Pos {
x: x.parse().map_err(|_| said())?,
y: y.parse().map_err(|_| said())?,
})
}
fn render_once(diagram: &Diagram, window: Window) -> String {
picture(diagram, &monospace_glyph_sets::all(), window)
}
fn sheet() -> String {
let catalog = monospace_glyph_sets::all();
let styles = catalog.symbol_styles();
let name_width = styles
.iter()
.map(|style| style.0.chars().count())
.max()
.unwrap_or(0);
let mut out = String::new();
for style in styles {
out.push_str(&style.0);
for _ in 0..=name_width - style.0.chars().count() {
out.push(' ');
}
out.push_str(&symbol_row(&catalog, style));
}
out
}
fn symbol_row(catalog: &GlyphCatalog, style: &SymbolStyleName) -> String {
let window = Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: u16::try_from(Symbol::ALL.len()).expect("seven symbols fit a row"),
height: 1,
},
};
let mut buffer = Buffer::new(window);
for (x, &symbol) in Symbol::ALL.iter().enumerate() {
buffer.stamp(
Pos {
x: i16::try_from(x).expect("seven symbols fit a row"),
y: 0,
},
Cell::Symbol(SymbolCell {
symbol,
style: style.clone(),
}),
StampMode::Above,
None,
);
}
render(&buffer, catalog, window)
}
fn drawn(diagram: &Diagram, catalog: &GlyphCatalog, window: Window) -> (Buffer, String) {
let mut buffer = Buffer::new(window);
diagram.draw(&mut buffer);
let text = monospace_core::render(&buffer, catalog, buffer.window());
(buffer, text)
}
fn owner_at(buffer: &Buffer, at: Offset) -> Option<ShapeId> {
buffer.window().position(at).and_then(|at| buffer.owner(at))
}
fn picture(diagram: &Diagram, catalog: &GlyphCatalog, window: Window) -> String {
drawn(diagram, catalog, window).1
}
fn demonstrate(diagram: Diagram, window: Window) -> String {
let catalog = monospace_glyph_sets::all();
let mut session = Session::new(diagram);
let (as_written, first_picture) = drawn(session.diagram(), &catalog, window);
let the_back_most = owner_at(&as_written, THE_BACK_MOST_AT);
let the_hung_from = owner_at(&as_written, THE_HUNG_FROM_AT);
let the_arrow = owner_at(&as_written, THE_ARROW_AT);
let first_caption = format!(
"As written, asking which shape decided ({}, {}), ({}, {}) and ({}, {}):\n",
THE_BACK_MOST_AT.x,
THE_BACK_MOST_AT.y,
THE_HUNG_FROM_AT.x,
THE_HUNG_FROM_AT.y,
THE_ARROW_AT.x,
THE_ARROW_AT.y
);
let mut out = first_caption;
out.push_str(&first_picture);
let by = Delta { dx: 0, dy: 3 };
let four_right = Delta { dx: 4, dy: 0 };
let two_down = Delta { dx: 0, dy: 2 };
if let Some(the_back_most) = the_back_most {
session = session.perform(Command::Forward { id: the_back_most });
}
out.push_str("\nWith the back-most shape moved one place forward:\n");
out.push_str(&picture(session.diagram(), &catalog, window));
if let Some(the_back_most) = the_back_most {
session = session.perform(Command::Move {
id: the_back_most,
by,
});
}
out.push_str("\nWith that same shape displaced:\n");
out.push_str(&picture(session.diagram(), &catalog, window));
if let Some(the_back_most) = the_back_most {
session = session.perform(Command::Remove { id: the_back_most });
}
out.push_str("\nWith that same shape taken out:\n");
out.push_str(&picture(session.diagram(), &catalog, window));
if let (Some(the_arrow), Some(the_hung_from)) = (the_arrow, the_hung_from) {
session = session.perform(Command::Hang {
id: the_arrow,
end: End::From,
target: the_hung_from,
anchor: Anchor::Right,
});
}
if let Some(the_hung_from) = the_hung_from {
session = session.perform(Command::Move {
id: the_hung_from,
by: four_right,
});
}
out.push_str("\nWith the arrow now hanging from that box, and the box displaced:\n");
out.push_str(&picture(session.diagram(), &catalog, window));
if let Some(the_arrow) = the_arrow {
session = session.perform(Command::Move {
id: the_arrow,
by: two_down,
});
}
out.push_str("\nWith the arrow displaced as well:\n");
out.push_str(&picture(session.diagram(), &catalog, window));
if let Some(the_hung_from) = the_hung_from {
session = session.perform(Command::Remove { id: the_hung_from });
}
out.push_str("\nWith the box the arrow hangs from taken out:\n");
out.push_str(&picture(session.diagram(), &catalog, window));
if let Some(named) = owner_at(&as_written, THE_CROSSING_AT) {
session = session.perform(Command::Remove { id: named });
}
let crossing_caption = format!(
"\nWith the shape the picture names at ({}, {}) taken out:\n",
THE_CROSSING_AT.x, THE_CROSSING_AT.y
);
out.push_str(&crossing_caption);
out.push_str(&picture(session.diagram(), &catalog, window));
while session.can_undo() {
session = session.undo();
let caption = if session.can_undo() {
"\nGiven back one step:\n"
} else {
"\nGiven back every step, which is the picture this run began with:\n"
};
out.push_str(caption);
out.push_str(&picture(session.diagram(), &catalog, window));
}
out
}
#[cfg(test)]
mod tests {
use std::num::NonZeroU16;
use std::ops::Range;
use monospace_core::{
BoxShape, Buffer, FigureLayer, Glyph, GlyphCatalog, Offset, OwnerReader, Pos, Shape, Size,
StampMode, Stroke, Window, render,
};
use monospace_diagram::ShapeId;
use super::{Mode, demonstrate, drawn, parse_args, render_once, sheet};
use crate::parse;
fn in_the_demo_window() -> Window {
super::DEMO_WINDOW
}
fn a_four_by_three_window() -> Window {
Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 4,
height: 3,
},
}
}
fn a_six_by_four_window() -> Window {
Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 6,
height: 4,
},
}
}
fn render_json(json: &str, window: Window) -> String {
let diagram = parse(json).expect("well-formed description");
render_once(&diagram, window)
}
fn demonstrate_json(json: &str, window: Window) -> String {
let diagram = parse(json).expect("well-formed description");
demonstrate(diagram, window)
}
fn identity(ordinal: u16) -> ShapeId {
ShapeId::new(NonZeroU16::new(ordinal).expect("no test names zero"))
}
fn one_box_json() -> &'static str {
r#"{
"next_id": 2,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 0, "y": 0 }, "size": { "width": 4, "height": 3 },
"stroke": "light", "fill": "░" }
]
}"#
}
#[test]
fn a_one_box_description_renders_the_same_as_a_box_shape_drawn_directly() {
let diagram = parse(one_box_json()).expect("well-formed description");
let origin = Pos { x: 0, y: 0 };
let size = Size {
width: 4,
height: 3,
};
let window = Window { origin, size };
let mut buffer = Buffer::new(window);
BoxShape {
at: origin,
size,
stroke: Stroke::from("light"),
fill: Some(Glyph::new("░").expect("\"░\" is one glyph")),
}
.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
let expected = render(&buffer, &GlyphCatalog::light(), buffer.window());
assert_eq!(render_once(&diagram, window), expected);
}
#[test]
fn rendering_once_is_the_demonstrations_first_picture_and_nothing_else() {
let window = a_four_by_three_window();
let (first, ..) = demonstrated_pictures(one_box_json(), window);
let diagram = parse(one_box_json()).expect("well-formed description");
assert_eq!(render_once(&diagram, window), first);
}
fn demonstrated_blocks(json: &str, window: Window) -> Vec<String> {
let output = demonstrate_json(json, window);
output
.trim_end_matches('\n')
.split("\n\n")
.map(|block| {
let (_caption, picture) = block
.split_once('\n')
.expect("a caption line precedes each picture");
format!("{}\n", picture.strip_suffix('\n').unwrap_or(picture))
})
.collect()
}
#[allow(clippy::type_complexity)]
fn demonstrated_pictures(
json: &str,
window: Window,
) -> (
String,
String,
String,
String,
String,
String,
String,
String,
) {
let blocks = demonstrated_blocks(json, window);
let mut taken = 0;
let mut next_picture = || {
let block = blocks
.get(taken)
.expect("eight captioned pictures, each after a blank line")
.clone();
taken += 1;
block
};
(
next_picture(),
next_picture(),
next_picture(),
next_picture(),
next_picture(),
next_picture(),
next_picture(),
next_picture(),
)
}
#[test]
fn two_overlapping_boxes_demonstrate_in_opposite_orders() {
let (first, second, ..) = demonstrated_pictures(
r#"{
"next_id": 3,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 0, "y": 0 }, "size": { "width": 4, "height": 3 },
"stroke": "light", "fill": "░" },
{ "kind": "box", "id": 2, "at": { "x": 2, "y": 1 }, "size": { "width": 4, "height": 3 },
"stroke": "light", "fill": "▓" }
]
}"#,
a_six_by_four_window(),
);
let size = Size {
width: 4,
height: 3,
};
let a = BoxShape {
at: Pos { x: 0, y: 0 },
size,
stroke: Stroke::from("light"),
fill: Some(Glyph::new("░").expect("\"░\" is one glyph")),
};
let b = BoxShape {
at: Pos { x: 2, y: 1 },
size,
stroke: Stroke::from("light"),
fill: Some(Glyph::new("▓").expect("\"▓\" is one glyph")),
};
assert_eq!(first, stamped_back_to_front(&a, &b));
assert_eq!(second, stamped_back_to_front(&b, &a));
}
fn stamped_back_to_front(back: &BoxShape, front: &BoxShape) -> String {
let origin = Pos { x: 0, y: 0 };
let size = Size {
width: 6,
height: 4,
};
let mut buffer = Buffer::new(Window { origin, size });
back.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
front.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
render(&buffer, &GlyphCatalog::light(), buffer.window())
}
#[test]
fn the_tenth_entry_naming_a_reference_leaves_the_first_five_pictures_exactly_as_they_were() {
let mut value: serde_json::Value =
serde_json::from_str(super::DEMO).expect("the embedded description is well-formed");
let tenth = value["shapes"]
.as_array_mut()
.expect("the description lists its shapes")
.iter_mut()
.find(|shape| {
shape["kind"] == serde_json::json!("connector")
&& shape["id"] == serde_json::json!(10)
})
.expect("the tenth entry is the one connector this description holds");
assert_eq!(
tenth["to"]["at"]["kind"],
serde_json::json!("reference"),
"the tenth entry spells its far endpoint as a reference, which is the other side of every \
comparison below"
);
tenth["to"]["at"] = serde_json::json!({ "kind": "point", "x": 22, "y": 4 });
tenth["to"]["leaving"] = serde_json::json!("down");
let with_the_point = value.to_string();
let (first, second, third, fourth, fifth, sixth, seventh, eighth) =
demonstrated_pictures(super::DEMO, super::DEMO_WINDOW);
let (first2, second2, third2, fourth2, fifth2, sixth2, seventh2, eighth2) =
demonstrated_pictures(&with_the_point, super::DEMO_WINDOW);
assert_eq!(
(&first, &second, &third, &fourth, &fifth),
(&first2, &second2, &third2, &fourth2, &fifth2),
"naming the far endpoint as a reference changed one of the first five pictures"
);
assert_eq!(
sixth, sixth2,
"the sixth picture differs between a spelled endpoint and a named one: both are this \
slice's own step, and both reach the same cell two rows lower"
);
assert_eq!(
seventh, seventh2,
"the seventh picture differs between a spelled endpoint and a named one"
);
assert_eq!(
eighth, eighth2,
"the eighth picture differs between a spelled endpoint and a named one"
);
assert_eq!(first, render_json(super::DEMO, super::DEMO_WINDOW));
assert_eq!(first2, render_json(&with_the_point, super::DEMO_WINDOW));
assert_ne!(
fourth, fifth,
"the fifth picture must be the fourth with a figure moved"
);
}
#[test]
fn a_bare_run_prints_eight_captioned_pictures_the_first_being_the_description_as_written() {
let output = demonstrate_json(super::DEMO, super::DEMO_WINDOW);
assert_eq!(
output.split("\n\n").count(),
15,
"eight pictures as the diagram changes and seven walked back: {output:?}"
);
let (first, ..) = demonstrated_pictures(super::DEMO, super::DEMO_WINDOW);
assert_eq!(first, render_json(super::DEMO, super::DEMO_WINDOW));
}
#[test]
fn the_demonstration_runs_every_step_through_a_session() {
let blocks = demonstrated_blocks(super::DEMO, super::DEMO_WINDOW);
assert_eq!(
blocks.len(),
15,
"eight pictures as the diagram changes and seven steps walked back"
);
let empty = demonstrated_blocks(
r#"{
"next_id": 1,
"shapes": []
}"#,
a_four_by_three_window(),
);
assert_eq!(
empty.len(),
8,
"a description no offset reaches records no step, so there is nothing to walk back over \
and the eight pictures are all it prints"
);
}
#[test]
fn the_demonstration_walks_back_to_the_picture_it_began_with() {
let blocks = demonstrated_blocks(super::DEMO, super::DEMO_WINDOW);
assert_eq!(
*blocks.last().expect("the run printed pictures"),
blocks[0],
"the last picture walked back is the one the run began with"
);
for step in 0..7 {
let walked = 8 + step;
let forward = 6 - step;
assert_eq!(
blocks[walked], blocks[forward],
"the picture walked back at step {step} matches the forward picture for that state"
);
}
}
#[test]
fn a_path_draws_one_picture_and_walks_nowhere() {
let (first, ..) = demonstrated_pictures(super::DEMO, super::DEMO_WINDOW);
let once = render_json(super::DEMO, super::DEMO_WINDOW);
assert_eq!(
once, first,
"a path prints the first picture, byte for byte"
);
assert!(
!once.contains("\n\n"),
"a path prints no caption and no second picture: {once:?}"
);
assert_eq!(
once.lines().count(),
17,
"the shipped window is seventeen rows tall and nothing is added: {once:?}"
);
}
#[test]
fn the_bare_run_prints_a_labelled_box() {
for picture in demonstrated_blocks(super::DEMO, super::DEMO_WINDOW) {
let rows: Vec<&str> = picture
.lines()
.skip(13)
.take(4)
.map(str::trim_end)
.collect();
assert_eq!(
rows,
["┌────────┐", "│ Hello │", "│ World! │", "└────────┘"],
"every picture carries the labelled box at its bottom left: {picture:?}"
);
}
}
fn the_glyph_at(picture: &str, at: (usize, usize)) -> char {
let (x, y) = at;
picture
.lines()
.nth(y)
.and_then(|row| row.chars().nth(x))
.unwrap_or(' ')
}
fn the_first_figures_footprint() -> (Range<usize>, Range<usize>) {
(0..4, 0..6)
}
fn differing(first: &str, second: &str) -> Vec<(usize, usize)> {
first
.lines()
.zip(second.lines())
.enumerate()
.flat_map(|(y, (one, other))| {
one.chars()
.zip(other.chars())
.enumerate()
.filter(|(_, (one, other))| one != other)
.map(move |(x, _)| (x, y))
})
.collect()
}
#[test]
fn the_third_picture_moves_one_figure_and_the_fourth_takes_that_figure_out() {
let (_first, second, third, fourth, ..) =
demonstrated_pictures(super::DEMO, super::DEMO_WINDOW);
let (columns, rows) = the_first_figures_footprint();
let moved = differing(&second, &third);
assert!(!moved.is_empty(), "the displacement changed nothing at all");
assert!(
moved
.iter()
.all(|(x, y)| columns.contains(x) && rows.contains(y)),
"the displacement reached outside the figure's own cells: {moved:?}"
);
assert_eq!(
fourth,
render_json(&demo_without_its_first_entry(), super::DEMO_WINDOW)
);
}
fn the_hung_figures_footprint() -> (
Range<usize>,
Range<usize>,
Range<usize>,
Range<usize>,
usize,
) {
((9..13), (2..5), (13..17), (2..5), 22)
}
#[test]
fn the_fifth_picture_moves_the_box_and_takes_the_arrow_with_it() {
let (_first, _second, _third, fourth, fifth, ..) =
demonstrated_pictures(super::DEMO, super::DEMO_WINDOW);
let (old_columns, old_rows, new_columns, new_rows, the_far_end_column) =
the_hung_figures_footprint();
let reached = |at: (usize, usize)| {
let (x, y) = at;
let in_a_footprint = (old_columns.contains(&x) && old_rows.contains(&y))
|| (new_columns.contains(&x) && new_rows.contains(&y));
let on_the_route = (new_rows.start..=new_rows.end + 1).contains(&y)
&& (new_columns.end..=22).contains(&x);
in_a_footprint || on_the_route
};
let changed = differing(&fourth, &fifth);
assert!(
!changed.is_empty(),
"the fifth picture changed nothing at all"
);
assert!(
changed.iter().copied().all(reached),
"the fifth picture changed a cell neither box holds nor the arrow runs through: {changed:?}"
);
assert!(
changed.iter().all(|(x, _)| *x <= the_far_end_column),
"the change reached past the endpoint that did not move: {changed:?}"
);
let occupied_before = |(x, y): (usize, usize)| {
fourth
.lines()
.nth(y)
.and_then(|row| row.chars().nth(x))
.is_some_and(|glyph| glyph != ' ')
};
assert!(
(new_columns.clone())
.any(|x| (new_rows.clone()).any(|y| reached((x, y)) && !occupied_before((x, y)))),
"the box did not land where the demonstration says it lands"
);
}
fn demo_without_its_first_entry() -> String {
let mut value: serde_json::Value =
serde_json::from_str(super::DEMO).expect("the embedded description is well-formed");
let shapes = value["shapes"]
.as_array_mut()
.expect("the description lists its shapes");
shapes.remove(0);
value.to_string()
}
#[test]
fn an_empty_description_demonstrates_as_eight_identical_pictures() {
let pictures = demonstrated_pictures(
r#"{
"next_id": 2,
"shapes": []
}"#,
a_four_by_three_window(),
);
assert_eq!(pictures.0, pictures.1);
assert_eq!(pictures.0, pictures.2);
assert_eq!(pictures.0, pictures.3);
assert_eq!(pictures.0, pictures.4);
assert_eq!(pictures.0, pictures.5);
assert_eq!(pictures.0, pictures.6);
assert_eq!(pictures.0, pictures.7);
}
#[test]
fn one_shape_demonstrates_as_two_copies_of_itself_and_then_an_empty_window() {
let pictures = demonstrated_pictures(one_box_json(), a_four_by_three_window());
assert_eq!(pictures.0, pictures.1);
assert_eq!(pictures.2, pictures.3);
assert_ne!(pictures.0, pictures.2);
assert_eq!(pictures.3, pictures.4);
assert_eq!(pictures.4, pictures.5);
assert_eq!(pictures.5, pictures.6);
assert_eq!(pictures.6, pictures.7);
}
fn the_two_boxes_and_the_attachment() -> [(Range<usize>, Range<usize>); 2] {
[(13..17, 2..5), (20..24, 1..4)]
}
const THE_ATTACHMENT: (usize, usize) = (16, 3);
fn the_arrow_and_its_removed_box() -> [(Range<usize>, Range<usize>); 2] {
[(16..23, 5..8), (13..17, 2..5)]
}
#[test]
fn the_seventh_picture_takes_the_box_away_and_leaves_the_arrow() {
let (_first, _second, _third, _fourth, _fifth, sixth, seventh, _eighth) =
demonstrated_pictures(super::DEMO, super::DEMO_WINDOW);
let [arrow, the_box] = the_arrow_and_its_removed_box();
let (arrow_columns, arrow_rows) = (arrow.0, arrow.1);
let changed = differing(&sixth, &seventh);
assert_eq!(
changed.len(),
12,
"the seventh differs from the sixth in something other than the twelve cells the box \
held: {changed:?}"
);
assert!(
changed
.iter()
.all(|(x, y)| the_box.0.contains(x) && the_box.1.contains(y)),
"the removal reached outside the box's own rectangle: {changed:?}"
);
assert!(
changed.iter().all(|at| the_glyph_at(&seventh, *at) == ' '),
"a cell the removed box held is still drawn in the seventh: {changed:?}"
);
let the_arrow_held: Vec<(usize, usize)> = arrow_rows
.flat_map(|y| arrow_columns.clone().map(move |x| (x, y)))
.filter(|at| the_glyph_at(&sixth, *at) != ' ')
.collect();
assert_eq!(
the_arrow_held.len(),
10,
"the arrow's footprint is not the ten cells this test measured: {the_arrow_held:?}"
);
for at in the_arrow_held {
assert_eq!(
the_glyph_at(&sixth, at),
the_glyph_at(&seventh, at),
"the arrow did not stand where it stood, at {at:?}"
);
}
}
#[test]
fn the_sixth_picture_moves_only_the_arrow() {
let (_first, _second, _third, _fourth, fifth, sixth, _seventh, _eighth) =
demonstrated_pictures(super::DEMO, super::DEMO_WINDOW);
let changed = differing(&fifth, &sixth);
assert!(
!changed.is_empty(),
"the sixth picture changed nothing at all"
);
let inside: Vec<(usize, usize)> = changed
.iter()
.copied()
.filter(|(x, y)| {
the_two_boxes_and_the_attachment()
.iter()
.any(|(columns, rows)| columns.contains(x) && rows.contains(y))
})
.collect();
assert_eq!(
inside,
vec![THE_ATTACHMENT],
"the sixth picture changed a cell inside a box's own footprint other than the one the \
arrow attaches to, so a box moved: {changed:?}"
);
assert!(
changed.contains(&(22, 4)),
"the arrow's far end is still welded where it was: {changed:?}"
);
assert!(
changed.contains(&(22, 6)),
"the arrow's far end did not travel the two rows the demonstration's delta names: \
{changed:?}"
);
}
const THE_CROSSING_CELLS: [(usize, usize, char, u16); 3] =
[(9, 2, '░', 4), (10, 3, '┘', 4), (11, 3, '░', 3)];
#[test]
fn the_shape_a_position_resolves_to_is_the_front_most_of_the_two_that_wrote_it() {
let diagram = parse(super::DEMO).expect("the shipped description reads");
let catalog = monospace_glyph_sets::all();
let window = in_the_demo_window();
let (buffer, picture) = drawn(&diagram, &catalog, window);
for (x, y, glyph, expected) in THE_CROSSING_CELLS {
let at = Pos {
x: i16::try_from(x).expect("a fifty-column window"),
y: i16::try_from(y).expect("a seventeen-row window"),
};
assert_eq!(the_glyph_at(&picture, (x, y)), glyph, "the cell at {at:?}");
assert_eq!(
buffer.owner(at),
Some(identity(expected)),
"{at:?} is written by two shapes and resolves to the one in front"
);
}
assert_eq!(
super::owner_at(&buffer, Offset { x: 9, y: 2 }),
Some(identity(4))
);
assert_eq!(
super::owner_at(&buffer, Offset { x: 11, y: 3 }),
Some(identity(3)),
"a cell only 3 wrote is 3's, whichever way the order runs"
);
}
#[test]
fn the_shape_the_demonstration_finds_is_the_one_the_crossing_cell_names() {
let (_first, .., seventh, eighth) = demonstrated_pictures(super::DEMO, super::DEMO_WINDOW);
assert_eq!(
the_glyph_at(&seventh, (20, 2)),
'┼',
"the crossing cell renders as a full junction, which is what two shapes wrote there"
);
assert_eq!(
the_glyph_at(&eighth, (20, 2)),
'│',
"and with the shape the record named taken out, only the other shape's arm stands"
);
assert_ne!(seventh, eighth, "so the eighth picture is not the seventh");
}
#[test]
fn the_four_offsets_answer_the_shapes_the_demonstration_acts_on() {
let diagram = parse(super::DEMO).expect("the shipped description reads");
let window = in_the_demo_window();
let (buffer, _) = drawn(&diagram, &monospace_glyph_sets::all(), window);
for (at, expected, what) in [
(
super::THE_BACK_MOST_AT,
1,
"the back-most shape's own top-left corner",
),
(
super::THE_HUNG_FROM_AT,
3,
"the box the arrow hangs from, at its bottom-left corner",
),
(
super::THE_ARROW_AT,
10,
"the middle of the arrow's own horizontal run",
),
(
super::THE_CROSSING_AT,
6,
"a crossing two shapes wrote, and the record names the one behind",
),
] {
assert_eq!(
super::owner_at(&buffer, at),
Some(identity(expected)),
"the offset ({}, {}) is {what}, and the record names {expected}",
at.x,
at.y
);
}
}
#[test]
fn the_demonstration_asks_the_picture_which_shapes_to_act_on() {
let mut value: serde_json::Value =
serde_json::from_str(super::DEMO).expect("the embedded description is well-formed");
value["shapes"]
.as_array_mut()
.expect("the description lists its shapes")
.push(serde_json::json!({
"kind": "box", "id": 27, "at": { "x": 0, "y": 0 },
"size": { "width": 4, "height": 3 }, "stroke": "light", "fill": "▓"
}));
let (first, second, third, fourth, ..) =
demonstrated_pictures(&value.to_string(), super::DEMO_WINDOW);
let (shipped_first, shipped_second, shipped_third, shipped_fourth, ..) =
demonstrated_pictures(super::DEMO, super::DEMO_WINDOW);
assert_ne!(first, shipped_first, "the extra box is not on the screen");
assert_ne!(second, shipped_second);
assert_ne!(third, shipped_third);
assert_ne!(fourth, shipped_fourth);
assert_eq!(
the_glyph_at(&fourth, (1, 1)),
'░',
"the fill of the entry the demonstration no longer names is back where it was"
);
assert_ne!(
the_glyph_at(&first, (1, 1)),
the_glyph_at(&fourth, (1, 1)),
"and the newcomer that stood over it is gone"
);
}
#[test]
fn the_arguments_are_a_path_and_a_window_and_neither_is_required() {
let args = |words: &[&str]| {
words
.iter()
.map(|word| (*word).to_owned())
.collect::<Vec<String>>()
};
assert_eq!(
parse_args(&args(&[])).expect("a bare run is a run"),
Mode::Demonstration,
"no argument at all is the demonstration"
);
assert_eq!(
parse_args(&args(&["x.json"])).expect("a path is a path"),
Mode::Description {
path: "x.json".to_owned(),
window: None,
},
"a path alone names no window"
);
assert_eq!(
parse_args(&args(&["--size", "20x7", "x.json"])).expect("a size is a window"),
Mode::Description {
path: "x.json".to_owned(),
window: Some(Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 20,
height: 7
}
}),
},
"a size alone is a window at the origin every marker but four uses"
);
assert_eq!(
parse_args(&args(&["--size", "7x5", "--origin", "-3,-2", "x.json"]))
.expect("both halves are a window"),
Mode::Description {
path: "x.json".to_owned(),
window: Some(Window {
origin: Pos { x: -3, y: -2 },
size: Size {
width: 7,
height: 5
}
}),
},
"an origin may be negative, which is what `specs/086` draws at"
);
assert_eq!(
parse_args(&args(&["--origin", "-3,-2", "--size", "7x5", "x.json"]))
.expect("the order on the command line is the caller's"),
parse_args(&args(&["--size", "7x5", "--origin", "-3,-2", "x.json"]))
.expect("the same window either way"),
"a flag after the path is a flag, not a second path"
);
assert_eq!(
parse_args(&args(&["--symbols"])).expect("the sheet names nothing else"),
Mode::Symbols,
"the sheet is a mode of its own and wants neither a path nor a window"
);
}
#[test]
fn the_sheet_pads_each_name_and_keeps_the_space_a_miss_draws() {
let printed = sheet();
let lines: Vec<&str> = printed.lines().collect();
assert_eq!(lines.len(), 7, "one row per style");
assert_eq!(lines[0], "arrow ↑→↓← ");
assert_eq!(lines[3], "small-outline ▵▹▿◃ ");
assert!(
lines.iter().all(|line| line.chars().count() == 21),
"every row is the padded name and seven cells: {printed:?}"
);
}
#[test]
fn arguments_this_binary_cannot_read_are_refused_with_what_was_written() {
let args = |words: &[&str]| {
words
.iter()
.map(|word| (*word).to_owned())
.collect::<Vec<String>>()
};
for (words, said) in [
(
&["--size", "20-7"][..],
"`--size` takes `<width>x<height>` and not `20-7`",
),
(
&["--origin", "-3-2"][..],
"`--origin` takes `<x>,<y>` and not `-3-2`",
),
(&["--width", "20"][..], "`--width` is not an option"),
(&["--size"][..], "`--size` was given no size"),
(&["--origin"][..], "`--origin` was given no origin"),
(
&["--size", "4x3", "--size", "6x4"][..],
"`--size` was given twice",
),
(
&["--origin", "0,0", "--origin", "1,1"][..],
"`--origin` was given twice",
),
(&["a.json", "b.json"][..], "`b.json` is a second path"),
(
&["--symbols", "--symbols"][..],
"`--symbols` was given twice",
),
(
&["--symbols", "x.json"][..],
"`x.json` cannot be given beside `--symbols`",
),
(
&["x.json", "--symbols"][..],
"`x.json` cannot be given beside `--symbols`",
),
(
&["--size", "4x3", "--symbols"][..],
"`--size` cannot be given beside `--symbols`",
),
(
&["--symbols", "--origin", "0,0"][..],
"`--origin` cannot be given beside `--symbols`",
),
] {
let error = parse_args(&args(words))
.err()
.unwrap_or_else(|| panic!("{words:?} should have been refused"));
assert!(error.starts_with(said), "expected `{said}`, got: {error}");
assert!(
error.ends_with(super::USAGE),
"every refusal carries the usage beside it: {error}"
);
}
}
#[test]
fn the_demonstrations_eight_pictures_are_unchanged() {
let (first, second, third, fourth, fifth, sixth, seventh, eighth) =
demonstrated_pictures(super::DEMO, super::DEMO_WINDOW);
let eight = [first, second, third, fourth, fifth, sixth, seventh, eighth].join("\n\n");
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/snapshots/contract"
));
settings.bind(|| {
insta::assert_snapshot!("the_demonstrations_eight_pictures_are_unchanged", eight);
});
}
#[test]
fn specs_offsets_resolve_to_the_cells_they_do_today() {
let json = r#"{
"next_id": 4,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 0, "y": 0 }, "size": { "width": 4, "height": 3 },
"stroke": "light" },
{ "kind": "box", "id": 2, "at": { "x": -2, "y": -1 }, "size": { "width": 4, "height": 3 },
"stroke": "double", "fill": "░" },
{ "kind": "box", "id": 3, "at": { "x": 6, "y": 3 }, "size": { "width": 4, "height": 3 },
"stroke": "heavy" } ]
}"#;
let diagram = parse(json).expect("the description reads");
let origin = Pos { x: -3, y: -2 };
let size = Size {
width: 7,
height: 5,
};
let (buffer, picture) = drawn(
&diagram,
&monospace_glyph_sets::all(),
Window { origin, size },
);
for (asked, offset, cell, owner) in [
((-3, -2), (0, 0), ' ', None),
((-3, 0), (0, 2), ' ', None),
((-2, -1), (1, 1), '╔', Some(2)),
((0, 0), (3, 2), '░', Some(2)),
((1, 0), (4, 2), '╟', Some(2)),
((2, 2), (5, 4), '─', Some(1)),
((3, 1), (6, 3), '│', Some(1)),
] {
let at = Offset {
x: offset.0,
y: offset.1,
};
assert_eq!(
super::owner_at(&buffer, at),
owner.map(identity),
"the offset {at:?}, asked for ({}, {}), is {owner:?}",
asked.0,
asked.1
);
assert_eq!(
the_glyph_at(&picture, (offset.0 as usize, offset.1 as usize)),
cell,
"the cell at {at:?} renders {cell:?}"
);
}
assert_eq!(
super::owner_at(&buffer, Offset { x: 7, y: 0 }),
None,
"the offset past the window's right edge reaches a cell nothing holds"
);
}
}