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