1use ratatui::style::Color;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
8pub enum TimeFormat {
9 #[default]
10 TwentyFourHour,
11 TwelveHour,
12}
13
14impl TimeFormat {
15 pub fn toggle(&self) -> Self {
17 match self {
18 TimeFormat::TwentyFourHour => TimeFormat::TwelveHour,
19 TimeFormat::TwelveHour => TimeFormat::TwentyFourHour,
20 }
21 }
22}
23
24#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
26pub enum AnimationStyle {
27 #[default]
28 None,
29 Shifting,
30 Pulsing,
31 Wave,
32 Reactive,
33}
34
35const ALL_ANIMATION_STYLES: &[AnimationStyle] = &[
37 AnimationStyle::None,
38 AnimationStyle::Shifting,
39 AnimationStyle::Pulsing,
40 AnimationStyle::Wave,
41 AnimationStyle::Reactive,
42];
43
44impl AnimationStyle {
45 pub fn next(&self) -> Self {
47 let current_idx = ALL_ANIMATION_STYLES
48 .iter()
49 .position(|s| s == self)
50 .unwrap_or(0);
51 let next_idx = (current_idx + 1) % ALL_ANIMATION_STYLES.len();
52 ALL_ANIMATION_STYLES[next_idx]
53 }
54
55 pub fn prev(&self) -> Self {
57 let current_idx = ALL_ANIMATION_STYLES
58 .iter()
59 .position(|s| s == self)
60 .unwrap_or(0);
61 let prev_idx = if current_idx == 0 {
62 ALL_ANIMATION_STYLES.len() - 1
63 } else {
64 current_idx - 1
65 };
66 ALL_ANIMATION_STYLES[prev_idx]
67 }
68
69 pub fn display_name(self) -> &'static str {
71 match self {
72 AnimationStyle::None => "None",
73 AnimationStyle::Shifting => "Shifting",
74 AnimationStyle::Pulsing => "Pulsing",
75 AnimationStyle::Wave => "Wave",
76 AnimationStyle::Reactive => "Reactive",
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
83pub enum BackgroundStyle {
84 #[default]
85 None,
86 Starfield,
87 MatrixRain,
88 GradientWave,
89 SystemPulse,
91 ResourceWave,
92 DataFlow,
93 HeatMap,
94}
95
96const ALL_BACKGROUND_STYLES: &[BackgroundStyle] = &[
98 BackgroundStyle::None,
99 BackgroundStyle::Starfield,
100 BackgroundStyle::MatrixRain,
101 BackgroundStyle::GradientWave,
102 BackgroundStyle::SystemPulse,
103 BackgroundStyle::ResourceWave,
104 BackgroundStyle::DataFlow,
105 BackgroundStyle::HeatMap,
106];
107
108impl BackgroundStyle {
109 pub fn next(&self) -> Self {
111 let current_idx = ALL_BACKGROUND_STYLES
112 .iter()
113 .position(|s| s == self)
114 .unwrap_or(0);
115 let next_idx = (current_idx + 1) % ALL_BACKGROUND_STYLES.len();
116 ALL_BACKGROUND_STYLES[next_idx]
117 }
118
119 pub fn prev(&self) -> Self {
121 let current_idx = ALL_BACKGROUND_STYLES
122 .iter()
123 .position(|s| s == self)
124 .unwrap_or(0);
125 let prev_idx = if current_idx == 0 {
126 ALL_BACKGROUND_STYLES.len() - 1
127 } else {
128 current_idx - 1
129 };
130 ALL_BACKGROUND_STYLES[prev_idx]
131 }
132
133 pub fn display_name(self) -> &'static str {
135 match self {
136 BackgroundStyle::None => "None",
137 BackgroundStyle::Starfield => "Starfield",
138 BackgroundStyle::MatrixRain => "Matrix",
139 BackgroundStyle::GradientWave => "Gradient",
140 BackgroundStyle::SystemPulse => "Sys Pulse",
141 BackgroundStyle::ResourceWave => "Resource",
142 BackgroundStyle::DataFlow => "Data Flow",
143 BackgroundStyle::HeatMap => "Heat Map",
144 }
145 }
146
147 pub fn is_reactive(self) -> bool {
149 matches!(
150 self,
151 BackgroundStyle::SystemPulse
152 | BackgroundStyle::ResourceWave
153 | BackgroundStyle::DataFlow
154 | BackgroundStyle::HeatMap
155 )
156 }
157}
158
159#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
161pub enum AnimationSpeed {
162 Slow,
163 #[default]
164 Medium,
165 Fast,
166}
167
168const ALL_ANIMATION_SPEEDS: &[AnimationSpeed] = &[
170 AnimationSpeed::Slow,
171 AnimationSpeed::Medium,
172 AnimationSpeed::Fast,
173];
174
175impl AnimationSpeed {
176 pub fn next(&self) -> Self {
178 let current_idx = ALL_ANIMATION_SPEEDS
179 .iter()
180 .position(|s| s == self)
181 .unwrap_or(0);
182 let next_idx = (current_idx + 1) % ALL_ANIMATION_SPEEDS.len();
183 ALL_ANIMATION_SPEEDS[next_idx]
184 }
185
186 pub fn prev(&self) -> Self {
188 let current_idx = ALL_ANIMATION_SPEEDS
189 .iter()
190 .position(|s| s == self)
191 .unwrap_or(0);
192 let prev_idx = if current_idx == 0 {
193 ALL_ANIMATION_SPEEDS.len() - 1
194 } else {
195 current_idx - 1
196 };
197 ALL_ANIMATION_SPEEDS[prev_idx]
198 }
199
200 pub fn display_name(self) -> &'static str {
202 match self {
203 AnimationSpeed::Slow => "Slow",
204 AnimationSpeed::Medium => "Medium",
205 AnimationSpeed::Fast => "Fast",
206 }
207 }
208
209 pub fn shift_cycle_ms(self) -> u64 {
211 match self {
212 AnimationSpeed::Slow => 30_000,
213 AnimationSpeed::Medium => 15_000,
214 AnimationSpeed::Fast => 5_000,
215 }
216 }
217
218 pub fn pulse_period_ms(self) -> u64 {
220 match self {
221 AnimationSpeed::Slow => 3_000,
222 AnimationSpeed::Medium => 1_500,
223 AnimationSpeed::Fast => 750,
224 }
225 }
226
227 pub fn wave_period_ms(self) -> u64 {
229 match self {
230 AnimationSpeed::Slow => 4_000,
231 AnimationSpeed::Medium => 2_000,
232 AnimationSpeed::Fast => 1_000,
233 }
234 }
235
236 pub fn flash_decay_ms(self) -> u64 {
238 match self {
239 AnimationSpeed::Slow => 800,
240 AnimationSpeed::Medium => 400,
241 AnimationSpeed::Fast => 200,
242 }
243 }
244
245 pub fn star_twinkle_period_ms(self) -> u64 {
247 match self {
248 AnimationSpeed::Slow => 500,
249 AnimationSpeed::Medium => 300,
250 AnimationSpeed::Fast => 150,
251 }
252 }
253
254 pub fn matrix_fall_speed(self) -> f32 {
256 match self {
257 AnimationSpeed::Slow => 0.5,
258 AnimationSpeed::Medium => 1.0,
259 AnimationSpeed::Fast => 2.0,
260 }
261 }
262
263 pub fn gradient_scroll_period_ms(self) -> u64 {
265 match self {
266 AnimationSpeed::Slow => 5000,
267 AnimationSpeed::Medium => 3000,
268 AnimationSpeed::Fast => 1500,
269 }
270 }
271}
272
273#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
275pub enum ColorTheme {
276 #[default]
277 Cyan,
278 Green,
279 White,
280 Magenta,
281 Yellow,
282 Red,
283 Blue,
284 Rainbow,
286 RainbowVertical,
287 GradientWarm,
288 GradientCool,
289 GradientOcean,
290 GradientNeon,
291 GradientFire,
292}
293
294const ALL_THEMES: &[ColorTheme] = &[
296 ColorTheme::Cyan,
297 ColorTheme::Green,
298 ColorTheme::Magenta,
299 ColorTheme::Yellow,
300 ColorTheme::Red,
301 ColorTheme::Blue,
302 ColorTheme::White,
303 ColorTheme::Rainbow,
304 ColorTheme::RainbowVertical,
305 ColorTheme::GradientWarm,
306 ColorTheme::GradientCool,
307 ColorTheme::GradientOcean,
308 ColorTheme::GradientNeon,
309 ColorTheme::GradientFire,
310];
311
312impl ColorTheme {
313 pub fn next(&self) -> Self {
315 let current_idx = ALL_THEMES.iter().position(|t| t == self).unwrap_or(0);
316 let next_idx = (current_idx + 1) % ALL_THEMES.len();
317 ALL_THEMES[next_idx]
318 }
319
320 pub fn prev(&self) -> Self {
322 let current_idx = ALL_THEMES.iter().position(|t| t == self).unwrap_or(0);
323 let prev_idx = if current_idx == 0 {
324 ALL_THEMES.len() - 1
325 } else {
326 current_idx - 1
327 };
328 ALL_THEMES[prev_idx]
329 }
330
331 pub fn color(self) -> Color {
333 match self {
334 ColorTheme::Cyan => Color::Cyan,
335 ColorTheme::Green => Color::Green,
336 ColorTheme::White => Color::White,
337 ColorTheme::Magenta => Color::Magenta,
338 ColorTheme::Yellow => Color::Yellow,
339 ColorTheme::Red => Color::Red,
340 ColorTheme::Blue => Color::Blue,
341 ColorTheme::Rainbow | ColorTheme::RainbowVertical | ColorTheme::GradientNeon => {
343 Color::Magenta
344 }
345 ColorTheme::GradientWarm | ColorTheme::GradientFire => Color::Red,
346 ColorTheme::GradientCool | ColorTheme::GradientOcean => Color::Cyan,
347 }
348 }
349
350 pub fn is_dynamic(self) -> bool {
352 matches!(
353 self,
354 ColorTheme::Rainbow
355 | ColorTheme::RainbowVertical
356 | ColorTheme::GradientWarm
357 | ColorTheme::GradientCool
358 | ColorTheme::GradientOcean
359 | ColorTheme::GradientNeon
360 | ColorTheme::GradientFire
361 )
362 }
363
364 pub fn color_at_position(self, x: usize, y: usize, width: usize, height: usize) -> Color {
368 match self {
369 ColorTheme::Rainbow => {
370 let colors = [
371 Color::Red,
372 Color::Rgb(255, 127, 0), Color::Yellow,
374 Color::Green,
375 Color::Cyan,
376 Color::Blue,
377 Color::Magenta,
378 ];
379 let idx = if width > 0 {
380 (x * colors.len() / width.max(1)) % colors.len()
381 } else {
382 0
383 };
384 colors[idx]
385 }
386 ColorTheme::RainbowVertical => {
387 let colors = [
388 Color::Red,
389 Color::Rgb(255, 127, 0), Color::Yellow,
391 Color::Green,
392 Color::Cyan,
393 Color::Blue,
394 Color::Magenta,
395 ];
396 let idx = if height > 0 {
397 (y * colors.len() / height.max(1)) % colors.len()
398 } else {
399 0
400 };
401 colors[idx]
402 }
403 ColorTheme::GradientWarm => {
404 let progress = if width > 0 {
406 (x as f32) / (width.max(1) as f32)
407 } else {
408 0.0
409 };
410 if progress < 0.5 {
411 let g = (127.0 * (progress * 2.0)) as u8;
413 Color::Rgb(255, g, 0)
414 } else {
415 let g = 127 + ((128.0 * ((progress - 0.5) * 2.0)) as u8);
417 Color::Rgb(255, g, 0)
418 }
419 }
420 ColorTheme::GradientCool => {
421 let progress = if width > 0 {
423 (x as f32) / (width.max(1) as f32)
424 } else {
425 0.0
426 };
427 if progress < 0.5 {
428 let g = (255.0 * (progress * 2.0)) as u8;
430 Color::Rgb(0, g, 255)
431 } else {
432 let b = 255 - ((255.0 * ((progress - 0.5) * 2.0)) as u8);
434 Color::Rgb(0, 255, b)
435 }
436 }
437 ColorTheme::GradientOcean => {
438 let progress = if width > 0 {
440 (x as f32) / (width.max(1) as f32)
441 } else {
442 0.0
443 };
444 if progress < 0.5 {
445 let r = (100.0 * (progress * 2.0)) as u8;
447 let g = (150.0 + 105.0 * (progress * 2.0)) as u8;
448 Color::Rgb(r, g, 255)
449 } else {
450 let b = 255 - ((127.0 * ((progress - 0.5) * 2.0)) as u8);
452 Color::Rgb(100, 255, b)
453 }
454 }
455 ColorTheme::GradientNeon => {
456 let progress = if width > 0 {
458 (x as f32) / (width.max(1) as f32)
459 } else {
460 0.0
461 };
462 let r = 255 - ((255.0 * progress) as u8);
463 let g = (255.0 * progress) as u8;
464 let b = 255;
465 Color::Rgb(r, g, b)
466 }
467 ColorTheme::GradientFire => {
468 let progress = if width > 0 {
470 (x as f32) / (width.max(1) as f32)
471 } else {
472 0.0
473 };
474 if progress < 0.33 {
475 let r = 128 + ((127.0 * (progress * 3.0)) as u8);
477 Color::Rgb(r, 0, 0)
478 } else if progress < 0.66 {
479 let g = (165.0 * ((progress - 0.33) * 3.0)) as u8;
481 Color::Rgb(255, g, 0)
482 } else {
483 let g = 165 + ((90.0 * ((progress - 0.66) * 3.0)) as u8);
485 Color::Rgb(255, g, 0)
486 }
487 }
488 _ => self.color(),
490 }
491 }
492
493 pub fn display_name(self) -> &'static str {
495 match self {
496 ColorTheme::Cyan => "Cyan",
497 ColorTheme::Green => "Green",
498 ColorTheme::White => "White",
499 ColorTheme::Magenta => "Magenta",
500 ColorTheme::Yellow => "Yellow",
501 ColorTheme::Red => "Red",
502 ColorTheme::Blue => "Blue",
503 ColorTheme::Rainbow => "Rainbow",
504 ColorTheme::RainbowVertical => "Rainbow V",
505 ColorTheme::GradientWarm => "Warm",
506 ColorTheme::GradientCool => "Cool",
507 ColorTheme::GradientOcean => "Ocean",
508 ColorTheme::GradientNeon => "Neon",
509 ColorTheme::GradientFire => "Fire",
510 }
511 }
512}
513
514pub fn apply_animation(
516 base_color: Color,
517 animation_style: AnimationStyle,
518 speed: AnimationSpeed,
519 elapsed_ms: u64,
520 x: usize,
521 width: usize,
522 flash_intensity: f32,
523) -> Color {
524 match animation_style {
525 AnimationStyle::None => base_color,
526 AnimationStyle::Shifting => apply_shifting(base_color, elapsed_ms, speed),
527 AnimationStyle::Pulsing => apply_pulsing(base_color, elapsed_ms, speed),
528 AnimationStyle::Wave => apply_wave(base_color, elapsed_ms, speed, x, width),
529 AnimationStyle::Reactive => apply_reactive(base_color, flash_intensity),
530 }
531}
532
533fn apply_shifting(color: Color, elapsed_ms: u64, speed: AnimationSpeed) -> Color {
535 let (r, g, b) = color_to_rgb(color);
536 let (h, s, l) = rgb_to_hsl(r, g, b);
537
538 let cycle_ms = speed.shift_cycle_ms();
539 let hue_offset = ((elapsed_ms % cycle_ms) as f32 / cycle_ms as f32) * 360.0;
540 let new_h = (h + hue_offset) % 360.0;
541
542 let (nr, ng, nb) = hsl_to_rgb(new_h, s, l);
543 Color::Rgb(nr, ng, nb)
544}
545
546fn apply_pulsing(color: Color, elapsed_ms: u64, speed: AnimationSpeed) -> Color {
548 let (r, g, b) = color_to_rgb(color);
549
550 let period_ms = speed.pulse_period_ms();
551 let phase = (elapsed_ms % period_ms) as f32 / period_ms as f32;
552 let brightness = 0.5 + 0.5 * (phase * 2.0 * std::f32::consts::PI).sin();
553
554 let factor = 0.3 + 0.7 * brightness;
556 Color::Rgb(
557 (r as f32 * factor) as u8,
558 (g as f32 * factor) as u8,
559 (b as f32 * factor) as u8,
560 )
561}
562
563fn apply_wave(
565 color: Color,
566 elapsed_ms: u64,
567 speed: AnimationSpeed,
568 x: usize,
569 width: usize,
570) -> Color {
571 let (r, g, b) = color_to_rgb(color);
572
573 let period_ms = speed.wave_period_ms();
574 let time_phase = (elapsed_ms % period_ms) as f32 / period_ms as f32;
575 let x_phase = if width > 0 {
576 x as f32 / width as f32
577 } else {
578 0.0
579 };
580
581 let wave = ((x_phase + time_phase) * 2.0 * std::f32::consts::PI).sin();
582 let brightness = 0.6 + 0.4 * wave;
583
584 Color::Rgb(
585 (r as f32 * brightness) as u8,
586 (g as f32 * brightness) as u8,
587 (b as f32 * brightness) as u8,
588 )
589}
590
591fn apply_reactive(color: Color, flash_intensity: f32) -> Color {
593 let (r, g, b) = color_to_rgb(color);
594
595 let factor = 1.0 + flash_intensity;
597 Color::Rgb(
598 (r as f32 * factor).min(255.0) as u8,
599 (g as f32 * factor).min(255.0) as u8,
600 (b as f32 * factor).min(255.0) as u8,
601 )
602}
603
604fn color_to_rgb(color: Color) -> (u8, u8, u8) {
606 match color {
607 Color::Rgb(r, g, b) => (r, g, b),
608 Color::Red => (255, 0, 0),
609 Color::Green => (0, 255, 0),
610 Color::Blue => (0, 0, 255),
611 Color::Yellow => (255, 255, 0),
612 Color::Magenta => (255, 0, 255),
613 Color::Cyan => (0, 255, 255),
614 Color::White => (255, 255, 255),
615 _ => (128, 128, 128),
616 }
617}
618
619fn rgb_to_hsl(r: u8, g: u8, b: u8) -> (f32, f32, f32) {
621 let r = r as f32 / 255.0;
622 let g = g as f32 / 255.0;
623 let b = b as f32 / 255.0;
624
625 let max = r.max(g).max(b);
626 let min = r.min(g).min(b);
627 let l = (max + min) / 2.0;
628
629 if max == min {
630 return (0.0, 0.0, l);
631 }
632
633 let d = max - min;
634 let s = if l > 0.5 {
635 d / (2.0 - max - min)
636 } else {
637 d / (max + min)
638 };
639
640 let h = if max == r {
641 ((g - b) / d + if g < b { 6.0 } else { 0.0 }) * 60.0
642 } else if max == g {
643 ((b - r) / d + 2.0) * 60.0
644 } else {
645 ((r - g) / d + 4.0) * 60.0
646 };
647
648 (h, s, l)
649}
650
651fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (u8, u8, u8) {
653 if s == 0.0 {
654 let v = (l * 255.0) as u8;
655 return (v, v, v);
656 }
657
658 let q = if l < 0.5 {
659 l * (1.0 + s)
660 } else {
661 l + s - l * s
662 };
663 let p = 2.0 * l - q;
664
665 let h = h / 360.0;
666
667 let r = hue_to_rgb(p, q, h + 1.0 / 3.0);
668 let g = hue_to_rgb(p, q, h);
669 let b = hue_to_rgb(p, q, h - 1.0 / 3.0);
670
671 ((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
672}
673
674fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
675 if t < 0.0 {
676 t += 1.0;
677 }
678 if t > 1.0 {
679 t -= 1.0;
680 }
681
682 if t < 1.0 / 6.0 {
683 p + (q - p) * 6.0 * t
684 } else if t < 1.0 / 2.0 {
685 q
686 } else if t < 2.0 / 3.0 {
687 p + (q - p) * (2.0 / 3.0 - t) * 6.0
688 } else {
689 p
690 }
691}
692
693pub fn is_colon_visible(elapsed_ms: u64) -> bool {
696 let phase = (elapsed_ms % 1000) as f32 / 1000.0;
697 phase < 0.5
698}