1use colored::Colorize;
21use indicatif::{ProgressBar, ProgressStyle};
22use std::sync::atomic::{AtomicBool, Ordering};
23use std::time::Duration;
24
25static COLOR_DISABLED: AtomicBool = AtomicBool::new(false);
28
29pub fn disable_color() {
31 COLOR_DISABLED.store(true, Ordering::Relaxed);
32 colored::control::set_override(false);
33}
34
35pub fn init_color(no_color_flag: bool) {
38 if no_color_flag
39 || std::env::var("NO_COLOR")
40 .map(|v| !v.is_empty())
41 .unwrap_or(false)
42 {
43 disable_color();
44 }
45}
46
47fn is_color() -> bool {
48 !COLOR_DISABLED.load(Ordering::Relaxed)
49}
50
51pub mod palette {
55 pub const ACCENT: (u8, u8, u8) = (0xFF, 0x5A, 0x2D);
56 pub const ACCENT_BRIGHT: (u8, u8, u8) = (0xFF, 0x7A, 0x3D);
57 pub const ACCENT_DIM: (u8, u8, u8) = (0xD1, 0x4A, 0x22);
58 pub const INFO: (u8, u8, u8) = (0xFF, 0x8A, 0x5B);
59 pub const SUCCESS: (u8, u8, u8) = (0x2F, 0xBF, 0x71);
60 pub const WARN: (u8, u8, u8) = (0xFF, 0xB0, 0x20);
61 pub const ERROR: (u8, u8, u8) = (0xE2, 0x3D, 0x2D);
62 pub const MUTED: (u8, u8, u8) = (0x8B, 0x7F, 0x77);
63}
64
65fn apply(text: &str, rgb: (u8, u8, u8)) -> String {
70 if is_color() {
71 text.truecolor(rgb.0, rgb.1, rgb.2).to_string()
72 } else {
73 text.to_string()
74 }
75}
76
77fn apply_bold(text: &str, rgb: (u8, u8, u8)) -> String {
78 if is_color() {
79 text.truecolor(rgb.0, rgb.1, rgb.2).bold().to_string()
80 } else {
81 text.to_string()
82 }
83}
84
85pub fn accent(text: &str) -> String {
87 apply(text, palette::ACCENT)
88}
89
90pub fn accent_bright(text: &str) -> String {
92 apply(text, palette::ACCENT_BRIGHT)
93}
94
95pub fn accent_dim(text: &str) -> String {
97 apply(text, palette::ACCENT_DIM)
98}
99
100pub fn info(text: &str) -> String {
102 apply(text, palette::INFO)
103}
104
105pub fn success(text: &str) -> String {
107 apply(text, palette::SUCCESS)
108}
109
110pub fn warn(text: &str) -> String {
112 apply(text, palette::WARN)
113}
114
115pub fn error(text: &str) -> String {
117 apply(text, palette::ERROR)
118}
119
120pub fn muted(text: &str) -> String {
122 apply(text, palette::MUTED)
123}
124
125pub fn heading(text: &str) -> String {
127 apply_bold(text, palette::ACCENT)
128}
129
130pub fn bold(text: &str) -> String {
132 if is_color() {
133 text.bold().to_string()
134 } else {
135 text.to_string()
136 }
137}
138
139pub fn dim(text: &str) -> String {
141 if is_color() {
142 text.dimmed().to_string()
143 } else {
144 text.to_string()
145 }
146}
147
148pub fn icon_ok(label: &str) -> String {
154 format!("{} {}", success("✓"), label)
155}
156
157pub fn icon_fail(label: &str) -> String {
159 format!("{} {}", error("✗"), label)
160}
161
162pub fn icon_warn(label: &str) -> String {
164 format!("{} {}", warn("⚠"), label)
165}
166
167pub fn icon_muted(label: &str) -> String {
169 format!("{} {}", muted("·"), muted(label))
170}
171
172pub fn label_value(label: &str, value: &str) -> String {
176 format!(" {} : {}", muted(label), info(value))
177}
178
179const SPINNER_CHARS: &[&str] = &["◒", "◐", "◓", "◑"];
183
184pub fn spinner(message: &str) -> ProgressBar {
189 let pb = ProgressBar::new_spinner();
190 let style = if is_color() {
191 ProgressStyle::with_template(&format!(
192 "{{spinner:.{}}} {{msg}}",
193 "red" ))
195 .unwrap()
196 .tick_strings(SPINNER_CHARS)
197 } else {
198 ProgressStyle::with_template("{spinner} {msg}")
199 .unwrap()
200 .tick_strings(SPINNER_CHARS)
201 };
202 pb.set_style(style);
203 pb.set_message(message.to_string());
204 pb.enable_steady_tick(Duration::from_millis(80));
205 pb
206}
207
208pub fn spinner_ok(pb: &ProgressBar, message: &str) {
210 pb.finish_with_message(icon_ok(message));
211}
212
213pub fn spinner_fail(pb: &ProgressBar, message: &str) {
215 pb.finish_with_message(icon_fail(message));
216}
217
218pub fn spinner_warn(pb: &ProgressBar, message: &str) {
220 pb.finish_with_message(icon_warn(message));
221}
222
223pub fn print_header(title: &str) {
227 use unicode_width::UnicodeWidthStr;
228
229 let display_w = UnicodeWidthStr::width(title);
230 let inner = (display_w + 4).max(42);
232 let pad = inner - display_w;
233 let left = pad / 2;
234 let right = pad - left;
235 println!();
236 println!("{}", accent(&format!("┌{}┐", "─".repeat(inner))));
237 println!(
238 "{}",
239 accent(&format!(
240 "│{}{}{}│",
241 " ".repeat(left),
242 title,
243 " ".repeat(right)
244 ))
245 );
246 println!("{}", accent(&format!("└{}┘", "─".repeat(inner))));
247 println!();
248}
249
250#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_no_color_output() {
258 COLOR_DISABLED.store(true, Ordering::Relaxed);
260 colored::control::set_override(false);
261 assert_eq!(accent("hello"), "hello");
262 assert_eq!(success("ok"), "ok");
263 assert_eq!(error("fail"), "fail");
264 assert_eq!(icon_ok("done"), "✓ done");
265 assert_eq!(icon_fail("bad"), "✗ bad");
266 colored::control::unset_override();
268 COLOR_DISABLED.store(false, Ordering::Relaxed);
269 }
270
271 #[test]
272 fn test_label_value() {
273 COLOR_DISABLED.store(true, Ordering::Relaxed);
274 let out = label_value("Key", "/some/path");
275 assert!(out.contains("Key"));
276 assert!(out.contains("/some/path"));
277 COLOR_DISABLED.store(false, Ordering::Relaxed);
278 }
279}
280
281