starbase_styles/
color.rs

1// Colors based on 4th column, except for gray:
2// https://upload.wikimedia.org/wikipedia/commons/1/15/Xterm_256color_chart.svg
3
4use crate::theme::is_light_theme;
5use owo_colors::{OwoColorize, XtermColors};
6use std::env;
7use std::path::Path;
8
9pub use owo_colors as owo;
10pub use owo_colors::Style as OwoStyle;
11
12/// ANSI colors for a dark theme.
13pub enum Color {
14    White = 15,
15    Black = 16,
16    Teal = 36,
17    Cyan = 38,
18    Blue = 39,
19    Green = 41,
20    Purple = 111,
21    Lime = 112,
22    Lavender = 147,
23    Red = 161,
24    Brown = 172,
25    Pink = 183,
26    Yellow = 185,
27    Orange = 208,
28    Gray = 239,
29    GrayLight = 246,
30}
31
32/// ANSI colors for a dark theme.
33pub type DarkColor = Color;
34
35/// ANSI colors for a light theme.
36pub enum LightColor {
37    White = 15,
38    Black = 16,
39    Teal = 29,
40    Cyan = 30,
41    Blue = 25,
42    Green = 28,
43    Purple = 93,
44    Lime = 101,
45    Lavender = 135,
46    Red = 160,
47    Brown = 94,
48    Pink = 170,
49    Yellow = 178,
50    Orange = 202,
51    Gray = 238,
52    GrayLight = 241,
53}
54
55/// Types of colors based on state and usage.
56#[derive(Clone, Debug, PartialEq)]
57pub enum Style {
58    Tag(String),
59
60    // States
61    Caution,
62    Failure,
63    Invalid,
64    Muted,
65    MutedLight,
66    Success,
67
68    // Types
69    File,     // rel file paths, file names/exts
70    Hash,     // hashes, shas, commits
71    Id,       // ids, names
72    Label,    // titles, strings
73    Path,     // abs file paths
74    Property, // properties, keys, fields, settings
75    Shell,    // shell, cli, commands
76    Symbol,   // symbols, chars
77    Url,      // urls
78}
79
80impl Style {
81    /// Convert the style a specific ANSI color code, based on the current theme.
82    pub fn ansi_color(&self) -> u8 {
83        if is_light_theme() {
84            self.light_color() as u8
85        } else {
86            self.dark_color() as u8
87        }
88    }
89
90    /// Convert the style to a specific [Color].
91    pub fn color(&self) -> Color {
92        self.dark_color()
93    }
94
95    /// Convert the style to a specific [DarkColor].
96    pub fn dark_color(&self) -> DarkColor {
97        match self {
98            Style::Caution => DarkColor::Orange,
99            Style::Failure => DarkColor::Red,
100            Style::Invalid => DarkColor::Yellow,
101            Style::Muted => DarkColor::Gray,
102            Style::MutedLight => DarkColor::GrayLight,
103            Style::Success => DarkColor::Green,
104            Style::File => DarkColor::Teal,
105            Style::Hash => DarkColor::Green,
106            Style::Id => DarkColor::Purple,
107            Style::Label => DarkColor::Blue,
108            Style::Path => DarkColor::Cyan,
109            Style::Property => DarkColor::Lavender,
110            Style::Shell => DarkColor::Pink,
111            Style::Symbol => DarkColor::Lime,
112            Style::Url => DarkColor::Blue,
113            Style::Tag(_) => DarkColor::White,
114        }
115    }
116
117    /// Convert the style to a specific [LightColor].
118    pub fn light_color(&self) -> LightColor {
119        match self {
120            Style::Caution => LightColor::Orange,
121            Style::Failure => LightColor::Red,
122            Style::Invalid => LightColor::Yellow,
123            Style::Muted => LightColor::Gray,
124            Style::MutedLight => LightColor::GrayLight,
125            Style::Success => LightColor::Green,
126            Style::File => LightColor::Teal,
127            Style::Hash => LightColor::Green,
128            Style::Id => LightColor::Purple,
129            Style::Label => LightColor::Blue,
130            Style::Path => LightColor::Cyan,
131            Style::Property => LightColor::Lavender,
132            Style::Shell => LightColor::Pink,
133            Style::Symbol => LightColor::Lime,
134            Style::Url => LightColor::Blue,
135            Style::Tag(_) => LightColor::Black,
136        }
137    }
138}
139
140/// Create a new `owo_colors` [Style][OwoStyle] instance and apply the given color.
141pub fn create_style(color: u8) -> OwoStyle {
142    OwoStyle::new().color(XtermColors::from(color))
143}
144
145/// Paint and wrap the string with the appropriate ANSI color escape code.
146/// If colors are disabled, the string is returned as-is.
147pub fn paint<T: AsRef<str>>(color: u8, value: T) -> String {
148    if no_color() {
149        value.as_ref().to_string()
150    } else {
151        value.as_ref().style(create_style(color)).to_string()
152    }
153}
154
155/// Paint the string with the given style.
156pub fn paint_style<T: AsRef<str>>(style: Style, value: T) -> String {
157    if matches!(style, Style::File | Style::Path | Style::Shell) {
158        paint(style.ansi_color(), clean_path(value.as_ref()))
159    } else {
160        paint(style.ansi_color(), value)
161    }
162}
163
164// States
165
166/// Paint a caution state.
167pub fn caution<T: AsRef<str>>(value: T) -> String {
168    paint_style(Style::Caution, value)
169}
170
171/// Paint a failure state.
172pub fn failure<T: AsRef<str>>(value: T) -> String {
173    paint_style(Style::Failure, value)
174}
175
176/// Paint an invalid state.
177pub fn invalid<T: AsRef<str>>(value: T) -> String {
178    paint_style(Style::Invalid, value)
179}
180
181/// Paint a muted dark state.
182pub fn muted<T: AsRef<str>>(value: T) -> String {
183    paint_style(Style::Muted, value)
184}
185
186/// Paint a muted light state.
187pub fn muted_light<T: AsRef<str>>(value: T) -> String {
188    paint_style(Style::MutedLight, value)
189}
190
191/// Paint a success state.
192pub fn success<T: AsRef<str>>(value: T) -> String {
193    paint_style(Style::Success, value)
194}
195
196// Types
197
198/// Paint a partial file path or glob pattern.
199pub fn file<T: AsRef<str>>(path: T) -> String {
200    paint_style(Style::File, path)
201}
202
203/// Paint a hash-like value.
204pub fn hash<T: AsRef<str>>(value: T) -> String {
205    paint_style(Style::Hash, value)
206}
207
208/// Paint an identifier.
209pub fn id<T: AsRef<str>>(value: T) -> String {
210    paint_style(Style::Id, value)
211}
212
213/// Paint a label, heading, or title.
214pub fn label<T: AsRef<str>>(value: T) -> String {
215    paint_style(Style::Label, value)
216}
217
218/// Paint an absolute file path.
219pub fn path<T: AsRef<Path>>(path: T) -> String {
220    paint_style(Style::Path, path.as_ref().to_str().unwrap_or("<unknown>"))
221}
222
223/// Paint an relative file path.
224#[cfg(feature = "relative-path")]
225pub fn rel_path<T: AsRef<relative_path::RelativePath>>(path: T) -> String {
226    paint_style(Style::Path, path.as_ref().as_str())
227}
228
229/// Paint a property, key, or setting.
230pub fn property<T: AsRef<str>>(value: T) -> String {
231    paint_style(Style::Property, value)
232}
233
234/// Paint a shell command or input string.
235pub fn shell<T: AsRef<str>>(cmd: T) -> String {
236    paint_style(Style::Shell, cmd)
237}
238
239/// Paint a symbol, value, or number.
240pub fn symbol<T: AsRef<str>>(value: T) -> String {
241    paint_style(Style::Symbol, value)
242}
243
244/// Paint a URL.
245pub fn url<T: AsRef<str>>(url: T) -> String {
246    paint_style(Style::Url, url)
247}
248
249// Helpers
250
251/// Clean a file system path by replacing the home directory with `~`.
252pub fn clean_path<T: AsRef<str>>(path: T) -> String {
253    let path = path.as_ref();
254
255    #[cfg(not(target_arch = "wasm32"))]
256    if let Some(home) = dirs::home_dir() {
257        return path.replace(home.to_str().unwrap_or_default(), "~");
258    }
259
260    path.to_string()
261}
262
263/// Dynamically apply a color to the log target/module/namespace based
264/// on the characters in the string.
265pub fn log_target<T: AsRef<str>>(value: T) -> String {
266    let value = value.as_ref();
267    let mut hash: u32 = 0;
268
269    for b in value.bytes() {
270        hash = (hash << 5).wrapping_sub(hash) + b as u32;
271    }
272
273    // Lot of casting going on here...
274    if supports_color() >= 2 {
275        let mut list = vec![];
276
277        if is_light_theme() {
278            list.extend(COLOR_LIST_LIGHT);
279        } else {
280            list.extend(COLOR_LIST_DARK);
281        };
282
283        let index = i32::abs(hash as i32) as usize % list.len();
284
285        return paint(list[index], value);
286    }
287
288    let index = i32::abs(hash as i32) as usize % COLOR_LIST_UNSUPPORTED.len();
289
290    paint(COLOR_LIST_UNSUPPORTED[index], value)
291}
292
293/// Return true if color has been disabled for the `stderr` stream.
294#[cfg(not(target_arch = "wasm32"))]
295pub fn no_color() -> bool {
296    env::var("NO_COLOR").is_ok() || supports_color::on(supports_color::Stream::Stderr).is_none()
297}
298
299#[cfg(target_arch = "wasm32")]
300pub fn no_color() -> bool {
301    true
302}
303
304/// Return a color level support for the `stderr` stream. 0 = no support, 1 = basic support,
305/// 2 = 256 colors, and 3 = 16 million colors.
306pub fn supports_color() -> u8 {
307    if no_color() {
308        return 0;
309    }
310
311    if let Some(support) = supports_color::on(supports_color::Stream::Stderr) {
312        if support.has_16m {
313            return 3;
314        } else if support.has_256 {
315            return 2;
316        } else if support.has_basic {
317            return 1;
318        }
319    }
320
321    1
322}
323
324pub(crate) const COLOR_LIST_DARK: [u8; 76] = [
325    20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62, 63, 68, 69, 74, 75, 76, 77,
326    78, 79, 80, 81, 92, 93, 98, 99, 112, 113, 128, 129, 134, 135, 148, 149, 160, 161, 162, 163,
327    164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 178, 179, 184, 185, 196, 197, 198, 199, 200,
328    201, 202, 203, 204, 205, 206, 207, 208, 209, 214, 215, 220, 221,
329];
330
331pub(crate) const COLOR_LIST_LIGHT: [u8; 72] = [
332    20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62, 63, 68, 69, 74, 75, 76, 77,
333    78, 79, 80, 81, 92, 93, 98, 99, 112, 113, 128, 129, 127, 126, 142, 143, 160, 161, 162, 163,
334    164, 165, 166, 167, 168, 169, 172, 173, 178, 179, 196, 197, 198, 199, 200, 201, 202, 203, 204,
335    205, 206, 207, 208, 209, 214, 215, 220, 221,
336];
337
338pub(crate) const COLOR_LIST_UNSUPPORTED: [u8; 6] = [6, 2, 3, 4, 5, 1];