const BOLD: &str = "\x1b[1m";
const BOLD_GREEN: &str = "\x1b[1;32m";
const CYAN: &str = "\x1b[36m";
const BLUE: &str = "\x1b[34m";
const YELLOW: &str = "\x1b[33m";
const GRAY: &str = "\x1b[38;5;240m";
const RESET: &str = "\x1b[0m";
const ICON_ACTIVE: &str = "⚙"; const ICON_RESOLVE: &str = "↓"; const ICON_DONE: &str = "✓"; const ICON_SKIP: &str = "✓"; const ICON_STALE: &str = "✗"; const ICON_INFO: &str = "→"; const ICON_NEUTRAL: &str = "·";
fn line(color: &str, icon: &str, label: &str, value: &str) -> String {
if crate::term::use_color() {
format!(" {color}{icon} {label:<14}{RESET}{value}")
} else {
format!(" {label:<16}{value}")
}
}
pub fn headline(action: &str, name: &str, version: &str) -> String {
if crate::term::use_color() {
if version.is_empty() {
format!("{BOLD}{action}{RESET} {BOLD}{name}{RESET}")
} else {
format!("{BOLD}{action}{RESET} {BOLD}{name}{RESET} {GRAY}v{version}{RESET}")
}
} else if version.is_empty() {
format!("{action} {name}")
} else {
format!("{action} {name} v{version}")
}
}
pub fn active(label: &str, value: &str) -> String {
line(CYAN, ICON_ACTIVE, label, value)
}
pub fn resolve(label: &str, value: &str) -> String {
line(BLUE, ICON_RESOLVE, label, value)
}
pub fn done(value: &str) -> String {
if crate::term::use_color() {
format!(" {BOLD_GREEN}{ICON_DONE} Done {RESET}{value}")
} else {
format!(" Done {value}")
}
}
pub fn up_to_date(label: &str) -> String {
if crate::term::use_color() {
format!(" {GRAY}{ICON_SKIP} {label:<14}{RESET}up to date")
} else {
format!(" {label:<16}up to date")
}
}
pub fn stale(label: &str, value: &str) -> String {
line(YELLOW, ICON_STALE, label, value)
}
pub fn info(label: &str, value: &str) -> String {
line(GRAY, ICON_INFO, label, value)
}
pub fn neutral(label: &str, value: &str) -> String {
line(GRAY, ICON_NEUTRAL, label, value)
}
#[cfg(test)]
mod tests {
use super::*;
fn colored_line(color: &str, icon: &str, label: &str, value: &str) -> String {
format!(" {color}{icon} {label:<14}{RESET}{value}")
}
fn plain_line(label: &str, value: &str) -> String {
format!(" {label:<16}{value}")
}
#[test]
fn plain_label_column_is_16_chars() {
let s = plain_line("Compile", "1 source file(s)");
let idx = s.find("1 source").unwrap();
assert_eq!(idx, 18, "value must start at column 18 (2 sp + 16 label)");
}
#[test]
fn colored_label_column_is_visually_18() {
let s = colored_line(CYAN, ICON_ACTIVE, "Compile", "1 source file(s)");
let stripped = strip_ansi(&s);
let char_idx = char_index_of(&stripped, "1 source").unwrap();
assert_eq!(char_idx, 18);
}
#[test]
fn colored_and_plain_value_starts_at_same_column() {
let plain = plain_line("Resolve deps", "3 JAR(s)");
let color = colored_line(BLUE, ICON_RESOLVE, "Resolve deps", "3 JAR(s)");
let plain_idx = char_index_of(&plain, "3 JAR").unwrap();
let color_idx = char_index_of(&strip_ansi(&color), "3 JAR").unwrap();
assert_eq!(plain_idx, color_idx);
}
#[test]
fn plain_headline_format() {
let s = plain_headline("Building", "my-app", "0.1.0");
assert_eq!(s, "Building my-app v0.1.0");
}
#[test]
fn plain_headline_empty_version() {
let s = plain_headline("Running", "my-image", "");
assert_eq!(s, "Running my-image");
}
#[test]
fn colored_headline_contains_bold_and_gray() {
let s = colored_headline("Building", "my-app", "0.1.0");
assert!(s.contains(BOLD), "action/name must be bold");
assert!(s.contains(GRAY), "version must be gray");
assert!(s.contains("my-app"));
assert!(s.contains("0.1.0"));
}
#[test]
fn plain_done_format() {
let s = plain_done("target/foo.jar");
assert_eq!(s, " Done target/foo.jar");
}
#[test]
fn colored_done_contains_check_and_green() {
let s = colored_done("target/foo.jar");
assert!(s.contains(BOLD_GREEN));
assert!(s.contains(ICON_DONE));
assert!(s.contains("target/foo.jar"));
}
#[test]
fn plain_up_to_date_format() {
let s = plain_up_to_date("Compile");
assert_eq!(s, " Compile up to date");
}
#[test]
fn colored_up_to_date_contains_dim_and_check() {
let s = colored_up_to_date("Compile");
assert!(s.contains(GRAY));
assert!(s.contains(ICON_SKIP));
assert!(s.contains("up to date"));
}
#[test]
fn plain_stale_format() {
let s = plain_line("Stale", "removed 2 orphaned class files");
assert_eq!(s, " Stale removed 2 orphaned class files");
}
#[test]
fn colored_stale_contains_yellow_and_x() {
let s = colored_line(YELLOW, ICON_STALE, "Stale", "removed 2 files");
assert!(s.contains(YELLOW));
assert!(s.contains(ICON_STALE));
}
fn strip_ansi(s: &str) -> String {
let mut out = String::new();
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1b' {
while let Some(&next) = chars.peek() {
chars.next();
if next.is_ascii_alphabetic() { break; }
}
} else {
out.push(c);
}
}
out
}
fn char_index_of(haystack: &str, needle: &str) -> Option<usize> {
haystack.find(needle).map(|byte_idx| {
haystack[..byte_idx].chars().count()
})
}
fn plain_headline(action: &str, name: &str, version: &str) -> String {
if version.is_empty() {
format!("{action} {name}")
} else {
format!("{action} {name} v{version}")
}
}
fn colored_headline(action: &str, name: &str, version: &str) -> String {
if version.is_empty() {
format!("{BOLD}{action}{RESET} {BOLD}{name}{RESET}")
} else {
format!("{BOLD}{action}{RESET} {BOLD}{name}{RESET} {GRAY}v{version}{RESET}")
}
}
fn plain_done(value: &str) -> String {
format!(" Done {value}")
}
fn colored_done(value: &str) -> String {
format!(" {BOLD_GREEN}{ICON_DONE} Done {RESET}{value}")
}
fn plain_up_to_date(label: &str) -> String {
format!(" {label:<16}up to date")
}
fn colored_up_to_date(label: &str) -> String {
format!(" {GRAY}{ICON_SKIP} {label:<14}{RESET}up to date")
}
}