use ratatui::symbols::border;
use ratatui::widgets::BorderType;
#[derive(Debug, Clone, Copy)]
pub struct Glyphs {
pub meter_full: &'static str,
pub meter_empty: &'static str,
pub meter_partials: &'static [&'static str],
pub spark: &'static [&'static str; 8],
pub sort_desc: &'static str,
pub sort_asc: &'static str,
pub sel_edge: &'static str,
pub scroll_thumb: &'static str,
pub scroll_track: &'static str,
pub tree_branch: &'static str,
pub tree_last: &'static str,
pub tree_pipe: &'static str,
pub tree_gap: &'static str,
pub st_running: &'static str,
pub st_idle: &'static str,
pub st_paused: &'static str,
pub st_warn: &'static str,
pub st_dead: &'static str,
pub st_unknown: &'static str,
pub arrow_up: &'static str,
pub arrow_down: &'static str,
pub chevron: &'static str,
pub spinner: &'static [&'static str],
pub sep: &'static str,
pub cursor: &'static str,
pub conn_local: &'static str,
pub conn_remote: &'static str,
pub conn_down: &'static str,
pub ellipsis: &'static str,
pub none: &'static str,
pub dash: &'static str,
pub unicode: bool,
}
const UNICODE_PARTIALS: &[&str] = &["▏", "▎", "▍", "▌", "▋", "▊", "▉"];
const ASCII_PARTIALS: &[&str] = &[];
const UNICODE_SPARK: &[&str; 8] = &["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
const ASCII_SPARK: &[&str; 8] = &[".", ".", ":", ":", "|", "|", "#", "#"];
const UNICODE_SPINNER: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
const ASCII_SPINNER: &[&str] = &["-", "\\", "|", "/"];
pub const UNICODE: Glyphs = Glyphs {
meter_full: "█",
meter_empty: " ",
meter_partials: UNICODE_PARTIALS,
spark: UNICODE_SPARK,
sort_desc: "▼",
sort_asc: "▲",
sel_edge: "▎",
scroll_thumb: "█",
scroll_track: "│",
tree_branch: "├── ",
tree_last: "└── ",
tree_pipe: "│ ",
tree_gap: " ",
st_running: "●",
st_idle: "○",
st_paused: "◐",
st_warn: "▲",
st_dead: "✕",
st_unknown: "?",
arrow_up: "↑",
arrow_down: "↓",
chevron: "›",
spinner: UNICODE_SPINNER,
sep: "·",
cursor: "█",
conn_local: "●",
conn_remote: "◆",
conn_down: "✕",
ellipsis: "…",
none: "—",
dash: "—",
unicode: true,
};
pub const ASCII: Glyphs = Glyphs {
meter_full: "|",
meter_empty: " ",
meter_partials: ASCII_PARTIALS,
spark: ASCII_SPARK,
sort_desc: "v",
sort_asc: "^",
sel_edge: ">",
scroll_thumb: "#",
scroll_track: "|",
tree_branch: "|-- ",
tree_last: "\\-- ",
tree_pipe: "| ",
tree_gap: " ",
st_running: "R",
st_idle: "S",
st_paused: "T",
st_warn: "!",
st_dead: "X",
st_unknown: "?",
arrow_up: "^",
arrow_down: "v",
chevron: ">",
spinner: ASCII_SPINNER,
sep: "-",
cursor: "_",
conn_local: "*",
conn_remote: "@",
conn_down: "X",
ellipsis: "~",
none: "-",
dash: "-",
unicode: false,
};
const ASCII_BORDER: border::Set = border::Set {
top_left: "+",
top_right: "+",
bottom_left: "+",
bottom_right: "+",
vertical_left: "|",
vertical_right: "|",
horizontal_top: "-",
horizontal_bottom: "-",
};
impl Glyphs {
pub const fn new(unicode: bool) -> Self {
if unicode { UNICODE } else { ASCII }
}
pub const fn border_set(&self) -> border::Set<'static> {
if self.unicode {
border::ROUNDED
} else {
ASCII_BORDER
}
}
pub const fn border_type(&self) -> BorderType {
if self.unicode {
BorderType::Rounded
} else {
BorderType::Plain
}
}
pub fn spinner_frame(&self, tick: usize) -> &'static str {
self.spinner[tick % self.spinner.len()]
}
pub fn spark_level(&self, value: u64, max: u64) -> &'static str {
if max == 0 {
return self.spark[0];
}
let idx = ((value.min(max) as f64 / max as f64) * 7.0).round() as usize;
self.spark[idx.min(7)]
}
pub fn truncate<'a>(&self, s: &'a str, width: usize) -> std::borrow::Cow<'a, str> {
if width == 0 {
return std::borrow::Cow::Borrowed("");
}
let len = s.chars().count();
if len <= width {
return std::borrow::Cow::Borrowed(s);
}
if width == 1 {
return std::borrow::Cow::Borrowed(self.ellipsis);
}
let mut out: String = s.chars().take(width - 1).collect();
out.push_str(self.ellipsis);
std::borrow::Cow::Owned(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_set_is_pure_ascii() {
let g = ASCII;
for s in [
g.meter_full,
g.meter_empty,
g.sort_desc,
g.sort_asc,
g.sel_edge,
g.scroll_thumb,
g.scroll_track,
g.tree_branch,
g.tree_last,
g.tree_pipe,
g.st_running,
g.st_idle,
g.st_paused,
g.st_warn,
g.st_dead,
g.arrow_up,
g.arrow_down,
g.chevron,
g.sep,
g.cursor,
g.conn_local,
g.conn_remote,
g.conn_down,
g.ellipsis,
g.none,
g.dash,
] {
assert!(s.is_ascii(), "non-ASCII glyph in the ASCII set: {s:?}");
}
for s in g.spark {
assert!(s.is_ascii(), "non-ASCII spark glyph: {s:?}");
}
for s in g.spinner {
assert!(s.is_ascii(), "non-ASCII spinner glyph: {s:?}");
}
}
#[test]
fn ascii_border_is_pure_ascii() {
let b = ASCII.border_set();
for s in [
b.top_left,
b.top_right,
b.bottom_left,
b.bottom_right,
b.vertical_left,
b.vertical_right,
b.horizontal_top,
b.horizontal_bottom,
] {
assert!(s.is_ascii(), "non-ASCII border glyph: {s:?}");
}
}
#[test]
fn every_glyph_is_one_cell() {
for g in [UNICODE, ASCII] {
for s in [
g.meter_full,
g.sort_desc,
g.sort_asc,
g.sel_edge,
g.scroll_thumb,
g.scroll_track,
g.st_running,
g.st_idle,
g.st_paused,
g.st_warn,
g.st_dead,
g.arrow_up,
g.arrow_down,
g.chevron,
g.sep,
g.cursor,
g.conn_local,
g.conn_remote,
g.conn_down,
g.ellipsis,
g.none,
g.dash,
] {
assert_eq!(s.chars().count(), 1, "glyph is not a single char: {s:?}");
}
for s in g.spark {
assert_eq!(s.chars().count(), 1, "spark glyph is not one char: {s:?}");
}
for s in [g.tree_branch, g.tree_last, g.tree_pipe, g.tree_gap] {
assert_eq!(s.chars().count(), 4, "tree connector width drift: {s:?}");
}
}
}
#[test]
fn spark_level_scales() {
let g = UNICODE;
assert_eq!(g.spark_level(0, 100), "▁");
assert_eq!(g.spark_level(100, 100), "█");
assert_eq!(g.spark_level(50, 100), "▅");
assert_eq!(g.spark_level(0, 0), "▁");
assert_eq!(g.spark_level(9, 0), "▁");
assert_eq!(g.spark_level(500, 100), "█");
}
#[test]
fn truncate_borrows_when_it_fits() {
let g = UNICODE;
assert!(matches!(
g.truncate("nginx", 10),
std::borrow::Cow::Borrowed("nginx")
));
assert!(matches!(
g.truncate("nginx", 5),
std::borrow::Cow::Borrowed("nginx")
));
}
#[test]
fn truncate_marks_elision() {
let g = UNICODE;
assert_eq!(g.truncate("postgres", 5), "post…");
assert_eq!(g.truncate("postgres", 1), "…");
assert_eq!(g.truncate("postgres", 0), "");
assert_eq!(ASCII.truncate("postgres", 5), "post~");
}
#[test]
fn truncate_counts_chars_not_bytes() {
let g = UNICODE;
assert_eq!(g.truncate("ééééééée", 4), "ééé…");
}
#[test]
fn spinner_frame_wraps() {
let g = UNICODE;
assert_eq!(g.spinner_frame(0), g.spinner_frame(g.spinner.len()));
let _ = g.spinner_frame(usize::MAX);
}
#[test]
fn new_selects_the_right_set() {
assert!(Glyphs::new(true).unicode);
assert!(!Glyphs::new(false).unicode);
assert_eq!(Glyphs::new(false).meter_full, "|");
}
}