sigye_core/
lib.rs

1//! Core types for the sigye clock application.
2
3use ratatui::style::Color;
4use serde::{Deserialize, Serialize};
5
6/// System resource metrics for reactive backgrounds.
7///
8/// All values are normalized to the range 0.0 - 1.0.
9#[derive(Debug, Clone, Default)]
10pub struct SystemMetrics {
11    /// CPU usage as a percentage (0.0 - 1.0).
12    pub cpu_usage: f32,
13    /// Memory usage as a percentage (0.0 - 1.0).
14    pub memory_usage: f32,
15    /// Network receive rate, normalized (0.0 - 1.0).
16    pub network_rx_rate: f32,
17    /// Network transmit rate, normalized (0.0 - 1.0).
18    pub network_tx_rate: f32,
19    /// Disk read rate, normalized (0.0 - 1.0).
20    pub disk_read_rate: f32,
21    /// Disk write rate, normalized (0.0 - 1.0).
22    pub disk_write_rate: f32,
23    /// Battery level (0.0 - 1.0), None if no battery.
24    pub battery_level: Option<f32>,
25    /// Whether battery is charging, None if no battery.
26    pub battery_charging: Option<bool>,
27}
28
29/// Time format for the clock display.
30#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
31pub enum TimeFormat {
32    #[default]
33    TwentyFourHour,
34    TwelveHour,
35}
36
37impl TimeFormat {
38    /// Toggle between 12-hour and 24-hour format.
39    pub fn toggle(&self) -> Self {
40        match self {
41            TimeFormat::TwentyFourHour => TimeFormat::TwelveHour,
42            TimeFormat::TwelveHour => TimeFormat::TwentyFourHour,
43        }
44    }
45}
46
47/// Animation style for color themes.
48#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
49pub enum AnimationStyle {
50    #[default]
51    None,
52    Shifting,
53    Pulsing,
54    Wave,
55    Reactive,
56}
57
58/// All animation styles for cycling.
59const ALL_ANIMATION_STYLES: &[AnimationStyle] = &[
60    AnimationStyle::None,
61    AnimationStyle::Shifting,
62    AnimationStyle::Pulsing,
63    AnimationStyle::Wave,
64    AnimationStyle::Reactive,
65];
66
67impl AnimationStyle {
68    /// Cycle to the next animation style.
69    pub fn next(&self) -> Self {
70        let current_idx = ALL_ANIMATION_STYLES
71            .iter()
72            .position(|s| s == self)
73            .unwrap_or(0);
74        let next_idx = (current_idx + 1) % ALL_ANIMATION_STYLES.len();
75        ALL_ANIMATION_STYLES[next_idx]
76    }
77
78    /// Cycle to the previous animation style.
79    pub fn prev(&self) -> Self {
80        let current_idx = ALL_ANIMATION_STYLES
81            .iter()
82            .position(|s| s == self)
83            .unwrap_or(0);
84        let prev_idx = if current_idx == 0 {
85            ALL_ANIMATION_STYLES.len() - 1
86        } else {
87            current_idx - 1
88        };
89        ALL_ANIMATION_STYLES[prev_idx]
90    }
91
92    /// Get display name for the animation style.
93    pub fn display_name(self) -> &'static str {
94        match self {
95            AnimationStyle::None => "None",
96            AnimationStyle::Shifting => "Shifting",
97            AnimationStyle::Pulsing => "Pulsing",
98            AnimationStyle::Wave => "Wave",
99            AnimationStyle::Reactive => "Reactive",
100        }
101    }
102}
103
104/// Background animation style for the terminal.
105#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
106pub enum BackgroundStyle {
107    #[default]
108    None,
109    Starfield,
110    MatrixRain,
111    GradientWave,
112    // Winter theme backgrounds
113    Snowfall,
114    Frost,
115    Aurora,
116    // Weather theme backgrounds
117    Sunny,
118    Rainy,
119    Stormy,
120    Windy,
121    Cloudy,
122    Foggy,
123    // Reactive backgrounds that respond to system resource usage
124    SystemPulse,
125    ResourceWave,
126    DataFlow,
127    HeatMap,
128}
129
130/// All background styles for cycling.
131const ALL_BACKGROUND_STYLES: &[BackgroundStyle] = &[
132    BackgroundStyle::None,
133    BackgroundStyle::Starfield,
134    BackgroundStyle::MatrixRain,
135    BackgroundStyle::GradientWave,
136    BackgroundStyle::Snowfall,
137    BackgroundStyle::Frost,
138    BackgroundStyle::Aurora,
139    BackgroundStyle::Sunny,
140    BackgroundStyle::Rainy,
141    BackgroundStyle::Stormy,
142    BackgroundStyle::Windy,
143    BackgroundStyle::Cloudy,
144    BackgroundStyle::Foggy,
145    BackgroundStyle::SystemPulse,
146    BackgroundStyle::ResourceWave,
147    BackgroundStyle::DataFlow,
148    BackgroundStyle::HeatMap,
149];
150
151impl BackgroundStyle {
152    /// Cycle to the next background style.
153    pub fn next(&self) -> Self {
154        let current_idx = ALL_BACKGROUND_STYLES
155            .iter()
156            .position(|s| s == self)
157            .unwrap_or(0);
158        let next_idx = (current_idx + 1) % ALL_BACKGROUND_STYLES.len();
159        ALL_BACKGROUND_STYLES[next_idx]
160    }
161
162    /// Cycle to the previous background style.
163    pub fn prev(&self) -> Self {
164        let current_idx = ALL_BACKGROUND_STYLES
165            .iter()
166            .position(|s| s == self)
167            .unwrap_or(0);
168        let prev_idx = if current_idx == 0 {
169            ALL_BACKGROUND_STYLES.len() - 1
170        } else {
171            current_idx - 1
172        };
173        ALL_BACKGROUND_STYLES[prev_idx]
174    }
175
176    /// Get display name for the background style.
177    pub fn display_name(self) -> &'static str {
178        match self {
179            BackgroundStyle::None => "None",
180            BackgroundStyle::Starfield => "Starfield",
181            BackgroundStyle::MatrixRain => "Matrix",
182            BackgroundStyle::GradientWave => "Gradient",
183            BackgroundStyle::Snowfall => "Snowfall",
184            BackgroundStyle::Frost => "Frost",
185            BackgroundStyle::Aurora => "Aurora",
186            BackgroundStyle::Sunny => "Sunny",
187            BackgroundStyle::Rainy => "Rainy",
188            BackgroundStyle::Stormy => "Stormy",
189            BackgroundStyle::Windy => "Windy",
190            BackgroundStyle::Cloudy => "Cloudy",
191            BackgroundStyle::Foggy => "Foggy",
192            BackgroundStyle::SystemPulse => "Sys Pulse",
193            BackgroundStyle::ResourceWave => "Resource",
194            BackgroundStyle::DataFlow => "Data Flow",
195            BackgroundStyle::HeatMap => "Heat Map",
196        }
197    }
198
199    /// Check if this background style requires system metrics (reactive).
200    pub fn is_reactive(self) -> bool {
201        matches!(
202            self,
203            BackgroundStyle::SystemPulse
204                | BackgroundStyle::ResourceWave
205                | BackgroundStyle::DataFlow
206                | BackgroundStyle::HeatMap
207        )
208    }
209}
210
211/// Animation speed setting.
212#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
213pub enum AnimationSpeed {
214    Slow,
215    #[default]
216    Medium,
217    Fast,
218}
219
220/// All animation speeds for cycling.
221const ALL_ANIMATION_SPEEDS: &[AnimationSpeed] = &[
222    AnimationSpeed::Slow,
223    AnimationSpeed::Medium,
224    AnimationSpeed::Fast,
225];
226
227impl AnimationSpeed {
228    /// Cycle to the next speed.
229    pub fn next(&self) -> Self {
230        let current_idx = ALL_ANIMATION_SPEEDS
231            .iter()
232            .position(|s| s == self)
233            .unwrap_or(0);
234        let next_idx = (current_idx + 1) % ALL_ANIMATION_SPEEDS.len();
235        ALL_ANIMATION_SPEEDS[next_idx]
236    }
237
238    /// Cycle to the previous speed.
239    pub fn prev(&self) -> Self {
240        let current_idx = ALL_ANIMATION_SPEEDS
241            .iter()
242            .position(|s| s == self)
243            .unwrap_or(0);
244        let prev_idx = if current_idx == 0 {
245            ALL_ANIMATION_SPEEDS.len() - 1
246        } else {
247            current_idx - 1
248        };
249        ALL_ANIMATION_SPEEDS[prev_idx]
250    }
251
252    /// Get display name for the speed.
253    pub fn display_name(self) -> &'static str {
254        match self {
255            AnimationSpeed::Slow => "Slow",
256            AnimationSpeed::Medium => "Medium",
257            AnimationSpeed::Fast => "Fast",
258        }
259    }
260
261    /// Get the cycle duration in milliseconds for shifting animation.
262    pub fn shift_cycle_ms(self) -> u64 {
263        match self {
264            AnimationSpeed::Slow => 30_000,
265            AnimationSpeed::Medium => 15_000,
266            AnimationSpeed::Fast => 5_000,
267        }
268    }
269
270    /// Get the pulse period in milliseconds.
271    pub fn pulse_period_ms(self) -> u64 {
272        match self {
273            AnimationSpeed::Slow => 3_000,
274            AnimationSpeed::Medium => 1_500,
275            AnimationSpeed::Fast => 750,
276        }
277    }
278
279    /// Get the wave period in milliseconds.
280    pub fn wave_period_ms(self) -> u64 {
281        match self {
282            AnimationSpeed::Slow => 4_000,
283            AnimationSpeed::Medium => 2_000,
284            AnimationSpeed::Fast => 1_000,
285        }
286    }
287
288    /// Get the flash decay duration in milliseconds for reactive animation.
289    pub fn flash_decay_ms(self) -> u64 {
290        match self {
291            AnimationSpeed::Slow => 800,
292            AnimationSpeed::Medium => 400,
293            AnimationSpeed::Fast => 200,
294        }
295    }
296
297    /// Get the star twinkle period in milliseconds.
298    pub fn star_twinkle_period_ms(self) -> u64 {
299        match self {
300            AnimationSpeed::Slow => 500,
301            AnimationSpeed::Medium => 300,
302            AnimationSpeed::Fast => 150,
303        }
304    }
305
306    /// Get the matrix rain fall speed multiplier.
307    pub fn matrix_fall_speed(self) -> f32 {
308        match self {
309            AnimationSpeed::Slow => 0.5,
310            AnimationSpeed::Medium => 1.0,
311            AnimationSpeed::Fast => 2.0,
312        }
313    }
314
315    /// Get the gradient scroll period in milliseconds.
316    pub fn gradient_scroll_period_ms(self) -> u64 {
317        match self {
318            AnimationSpeed::Slow => 5000,
319            AnimationSpeed::Medium => 3000,
320            AnimationSpeed::Fast => 1500,
321        }
322    }
323
324    /// Get the snowfall speed multiplier.
325    pub fn snow_fall_speed(self) -> f32 {
326        match self {
327            AnimationSpeed::Slow => 0.3,
328            AnimationSpeed::Medium => 0.6,
329            AnimationSpeed::Fast => 1.0,
330        }
331    }
332
333    /// Get the frost growth period in milliseconds.
334    pub fn frost_growth_period_ms(self) -> u64 {
335        match self {
336            AnimationSpeed::Slow => 8000,
337            AnimationSpeed::Medium => 5000,
338            AnimationSpeed::Fast => 3000,
339        }
340    }
341
342    /// Get the aurora wave period in milliseconds.
343    pub fn aurora_wave_period_ms(self) -> u64 {
344        match self {
345            AnimationSpeed::Slow => 6000,
346            AnimationSpeed::Medium => 4000,
347            AnimationSpeed::Fast => 2000,
348        }
349    }
350
351    // Weather animation timing methods
352
353    /// Get the rain fall speed multiplier.
354    pub fn rain_fall_speed(self) -> f32 {
355        match self {
356            AnimationSpeed::Slow => 0.8,
357            AnimationSpeed::Medium => 1.5,
358            AnimationSpeed::Fast => 2.5,
359        }
360    }
361
362    /// Get the lightning flash interval range in milliseconds (min, max).
363    pub fn lightning_interval_ms(self) -> (u64, u64) {
364        match self {
365            AnimationSpeed::Slow => (6000, 12000),
366            AnimationSpeed::Medium => (4000, 8000),
367            AnimationSpeed::Fast => (2000, 5000),
368        }
369    }
370
371    /// Get the wind streak speed multiplier.
372    pub fn wind_streak_speed(self) -> f32 {
373        match self {
374            AnimationSpeed::Slow => 0.5,
375            AnimationSpeed::Medium => 1.0,
376            AnimationSpeed::Fast => 2.0,
377        }
378    }
379
380    /// Get the cloud drift period in milliseconds.
381    pub fn cloud_drift_period_ms(self) -> u64 {
382        match self {
383            AnimationSpeed::Slow => 8000,
384            AnimationSpeed::Medium => 5000,
385            AnimationSpeed::Fast => 3000,
386        }
387    }
388
389    /// Get the sun ray shimmer period in milliseconds.
390    pub fn sun_shimmer_period_ms(self) -> u64 {
391        match self {
392            AnimationSpeed::Slow => 2000,
393            AnimationSpeed::Medium => 1200,
394            AnimationSpeed::Fast => 600,
395        }
396    }
397
398    /// Get the fog pulse period in milliseconds.
399    pub fn fog_pulse_period_ms(self) -> u64 {
400        match self {
401            AnimationSpeed::Slow => 6000,
402            AnimationSpeed::Medium => 4000,
403            AnimationSpeed::Fast => 2500,
404        }
405    }
406}
407
408/// Color theme for the clock display.
409#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
410pub enum ColorTheme {
411    #[default]
412    Cyan,
413    Green,
414    White,
415    Magenta,
416    Yellow,
417    Red,
418    Blue,
419    // Dynamic color themes
420    Rainbow,
421    RainbowVertical,
422    GradientWarm,
423    GradientCool,
424    GradientOcean,
425    GradientNeon,
426    GradientFire,
427    // Winter color themes
428    GradientFrost,
429    GradientAurora,
430    GradientWinter,
431}
432
433/// All color themes in order for cycling.
434const ALL_THEMES: &[ColorTheme] = &[
435    ColorTheme::Cyan,
436    ColorTheme::Green,
437    ColorTheme::Magenta,
438    ColorTheme::Yellow,
439    ColorTheme::Red,
440    ColorTheme::Blue,
441    ColorTheme::White,
442    ColorTheme::Rainbow,
443    ColorTheme::RainbowVertical,
444    ColorTheme::GradientWarm,
445    ColorTheme::GradientCool,
446    ColorTheme::GradientOcean,
447    ColorTheme::GradientNeon,
448    ColorTheme::GradientFire,
449    ColorTheme::GradientFrost,
450    ColorTheme::GradientAurora,
451    ColorTheme::GradientWinter,
452];
453
454impl ColorTheme {
455    /// Cycle to the next color theme.
456    pub fn next(&self) -> Self {
457        let current_idx = ALL_THEMES.iter().position(|t| t == self).unwrap_or(0);
458        let next_idx = (current_idx + 1) % ALL_THEMES.len();
459        ALL_THEMES[next_idx]
460    }
461
462    /// Cycle to the previous color theme.
463    pub fn prev(&self) -> Self {
464        let current_idx = ALL_THEMES.iter().position(|t| t == self).unwrap_or(0);
465        let prev_idx = if current_idx == 0 {
466            ALL_THEMES.len() - 1
467        } else {
468            current_idx - 1
469        };
470        ALL_THEMES[prev_idx]
471    }
472
473    /// Convert theme to Ratatui Color (for static themes).
474    pub fn color(self) -> Color {
475        match self {
476            ColorTheme::Cyan => Color::Cyan,
477            ColorTheme::Green => Color::Green,
478            ColorTheme::White => Color::White,
479            ColorTheme::Magenta => Color::Magenta,
480            ColorTheme::Yellow => Color::Yellow,
481            ColorTheme::Red => Color::Red,
482            ColorTheme::Blue => Color::Blue,
483            // Dynamic themes return a default color for backward compatibility
484            ColorTheme::Rainbow | ColorTheme::RainbowVertical | ColorTheme::GradientNeon => {
485                Color::Magenta
486            }
487            ColorTheme::GradientWarm | ColorTheme::GradientFire => Color::Red,
488            ColorTheme::GradientCool | ColorTheme::GradientOcean => Color::Cyan,
489            ColorTheme::GradientFrost | ColorTheme::GradientWinter => Color::Cyan,
490            ColorTheme::GradientAurora => Color::Green,
491        }
492    }
493
494    /// Check if this theme requires per-character coloring.
495    pub fn is_dynamic(self) -> bool {
496        matches!(
497            self,
498            ColorTheme::Rainbow
499                | ColorTheme::RainbowVertical
500                | ColorTheme::GradientWarm
501                | ColorTheme::GradientCool
502                | ColorTheme::GradientOcean
503                | ColorTheme::GradientNeon
504                | ColorTheme::GradientFire
505                | ColorTheme::GradientFrost
506                | ColorTheme::GradientAurora
507                | ColorTheme::GradientWinter
508        )
509    }
510
511    /// Get color at a specific position for dynamic themes.
512    /// `x` is the horizontal position (column), `y` is the vertical position (row).
513    /// `width` and `height` are the total dimensions for normalization.
514    pub fn color_at_position(self, x: usize, y: usize, width: usize, height: usize) -> Color {
515        match self {
516            ColorTheme::Rainbow => {
517                let colors = [
518                    Color::Red,
519                    Color::Rgb(255, 127, 0), // Orange
520                    Color::Yellow,
521                    Color::Green,
522                    Color::Cyan,
523                    Color::Blue,
524                    Color::Magenta,
525                ];
526                let idx = if width > 0 {
527                    (x * colors.len() / width.max(1)) % colors.len()
528                } else {
529                    0
530                };
531                colors[idx]
532            }
533            ColorTheme::RainbowVertical => {
534                let colors = [
535                    Color::Red,
536                    Color::Rgb(255, 127, 0), // Orange
537                    Color::Yellow,
538                    Color::Green,
539                    Color::Cyan,
540                    Color::Blue,
541                    Color::Magenta,
542                ];
543                let idx = if height > 0 {
544                    (y * colors.len() / height.max(1)) % colors.len()
545                } else {
546                    0
547                };
548                colors[idx]
549            }
550            ColorTheme::GradientWarm => {
551                // Red -> Orange -> Yellow
552                let progress = if width > 0 {
553                    (x as f32) / (width.max(1) as f32)
554                } else {
555                    0.0
556                };
557                if progress < 0.5 {
558                    // Red to Orange
559                    let g = (127.0 * (progress * 2.0)) as u8;
560                    Color::Rgb(255, g, 0)
561                } else {
562                    // Orange to Yellow
563                    let g = 127 + ((128.0 * ((progress - 0.5) * 2.0)) as u8);
564                    Color::Rgb(255, g, 0)
565                }
566            }
567            ColorTheme::GradientCool => {
568                // Blue -> Cyan -> Green
569                let progress = if width > 0 {
570                    (x as f32) / (width.max(1) as f32)
571                } else {
572                    0.0
573                };
574                if progress < 0.5 {
575                    // Blue to Cyan
576                    let g = (255.0 * (progress * 2.0)) as u8;
577                    Color::Rgb(0, g, 255)
578                } else {
579                    // Cyan to Green
580                    let b = 255 - ((255.0 * ((progress - 0.5) * 2.0)) as u8);
581                    Color::Rgb(0, 255, b)
582                }
583            }
584            ColorTheme::GradientOcean => {
585                // Dark blue -> Cyan -> Teal
586                let progress = if width > 0 {
587                    (x as f32) / (width.max(1) as f32)
588                } else {
589                    0.0
590                };
591                if progress < 0.5 {
592                    // Dark blue to Cyan
593                    let r = (100.0 * (progress * 2.0)) as u8;
594                    let g = (150.0 + 105.0 * (progress * 2.0)) as u8;
595                    Color::Rgb(r, g, 255)
596                } else {
597                    // Cyan to Teal
598                    let b = 255 - ((127.0 * ((progress - 0.5) * 2.0)) as u8);
599                    Color::Rgb(100, 255, b)
600                }
601            }
602            ColorTheme::GradientNeon => {
603                // Magenta -> Cyan (synthwave style)
604                let progress = if width > 0 {
605                    (x as f32) / (width.max(1) as f32)
606                } else {
607                    0.0
608                };
609                let r = 255 - ((255.0 * progress) as u8);
610                let g = (255.0 * progress) as u8;
611                let b = 255;
612                Color::Rgb(r, g, b)
613            }
614            ColorTheme::GradientFire => {
615                // Dark red -> Red -> Orange -> Yellow (fire effect)
616                let progress = if width > 0 {
617                    (x as f32) / (width.max(1) as f32)
618                } else {
619                    0.0
620                };
621                if progress < 0.33 {
622                    // Dark red to Red
623                    let r = 128 + ((127.0 * (progress * 3.0)) as u8);
624                    Color::Rgb(r, 0, 0)
625                } else if progress < 0.66 {
626                    // Red to Orange
627                    let g = (165.0 * ((progress - 0.33) * 3.0)) as u8;
628                    Color::Rgb(255, g, 0)
629                } else {
630                    // Orange to Yellow
631                    let g = 165 + ((90.0 * ((progress - 0.66) * 3.0)) as u8);
632                    Color::Rgb(255, g, 0)
633                }
634            }
635            ColorTheme::GradientFrost => {
636                // White -> Ice Blue -> Steel Blue
637                let progress = if width > 0 {
638                    (x as f32) / (width.max(1) as f32)
639                } else {
640                    0.0
641                };
642                if progress < 0.5 {
643                    // White to Ice Blue
644                    let t = progress * 2.0;
645                    let r = 255 - ((255 - 176) as f32 * t) as u8;
646                    let g = 255 - ((255 - 224) as f32 * t) as u8;
647                    let b = 255 - ((255 - 230) as f32 * t) as u8;
648                    Color::Rgb(r, g, b)
649                } else {
650                    // Ice Blue to Steel Blue
651                    let t = (progress - 0.5) * 2.0;
652                    let r = 176 - ((176 - 70) as f32 * t) as u8;
653                    let g = 224 - ((224 - 130) as f32 * t) as u8;
654                    let b = 230 - ((230 - 180) as f32 * t) as u8;
655                    Color::Rgb(r, g, b)
656                }
657            }
658            ColorTheme::GradientAurora => {
659                // Green -> Cyan -> Blue -> Purple (aurora colors)
660                let progress = if width > 0 {
661                    (x as f32) / (width.max(1) as f32)
662                } else {
663                    0.0
664                };
665                if progress < 0.33 {
666                    // Green to Cyan
667                    let t = progress * 3.0;
668                    let r = (0.0 + 0.0 * t) as u8;
669                    let g = (255.0 - 128.0 * t) as u8;
670                    let b = (127.0 + 128.0 * t) as u8;
671                    Color::Rgb(r, g, b)
672                } else if progress < 0.66 {
673                    // Cyan to Blue
674                    let t = (progress - 0.33) * 3.0;
675                    let r = (0.0 + 65.0 * t) as u8;
676                    let g = (127.0 - 22.0 * t) as u8;
677                    let b = (255.0 - 30.0 * t) as u8;
678                    Color::Rgb(r, g, b)
679                } else {
680                    // Blue to Purple
681                    let t = (progress - 0.66) * 3.0;
682                    let r = (65.0 + 73.0 * t) as u8;
683                    let g = (105.0 - 62.0 * t) as u8;
684                    let b = (225.0 + 1.0 * t) as u8;
685                    Color::Rgb(r, g, b)
686                }
687            }
688            ColorTheme::GradientWinter => {
689                // Deep Blue -> Royal Blue -> Ice Blue
690                let progress = if width > 0 {
691                    (x as f32) / (width.max(1) as f32)
692                } else {
693                    0.0
694                };
695                if progress < 0.5 {
696                    // Deep Blue to Royal Blue
697                    let t = progress * 2.0;
698                    let r = (25.0 + 40.0 * t) as u8;
699                    let g = (25.0 + 80.0 * t) as u8;
700                    let b = (112.0 + 113.0 * t) as u8;
701                    Color::Rgb(r, g, b)
702                } else {
703                    // Royal Blue to Ice Blue
704                    let t = (progress - 0.5) * 2.0;
705                    let r = (65.0 + 70.0 * t) as u8;
706                    let g = (105.0 + 101.0 * t) as u8;
707                    let b = (225.0 + 25.0 * t) as u8;
708                    Color::Rgb(r, g, b)
709                }
710            }
711            // Static themes just return their color
712            _ => self.color(),
713        }
714    }
715
716    /// Get display name for the theme.
717    pub fn display_name(self) -> &'static str {
718        match self {
719            ColorTheme::Cyan => "Cyan",
720            ColorTheme::Green => "Green",
721            ColorTheme::White => "White",
722            ColorTheme::Magenta => "Magenta",
723            ColorTheme::Yellow => "Yellow",
724            ColorTheme::Red => "Red",
725            ColorTheme::Blue => "Blue",
726            ColorTheme::Rainbow => "Rainbow",
727            ColorTheme::RainbowVertical => "Rainbow V",
728            ColorTheme::GradientWarm => "Warm",
729            ColorTheme::GradientCool => "Cool",
730            ColorTheme::GradientOcean => "Ocean",
731            ColorTheme::GradientNeon => "Neon",
732            ColorTheme::GradientFire => "Fire",
733            ColorTheme::GradientFrost => "Frost",
734            ColorTheme::GradientAurora => "Aurora",
735            ColorTheme::GradientWinter => "Winter",
736        }
737    }
738}
739
740/// Apply animation transformations to a color.
741pub fn apply_animation(
742    base_color: Color,
743    animation_style: AnimationStyle,
744    speed: AnimationSpeed,
745    elapsed_ms: u64,
746    x: usize,
747    width: usize,
748    flash_intensity: f32,
749) -> Color {
750    match animation_style {
751        AnimationStyle::None => base_color,
752        AnimationStyle::Shifting => apply_shifting(base_color, elapsed_ms, speed),
753        AnimationStyle::Pulsing => apply_pulsing(base_color, elapsed_ms, speed),
754        AnimationStyle::Wave => apply_wave(base_color, elapsed_ms, speed, x, width),
755        AnimationStyle::Reactive => apply_reactive(base_color, flash_intensity),
756    }
757}
758
759/// Shift hue over time.
760fn apply_shifting(color: Color, elapsed_ms: u64, speed: AnimationSpeed) -> Color {
761    let (r, g, b) = color_to_rgb(color);
762    let (h, s, l) = rgb_to_hsl(r, g, b);
763
764    let cycle_ms = speed.shift_cycle_ms();
765    let hue_offset = ((elapsed_ms % cycle_ms) as f32 / cycle_ms as f32) * 360.0;
766    let new_h = (h + hue_offset) % 360.0;
767
768    let (nr, ng, nb) = hsl_to_rgb(new_h, s, l);
769    Color::Rgb(nr, ng, nb)
770}
771
772/// Pulse brightness using sine wave.
773fn apply_pulsing(color: Color, elapsed_ms: u64, speed: AnimationSpeed) -> Color {
774    let (r, g, b) = color_to_rgb(color);
775
776    let period_ms = speed.pulse_period_ms();
777    let phase = (elapsed_ms % period_ms) as f32 / period_ms as f32;
778    let brightness = 0.5 + 0.5 * (phase * 2.0 * std::f32::consts::PI).sin();
779
780    // Apply brightness (minimum 30% to stay visible)
781    let factor = 0.3 + 0.7 * brightness;
782    Color::Rgb(
783        (r as f32 * factor) as u8,
784        (g as f32 * factor) as u8,
785        (b as f32 * factor) as u8,
786    )
787}
788
789/// Wave pattern flowing horizontally.
790fn apply_wave(
791    color: Color,
792    elapsed_ms: u64,
793    speed: AnimationSpeed,
794    x: usize,
795    width: usize,
796) -> Color {
797    let (r, g, b) = color_to_rgb(color);
798
799    let period_ms = speed.wave_period_ms();
800    let time_phase = (elapsed_ms % period_ms) as f32 / period_ms as f32;
801    let x_phase = if width > 0 {
802        x as f32 / width as f32
803    } else {
804        0.0
805    };
806
807    let wave = ((x_phase + time_phase) * 2.0 * std::f32::consts::PI).sin();
808    let brightness = 0.6 + 0.4 * wave;
809
810    Color::Rgb(
811        (r as f32 * brightness) as u8,
812        (g as f32 * brightness) as u8,
813        (b as f32 * brightness) as u8,
814    )
815}
816
817/// Apply flash intensity for reactive animation.
818fn apply_reactive(color: Color, flash_intensity: f32) -> Color {
819    let (r, g, b) = color_to_rgb(color);
820
821    // Boost brightness based on flash intensity
822    let factor = 1.0 + flash_intensity;
823    Color::Rgb(
824        (r as f32 * factor).min(255.0) as u8,
825        (g as f32 * factor).min(255.0) as u8,
826        (b as f32 * factor).min(255.0) as u8,
827    )
828}
829
830/// Extract RGB values from a Color.
831fn color_to_rgb(color: Color) -> (u8, u8, u8) {
832    match color {
833        Color::Rgb(r, g, b) => (r, g, b),
834        Color::Red => (255, 0, 0),
835        Color::Green => (0, 255, 0),
836        Color::Blue => (0, 0, 255),
837        Color::Yellow => (255, 255, 0),
838        Color::Magenta => (255, 0, 255),
839        Color::Cyan => (0, 255, 255),
840        Color::White => (255, 255, 255),
841        _ => (128, 128, 128),
842    }
843}
844
845/// Convert RGB to HSL.
846fn rgb_to_hsl(r: u8, g: u8, b: u8) -> (f32, f32, f32) {
847    let r = r as f32 / 255.0;
848    let g = g as f32 / 255.0;
849    let b = b as f32 / 255.0;
850
851    let max = r.max(g).max(b);
852    let min = r.min(g).min(b);
853    let l = (max + min) / 2.0;
854
855    if max == min {
856        return (0.0, 0.0, l);
857    }
858
859    let d = max - min;
860    let s = if l > 0.5 {
861        d / (2.0 - max - min)
862    } else {
863        d / (max + min)
864    };
865
866    let h = if max == r {
867        ((g - b) / d + if g < b { 6.0 } else { 0.0 }) * 60.0
868    } else if max == g {
869        ((b - r) / d + 2.0) * 60.0
870    } else {
871        ((r - g) / d + 4.0) * 60.0
872    };
873
874    (h, s, l)
875}
876
877/// Convert HSL to RGB.
878fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (u8, u8, u8) {
879    if s == 0.0 {
880        let v = (l * 255.0) as u8;
881        return (v, v, v);
882    }
883
884    let q = if l < 0.5 {
885        l * (1.0 + s)
886    } else {
887        l + s - l * s
888    };
889    let p = 2.0 * l - q;
890
891    let h = h / 360.0;
892
893    let r = hue_to_rgb(p, q, h + 1.0 / 3.0);
894    let g = hue_to_rgb(p, q, h);
895    let b = hue_to_rgb(p, q, h - 1.0 / 3.0);
896
897    ((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
898}
899
900fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
901    if t < 0.0 {
902        t += 1.0;
903    }
904    if t > 1.0 {
905        t -= 1.0;
906    }
907
908    if t < 1.0 / 6.0 {
909        p + (q - p) * 6.0 * t
910    } else if t < 1.0 / 2.0 {
911        q
912    } else if t < 2.0 / 3.0 {
913        p + (q - p) * (2.0 / 3.0 - t) * 6.0
914    } else {
915        p
916    }
917}
918
919/// Check if colon should be visible in the blink cycle.
920/// Returns true during the "on" phase (first 500ms of each second).
921pub fn is_colon_visible(elapsed_ms: u64) -> bool {
922    let phase = (elapsed_ms % 1000) as f32 / 1000.0;
923    phase < 0.5
924}