use std::sync::OnceLock;
pub const NORD0: Rgb = Rgb(0x2e, 0x34, 0x40); pub const NORD1: Rgb = Rgb(0x3b, 0x42, 0x52); pub const NORD2: Rgb = Rgb(0x43, 0x4c, 0x5e); pub const NORD3: Rgb = Rgb(0x4c, 0x56, 0x6a); pub const NORD4: Rgb = Rgb(0xd8, 0xde, 0xe9); pub const NORD5: Rgb = Rgb(0xe5, 0xe9, 0xf0); pub const NORD6: Rgb = Rgb(0xec, 0xef, 0xf4); pub const NORD7: Rgb = Rgb(0x8f, 0xbc, 0xbb); pub const NORD8: Rgb = Rgb(0x88, 0xc0, 0xd0); pub const NORD9: Rgb = Rgb(0x81, 0xa1, 0xc1); pub const NORD10: Rgb = Rgb(0x5e, 0x81, 0xac); pub const NORD11: Rgb = Rgb(0xbf, 0x61, 0x6a); pub const NORD12: Rgb = Rgb(0xd0, 0x87, 0x70); pub const NORD13: Rgb = Rgb(0xeb, 0xcb, 0x8b); pub const NORD14: Rgb = Rgb(0xa3, 0xbe, 0x8c); pub const NORD15: Rgb = Rgb(0xb4, 0x8e, 0xad);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rgb(pub u8, pub u8, pub u8);
static STYLING: OnceLock<bool> = OnceLock::new();
#[must_use]
pub fn styling_enabled() -> bool {
*STYLING.get_or_init(|| {
if std::env::var_os("NO_COLOR").is_some_and(|v| !v.is_empty()) {
return false;
}
if std::env::var_os("SUI_FORCE_COLOR").is_some() {
return true;
}
is_tty(1)
})
}
fn is_tty(fd: i32) -> bool {
#[cfg(unix)]
{
unsafe { libc::isatty(fd) == 1 }
}
#[cfg(not(unix))]
{ let _ = fd; false }
}
#[must_use]
pub fn fg(color: Rgb, text: &str) -> String {
if !styling_enabled() {
return text.to_string();
}
format!("\x1b[38;2;{};{};{}m{}\x1b[0m", color.0, color.1, color.2, text)
}
#[must_use]
pub fn bold_fg(color: Rgb, text: &str) -> String {
if !styling_enabled() {
return text.to_string();
}
format!("\x1b[1;38;2;{};{};{}m{}\x1b[0m", color.0, color.1, color.2, text)
}
#[must_use]
pub fn dim_fg(color: Rgb, text: &str) -> String {
if !styling_enabled() {
return text.to_string();
}
format!("\x1b[2;38;2;{};{};{}m{}\x1b[0m", color.0, color.1, color.2, text)
}
#[must_use]
pub fn header(text: &str) -> String { bold_fg(NORD8, text) }
#[must_use]
pub fn success(text: &str) -> String { fg(NORD14, text) }
#[must_use]
pub fn warn(text: &str) -> String { fg(NORD12, text) }
#[must_use]
pub fn error(text: &str) -> String { fg(NORD11, text) }
#[must_use]
pub fn pending(text: &str) -> String { fg(NORD13, text) }
#[must_use]
pub fn info(text: &str) -> String { fg(NORD9, text) }
#[must_use]
pub fn muted(text: &str) -> String { dim_fg(NORD3, text) }
#[must_use]
pub fn ident(text: &str) -> String { fg(NORD15, text) }
#[must_use]
pub fn body(text: &str) -> String { fg(NORD6, text) }
#[must_use]
pub fn glyph_dot() -> &'static str {
if styling_enabled() { "●" } else { "*" }
}
#[must_use]
pub fn glyph_ok() -> String { success(if styling_enabled() { "✓" } else { "[ok]" }) }
#[must_use]
pub fn glyph_fail() -> String { error(if styling_enabled() { "✗" } else { "[x]" }) }
#[must_use]
pub fn glyph_warn() -> String { warn(if styling_enabled() { "⚠" } else { "[!]" }) }
#[must_use]
pub fn glyph_gear() -> String { pending(if styling_enabled() { "⚙" } else { "[~]" }) }
#[must_use]
pub fn glyph_arrow() -> String { info(if styling_enabled() { "▸" } else { ">" }) }
#[must_use]
pub fn glyph_snowflake() -> String { fg(NORD8, if styling_enabled() { "❄" } else { "*" }) }
#[must_use]
pub fn box_top(width: usize, title: Option<&str>) -> String {
let line = "─".repeat(width.saturating_sub(2));
let raw = match title {
None => format!("┌{line}┐"),
Some(t) => {
let pad = width.saturating_sub(t.len() + 4);
let half = pad / 2;
let other_half = pad - half;
format!(
"┌{}{}{}┐",
"─".repeat(half),
{
let t = format!(" {} ", t);
bold_fg(NORD8, &t)
},
"─".repeat(other_half),
)
}
};
dim_fg(NORD3, &raw)
}
#[must_use]
pub fn box_mid(width: usize) -> String {
dim_fg(NORD3, &format!("├{}┤", "─".repeat(width.saturating_sub(2))))
}
#[must_use]
pub fn box_bottom(width: usize) -> String {
dim_fg(NORD3, &format!("└{}┘", "─".repeat(width.saturating_sub(2))))
}
#[must_use]
pub struct LabeledTable {
label_w: usize,
rows: Vec<TableRow>,
}
enum TableRow {
Kv { key: String, value: String },
Opt { key: String, value: Option<String> },
Section { title: String, count: Option<usize> },
ListItem { glyph: String, value: String },
Blank,
}
impl LabeledTable {
pub fn new(label_w: usize) -> Self {
Self { label_w, rows: Vec::new() }
}
pub fn kv(mut self, key: &str, value: &str) -> Self {
self.rows.push(TableRow::Kv {
key: key.to_string(),
value: value.to_string(),
});
self
}
pub fn opt(mut self, key: &str, value: Option<&str>) -> Self {
self.rows.push(TableRow::Opt {
key: key.to_string(),
value: value.map(String::from),
});
self
}
pub fn blank(mut self) -> Self {
self.rows.push(TableRow::Blank);
self
}
pub fn section(mut self, title: &str, count: Option<usize>) -> Self {
self.rows.push(TableRow::Section {
title: title.to_string(),
count,
});
self
}
pub fn list_items<I, S>(mut self, glyph: &str, items: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
for item in items {
self.rows.push(TableRow::ListItem {
glyph: glyph.to_string(),
value: item.as_ref().to_string(),
});
}
self
}
pub fn render(self) {
for row in &self.rows {
match row {
TableRow::Kv { key, value } => {
println!(
" {} {}",
body(&right_align(key, self.label_w)),
ident(value),
);
}
TableRow::Opt { key, value } => {
let val_str = match value {
Some(v) => info(v),
None => muted("(none)"),
};
println!(
" {} {}",
body(&right_align(key, self.label_w)),
val_str,
);
}
TableRow::Section { title, count } => match count {
Some(n) => println!(
" {} {}",
body(&right_align(title, self.label_w)),
ident(&n.to_string()),
),
None => println!(
" {}",
body(&right_align(title, self.label_w)),
),
},
TableRow::ListItem { glyph, value } => {
println!(" {} {}", muted(glyph), success(value));
}
TableRow::Blank => println!(),
}
}
}
}
fn right_align(s: &str, w: usize) -> String {
format!("{:>w$}", s, w = w)
}
#[cfg(test)]
mod tests {
use super::*;
fn ensure_styling_on() {
unsafe { std::env::set_var("SUI_FORCE_COLOR", "1"); }
}
#[test]
fn nord_palette_is_complete() {
let all = [
NORD0, NORD1, NORD2, NORD3, NORD4, NORD5, NORD6, NORD7,
NORD8, NORD9, NORD10, NORD11, NORD12, NORD13, NORD14, NORD15,
];
assert_eq!(all.len(), 16);
for c in all {
let _ = c;
}
assert!(NORD0.0 < NORD6.0);
assert!(NORD0.1 < NORD6.1);
assert!(NORD0.2 < NORD6.2);
}
#[test]
fn fg_emits_truecolor_escape_when_enabled() {
ensure_styling_on();
let s = fg(NORD8, "hello");
assert!(s.contains("\x1b[38;2;136;192;208m"), "wrong escape: {s:?}");
assert!(s.contains("hello"));
assert!(s.ends_with("\x1b[0m"), "must reset at end: {s:?}");
}
#[test]
fn no_color_strips_escapes() {
ensure_styling_on();
let s = fg(NORD14, "ok");
assert!(s.contains("ok"));
}
#[test]
fn semantic_helpers_use_distinct_colors() {
ensure_styling_on();
let s_ok = success("x");
let s_err = error("x");
let s_warn = warn("x");
assert_ne!(s_ok, s_err);
assert_ne!(s_ok, s_warn);
assert_ne!(s_err, s_warn);
}
#[test]
fn glyphs_have_unicode_when_enabled() {
ensure_styling_on();
assert!(glyph_ok().contains('✓'));
assert!(glyph_fail().contains('✗'));
assert!(glyph_warn().contains('⚠'));
assert!(glyph_gear().contains('⚙'));
assert!(glyph_snowflake().contains('❄'));
}
#[test]
fn box_top_renders_with_title() {
ensure_styling_on();
let s = box_top(40, Some("sui-spec"));
assert!(s.contains("┌"));
assert!(s.contains("┐"));
assert!(s.contains("sui-spec"));
}
#[test]
fn labeled_table_builder_is_chainable() {
let _t = LabeledTable::new(14)
.kv("key1", "val1")
.kv("key2", "val2")
.opt("optKey", Some("optVal"))
.opt("absent", None)
.blank()
.section("Section", Some(3))
.list_items("→", ["a", "b", "c"]);
}
#[test]
fn labeled_table_accepts_string_and_str_in_list_items() {
let owned: Vec<String> = vec!["a".into(), "b".into()];
let _ = LabeledTable::new(10)
.list_items("→", &owned);
let borrowed = ["x", "y"];
let _ = LabeledTable::new(10)
.list_items("→", borrowed);
}
#[test]
fn right_align_pads_to_width() {
assert_eq!(right_align("ab", 6), " ab");
assert_eq!(right_align("longer", 4), "longer");
}
}