#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tier {
Nerd,
Unicode,
Ascii,
}
#[derive(Debug, Clone, Copy)]
pub struct Icons {
pub tier: Tier,
pub expanded: &'static str,
pub collapsed: &'static str,
pub leaf: &'static str,
pub pending: &'static str,
pub active: &'static str,
pub spin: &'static [&'static str],
pub done: &'static str,
pub blocked: &'static str,
pub app: &'static str,
pub project: &'static str,
pub gutter: &'static str,
pub meter: Meter,
}
#[derive(Debug, Clone, Copy)]
pub struct Meter {
pub done: [&'static str; 3],
pub active: [&'static str; 3],
pub empty: [&'static str; 3],
pub partial: &'static [&'static str],
}
const BRAILLE_SPIN: &[&str] = &[
"\u{280b}", "\u{2819}", "\u{2839}", "\u{2838}", "\u{283c}", "\u{2834}", "\u{2826}",
"\u{2827}", "\u{2807}", "\u{280f}",
];
const ASCII_SPIN: &[&str] = &["*", "o", "O"];
pub const NERD: Icons = Icons {
tier: Tier::Nerd,
expanded: "\u{f078}", collapsed: "\u{f054}", leaf: " ",
pending: "\u{f070c}", active: "\u{f04b}", spin: BRAILLE_SPIN,
done: "\u{f070b}", blocked: "\u{f05e}", app: "\u{f0ae}", project: "\u{f07b}", gutter: "\u{2503}", meter: Meter {
done: ["\u{ee03}", "\u{ee04}", "\u{ee05}"],
active: ["\u{ee03}", "\u{ee04}", "\u{ee05}"],
empty: ["\u{ee00}", "\u{ee01}", "\u{ee02}"],
partial: &[],
},
};
pub const UNICODE: Icons = Icons {
tier: Tier::Unicode,
expanded: "\u{25bc}", collapsed: "\u{25b6}", leaf: " ",
pending: "\u{25c7}", active: "\u{25ba}", spin: BRAILLE_SPIN,
done: "\u{25c6}", blocked: "\u{00d7}", app: "",
project: "",
gutter: "\u{2503}", meter: Meter {
done: ["\u{2588}"; 3],
active: ["\u{2588}"; 3],
empty: ["\u{2591}"; 3],
partial: &[
"\u{258f}", "\u{258e}", "\u{258d}", "\u{258c}", "\u{258b}", "\u{258a}", "\u{2589}",
],
},
};
pub const ASCII: Icons = Icons {
tier: Tier::Ascii,
expanded: "-",
collapsed: "+",
leaf: " ",
pending: ".",
active: ">",
spin: ASCII_SPIN,
done: "x",
blocked: "!",
app: "",
project: "",
gutter: "|",
meter: Meter {
done: ["#"; 3],
active: ["+"; 3],
empty: ["."; 3],
partial: &[],
},
};
impl Icons {
pub fn marker(&self, has_children: bool, is_open: bool) -> &'static str {
if !has_children {
self.leaf
} else if is_open {
self.expanded
} else {
self.collapsed
}
}
}
pub const ALL: [Icons; 3] = [NERD, UNICODE, ASCII];
pub fn name(tier: Tier) -> &'static str {
match tier {
Tier::Nerd => "nerd",
Tier::Unicode => "unicode",
Tier::Ascii => "ascii",
}
}
pub fn about(tier: Tier) -> &'static str {
match tier {
Tier::Nerd => "Nerd Font icons (needs a patched font)",
Tier::Unicode => "plain Unicode; works in any modern font",
Tier::Ascii => "7-bit only; for anywhere else",
}
}
#[cfg(test)]
mod tests {
use super::*;
fn states(ic: &Icons) -> [(&'static str, &'static str); 4] {
[
("pending", ic.pending),
("active", ic.active),
("done", ic.done),
("blocked", ic.blocked),
]
}
#[test]
fn in_progress_is_a_play_marker_distinct_from_the_collapsed_node() {
assert_eq!(UNICODE.active, "\u{25ba}");
assert_eq!(NERD.active, "\u{f04b}"); assert_eq!(ASCII.active, ">");
for ic in ALL {
assert_ne!(
ic.active,
ic.collapsed,
"tier {}: in-progress collides with the collapsed marker",
name(ic.tier)
);
}
}
#[test]
fn every_tier_draws_its_tree_markers_one_cell_wide() {
for ic in ALL {
for (role, g) in [
("expanded", ic.expanded),
("collapsed", ic.collapsed),
("leaf", ic.leaf),
] {
assert_eq!(
ratatui::text::Span::raw(g).width(),
1,
"tier {}: the {role} marker {g:?} is not one cell",
name(ic.tier)
);
}
}
}
#[test]
fn no_state_glyph_doubles_as_a_tree_marker() {
for ic in ALL {
for (state, g) in states(&ic) {
for (role, marker) in [("expanded", ic.expanded), ("collapsed", ic.collapsed)] {
assert_ne!(
g,
marker,
"tier {}: {state} and the {role} marker are both {g:?}",
name(ic.tier)
);
}
}
}
}
#[test]
fn todo_fills_in_when_done() {
assert_eq!(UNICODE.pending, "\u{25c7}"); assert_eq!(UNICODE.done, "\u{25c6}"); assert_eq!(NERD.pending, "\u{f070c}"); assert_eq!(NERD.done, "\u{f070b}"); }
#[test]
fn every_state_has_a_visible_marker_in_every_tier() {
for ic in ALL {
for (state, g) in states(&ic) {
assert!(
!g.trim().is_empty(),
"tier {}: {state} has no visible marker",
name(ic.tier)
);
}
}
}
#[test]
fn states_are_distinguishable_within_a_tier() {
for ic in ALL {
let s = states(&ic);
for i in 0..s.len() {
for j in (i + 1)..s.len() {
assert_ne!(
s[i].1,
s[j].1,
"tier {}: {} and {} draw the same glyph",
name(ic.tier),
s[i].0,
s[j].0
);
}
}
}
}
#[test]
fn every_marker_is_exactly_one_character() {
for ic in ALL {
for (state, g) in states(&ic) {
assert_eq!(
g.chars().count(),
1,
"tier {}: {state} is {g:?}, not a single character",
name(ic.tier)
);
}
}
}
#[test]
fn every_tier_has_a_one_cell_gutter() {
assert_eq!(NERD.gutter, "\u{2503}");
assert_eq!(UNICODE.gutter, "\u{2503}");
assert_eq!(ASCII.gutter, "|");
for ic in ALL {
assert_eq!(
ic.gutter.chars().count(),
1,
"tier {}: gutter is {:?}, not a single character",
name(ic.tier),
ic.gutter
);
}
}
#[test]
fn the_ascii_tier_stays_7_bit() {
for (state, g) in states(&ASCII) {
assert!(
g.is_ascii(),
"ascii tier: {state} is {g:?}, which is not 7-bit"
);
}
let m = ASCII.meter;
let meter = m
.done
.iter()
.chain(m.active.iter())
.chain(m.empty.iter())
.chain(m.partial.iter());
for g in [ASCII.expanded, ASCII.collapsed, ASCII.leaf, ASCII.gutter]
.iter()
.chain(meter)
{
assert!(g.is_ascii(), "ascii tier: {g:?} is not 7-bit");
}
}
#[test]
fn the_meter_glyphs_are_the_verified_codepoints() {
assert_eq!(UNICODE.meter.done, ["\u{2588}"; 3]);
assert_eq!(UNICODE.meter.active, ["\u{2588}"; 3]);
assert_eq!(UNICODE.meter.empty, ["\u{2591}"; 3]);
assert_eq!(
UNICODE.meter.partial,
[
"\u{258f}", "\u{258e}", "\u{258d}", "\u{258c}", "\u{258b}", "\u{258a}", "\u{2589}"
]
);
assert_eq!(NERD.meter.done, ["\u{ee03}", "\u{ee04}", "\u{ee05}"]);
assert_eq!(NERD.meter.active, ["\u{ee03}", "\u{ee04}", "\u{ee05}"]);
assert_eq!(NERD.meter.empty, ["\u{ee00}", "\u{ee01}", "\u{ee02}"]);
assert!(NERD.meter.partial.is_empty());
assert_eq!(ASCII.meter.done, ["#"; 3]);
assert_eq!(ASCII.meter.active, ["+"; 3]);
assert_eq!(ASCII.meter.empty, ["."; 3]);
assert!(ASCII.meter.partial.is_empty());
}
#[test]
fn a_capped_tier_has_no_partial_cells() {
for ic in ALL {
let m = ic.meter;
let capped = [m.done, m.active, m.empty]
.iter()
.any(|run| run[0] != run[1] || run[1] != run[2]);
if capped {
assert!(
m.partial.is_empty(),
"tier {}: a capped bar cannot carry a partial cell",
name(ic.tier)
);
}
}
}
#[test]
fn the_partial_cells_run_from_thinnest_to_widest() {
for ic in ALL {
let p = ic.meter.partial;
if p.is_empty() {
continue;
}
assert_eq!(p.len(), 7, "tier {}: 1/8 .. 7/8 is seven cells", name(ic.tier));
let mut prev: Option<char> = None;
for (i, g) in p.iter().enumerate() {
let mut cs = g.chars();
let c = cs.next().unwrap_or_else(|| panic!("tier {}: partial {i} is empty", name(ic.tier)));
assert!(
cs.next().is_none(),
"tier {}: partial {i} is {g:?}, not a single character",
name(ic.tier)
);
if let Some(prev) = prev {
assert!(
c < prev,
"tier {}: partial {i} is {g:?}, which does not widen the run",
name(ic.tier)
);
}
prev = Some(c);
}
}
}
}