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 CYAN_UNDERLINE: &str = "\x1b[4;36m";
15// const BLUE: &str = "\x1b[34m";
16// const BLUE_BOLD: &str = "\x1b[1;34m";
17// const BLUE_UNDERLINE: &str = "\x1b[4;34m";
18const MAGENTA: &str = "\x1b[35m";
19const CYAN: &str = "\x1b[36m";
20const DIM: &str = "\x1b[2m";
21const DIM_UNDERLINE: &str = "\x1b[2;4m";
22const BOLD: &str = "\x1b[1m";
23const UNDERLINE: &str = "\x1b[4m";
24const BOLD_UNDERLINE: &str = "\x1b[1;4m";
25const GREEN: &str = "\x1b[32m";
26const YELLOW: &str = "\x1b[33m";
27const RED: &str = "\x1b[31m";
28const GOLD: &str = "\x1b[38;5;220m";
29const LIGHT_BLUE: &str = "\x1b[38;5;117m";
30const DIM_BOLD: &str = "\x1b[1;2m";
31const NEWLINE: &str = " ";
32
33// Decorative startup banner for `bond` (shown on bare `bond` command).
34// Kept as raw string so spacing/alignment is preserved exactly.
35const BONDS_BANNER: &str = r#"
36 __                        __
37/\ \                      /\ \
38\ \ \____   ___    ___    \_\ \    ____
39 \ \ '__`\ / __`\/' _ `\  /'_` \  /',__\
40  \ \ \L\ /\ \L\ /\ \/\ \/\ \L\ \/\__, `\
41   \ \_,__\ \____\ \_\ \_\ \___,_\/\____/
42    \/___/ \/___/ \/_/\/_/\/__,_ /\/___/
43"#;
44
45/// Default landing output shown when running `bond` without subcommands.
46/// TODO: This should automatically display the command list and descriptions, but for now it's just a branded message with quick start instructions.
47pub fn landing(version: &str) {
48    paint(BONDS_BANNER, BOLD);
49    paint("                              ", DIM_UNDERLINE);
50    newline();
51    paint("      https://bonds.fyi      ", LIGHT_BLUE);
52    paint("                              ", DIM_UNDERLINE);
53    newline();
54    info(format!("Version {version}"));
55    dim("Organize and manage source-target directory bonds.");
56    newline();
57
58    heading("Quick start:");
59    normal("  bond add <source> [target] [--name <name>] [--dry-run] [--verbose]");
60    normal("  bond list");
61    normal("  bond info <id|name>");
62    normal("  bond remove <id|name> [--with-target] [--dry-run] [--verbose]");
63    normal(
64        "  bond update <id|name> [--source <path>] [--target <path>] [--name <name>] [--dry-run] [--verbose]",
65    );
66    normal("  bond migrate <id|name> [dest] [--dry-run] [--verbose]");
67    newline();
68
69    dim("Use `bond --help` for the full command reference.");
70}
71
72fn colors_enabled() -> bool {
73    // Respect common no-color conventions and avoid ANSI noise when redirected.
74    if std::env::var_os("NO_COLOR").is_some() {
75        return false;
76    }
77    if std::env::var_os("CLICOLOR_FORCE").is_some() {
78        return true;
79    }
80
81    std::io::stderr().is_terminal() && std::env::var("TERM").map_or(true, |term| term != "dumb")
82}
83
84fn paint(text: impl Display, style: &str) -> String {
85    let text = text.to_string();
86    #[allow(unused_assignments)]
87    let mut result = String::with_capacity(style.len() + text.len() + RESET.len());
88    if colors_enabled() {
89        result = format!("{style}{text}{RESET}");
90    } else {
91        result = text;
92    }
93    println!("{}", result);
94    result
95}
96
97fn style_for(kind: ErrorKind) -> &'static str {
98    match kind {
99        // These are usually recoverable user-facing situations.
100        ErrorKind::NotFound | ErrorKind::Conflict => YELLOW_BOLD,
101        // These are hard failures.
102        ErrorKind::Input | ErrorKind::Runtime | ErrorKind::Config => RED_BOLD,
103    }
104}
105
106/// Prints a user-facing message for an error, with a prefix indicating the error type.
107pub fn error_prefix(kind: ErrorKind) -> String {
108    // Keep the label stable; only the color changes by category.
109    paint("Error:", style_for(kind))
110}
111
112/// Formats an error message for display to the user, including a colored prefix based on the error kind.
113pub fn format_error(err: &BondError) -> String {
114    // Used for the final top-level command failure.
115    format!("{} {}", error_prefix(err.kind()), err)
116}
117
118/// 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.
119pub fn format_context_error(context: &str, err: &BondError) -> String {
120    // Useful for startup/init failures where extra context helps.
121    format!("{} {}: {}", error_prefix(err.kind()), context, err)
122}
123
124/// Prints a user-facing message for a successful operation.
125#[allow(dead_code)]
126pub fn success(text: impl Display) -> String {
127    paint(text, GREEN)
128}
129
130/// Prints a user-facing message for informational purposes.
131#[allow(dead_code)]
132pub fn info(text: impl Display) -> String {
133    paint(text, CYAN)
134}
135
136/// Prints a user-facing message for a warning or recoverable issue.
137#[allow(dead_code)]
138pub fn warning(text: impl Display) -> String {
139    paint(text, YELLOW)
140}
141
142/// Prints a user-facing message for an error or failure.
143#[allow(dead_code)]
144pub fn error(text: impl Display) -> String {
145    paint(text, RED)
146}
147
148/// Prints a user-facing heading or section title.
149#[allow(dead_code)]
150pub fn title(text: impl Display) -> String {
151    paint(format!("\n{}\n", text), BOLD_UNDERLINE)
152}
153
154/// Prints a user-facing message for a title or important section header.
155#[allow(dead_code)]
156pub fn heading(text: impl Display) -> String {
157    paint(format!("{}", text), BOLD)
158}
159
160/// Prints a user-facing message with underlined text, often used for emphasis or to indicate a sub-section.
161#[allow(dead_code)]
162pub fn underline(text: impl Display) -> String {
163    paint(format!("{}", text), UNDERLINE)
164}
165
166/// Prints a user-facing message for a subheading or less prominent section header.
167#[allow(dead_code)]
168pub fn subheading(text: impl Display) -> String {
169    paint(format!("{}", text), DIM_BOLD)
170}
171
172/// 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.
173#[allow(dead_code)]
174pub fn normal(text: impl Display) -> String {
175    paint(text, RESET)
176}
177
178/// Prints a user-facing label for a key or identifier.
179#[allow(dead_code)]
180pub fn key(text: impl Display) -> String {
181    paint(text, GOLD)
182}
183
184/// Prints a user-facing label for an ID or unique identifier.
185#[allow(dead_code)]
186pub fn id(text: impl Display) -> String {
187    paint(text, LIGHT_BLUE)
188}
189
190/// Prints a user-facing label for a file path or location.
191#[allow(dead_code)]
192pub fn path(text: impl Display) -> String {
193    paint(text, MAGENTA)
194}
195
196/// Prints a user-facing message in a dimmed or less prominent style.
197#[allow(dead_code)]
198pub fn dim(text: impl Display) -> String {
199    paint(text, DIM)
200}
201
202/// Prints a user-facing message for a successful status.
203#[allow(dead_code)]
204pub fn status_ok(text: impl Display) -> String {
205    paint(text, GREEN_BOLD)
206}
207
208/// Prints a user-facing message for a warning status.
209#[allow(dead_code)]
210pub fn status_warn(text: impl Display) -> String {
211    paint(text, YELLOW_BOLD)
212}
213
214/// Prints a user-facing message for an error status.
215#[allow(dead_code)]
216pub fn status_bad(text: impl Display) -> String {
217    paint(text, RED_BOLD)
218}
219
220/// Prints a newline to the user-facing output. This can be used to separate sections or add spacing between messages for better readability.
221#[allow(dead_code)]
222pub fn newline() {
223    paint("", NEWLINE);
224}
225
226/// Prints a user-facing message for a debug or verbose log, which is only shown when the verbose mode is enabled. This can be used to provide additional information about the internal state or operations of the application that may be helpful for troubleshooting or understanding the behavior of the application, but is not necessary for regular usage.
227#[allow(dead_code)]
228pub fn debug(text: impl Display) -> String {
229    paint(text, DIM)
230}