1use std::borrow::Cow;
13use std::io::IsTerminal;
14
15pub const NO_COLOR_ENV: &str = "NO_COLOR";
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21#[non_exhaustive]
22pub enum ColorChoice {
23 #[default]
25 Auto,
26 Always,
28 Never,
30}
31
32impl ColorChoice {
33 #[must_use]
38 pub fn from_name(name: &str) -> Option<Self> {
39 match name {
40 "auto" => Some(Self::Auto),
41 "always" => Some(Self::Always),
42 "never" => Some(Self::Never),
43 _ => None,
44 }
45 }
46
47 #[must_use]
49 pub const fn as_str(self) -> &'static str {
50 match self {
51 Self::Auto => "auto",
52 Self::Always => "always",
53 Self::Never => "never",
54 }
55 }
56}
57
58#[must_use]
65pub fn resolve_color(choice: ColorChoice, is_terminal: bool) -> bool {
66 resolve_color_with(choice, no_color_env_set(), is_terminal)
67}
68
69#[must_use]
75pub const fn resolve_color_with(choice: ColorChoice, no_color: bool, is_terminal: bool) -> bool {
76 if no_color {
77 return false;
78 }
79 match choice {
80 ColorChoice::Always => true,
81 ColorChoice::Never => false,
82 ColorChoice::Auto => is_terminal,
83 }
84}
85
86#[must_use]
91pub fn no_color_env_set() -> bool {
92 std::env::var_os(NO_COLOR_ENV).is_some()
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub struct Palette {
103 enabled: bool,
104}
105
106impl Palette {
107 #[must_use]
109 pub const fn new(enabled: bool) -> Self {
110 Self { enabled }
111 }
112
113 #[must_use]
118 pub fn for_stream(choice: ColorChoice, stream: &impl IsTerminal) -> Self {
119 Self::new(resolve_color(choice, stream.is_terminal()))
120 }
121
122 #[must_use]
124 pub const fn enabled(self) -> bool {
125 self.enabled
126 }
127
128 #[must_use]
130 pub fn success(self, text: &str) -> Cow<'_, str> {
131 self.paint("32", text)
132 }
133
134 #[must_use]
136 pub fn error(self, text: &str) -> Cow<'_, str> {
137 self.paint("31", text)
138 }
139
140 #[must_use]
142 pub fn warn(self, text: &str) -> Cow<'_, str> {
143 self.paint("33", text)
144 }
145
146 #[must_use]
148 pub fn info(self, text: &str) -> Cow<'_, str> {
149 self.paint("36", text)
150 }
151
152 #[must_use]
154 pub fn dim(self, text: &str) -> Cow<'_, str> {
155 self.paint("2", text)
156 }
157
158 #[must_use]
160 pub fn bold(self, text: &str) -> Cow<'_, str> {
161 self.paint("1", text)
162 }
163
164 fn paint<'a>(self, code: &str, text: &'a str) -> Cow<'a, str> {
169 if self.enabled {
170 Cow::Owned(format!("\u{1b}[{code}m{text}\u{1b}[0m"))
171 } else {
172 Cow::Borrowed(text)
173 }
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::{ColorChoice, Palette, no_color_env_set, resolve_color, resolve_color_with};
180 use std::borrow::Cow;
181
182 #[test]
183 fn no_color_overrides_always() {
184 assert!(!resolve_color_with(ColorChoice::Always, true, true));
185 assert!(!resolve_color_with(ColorChoice::Always, true, false));
186 }
187
188 #[test]
189 fn always_and_never_are_absolute_without_no_color() {
190 assert!(resolve_color_with(ColorChoice::Always, false, false));
191 assert!(!resolve_color_with(ColorChoice::Never, false, true));
192 }
193
194 #[test]
195 fn auto_follows_terminal() {
196 assert!(resolve_color_with(ColorChoice::Auto, false, true));
197 assert!(!resolve_color_with(ColorChoice::Auto, false, false));
198 }
199
200 #[test]
201 fn choice_round_trips_by_name() {
202 for choice in [ColorChoice::Auto, ColorChoice::Always, ColorChoice::Never] {
203 assert_eq!(ColorChoice::from_name(choice.as_str()), Some(choice));
204 }
205 assert_eq!(ColorChoice::from_name("bogus"), None);
206 }
207
208 #[test]
209 fn disabled_palette_is_the_identity() {
210 let plain = Palette::new(false);
211 assert_eq!(plain.success("ok"), Cow::Borrowed("ok"));
212 assert_eq!(plain.error("boom"), Cow::Borrowed("boom"));
213 assert!(matches!(plain.success("ok"), Cow::Borrowed(_)));
214 assert!(!plain.enabled());
215 }
216
217 #[test]
218 fn enabled_palette_wraps_in_sgr_and_resets() {
219 let color = Palette::new(true);
220 assert_eq!(color.success("ok"), "\u{1b}[32mok\u{1b}[0m");
221 assert_eq!(color.error("boom"), "\u{1b}[31mboom\u{1b}[0m");
222 assert!(color.enabled());
223 }
224
225 #[test]
226 fn never_choice_stays_disabled_through_the_env_aware_resolver() {
227 assert!(!resolve_color(ColorChoice::Never, true));
230 let _ = no_color_env_set();
231 }
232
233 #[test]
234 fn for_stream_resolves_a_palette_against_a_real_stream() {
235 let palette = Palette::for_stream(ColorChoice::Never, &std::io::stderr());
237 assert!(!palette.enabled());
238 }
239}