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]
117 pub fn for_stream(choice: ColorChoice, stream: &impl IsTerminal) -> Self {
118 Self::new(resolve_color(choice, stream.is_terminal()))
119 }
120
121 #[must_use]
123 pub const fn enabled(self) -> bool {
124 self.enabled
125 }
126
127 #[must_use]
129 pub fn success(self, text: &str) -> Cow<'_, str> {
130 self.paint("32", text)
131 }
132
133 #[must_use]
135 pub fn error(self, text: &str) -> Cow<'_, str> {
136 self.paint("31", text)
137 }
138
139 #[must_use]
141 pub fn warn(self, text: &str) -> Cow<'_, str> {
142 self.paint("33", text)
143 }
144
145 #[must_use]
147 pub fn info(self, text: &str) -> Cow<'_, str> {
148 self.paint("36", text)
149 }
150
151 #[must_use]
153 pub fn dim(self, text: &str) -> Cow<'_, str> {
154 self.paint("2", text)
155 }
156
157 #[must_use]
159 pub fn bold(self, text: &str) -> Cow<'_, str> {
160 self.paint("1", text)
161 }
162
163 fn paint<'a>(self, code: &str, text: &'a str) -> Cow<'a, str> {
168 if self.enabled {
169 Cow::Owned(format!("\u{1b}[{code}m{text}\u{1b}[0m"))
170 } else {
171 Cow::Borrowed(text)
172 }
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::{ColorChoice, Palette, no_color_env_set, resolve_color, resolve_color_with};
179 use std::borrow::Cow;
180
181 #[test]
182 fn no_color_overrides_always() {
183 assert!(!resolve_color_with(ColorChoice::Always, true, true));
184 assert!(!resolve_color_with(ColorChoice::Always, true, false));
185 }
186
187 #[test]
188 fn always_and_never_are_absolute_without_no_color() {
189 assert!(resolve_color_with(ColorChoice::Always, false, false));
190 assert!(!resolve_color_with(ColorChoice::Never, false, true));
191 }
192
193 #[test]
194 fn auto_follows_terminal() {
195 assert!(resolve_color_with(ColorChoice::Auto, false, true));
196 assert!(!resolve_color_with(ColorChoice::Auto, false, false));
197 }
198
199 #[test]
200 fn choice_round_trips_by_name() {
201 for choice in [ColorChoice::Auto, ColorChoice::Always, ColorChoice::Never] {
202 assert_eq!(ColorChoice::from_name(choice.as_str()), Some(choice));
203 }
204 assert_eq!(ColorChoice::from_name("bogus"), None);
205 }
206
207 #[test]
208 fn disabled_palette_is_the_identity() {
209 let plain = Palette::new(false);
210 assert_eq!(plain.success("ok"), Cow::Borrowed("ok"));
211 assert_eq!(plain.error("boom"), Cow::Borrowed("boom"));
212 assert!(matches!(plain.success("ok"), Cow::Borrowed(_)));
213 assert!(!plain.enabled());
214 }
215
216 #[test]
217 fn enabled_palette_wraps_in_sgr_and_resets() {
218 let color = Palette::new(true);
219 assert_eq!(color.success("ok"), "\u{1b}[32mok\u{1b}[0m");
220 assert_eq!(color.error("boom"), "\u{1b}[31mboom\u{1b}[0m");
221 assert!(color.enabled());
222 }
223
224 #[test]
225 fn never_choice_stays_disabled_through_the_env_aware_resolver() {
226 assert!(!resolve_color(ColorChoice::Never, true));
229 let _ = no_color_env_set();
230 }
231
232 #[test]
233 fn for_stream_resolves_a_palette_against_a_real_stream() {
234 let palette = Palette::for_stream(ColorChoice::Never, &std::io::stderr());
236 assert!(!palette.enabled());
237 }
238}