1use super::Color;
3use clap::ValueEnum;
4use mlua::{FromLua, Lua};
5use owo_colors::{
6 AnsiColors::{
7 self, Black, Blue, BrightBlack, BrightBlue, BrightCyan, BrightGreen, BrightMagenta,
8 BrightRed, BrightWhite, BrightYellow, Cyan, Green, Magenta, Red, White, Yellow,
9 },
10 DynColors, OwoColorize,
11 Stream::Stdout,
12};
13use std::fmt::Display;
14use std::io::{self, Write};
15
16#[derive(Debug, ValueEnum, Clone, Copy)]
18pub enum ColorChoice {
19 Auto,
23 On,
25 Ansi,
27 Off,
29}
30
31impl ColorChoice {
32 #[inline]
34 pub fn is_auto(&self) -> bool {
35 matches!(self, Self::Auto)
36 }
37
38 #[inline]
40 pub fn is_on(&self) -> bool {
41 matches!(self, Self::On)
42 }
43
44 #[inline]
46 pub fn is_ansi(&self) -> bool {
47 matches!(self, Self::Ansi)
48 }
49
50 #[inline]
52 pub fn is_off(&self) -> bool {
53 matches!(self, Self::Off)
54 }
55
56 pub fn write_to<W, D>(
58 &self,
59 writer: &mut W,
60 display: D,
61 fg: Option<Color>,
62 bg: Option<Color>,
63 ) -> io::Result<()>
64 where
65 W: Write,
66 D: Display + OwoColorize,
67 {
68 match (self, fg, bg) {
69 (Self::Off, _, _) | (_, None, None) => Self::off_write_to(writer, display),
71 (Self::Auto, fg, bg) => Self::auto_write_to(writer, display, fg, bg),
72 (Self::On, fg, bg) => Self::on_write_to(writer, display, fg, bg),
73 (Self::Ansi, fg, bg) => Self::ansi_write_to(writer, display, fg, bg),
74 }
75 }
76
77 fn auto_write_to<W, D, Fg, Bg>(
79 writer: &mut W,
80 display: D,
81 fg: Option<Fg>,
82 bg: Option<Bg>,
83 ) -> io::Result<()>
84 where
85 W: Write,
86 D: Display + OwoColorize,
87 DynColors: From<Fg>,
88 DynColors: From<Bg>,
89 {
90 let fg = fg.map(DynColors::from);
91 let bg = bg.map(DynColors::from);
92
93 match (fg, bg) {
96 (None, None) => unreachable!("Should use the off writer"),
97 (Some(fg), None) => write!(
98 writer,
99 "{}",
100 display.if_supports_color(Stdout, |display| display.color(fg))
101 ),
102 (None, Some(bg)) => write!(
103 writer,
104 "{}",
105 display.if_supports_color(Stdout, |display| display.on_color(bg))
106 ),
107 (Some(fg), Some(bg)) => write!(
108 writer,
109 "{}",
110 display.if_supports_color(Stdout, |display| display.color(fg).on_color(bg))
111 ),
112 }
113 }
114
115 #[inline]
117 fn off_write_to<W, D>(writer: &mut W, display: D) -> io::Result<()>
118 where
119 W: Write,
120 D: Display,
121 {
122 write!(writer, "{display}")
123 }
124
125 fn on_write_to<W, D, Fg, Bg>(
127 writer: &mut W,
128 display: D,
129 fg: Option<Fg>,
130 bg: Option<Bg>,
131 ) -> io::Result<()>
132 where
133 W: Write,
134 D: Display + OwoColorize,
135 DynColors: From<Fg>,
136 DynColors: From<Bg>,
137 {
138 let fg = fg.map(DynColors::from);
139 let bg = bg.map(DynColors::from);
140 match (fg, bg) {
141 (None, None) => unreachable!("Should use the off writer"),
142 (Some(fg), None) => write!(writer, "{}", display.color(fg)),
143 (None, Some(bg)) => write!(writer, "{}", display.on_color(bg)),
144 (Some(fg), Some(bg)) => write!(writer, "{}", display.color(fg).on_color(bg)),
145 }
146 }
147
148 fn ansi_write_to<W, D>(
150 writer: &mut W,
151 display: D,
152 fg: Option<Color>,
153 bg: Option<Color>,
154 ) -> io::Result<()>
155 where
156 W: Write,
157 D: Display + OwoColorize,
158 {
159 let convert = |color: Color| DynColors::Ansi(Self::color_to_ansi(color));
160 let fg = fg.map(convert);
161 let bg = bg.map(convert);
162 Self::on_write_to(writer, display, fg, bg)
163 }
164
165 fn color_to_ansi(color: Color) -> AnsiColors {
167 match color {
168 Color::Ansi(ansi) => ansi,
169 Color::Rgb(r, g, b) => Self::ansi_from_rgb(r, g, b),
170 }
171 }
172
173 fn ansi_from_rgb(r: u8, g: u8, b: u8) -> AnsiColors {
175 const COLOR_INDEX: [AnsiColors; 16] = [
177 Black,
178 Red,
179 Green,
180 Yellow,
181 Blue,
182 Magenta,
183 Cyan,
184 White,
185 BrightBlack,
186 BrightRed,
187 BrightGreen,
188 BrightYellow,
189 BrightBlue,
190 BrightMagenta,
191 BrightCyan,
192 BrightWhite,
193 ];
194 #[inline]
196 const fn channel_bit(channel: u8, index: u8) -> u8 {
197 debug_assert!(index < 3);
198 (channel >> 7) << index
199 }
200
201 let brightness_index: usize = if Self::rgb_is_bright(r, g, b) { 8 } else { 0 };
202 let color_index = usize::from(channel_bit(r, 0) | channel_bit(g, 1) | channel_bit(b, 2));
203 debug_assert!(color_index <= 0b111);
204 let index = brightness_index + color_index;
205 debug_assert!(index < COLOR_INDEX.len());
206 COLOR_INDEX[index]
207 }
208
209 const fn rgb_is_bright(r: u8, g: u8, b: u8) -> bool {
211 const BRIGHT_THRESHOLD: u8 = 0b1100_0000;
214
215 (r | g | b) >= BRIGHT_THRESHOLD || Self::rgb_is_bright_black(r, g, b)
216 }
217
218 const fn rgb_is_bright_black(r: u8, g: u8, b: u8) -> bool {
221 const BRIGHT_BLACK_MIN: u8 = 0b0100_0000;
222 const BRIGHT_BLACK_MAX: u8 = 0b1000_0000;
223 #[inline]
224 const fn within_limits(channel: u8) -> bool {
225 BRIGHT_BLACK_MIN <= channel && channel < BRIGHT_BLACK_MAX
226 }
227
228 within_limits(r) && within_limits(g) && within_limits(b)
229 }
230}
231
232impl Default for ColorChoice {
233 #[inline]
234 fn default() -> Self {
236 Self::Auto
237 }
238}
239
240impl FromLua for ColorChoice {
241 fn from_lua(value: mlua::Value, _lua: &Lua) -> mlua::Result<Self> {
242 const VALID_VALUES: [&str; 4] = ["auto", "on", "off", "ansi"];
243 let type_name = value.type_name();
244 let make_conversion_error = || mlua::Error::FromLuaConversionError {
245 from: type_name,
246 to: String::from("ColorChoice"),
247 message: Some(format!("Must be one of {VALID_VALUES:?} or nil")),
248 };
249 let color_choice = value
250 .as_string()
251 .ok_or_else(make_conversion_error)?
252 .to_string_lossy();
253 let color_choice = color_choice.as_str();
254 let color_choice = match color_choice {
255 "auto" => Self::Auto,
256 "on" => Self::On,
257 "off" => Self::Off,
258 "ansi" => Self::Ansi,
259 _ => return Err(make_conversion_error()),
260 };
261 Ok(color_choice)
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use rstest::rstest;
269
270 #[rstest]
271 #[case::bright_black(85, 85, 85, BrightBlack)]
272 #[case::black(0, 0b10, 0b11, Black)]
273 #[case::green(0, 0b1000_0000, 0b11, Green)]
274 #[case::bright_green(0, 0b1100_0001, 0b11, BrightGreen)]
275 #[case::bright_yellow(0b1000_0000, 0b1100_0001, 0b11, BrightYellow)]
276 #[case::bright_white(0xFF, 0xFF, 0xFF, BrightWhite)]
277 fn test_color_choice_ansi_from_rgb(
278 #[case] r: u8,
279 #[case] g: u8,
280 #[case] b: u8,
281 #[case] expected: AnsiColors,
282 ) {
283 assert_eq!(expected, ColorChoice::ansi_from_rgb(r, g, b));
284 }
285}