1use colored::{ColoredString, Colorize};
2use is_terminal::IsTerminal;
3use std::env;
4use std::io::{stderr, stdout};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ColorMode {
9 Auto, Always, Never, }
13
14impl Default for ColorMode {
15 fn default() -> Self {
16 ColorMode::Auto
17 }
18}
19
20impl std::str::FromStr for ColorMode {
21 type Err = String;
22
23 fn from_str(s: &str) -> Result<Self, Self::Err> {
24 match s.to_lowercase().as_str() {
25 "auto" => Ok(ColorMode::Auto),
26 "always" => Ok(ColorMode::Always),
27 "never" => Ok(ColorMode::Never),
28 _ => Err(format!(
29 "Invalid color mode: '{}'. Valid options: auto, always, never",
30 s
31 )),
32 }
33 }
34}
35
36#[derive(Clone)]
38pub struct ColorHelper {
39 mode: ColorMode,
40 stdout_is_terminal: bool,
41 stderr_is_terminal: bool,
42 no_color: bool,
43}
44
45impl ColorHelper {
46 pub fn new(mode: ColorMode) -> Self {
48 Self {
49 mode,
50 stdout_is_terminal: stdout().is_terminal(),
51 stderr_is_terminal: stderr().is_terminal(),
52 no_color: env::var("NO_COLOR").is_ok()
53 && !env::var("NO_COLOR").unwrap_or_default().is_empty(),
54 }
55 }
56
57 pub fn should_color_stdout(&self) -> bool {
59 self.should_use_colors(self.stdout_is_terminal)
60 }
61
62 pub fn should_color_stderr(&self) -> bool {
64 self.should_use_colors(self.stderr_is_terminal)
65 }
66
67 fn should_use_colors(&self, is_terminal: bool) -> bool {
69 if self.no_color {
71 return false;
72 }
73
74 match self.mode {
75 ColorMode::Never => false,
76 ColorMode::Always => true,
77 ColorMode::Auto => is_terminal,
78 }
79 }
80
81 pub fn red(&self, text: &str) -> ColoredString {
83 if self.should_color_stdout() {
84 text.red()
85 } else {
86 text.normal()
87 }
88 }
89
90 pub fn green(&self, text: &str) -> ColoredString {
92 if self.should_color_stdout() {
93 text.green()
94 } else {
95 text.normal()
96 }
97 }
98
99 pub fn blue(&self, text: &str) -> ColoredString {
101 if self.should_color_stdout() {
102 text.blue()
103 } else {
104 text.normal()
105 }
106 }
107
108 pub fn yellow(&self, text: &str) -> ColoredString {
110 if self.should_color_stdout() {
111 text.yellow()
112 } else {
113 text.normal()
114 }
115 }
116
117 pub fn cyan(&self, text: &str) -> ColoredString {
119 if self.should_color_stdout() {
120 text.cyan()
121 } else {
122 text.normal()
123 }
124 }
125
126 pub fn magenta(&self, text: &str) -> ColoredString {
128 if self.should_color_stdout() {
129 text.magenta()
130 } else {
131 text.normal()
132 }
133 }
134
135 pub fn bold(&self, text: &str) -> ColoredString {
137 if self.should_color_stdout() {
138 text.bold()
139 } else {
140 text.normal()
141 }
142 }
143
144 pub fn dimmed(&self, text: &str) -> ColoredString {
146 if self.should_color_stdout() {
147 text.dimmed()
148 } else {
149 text.normal()
150 }
151 }
152
153 pub fn style(&self) -> StyleBuilder {
155 StyleBuilder::new(self.should_color_stdout())
156 }
157}
158
159pub struct StyleBuilder {
161 should_color: bool,
162}
163
164impl StyleBuilder {
165 pub fn new(should_color: bool) -> Self {
166 Self { should_color }
167 }
168
169 pub fn red(self, text: &str) -> ChainedStyle {
170 ChainedStyle::new(text, self.should_color).red()
171 }
172
173 pub fn green(self, text: &str) -> ChainedStyle {
174 ChainedStyle::new(text, self.should_color).green()
175 }
176
177 pub fn blue(self, text: &str) -> ChainedStyle {
178 ChainedStyle::new(text, self.should_color).blue()
179 }
180
181 pub fn yellow(self, text: &str) -> ChainedStyle {
182 ChainedStyle::new(text, self.should_color).yellow()
183 }
184
185 pub fn cyan(self, text: &str) -> ChainedStyle {
186 ChainedStyle::new(text, self.should_color).cyan()
187 }
188
189 pub fn magenta(self, text: &str) -> ChainedStyle {
190 ChainedStyle::new(text, self.should_color).magenta()
191 }
192
193 pub fn bold(self, text: &str) -> ChainedStyle {
194 ChainedStyle::new(text, self.should_color).bold()
195 }
196
197 pub fn dimmed(self, text: &str) -> ChainedStyle {
198 ChainedStyle::new(text, self.should_color).dimmed()
199 }
200}
201
202pub struct ChainedStyle {
204 text: String,
205 should_color: bool,
206}
207
208impl ChainedStyle {
209 fn new(text: &str, should_color: bool) -> Self {
210 Self {
211 text: text.to_string(),
212 should_color,
213 }
214 }
215
216 pub fn red(mut self) -> Self {
217 if self.should_color {
218 self.text = self.text.red().to_string();
219 }
220 self
221 }
222
223 pub fn green(mut self) -> Self {
224 if self.should_color {
225 self.text = self.text.green().to_string();
226 }
227 self
228 }
229
230 pub fn blue(mut self) -> Self {
231 if self.should_color {
232 self.text = self.text.blue().to_string();
233 }
234 self
235 }
236
237 pub fn yellow(mut self) -> Self {
238 if self.should_color {
239 self.text = self.text.yellow().to_string();
240 }
241 self
242 }
243
244 pub fn cyan(mut self) -> Self {
245 if self.should_color {
246 self.text = self.text.cyan().to_string();
247 }
248 self
249 }
250
251 pub fn magenta(mut self) -> Self {
252 if self.should_color {
253 self.text = self.text.magenta().to_string();
254 }
255 self
256 }
257
258 pub fn bold(mut self) -> Self {
259 if self.should_color {
260 self.text = self.text.bold().to_string();
261 }
262 self
263 }
264
265 pub fn dimmed(mut self) -> Self {
266 if self.should_color {
267 self.text = self.text.dimmed().to_string();
268 }
269 self
270 }
271}
272
273impl std::fmt::Display for ChainedStyle {
274 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275 write!(f, "{}", self.text)
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn test_color_mode_parsing() {
285 assert_eq!("auto".parse::<ColorMode>().unwrap(), ColorMode::Auto);
286 assert_eq!("always".parse::<ColorMode>().unwrap(), ColorMode::Always);
287 assert_eq!("never".parse::<ColorMode>().unwrap(), ColorMode::Never);
288 assert!("invalid".parse::<ColorMode>().is_err());
289 }
290
291 #[test]
292 fn test_color_helper_never() {
293 let helper = ColorHelper::new(ColorMode::Never);
294 assert!(!helper.should_color_stdout());
295 assert!(!helper.should_color_stderr());
296 }
297
298 #[test]
299 fn test_color_helper_always() {
300 let helper = ColorHelper::new(ColorMode::Always);
301 if !helper.no_color {
303 assert!(helper.should_color_stdout());
304 assert!(helper.should_color_stderr());
305 }
306 }
307}