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