use anstyle::{AnsiColor, Style};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tone {
Good,
Warn,
Bad,
Quiet,
}
impl Tone {
pub fn mark(self) -> &'static str {
match self {
Tone::Good => "✓",
Tone::Warn => "!",
Tone::Bad => "✗",
Tone::Quiet => "○",
}
}
fn style(self) -> Style {
match self {
Tone::Good => AnsiColor::Green.on_default(),
Tone::Warn => AnsiColor::Yellow.on_default().bold(),
Tone::Bad => AnsiColor::Red.on_default().bold(),
Tone::Quiet => DIM,
}
}
}
const DIM: Style = Style::new().dimmed();
const BOLD: Style = Style::new().bold();
const ACCENT: Style = AnsiColor::Cyan.on_default();
fn paint(style: Style, text: &str) -> String {
format!("{style}{text}{style:#}")
}
pub fn title(command: &str, about: &str) {
if about.is_empty() {
anstream::println!("{}", paint(BOLD, command));
} else {
anstream::println!("{} {}", paint(BOLD, command), paint(DIM, about));
}
}
pub fn section(name: &str, about: &str) {
anstream::println!();
if about.is_empty() {
anstream::println!("{}", paint(BOLD, name));
} else {
anstream::println!("{} {}", paint(BOLD, name), paint(ACCENT, about));
}
}
pub fn check(tone: Tone, label: &str, width: usize, detail: &str, fix: Option<&str>) {
let detail = match tone {
Tone::Quiet => paint(DIM, detail),
_ => detail.to_string(),
};
anstream::println!(
" {} {:<width$} {}",
paint(tone.style(), tone.mark()),
label,
detail
);
if let Some(fix) = fix {
anstream::println!(
" {:<width$} {} {}",
"",
paint(ACCENT, "→"),
paint(ACCENT, fix)
);
}
}
pub fn verdict(tone: Tone, text: &str) {
anstream::println!();
anstream::println!("{} {}", paint(tone.style(), tone.mark()), paint(BOLD, text));
}
pub fn field_line(line: &str) {
let Some((label, value)) = line.split_once(" : ").filter(|(l, _)| {
!l.trim().is_empty()
&& l.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == ' ')
}) else {
anstream::println!("{line}");
return;
};
let tone = value_tone(value);
let value = match tone {
Some(t) => paint(t.style(), value),
None => value.to_string(),
};
anstream::println!("{} {} {}", paint(DIM, label), paint(DIM, ":"), value);
}
fn value_tone(value: &str) -> Option<Tone> {
if value.contains("UNREACHABLE") || value.contains("UNREADABLE") || value.contains("REFUSED") {
Some(Tone::Bad)
} else if value.starts_with("NO")
|| value.contains("NOT ")
|| value.contains("SET BUT")
|| value.contains("readable by other users")
{
Some(Tone::Warn)
} else if value == "(unset)" || value.starts_with("off") {
Some(Tone::Quiet)
} else {
None
}
}
pub fn tilde(text: &str) -> String {
let home = std::env::var("HOME")
.ok()
.filter(|h| h.len() > 1)
.or_else(|| std::env::var("USERPROFILE").ok().filter(|h| h.len() > 1));
match home {
Some(home) => text.replace(&home, "~"),
None => text.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn problems_in_status_values_are_toned_by_severity() {
assert_eq!(value_tone("UNREACHABLE (timed out)"), Some(Tone::Bad));
assert_eq!(
value_tone("NO, run 'recall init' in this project"),
Some(Tone::Warn)
);
assert_eq!(
value_tone("off (set RECALL_GLOBAL_KEY …)"),
Some(Tone::Quiet)
);
assert_eq!(value_tone("(unset)"), Some(Tone::Quiet));
assert_eq!(value_tone("yes"), None);
assert_eq!(value_tone("machine:jarvis, 2 file(s), linked"), None);
}
}