1use colored::{Color as ColoredColor, Colorize};
6
7#[derive(Debug, Clone, Copy)]
9#[allow(dead_code)]
10pub struct Palette {
11 pub primary: Color,
13 pub secondary: Color,
15 pub success: Color,
17 pub warning: Color,
19 pub error: Color,
21 pub neutral: Color,
23 pub dimmed: Color,
25 pub highlight: Color,
27}
28
29impl Default for Palette {
30 fn default() -> Self {
31 Self {
32 primary: Color::MutedBlue,
33 secondary: Color::Purple,
34 success: Color::Green,
35 warning: Color::Amber,
36 error: Color::Red,
37 neutral: Color::Gray,
38 dimmed: Color::Gray,
39 highlight: Color::Cyan,
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46#[allow(dead_code)]
47pub enum Color {
48 Standard(ColoredColor),
50 MutedBlue,
52 Purple,
54 Amber,
56 Red,
58 Green,
60 Gray,
62 Cyan,
64}
65
66impl Color {
67 #[allow(dead_code)]
69 pub fn apply<T: colored::Colorize>(&self, text: T) -> colored::ColoredString {
70 match self {
71 Color::Standard(c) => text.color(*c),
72 Color::MutedBlue => text.cyan(),
73 Color::Purple => text.purple(),
74 Color::Amber => text.yellow(),
75 Color::Red => text.red(),
76 Color::Green => text.green(),
77 Color::Gray => text.dimmed(),
78 Color::Cyan => text.cyan(),
79 }
80 }
81
82 #[allow(dead_code)]
84 pub fn to_colored(self) -> ColoredColor {
85 match self {
86 Color::Standard(c) => c,
87 Color::MutedBlue => ColoredColor::Cyan,
88 Color::Purple => ColoredColor::Magenta,
89 Color::Amber => ColoredColor::Yellow,
90 Color::Red => ColoredColor::Red,
91 Color::Green => ColoredColor::Green,
92 Color::Gray => ColoredColor::White,
93 Color::Cyan => ColoredColor::Cyan,
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy, Default)]
100#[allow(dead_code)]
101pub struct Theme {
102 pub use_colors: bool,
104 pub use_emoji: bool,
106 pub divider_char: char,
108 pub box_chars: BoxStyle,
110 pub palette: Palette,
112}
113
114impl Theme {
115 pub fn new() -> Self {
117 Self::default()
118 }
119
120 #[allow(dead_code)]
122 pub fn minimal() -> Self {
123 Self {
124 use_colors: false,
125 use_emoji: false,
126 divider_char: '-',
127 box_chars: BoxStyle::Ascii,
128 palette: Palette::default(),
129 }
130 }
131
132 #[allow(dead_code)]
134 pub fn json() -> Self {
135 Self {
136 use_colors: false,
137 use_emoji: false,
138 divider_char: '-',
139 box_chars: BoxStyle::None,
140 palette: Palette::default(),
141 }
142 }
143
144 #[allow(dead_code)]
146 pub fn markdown() -> Self {
147 Self {
148 use_colors: false,
149 use_emoji: true,
150 divider_char: '-',
151 box_chars: BoxStyle::None,
152 palette: Palette::default(),
153 }
154 }
155}
156
157#[derive(Debug, Clone, Copy, Default)]
159#[allow(dead_code)]
160pub enum BoxStyle {
161 #[default]
163 None,
164 Ascii,
166 Unicode,
168 UnicodeSharp,
170}
171
172impl BoxStyle {
173 pub fn corners(&self) -> (char, char, char, char) {
175 match self {
176 BoxStyle::None => (' ', ' ', ' ', ' '),
177 BoxStyle::Ascii => ('+', '+', '+', '+'),
178 BoxStyle::Unicode => ('╭', '╮', '╰', '╯'),
179 BoxStyle::UnicodeSharp => ('┌', '┐', '└', '┘'),
180 }
181 }
182
183 pub fn horizontal(&self) -> char {
185 match self {
186 BoxStyle::None => ' ',
187 BoxStyle::Ascii => '-',
188 BoxStyle::Unicode | BoxStyle::UnicodeSharp => '─',
189 }
190 }
191
192 pub fn vertical(&self) -> char {
194 match self {
195 BoxStyle::None | BoxStyle::Ascii => '|',
196 BoxStyle::Unicode | BoxStyle::UnicodeSharp => '│',
197 }
198 }
199}
200
201#[derive(Debug, Clone, Default)]
203pub struct Styling;
204
205#[allow(dead_code)]
206impl Styling {
207 pub fn header(text: &str) -> String {
209 format!("{}", text.bold())
210 }
211
212 pub fn subheader(text: &str) -> String {
214 format!("{}", text.dimmed())
215 }
216
217 pub fn success(text: &str) -> String {
219 format!("{}", text.green())
220 }
221
222 pub fn warning(text: &str) -> String {
224 format!("{}", text.yellow())
225 }
226
227 pub fn error(text: &str) -> String {
229 format!("{}", text.red())
230 }
231
232 pub fn info(text: &str) -> String {
234 format!("{}", text.cyan())
235 }
236
237 pub fn hint(text: &str) -> String {
239 format!("{}", text.dimmed())
240 }
241
242 pub fn divider(length: usize) -> String {
244 "─".repeat(length)
245 }
246
247 pub fn section_box(title: &str, content: &str, theme: &Theme) -> String {
249 let width = 60;
250 let horizontal = theme.box_chars.horizontal();
251 let (tl, tr, bl, br) = theme.box_chars.corners();
252
253 let mut result = String::new();
254
255 result.push(tl);
257 result.push_str(&format!("{} ", title).bold().to_string());
258 for _ in title.len() + 2..width - 1 {
259 result.push(horizontal);
260 }
261 result.push(tr);
262 result.push('\n');
263
264 for line in content.lines() {
266 result.push(theme.box_chars.vertical());
267 result.push(' ');
268 result.push_str(line);
269 for _ in line.len() + 1..width - 1 {
271 result.push(' ');
272 }
273 result.push(theme.box_chars.vertical());
274 result.push('\n');
275 }
276
277 result.push(bl);
279 for _ in 0..width - 1 {
280 result.push(horizontal);
281 }
282 result.push(br);
283
284 result
285 }
286
287 pub fn key_value(key: &str, value: &str) -> String {
289 format!("{}: {}", key.dimmed(), value)
290 }
291
292 pub fn timing(component: &str, duration_ms: u64) -> String {
294 let duration = if duration_ms < 1000 {
295 format!("{}ms", duration_ms)
296 } else {
297 format!("{:.1}s", duration_ms as f64 / 1000.0)
298 };
299 format!("{} {}", component.dimmed(), duration.green())
300 }
301
302 pub fn print_section(title: &str, content: &str) {
304 let divider = Self::divider(50);
305 println!("\n{}", title.cyan().bold());
306 println!("{}", divider.dimmed());
307 println!("{}", content);
308 println!("{}", divider.dimmed());
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 #[test]
317 fn test_divider_length() {
318 let d = Styling::divider(10);
319 assert_eq!(d.chars().count(), 10);
321 assert!(d.chars().all(|c| c == '─'));
322 }
323
324 #[test]
325 fn test_key_value_format() {
326 let kv = Styling::key_value("Key", "Value");
327 assert!(kv.contains("Key:"));
328 assert!(kv.contains("Value"));
329 }
330
331 #[test]
332 fn test_timing_ms() {
333 let t = Styling::timing("test", 500);
334 assert!(t.contains("500ms"));
335 }
336
337 #[test]
338 fn test_timing_seconds() {
339 let t = Styling::timing("test", 2500);
340 assert!(t.contains("2.5s"));
341 }
342
343 #[test]
344 fn test_theme_has_colors_option() {
345 let theme = Theme::new();
346 let _ = theme.use_colors;
348 }
349
350 #[test]
351 fn test_palette_colors() {
352 let palette = Palette::default();
353 match palette.primary {
355 Color::MutedBlue => {}
356 Color::Standard(_) => {}
357 _ => panic!("Expected MutedBlue or Standard color"),
358 }
359 }
360}
361
362#[allow(dead_code)]
364pub mod emoji {
365 use once_cell::sync::Lazy;
366
367 pub static CHECK: Lazy<&'static str> = Lazy::new(|| "✓");
369 pub static CROSS: Lazy<&'static str> = Lazy::new(|| "✗");
371 pub static HINT: Lazy<&'static str> = Lazy::new(|| "💡");
373 pub static KEY: Lazy<&'static str> = Lazy::new(|| "🔐");
375}