Skip to main content

bonds_cli/
ui.rs

1//! The `ui` module provides functions for formatting error messages and other user-facing output in a consistent and visually appealing way. It is used throughout the CLI application to ensure that all messages are presented in a clear and user-friendly manner.
2
3use std::fmt::Display;
4use std::io::IsTerminal;
5
6use bonds_core::BondError;
7use bonds_core::error::ErrorKind;
8
9const RESET: &str = "\x1b[0m";
10const GREEN_BOLD: &str = "\x1b[1;32m";
11const YELLOW_BOLD: &str = "\x1b[1;33m";
12const RED_BOLD: &str = "\x1b[1;31m";
13// const CYAN_BOLD: &str = "\x1b[1;36m";
14// const BLUE_BOLD: &str = "\x1b[1;34m";
15const MAGENTA: &str = "\x1b[35m";
16const CYAN: &str = "\x1b[36m";
17const DIM: &str = "\x1b[2m";
18const BOLD: &str = "\x1b[1m";
19const UNDERLINE: &str = "\x1b[4m";
20const BOLD_UNDERLINE: &str = "\x1b[1;4m";
21const GREEN: &str = "\x1b[32m";
22const YELLOW: &str = "\x1b[33m";
23const RED: &str = "\x1b[31m";
24const GOLD: &str = "\x1b[38;5;220m";
25const LIGHT_BLUE: &str = "\x1b[38;5;117m";
26const DIM_BOLD: &str = "\x1b[1;2m";
27const NEWLINE: &str = " ";
28
29fn colors_enabled() -> bool {
30    // Respect common no-color conventions and avoid ANSI noise when redirected.
31    if std::env::var_os("NO_COLOR").is_some() {
32        return false;
33    }
34    if std::env::var_os("CLICOLOR_FORCE").is_some() {
35        return true;
36    }
37
38    std::io::stderr().is_terminal() && std::env::var("TERM").map_or(true, |term| term != "dumb")
39}
40
41fn paint(text: impl Display, style: &str) -> String {
42    let text = text.to_string();
43    #[allow(unused_assignments)]
44    let mut result = String::with_capacity(style.len() + text.len() + RESET.len());
45    if colors_enabled() {
46        result = format!("{style}{text}{RESET}");
47    } else {
48        result = text;
49    }
50    println!("{}", result);
51    result
52}
53
54fn style_for(kind: ErrorKind) -> &'static str {
55    match kind {
56        // These are usually recoverable user-facing situations.
57        ErrorKind::NotFound | ErrorKind::Conflict => YELLOW_BOLD,
58        // These are hard failures.
59        ErrorKind::Input | ErrorKind::Runtime | ErrorKind::Config => RED_BOLD,
60    }
61}
62
63/// Prints a user-facing message for an error, with a prefix indicating the error type.
64pub fn error_prefix(kind: ErrorKind) -> String {
65    // Keep the label stable; only the color changes by category.
66    paint("Error:", style_for(kind))
67}
68
69/// Formats an error message for display to the user, including a colored prefix based on the error kind.
70pub fn format_error(err: &BondError) -> String {
71    // Used for the final top-level command failure.
72    format!("{} {}", error_prefix(err.kind()), err)
73}
74
75/// Formats an error message with additional context for display to the user, including a colored prefix based on the error kind. This is useful for providing more information about the error, such as where it occurred or what operation was being attempted when the error happened.
76pub fn format_context_error(context: &str, err: &BondError) -> String {
77    // Useful for startup/init failures where extra context helps.
78    format!("{} {}: {}", error_prefix(err.kind()), context, err)
79}
80
81/// Prints a user-facing message for a successful operation.
82#[allow(dead_code)]
83pub fn success(text: impl Display) -> String {
84    paint(text, GREEN)
85}
86
87/// Prints a user-facing message for informational purposes.
88#[allow(dead_code)]
89pub fn info(text: impl Display) -> String {
90    paint(text, CYAN)
91}
92
93/// Prints a user-facing message for a warning or recoverable issue.
94#[allow(dead_code)]
95pub fn warning(text: impl Display) -> String {
96    paint(text, YELLOW)
97}
98
99/// Prints a user-facing message for an error or failure.
100#[allow(dead_code)]
101pub fn error(text: impl Display) -> String {
102    paint(text, RED)
103}
104
105/// Prints a user-facing heading or section title.
106#[allow(dead_code)]
107pub fn title(text: impl Display) -> String {
108    paint(format!("\n{}\n", text), BOLD_UNDERLINE)
109}
110
111/// Prints a user-facing message for a title or important section header.
112#[allow(dead_code)]
113pub fn heading(text: impl Display) -> String {
114    paint(format!("{}", text), BOLD)
115}
116
117/// Prints a user-facing message with underlined text, often used for emphasis or to indicate a sub-section.
118#[allow(dead_code)]
119pub fn underline(text: impl Display) -> String {
120    paint(format!("{}", text), UNDERLINE)
121}
122
123/// Prints a user-facing message for a subheading or less prominent section header.
124#[allow(dead_code)]
125pub fn subheading(text: impl Display) -> String {
126    paint(format!("{}", text), DIM_BOLD)
127}
128
129/// Prints normal text without any special styling. This can be used for messages that don't fit into the other categories or when you want to reset back to default styling after a heading or label.
130#[allow(dead_code)]
131pub fn normal(text: impl Display) -> String {
132    paint(text, RESET)
133}
134
135/// Prints a user-facing label for a key or identifier.
136#[allow(dead_code)]
137pub fn key(text: impl Display) -> String {
138    paint(text, GOLD)
139}
140
141/// Prints a user-facing label for an ID or unique identifier.
142#[allow(dead_code)]
143pub fn id(text: impl Display) -> String {
144    paint(text, LIGHT_BLUE)
145}
146
147/// Prints a user-facing label for a file path or location.
148#[allow(dead_code)]
149pub fn path(text: impl Display) -> String {
150    paint(text, MAGENTA)
151}
152
153/// Prints a user-facing message in a dimmed or less prominent style.
154#[allow(dead_code)]
155pub fn dim(text: impl Display) -> String {
156    paint(text, DIM)
157}
158
159/// Prints a user-facing message for a successful status.
160#[allow(dead_code)]
161pub fn status_ok(text: impl Display) -> String {
162    paint(text, GREEN_BOLD)
163}
164
165/// Prints a user-facing message for a warning status.
166#[allow(dead_code)]
167pub fn status_warn(text: impl Display) -> String {
168    paint(text, YELLOW_BOLD)
169}
170
171/// Prints a user-facing message for an error status.
172#[allow(dead_code)]
173pub fn status_bad(text: impl Display) -> String {
174    paint(text, RED_BOLD)
175}
176
177/// Prints a newline to the user-facing output. This can be used to separate sections or add spacing between messages for better readability.
178#[allow(dead_code)]
179pub fn newline() {
180    paint("", NEWLINE);
181}