1use anstyle::{AnsiColor, Color, RgbColor, Style};
4use serde::Serialize;
5use time::OffsetDateTime;
6
7pub fn json<T: Serialize>(v: &T) {
8 println!("{}", serde_json::to_string_pretty(v).expect("serializable"));
9}
10
11pub fn warn(msg: impl AsRef<str>) {
12 let warn = err().warn;
13 anstream::eprintln!("{warn}warning:{warn:#} {}", msg.as_ref());
14}
15
16pub fn error(msg: impl AsRef<str>) {
18 let error = err().error;
19 anstream::eprintln!("{error}error:{error:#} {}", msg.as_ref());
20}
21
22pub const LINK: Style = Style::new().underline();
29pub const DIM: Style = Style::new().dimmed();
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct Styles {
35 pub brand: Style,
37 pub done: Style,
39 pub warn: Style,
40 pub error: Style,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum ColourLevel {
46 True {
48 light: bool,
49 },
50 Ansi16,
52 Plain,
53}
54
55pub fn colour_level(colour: bool, colorterm: Option<&str>, colorfgbg: Option<&str>) -> ColourLevel {
60 if !colour {
61 return ColourLevel::Plain;
62 }
63 if !matches!(colorterm, Some("truecolor" | "24bit")) {
64 return ColourLevel::Ansi16;
65 }
66 let background = colorfgbg.and_then(|v| v.rsplit(';').next()).map(str::trim);
67 ColourLevel::True {
68 light: matches!(background, Some("7" | "15")),
69 }
70}
71
72pub fn styles(level: ColourLevel) -> Styles {
74 let rgb = |r, g, b| Color::Rgb(RgbColor(r, g, b));
75 let [brand, done, warn, error] = match level {
76 ColourLevel::Plain => {
77 let plain = Style::new();
78 return Styles {
79 brand: plain,
80 done: plain,
81 warn: plain,
82 error: plain,
83 };
84 }
85 ColourLevel::Ansi16 => [
86 Color::Ansi(AnsiColor::Magenta),
87 Color::Ansi(AnsiColor::Cyan),
88 Color::Ansi(AnsiColor::Yellow),
89 Color::Ansi(AnsiColor::Red),
90 ],
91 ColourLevel::True { light: false } => [
93 rgb(217, 139, 191),
94 rgb(111, 194, 184),
95 rgb(219, 174, 94),
96 rgb(254, 149, 127),
97 ],
98 ColourLevel::True { light: true } => [
100 rgb(123, 45, 94),
101 rgb(31, 111, 106),
102 rgb(111, 79, 5),
103 rgb(179, 64, 42),
104 ],
105 };
106 let bold = |c| Style::new().bold().fg_color(Some(c));
107 Styles {
108 brand: bold(brand),
109 done: bold(done),
110 warn: bold(warn),
111 error: bold(error),
112 }
113}
114
115pub const GLYPH: &str = "\u{2042}";
117
118pub fn banner(styles: Styles) -> String {
121 let plain = |s: Style| Style::new().fg_color(s.get_fg_color());
122 let (brand, done) = (plain(styles.brand), plain(styles.done));
123 format!("{brand}{GLYPH}{brand:#} semora {DIM}·{DIM:#} {done}Your projects, considered.{done:#}")
124}
125
126fn styles_of(choice: anstream::ColorChoice) -> Styles {
127 let var = |name: &str| std::env::var(name).ok();
128 styles(colour_level(
129 choice != anstream::ColorChoice::Never,
130 var("COLORTERM").as_deref(),
131 var("COLORFGBG").as_deref(),
132 ))
133}
134
135pub fn out() -> Styles {
137 styles_of(anstream::AutoStream::choice(&std::io::stdout()))
138}
139
140pub fn err() -> Styles {
142 styles_of(anstream::AutoStream::choice(&std::io::stderr()))
143}
144
145pub fn ts(t: OffsetDateTime) -> String {
147 let fmt = time::macros::format_description!("[year]-[month]-[day] [hour]:[minute]");
148 t.format(&fmt).unwrap_or_default()
149}
150
151pub fn size(bytes: u64) -> String {
152 match bytes {
153 b if b >= 1024 * 1024 => format!("{:.1} MB", b as f64 / (1024.0 * 1024.0)),
154 b if b >= 1024 => format!("{:.1} KB", b as f64 / 1024.0),
155 b => format!("{b} B"),
156 }
157}
158
159pub fn clean_name(name: &str) -> String {
163 name.chars()
164 .filter(|c| !c.is_control())
165 .map(|c| match c {
166 '[' => '(',
167 ']' => ')',
168 c => c,
169 })
170 .take(80)
171 .collect::<String>()
172 .trim()
173 .to_string()
174}
175
176#[cfg(test)]
177mod colour_tests {
178 use anstyle::{AnsiColor, Color, RgbColor, Style};
179
180 use super::{ColourLevel, banner, colour_level, styles};
181
182 #[test]
183 fn the_banner_is_plain_text_without_colour_and_coloured_by_meaning_with_it() {
184 assert_eq!(
185 banner(styles(ColourLevel::Plain))
186 .replace("\x1b[2m", "")
187 .replace("\x1b[0m", ""),
188 "\u{2042} semora · Your projects, considered."
189 );
190 assert_eq!(
191 banner(styles(ColourLevel::Ansi16)),
192 "\x1b[35m\u{2042}\x1b[0m semora \x1b[2m·\x1b[0m \x1b[36mYour projects, considered.\x1b[0m"
193 );
194 assert_eq!(
195 banner(styles(ColourLevel::True { light: false })),
196 "\x1b[38;2;217;139;191m\u{2042}\x1b[0m semora \x1b[2m·\x1b[0m \x1b[38;2;111;194;184mYour projects, considered.\x1b[0m"
197 );
198 }
199
200 #[test]
201 fn colour_is_truecolour_then_sixteen_then_none() {
202 use ColourLevel::{Ansi16, Plain, True};
203 assert_eq!(colour_level(false, Some("truecolor"), Some("0;15")), Plain);
205 assert_eq!(colour_level(false, None, None), Plain);
206 assert_eq!(colour_level(true, None, None), Ansi16);
208 assert_eq!(colour_level(true, Some(""), Some("0;15")), Ansi16);
209 assert_eq!(colour_level(true, Some("256"), None), Ansi16);
210 for claim in ["truecolor", "24bit"] {
211 assert_eq!(
212 colour_level(true, Some(claim), None),
213 True { light: false },
214 "dark unless the terminal says otherwise"
215 );
216 }
217 }
218
219 #[test]
220 fn a_terminal_is_light_only_when_colorfgbg_names_a_light_background() {
221 let light = |fgbg| {
222 colour_level(true, Some("truecolor"), fgbg) == (ColourLevel::True { light: true })
223 };
224 assert!(light(Some("0;15")));
225 assert!(light(Some("0;7")));
226 assert!(
227 light(Some("0;default;15")),
228 "rxvt's three fields: the last is the background"
229 );
230 assert!(
231 !light(Some("15;0")),
232 "a light foreground on a dark background"
233 );
234 assert!(!light(Some("0;8")));
235 assert!(!light(Some("")));
236 assert!(!light(Some("default;default")));
237 assert!(!light(None));
238 }
239
240 #[test]
241 fn each_level_has_the_brand_s_colours_in_bold() {
242 let bold = |c| Style::new().bold().fg_color(Some(c));
243 let rgb = |r, g, b| bold(Color::Rgb(RgbColor(r, g, b)));
244 let ansi = |c| bold(Color::Ansi(c));
245 let all = |level| {
246 let s = styles(level);
247 [s.brand, s.done, s.warn, s.error]
248 };
249 assert_eq!(all(ColourLevel::Plain), [Style::new(); 4]);
250 assert_eq!(
251 all(ColourLevel::Ansi16),
252 [
253 ansi(AnsiColor::Magenta),
254 ansi(AnsiColor::Cyan),
255 ansi(AnsiColor::Yellow),
256 ansi(AnsiColor::Red)
257 ]
258 );
259 assert_eq!(
260 all(ColourLevel::True { light: false }),
261 [
262 rgb(217, 139, 191),
263 rgb(111, 194, 184),
264 rgb(219, 174, 94),
265 rgb(254, 149, 127)
266 ]
267 );
268 assert_eq!(
269 all(ColourLevel::True { light: true }),
270 [
271 rgb(123, 45, 94),
272 rgb(31, 111, 106),
273 rgb(111, 79, 5),
274 rgb(179, 64, 42)
275 ]
276 );
277 }
278}