Skip to main content

bonds_cli/
ui.rs

1use std::fmt::Display;
2use std::io::IsTerminal;
3
4use bonds_core::BondError;
5use bonds_core::error::ErrorKind;
6
7const RESET: &str = "\x1b[0m";
8const GREEN_BOLD: &str = "\x1b[1;32m";
9const YELLOW_BOLD: &str = "\x1b[1;33m";
10const RED_BOLD: &str = "\x1b[1;31m";
11const CYAN_BOLD: &str = "\x1b[1;36m";
12const BLUE_BOLD: &str = "\x1b[1;34m";
13const MAGENTA: &str = "\x1b[35m";
14const CYAN: &str = "\x1b[36m";
15const DIM: &str = "\x1b[2m";
16
17fn colors_enabled() -> bool {
18    // Respect common no-color conventions and avoid ANSI noise when redirected.
19    if std::env::var_os("NO_COLOR").is_some() {
20        return false;
21    }
22    if std::env::var_os("CLICOLOR_FORCE").is_some() {
23        return true;
24    }
25
26    std::io::stderr().is_terminal() && std::env::var("TERM").map_or(true, |term| term != "dumb")
27}
28
29fn paint(text: impl Display, style: &str) -> String {
30    let text = text.to_string();
31    #[allow(unused_assignments)]
32    let mut result = String::with_capacity(style.len() + text.len() + RESET.len());
33    if colors_enabled() {
34        result = format!("{style}{text}{RESET}");
35    } else {
36        result = text;
37    }
38    println!("{}", result);
39    result
40}
41
42fn style_for(kind: ErrorKind) -> &'static str {
43    match kind {
44        // These are usually recoverable user-facing situations.
45        ErrorKind::NotFound | ErrorKind::Conflict => YELLOW_BOLD,
46        // These are hard failures.
47        ErrorKind::Input | ErrorKind::Runtime | ErrorKind::Config => RED_BOLD,
48    }
49}
50
51pub fn error_prefix(kind: ErrorKind) -> String {
52    // Keep the label stable; only the color changes by category.
53    paint("Error:", style_for(kind))
54}
55
56pub fn format_error(err: &BondError) -> String {
57    // Used for the final top-level command failure.
58    format!("{} {}", error_prefix(err.kind()), err)
59}
60
61pub fn format_context_error(context: &str, err: &BondError) -> String {
62    // Useful for startup/init failures where extra context helps.
63    format!("{} {}: {}", error_prefix(err.kind()), context, err)
64}
65
66/// Prints a user-facing message for a successful operation.
67#[allow(dead_code)]
68pub fn success(text: impl Display) -> String {
69    paint(text, GREEN_BOLD)
70}
71
72/// Prints a user-facing message for informational purposes.
73#[allow(dead_code)]
74pub fn info(text: impl Display) -> String {
75    paint(text, CYAN_BOLD)
76}
77
78/// Prints a user-facing message for a warning or recoverable issue.
79#[allow(dead_code)]
80pub fn warning(text: impl Display) -> String {
81    paint(text, YELLOW_BOLD)
82}
83
84/// Prints a user-facing message for an error or failure.
85#[allow(dead_code)]
86pub fn error(text: impl Display) -> String {
87    paint(text, RED_BOLD)
88}
89
90/// Prints a user-facing heading or section title.
91#[allow(dead_code)]
92pub fn heading(text: impl Display) -> String {
93    paint(text, CYAN_BOLD)
94}
95
96/// Prints a user-facing label for a key or identifier.
97#[allow(dead_code)]
98pub fn key(text: impl Display) -> String {
99    paint(text, BLUE_BOLD)
100}
101
102/// Prints a user-facing label for an ID or unique identifier.
103#[allow(dead_code)]
104pub fn id(text: impl Display) -> String {
105    paint(text, MAGENTA)
106}
107
108/// Prints a user-facing label for a file path or location.
109#[allow(dead_code)]
110pub fn path(text: impl Display) -> String {
111    paint(text, CYAN)
112}
113
114/// Prints a user-facing message in a dimmed or less prominent style.
115#[allow(dead_code)]
116pub fn dim(text: impl Display) -> String {
117    paint(text, DIM)
118}
119
120/// Prints a user-facing message for a successful status.
121#[allow(dead_code)]
122pub fn status_ok(text: impl Display) -> String {
123    paint(text, GREEN_BOLD)
124}
125
126/// Prints a user-facing message for a warning status.
127#[allow(dead_code)]
128pub fn status_warn(text: impl Display) -> String {
129    paint(text, YELLOW_BOLD)
130}
131
132/// Prints a user-facing message for an error status.
133#[allow(dead_code)]
134pub fn status_bad(text: impl Display) -> String {
135    paint(text, RED_BOLD)
136}