bel7_cli/output.rs
1// Copyright (C) 2025-2026 Michael S. Klishin and Contributors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Colored console output utilities.
16//!
17//! Provides consistent, colored output for CLI applications.
18//! Respects the `NO_COLOR` environment variable and detects non-TTY output.
19
20use std::env;
21use std::fmt::Display;
22use std::io::IsTerminal;
23
24use owo_colors::OwoColorize;
25
26/// Returns whether colored output should be used.
27///
28/// Returns `false` if the `NO_COLOR` environment variable is set (any value)
29/// or `stdout` is not a terminal (that is, piped or redirected).
30///
31/// This follows the [NO_COLOR standard](https://no-color.org/).
32#[must_use]
33pub fn should_colorize() -> bool {
34 env::var("NO_COLOR").is_err() && std::io::stdout().is_terminal()
35}
36
37/// Returns whether colored output should be used for stderr.
38///
39/// Returns `false` if the `NO_COLOR` environment variable is set (any value)
40/// or `stdout` is not a terminal (that is, piped or redirected).
41#[must_use]
42pub fn should_colorize_stderr() -> bool {
43 env::var("NO_COLOR").is_err() && std::io::stderr().is_terminal()
44}
45
46/// Prints a success message with a green checkmark prefix.
47///
48/// Respects `NO_COLOR` and terminal detection.
49pub fn print_success(message: impl Display) {
50 if should_colorize() {
51 println!("{} {}", "✓".green().bold(), message);
52 } else {
53 println!("✓ {}", message);
54 }
55}
56
57/// Prints an error message to stderr with a red X prefix.
58///
59/// Respects `NO_COLOR` and terminal detection.
60pub fn print_error(message: impl Display) {
61 if should_colorize_stderr() {
62 eprintln!("{} {}", "✗".red().bold(), message);
63 } else {
64 eprintln!("✗ {}", message);
65 }
66}
67
68/// Prints a warning message with a yellow exclamation prefix.
69///
70/// Respects `NO_COLOR` and terminal detection.
71pub fn print_warning(message: impl Display) {
72 if should_colorize() {
73 println!("{} {}", "!".yellow().bold(), message);
74 } else {
75 println!("! {}", message);
76 }
77}
78
79/// Prints an info message with a blue arrow prefix.
80///
81/// Respects `NO_COLOR` and terminal detection.
82pub fn print_info(message: impl Display) {
83 if should_colorize() {
84 println!("{} {}", "→".blue().bold(), message);
85 } else {
86 println!("→ {}", message);
87 }
88}
89
90/// Prints a dimmed/muted message.
91///
92/// Respects `NO_COLOR` and terminal detection.
93pub fn print_dimmed(message: impl Display) {
94 if should_colorize() {
95 println!("{}", message.to_string().dimmed());
96 } else {
97 println!("{}", message);
98 }
99}
100
101/// Formats a value as success (green) if colors are enabled.
102#[must_use]
103pub fn format_success<T: Display>(value: T) -> String {
104 if should_colorize() {
105 format!("{}", value.green())
106 } else {
107 value.to_string()
108 }
109}
110
111/// Formats a value as error (red) if colors are enabled.
112#[must_use]
113pub fn format_error<T: Display>(value: T) -> String {
114 if should_colorize() {
115 format!("{}", value.red())
116 } else {
117 value.to_string()
118 }
119}
120
121/// Formats a value as warning (yellow) if colors are enabled.
122#[must_use]
123pub fn format_warning<T: Display>(value: T) -> String {
124 if should_colorize() {
125 format!("{}", value.yellow())
126 } else {
127 value.to_string()
128 }
129}
130
131/// Formats a value as info (blue) if colors are enabled.
132#[must_use]
133pub fn format_info<T: Display>(value: T) -> String {
134 if should_colorize() {
135 format!("{}", value.blue())
136 } else {
137 value.to_string()
138 }
139}
140
141/// Formats a value as dimmed/muted if colors are enabled.
142#[must_use]
143pub fn format_dimmed<T: Display>(value: T) -> String {
144 if should_colorize() {
145 format!("{}", value.dimmed())
146 } else {
147 value.to_string()
148 }
149}
150
151/// Formats a value as bold if colors are enabled.
152#[must_use]
153pub fn format_bold<T: Display>(value: T) -> String {
154 if should_colorize() {
155 format!("{}", value.bold())
156 } else {
157 value.to_string()
158 }
159}