microsandbox_cli/
styles.rs1use clap::builder::styling::{AnsiColor, Effects, Style, Styles};
2use std::fmt::Write;
3
4#[cfg(not(test))]
9static IS_ANSI_TERMINAL: std::sync::LazyLock<bool> =
11 std::sync::LazyLock::new(|| microsandbox_utils::term::is_ansi_interactive_terminal());
12
13pub fn styles() -> Styles {
19 Styles::styled()
20 .header(AnsiColor::Yellow.on_default() | Effects::BOLD)
21 .usage(AnsiColor::Yellow.on_default() | Effects::BOLD)
22 .literal(AnsiColor::Blue.on_default() | Effects::BOLD)
23 .placeholder(AnsiColor::Green.on_default())
24 .error(AnsiColor::Red.on_default() | Effects::BOLD)
25 .valid(AnsiColor::Green.on_default() | Effects::BOLD)
26 .invalid(AnsiColor::Red.on_default() | Effects::BOLD)
27}
28
29fn apply_style(text: String, style: &Style) -> String {
31 #[cfg(not(test))]
32 if !*IS_ANSI_TERMINAL {
33 return text;
34 }
35
36 #[cfg(test)]
37 {
38 if std::env::var("TERM").unwrap_or_default() == "dumb" {
39 return text;
40 }
41 }
42
43 let mut styled = String::with_capacity(text.len() + 20); let _ = write!(styled, "{}", style);
45 styled.push_str(&text);
46 let _ = write!(styled, "{}", style.render_reset());
47 styled
48}
49
50pub trait AnsiStyles {
56 fn header(&self) -> String;
58
59 fn usage(&self) -> String;
61
62 fn literal(&self) -> String;
64
65 fn placeholder(&self) -> String;
67
68 fn error(&self) -> String;
70
71 fn valid(&self) -> String;
73
74 fn invalid(&self) -> String;
76}
77
78impl AnsiStyles for String {
83 fn header(&self) -> String {
84 apply_style(self.clone(), styles().get_header())
85 }
86
87 fn usage(&self) -> String {
88 apply_style(self.clone(), styles().get_usage())
89 }
90
91 fn literal(&self) -> String {
92 apply_style(self.clone(), styles().get_literal())
93 }
94
95 fn placeholder(&self) -> String {
96 apply_style(self.clone(), styles().get_placeholder())
97 }
98
99 fn error(&self) -> String {
100 apply_style(self.clone(), styles().get_error())
101 }
102
103 fn valid(&self) -> String {
104 apply_style(self.clone(), styles().get_valid())
105 }
106
107 fn invalid(&self) -> String {
108 apply_style(self.clone(), styles().get_invalid())
109 }
110}
111
112impl AnsiStyles for &str {
113 fn header(&self) -> String {
114 self.to_string().header()
115 }
116
117 fn usage(&self) -> String {
118 self.to_string().usage()
119 }
120
121 fn literal(&self) -> String {
122 self.to_string().literal()
123 }
124
125 fn placeholder(&self) -> String {
126 self.to_string().placeholder()
127 }
128
129 fn error(&self) -> String {
130 self.to_string().error()
131 }
132
133 fn valid(&self) -> String {
134 self.to_string().valid()
135 }
136
137 fn invalid(&self) -> String {
138 self.to_string().invalid()
139 }
140}
141
142#[cfg(test)]
147mod tests {
148 use serial_test::serial;
149
150 use super::*;
151
152 #[test]
153 #[ignore = "this test won't work correctly in cargo-nextest. run with `cargo test -- --ignored`"]
154 #[serial]
155 fn test_ansi_styles_string_non_interactive() {
156 helper::setup_non_interactive();
157
158 let text = String::from("test");
159 assert_eq!(text.header(), "test");
160 assert_eq!(text.usage(), "test");
161 assert_eq!(text.literal(), "test");
162 assert_eq!(text.placeholder(), "test");
163 assert_eq!(text.error(), "test");
164 assert_eq!(text.valid(), "test");
165 assert_eq!(text.invalid(), "test");
166 }
167
168 #[test]
169 #[ignore = "this test won't work correctly in cargo-nextest. run with `cargo test -- --ignored`"]
170 #[serial]
171 fn test_ansi_styles_str_non_interactive() {
172 helper::setup_non_interactive();
173
174 let text = "test";
175 assert_eq!(text.header(), "test");
176 assert_eq!(text.usage(), "test");
177 assert_eq!(text.literal(), "test");
178 assert_eq!(text.placeholder(), "test");
179 assert_eq!(text.error(), "test");
180 assert_eq!(text.valid(), "test");
181 assert_eq!(text.invalid(), "test");
182 }
183
184 #[test]
185 #[ignore = "this test won't work correctly in cargo-nextest. run with `cargo test -- --ignored`"]
186 #[serial]
187 fn test_ansi_styles_string_interactive() {
188 helper::setup_interactive();
189
190 let text = String::from("test");
191
192 let header = text.header();
193 println!("header: {}", header);
194 assert!(header.contains("\x1b[1m"));
196 assert!(header.contains("\x1b[33m"));
197 assert!(header.contains("test"));
198 assert!(header.contains("\x1b[0m"));
199
200 let usage = text.usage();
201 println!("usage: {}", usage);
202 assert!(usage.contains("\x1b[1m"));
203 assert!(usage.contains("\x1b[33m"));
204 assert!(usage.contains("test"));
205 assert!(usage.contains("\x1b[0m"));
206
207 let literal = text.literal();
208 println!("literal: {}", literal);
209 assert!(literal.contains("\x1b[1m"));
210 assert!(literal.contains("\x1b[34m"));
211 assert!(literal.contains("test"));
212 assert!(literal.contains("\x1b[0m"));
213
214 let placeholder = text.placeholder();
215 println!("placeholder: {}", placeholder);
216 assert!(placeholder.contains("\x1b[32m"));
218 assert!(placeholder.contains("test"));
219 assert!(placeholder.contains("\x1b[0m"));
220
221 let error = text.error();
222 println!("error: {}", error);
223 assert!(error.contains("\x1b[1m"));
224 assert!(error.contains("\x1b[31m"));
225 assert!(error.contains("test"));
226 assert!(error.contains("\x1b[0m"));
227
228 let valid = text.valid();
229 println!("valid: {}", valid);
230 assert!(valid.contains("\x1b[1m"));
231 assert!(valid.contains("\x1b[32m"));
232 assert!(valid.contains("test"));
233 assert!(valid.contains("\x1b[0m"));
234
235 let invalid = text.invalid();
236 println!("invalid: {}", invalid);
237 assert!(invalid.contains("\x1b[1m"));
238 assert!(invalid.contains("\x1b[31m"));
239 assert!(invalid.contains("test"));
240 assert!(invalid.contains("\x1b[0m"));
241 }
242
243 #[test]
244 #[ignore = "this test won't work correctly in cargo-nextest. run with `cargo test -- --ignored`"]
245 #[serial]
246 fn test_ansi_styles_str_interactive() {
247 helper::setup_interactive();
248
249 let text = "test";
250
251 let header = text.header();
252 assert!(header.contains("\x1b[1m"));
253 assert!(header.contains("\x1b[33m"));
254 assert!(header.contains("test"));
255 assert!(header.contains("\x1b[0m"));
256
257 let usage = text.usage();
258 assert!(usage.contains("\x1b[1m"));
259 assert!(usage.contains("\x1b[33m"));
260 assert!(usage.contains("test"));
261 assert!(usage.contains("\x1b[0m"));
262
263 let literal = text.literal();
264 assert!(literal.contains("\x1b[1m"));
265 assert!(literal.contains("\x1b[34m"));
266 assert!(literal.contains("test"));
267 assert!(literal.contains("\x1b[0m"));
268
269 let placeholder = text.placeholder();
270 assert!(placeholder.contains("\x1b[32m"));
271 assert!(placeholder.contains("test"));
272 assert!(placeholder.contains("\x1b[0m"));
273
274 let error = text.error();
275 assert!(error.contains("\x1b[1m"));
276 assert!(error.contains("\x1b[31m"));
277 assert!(error.contains("test"));
278 assert!(error.contains("\x1b[0m"));
279
280 let valid = text.valid();
281 assert!(valid.contains("\x1b[1m"));
282 assert!(valid.contains("\x1b[32m"));
283 assert!(valid.contains("test"));
284 assert!(valid.contains("\x1b[0m"));
285
286 let invalid = text.invalid();
287 assert!(invalid.contains("\x1b[1m"));
288 assert!(invalid.contains("\x1b[31m"));
289 assert!(invalid.contains("test"));
290 assert!(invalid.contains("\x1b[0m"));
291 }
292
293 #[test]
294 #[ignore = "this test won't work correctly in cargo-nextest. run with `cargo test -- --ignored`"]
295 #[serial]
296 fn test_ansi_styles_empty_string() {
297 helper::setup_interactive();
298
299 let empty = String::new();
300 assert!(empty.header().ends_with("\x1b[0m"));
301 assert!(empty.usage().ends_with("\x1b[0m"));
302 assert!(empty.literal().ends_with("\x1b[0m"));
303 assert!(empty.placeholder().ends_with("\x1b[0m"));
304 assert!(empty.error().ends_with("\x1b[0m"));
305 assert!(empty.valid().ends_with("\x1b[0m"));
306 assert!(empty.invalid().ends_with("\x1b[0m"));
307
308 helper::setup_non_interactive();
309 assert_eq!(empty.header(), "");
310 assert_eq!(empty.usage(), "");
311 assert_eq!(empty.literal(), "");
312 assert_eq!(empty.placeholder(), "");
313 assert_eq!(empty.error(), "");
314 assert_eq!(empty.valid(), "");
315 assert_eq!(empty.invalid(), "");
316 }
317
318 #[test]
319 #[ignore = "this test won't work correctly in cargo-nextest. run with `cargo test -- --ignored`"]
320 #[serial]
321 fn test_ansi_styles_unicode_string() {
322 helper::setup_interactive();
323
324 let text = "测试";
325 let header = text.header();
326 assert!(header.contains("测试"));
327 assert!(header.starts_with("\x1b["));
328 assert!(header.ends_with("\x1b[0m"));
329 }
330}
331
332#[cfg(test)]
333mod helper {
334 use std::env;
335
336 pub(super) fn setup_non_interactive() {
338 env::set_var("TERM", "dumb");
339 }
340
341 pub(super) fn setup_interactive() {
343 env::set_var("TERM", "xterm-256color");
344 }
345}