use ratatui::style::Style;
use crate::model::types::Status;
use crate::view::palette;
use crate::view::row::Row;
pub(super) fn tone(row: &Row) -> Style {
let finished = row.status.is_closed() && row.agent.is_none() && row.anomalies.is_none();
if row.agent.is_some() {
palette::TIER_STAFFED
} else if finished {
palette::TIER_FINISHED
} else {
palette::TIER_OPEN
}
}
pub(crate) fn status_style(status: &Status) -> Style {
match status {
Status::InProgress => palette::STATUS_IN_PROGRESS,
Status::Blocked => palette::STATUS_BLOCKED,
Status::Closed => palette::STATUS_CLOSED,
Status::Deferred => palette::STATUS_DEFERRED,
Status::Open => palette::STATUS_OPEN,
Status::Other(_) => palette::ATTENTION,
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use ratatui::style::{Color, Modifier};
use crate::model::anomaly::Anomaly;
use crate::view::draw::bead::{bead_line, elided_run};
use crate::view::draw::project::project_line;
use crate::view::draw::tests::*;
use crate::view::painted::Run;
use crate::view::row::{self, Row, AGENT, WARNING};
fn every_status() -> [Status; 6] {
[
Status::InProgress,
Status::Blocked,
Status::Open,
Status::Deferred,
Status::Closed,
Status::Other(String::new()),
]
}
#[test]
fn no_status_is_told_apart_by_colour_alone() {
for status in every_status() {
let node = node("smt-4kd3p.1", "a bead", status.clone());
let drawn = Painted::of(bead_line(&row(&node), BRANCH, 3), 40, 1).rows();
assert!(
drawn[0].contains(row::status_glyph(&status)),
"{status:?} lost its glyph: {drawn:?}"
);
}
}
#[test]
fn no_colour_is_given_to_two_statuses_and_only_open_goes_without_one() {
let coloured: Vec<Color> = every_status()
.iter()
.filter_map(|status| status_style(status).fg)
.collect();
for (nth, colour) in coloured.iter().enumerate() {
assert!(
!coloured[nth + 1..].contains(colour),
"{colour:?} is drawn for two statuses"
);
}
assert_eq!(
coloured.len(),
every_status().len() - 1,
"one status goes without a colour and it is open"
);
assert_eq!(status_style(&Status::Open).fg, None);
}
#[test]
fn a_status_glyph_is_painted_the_colour_bd_paints_it() {
let bds = [
(Status::InProgress, Color::Rgb(255, 180, 84)),
(Status::Blocked, Color::Rgb(242, 109, 120)),
(Status::Closed, Color::Rgb(128, 144, 160)),
(Status::Deferred, Color::Rgb(108, 118, 128)),
];
for (status, colour) in bds {
let bead = node("smt-4kd3p.1", "a bead", status.clone());
let painted = Painted::of(bead_line(&row(&bead), BRANCH, 3), 60, 1).row(0);
assert!(
painted[1].said.starts_with(row::status_glyph(&status)),
"{status:?}: {painted:?}"
);
assert_eq!(painted[1].style.fg, Some(colour), "{status:?}: {painted:?}");
}
}
#[test]
fn an_open_glyph_takes_the_treatment_of_the_row_it_sits_on() {
let mut staffed = node("smt-4kd3p.1", "a bead", Status::Open);
staffed.agent = Some(a_pane());
let painted = Painted::of(bead_line(&row(&staffed), BRANCH, 3), 90, 1).row(0);
assert!(painted[1].said.starts_with('○'), "{painted:?}");
assert_eq!(
painted[1].style.add_modifier,
Modifier::BOLD,
"the glyph took the row's weight: {painted:?}"
);
assert_eq!(
painted[0].style.add_modifier,
Modifier::empty(),
"and the box-drawing beside it did not: {painted:?}"
);
}
#[test]
fn a_row_with_an_agent_on_it_is_the_ground_and_a_weight_and_one_without_is_the_ground() {
let mut staffed = node("smt-4kd3p.1", "a bead", Status::Open);
staffed.agent = Some(a_pane());
let bright = Painted::of(bead_line(&row(&staffed), BRANCH, 3), 90, 1).row(0);
let plain = Painted::of(
bead_line(
&row(&node("smt-4kd3p.1", "a bead", Status::Open)),
BRANCH,
3,
),
90,
1,
)
.row(0);
assert_eq!(
the_words(&bright).style.fg,
Some(Color::Reset),
"an agent on it, so the row is the terminal's own: {bright:?}"
);
assert_eq!(
the_words(&bright).style.add_modifier,
Modifier::BOLD,
"and a weight is what puts it above the ground: {bright:?}"
);
assert_eq!(
the_words(&plain).style.fg,
Some(Color::Reset),
"nobody on it, so the row is the ground itself: {plain:?}"
);
assert_eq!(
the_words(&plain).style.add_modifier,
Modifier::empty(),
"untreated, which is what makes it the ground: {plain:?}"
);
}
fn the_words(painted: &[Run]) -> &Run {
painted
.iter()
.find(|run| run.said.contains("a bead"))
.expect("the title is drawn")
}
#[test]
fn a_finished_row_nobody_is_on_drops_to_the_one_rung_under_the_ground() {
let painted = Painted::of(
bead_line(
&row(&node("smt-4kd3p.1", "a bead", Status::Closed)),
BRANCH,
3,
),
60,
1,
)
.row(0);
assert_eq!(
painted[1].style.fg,
Some(Color::Rgb(128, 144, 160)),
"the glyph keeps its own status colour: {painted:?}"
);
assert_eq!(painted[2].style.fg, Some(Color::DarkGray), "{painted:?}");
}
#[test]
fn a_closed_bead_whose_pane_is_still_alive_is_not_dimmed() {
let mut alive = node("smt-4kd3p.1", "a bead", Status::Closed);
alive.agent = Some(a_pane());
alive.anomalies = vec![Anomaly::StalePane];
let painted = Painted::of(bead_line(&row(&alive), BRANCH, 3), 110, 1).row(0);
let words = painted
.iter()
.find(|run| run.said.contains("a bead"))
.expect("the row says its title");
assert_eq!(words.style.fg, Some(Color::Reset), "{painted:?}");
assert_eq!(words.style.add_modifier, Modifier::BOLD, "{painted:?}");
}
#[test]
fn a_closed_bead_with_an_anomaly_against_it_is_not_dimmed() {
let mut odd = node("smt-4kd3p.1", "a bead", Status::Closed);
odd.anomalies = vec![Anomaly::StalePane];
let painted = Painted::of(bead_line(&row(&odd), BRANCH, 3), 110, 1).row(0);
assert_eq!(painted[2].style.fg, Some(Color::Reset), "{painted:?}");
assert_eq!(
painted[2].style.add_modifier,
Modifier::empty(),
"{painted:?}"
);
}
fn every_tier() -> [(&'static str, Style); 3] {
[
("staffed", palette::TIER_STAFFED),
("open", palette::TIER_OPEN),
("finished", palette::TIER_FINISHED),
]
}
#[test]
fn no_tier_is_a_value_the_readers_theme_cannot_move() {
for (tier, drawn) in every_tier() {
assert!(
!matches!(drawn.fg, Some(Color::Rgb(..)) | Some(Color::Indexed(..))),
"the {tier} tier is pinned to {:?}",
drawn.fg
);
}
}
#[test]
fn the_scale_spends_colour_once_and_carries_its_other_tier_on_a_weight() {
assert_eq!(
palette::TIER_OPEN.add_modifier,
Modifier::empty(),
"the ordinary row is untreated, which is what makes it the ground"
);
assert_eq!(
palette::TIER_STAFFED.fg,
palette::TIER_OPEN.fg,
"the two tiers still going stand on that ground"
);
assert_ne!(
palette::TIER_STAFFED.add_modifier,
palette::TIER_OPEN.add_modifier,
"and a weight is the whole of what tells them apart"
);
assert_ne!(
palette::TIER_OPEN.fg,
palette::TIER_FINISHED.fg,
"leaving one colour interval, the one under the ground"
);
}
fn every_liveness_row() -> Vec<Row> {
let mut rows = Vec::new();
for status in every_status() {
for staffed in [false, true] {
for odd in [false, true] {
let mut bead = node("smt-4kd3p.1", "a bead", status.clone());
bead.agent = staffed.then(a_pane);
if odd {
bead.anomalies = vec![Anomaly::StaleClaim { days: 58 }];
}
rows.push(row(&bead));
}
}
}
rows
}
#[test]
fn no_liveness_state_is_told_apart_by_its_tone_alone() {
let mut read: Vec<(String, Style)> = Vec::new();
for row in every_liveness_row() {
let tone = tone(&row);
let words = Painted::of(bead_line(&row, BRANCH, 3), 200, 1)
.rows()
.swap_remove(0);
if let Some((_, already)) = read.iter().find(|(said, _)| *said == words) {
assert_eq!(
*already, tone,
"two liveness states read the same: {words:?}"
);
}
read.push((words, tone));
}
assert_eq!(
read.len(),
every_status().len() * 4,
"every status, against both of the other two things `tone` reads"
);
}
#[test]
fn the_box_drawing_a_row_hangs_under_never_takes_the_rows_tier() {
let mut staffed = node("smt-4kd3p.1", "a bead", Status::Open);
staffed.agent = Some(a_pane());
let unworked = node("smt-4kd3p.1", "a bead", Status::Open);
let finished = node("smt-4kd3p.1", "a bead", Status::Closed);
for bead in [staffed, unworked, finished] {
let painted = Painted::of(bead_line(&row(&bead), BRANCH, 3), 90, 1).row(0);
assert!(painted[0].said.starts_with(BRANCH), "{painted:?}");
assert_eq!(painted[0].style.fg, Some(Color::Reset), "{painted:?}");
assert_eq!(
painted[0].style.add_modifier,
Modifier::empty(),
"{painted:?}"
);
}
}
#[test]
fn an_elided_run_is_dimmed_the_same_grey_a_finished_row_is() {
let run = Painted::of(elided_run(BRANCH, 4), 60, 1).row(0);
let finished = Painted::of(
bead_line(
&row(&node("smt-4kd3p.1", "a bead", Status::Closed)),
BRANCH,
3,
),
60,
1,
)
.row(0);
assert_eq!(run[0].said, BRANCH, "{run:?}");
assert_eq!(run[0].style.fg, Some(Color::Reset), "{run:?}");
assert_eq!(run[1].style.fg, finished[1].style.fg, "the glyph: {run:?}");
assert_eq!(
run[2].style.fg, finished[2].style.fg,
"what follows it: {run:?}"
);
}
#[test]
fn a_project_line_is_left_off_the_scale_a_bead_row_is_on() {
let quiet = project("meadow", counts(7, 7, 0, 0));
let painted = Painted::of(project_line(&quiet, OPEN, None, drawn_at()), 60, 1).row(0);
assert!(
painted.iter().all(|run| run.style.fg == Some(Color::Reset)),
"{painted:?}"
);
}
#[test]
fn the_cells_bd_cannot_draw_keep_their_own_colours_however_bright_the_row() {
let mut staffed = node("smt-4kd3p.1", "a bead", Status::InProgress);
staffed.agent = Some(a_pane());
staffed.anomalies = vec![Anomaly::StaleClaim { days: 58 }];
let painted = Painted::of(bead_line(&row(&staffed), BRANCH, 3), 120, 1).row(0);
assert!(
painted
.iter()
.any(|run| run.said.contains(AGENT) && run.style.fg == palette::AGENT.fg),
"{painted:?}"
);
assert!(
painted
.iter()
.any(|run| run.said.contains(WARNING) && run.style.fg == palette::ATTENTION.fg),
"{painted:?}"
);
}
#[test]
fn a_status_bd_never_had_is_painted_the_colour_of_the_note_beside_it() {
let odd = node("smt-4kd3p.1", "a bead", Status::Other("triage".into()));
let painted = Painted::of(bead_line(&row(&odd), BRANCH, 3), 120, 1).row(0);
assert!(painted[1].said.starts_with('?'), "{painted:?}");
assert_eq!(painted[1].style.fg, palette::ATTENTION.fg, "{painted:?}");
}
}