sigye_core/
lib.rs

1//! Core types for the sigye clock application.
2
3use ratatui::style::Color;
4use serde::{Deserialize, Serialize};
5
6/// Time format for the clock display.
7#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
8pub enum TimeFormat {
9    #[default]
10    TwentyFourHour,
11    TwelveHour,
12}
13
14impl TimeFormat {
15    /// Toggle between 12-hour and 24-hour format.
16    pub fn toggle(&self) -> Self {
17        match self {
18            TimeFormat::TwentyFourHour => TimeFormat::TwelveHour,
19            TimeFormat::TwelveHour => TimeFormat::TwentyFourHour,
20        }
21    }
22}
23
24/// Animation style for color themes.
25#[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
35/// All animation styles for cycling.
36const ALL_ANIMATION_STYLES: &[AnimationStyle] = &[
37    AnimationStyle::None,
38    AnimationStyle::Shifting,
39    AnimationStyle::Pulsing,
40    AnimationStyle::Wave,
41    AnimationStyle::Reactive,
42];
43
44impl AnimationStyle {
45    /// Cycle to the next animation style.
46    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    /// Cycle to the previous animation style.
56    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    /// Get display name for the animation style.
70    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/// Background animation style for the terminal.
82#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
83pub enum BackgroundStyle {
84    #[default]
85    None,
86    Starfield,
87    MatrixRain,
88    GradientWave,
89    // Reactive backgrounds that respond to system resource usage
90    SystemPulse,
91    ResourceWave,
92    DataFlow,
93    HeatMap,
94}
95
96/// All background styles for cycling.
97const 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    /// Cycle to the next background style.
110    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    /// Cycle to the previous background style.
120    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    /// Get display name for the background style.
134    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    /// Check if this background style requires system metrics (reactive).
148    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/// Animation speed setting.
160#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
161pub enum AnimationSpeed {
162    Slow,
163    #[default]
164    Medium,
165    Fast,
166}
167
168/// All animation speeds for cycling.
169const ALL_ANIMATION_SPEEDS: &[AnimationSpeed] = &[
170    AnimationSpeed::Slow,
171    AnimationSpeed::Medium,
172    AnimationSpeed::Fast,
173];
174
175impl AnimationSpeed {
176    /// Cycle to the next speed.
177    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    /// Cycle to the previous speed.
187    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    /// Get display name for the speed.
201    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    /// Get the cycle duration in milliseconds for shifting animation.
210    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    /// Get the pulse period in milliseconds.
219    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    /// Get the wave period in milliseconds.
228    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    /// Get the flash decay duration in milliseconds for reactive animation.
237    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    /// Get the star twinkle period in milliseconds.
246    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    /// Get the matrix rain fall speed multiplier.
255    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    /// Get the gradient scroll period in milliseconds.
264    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/// Color theme for the clock display.
274#[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    // Dynamic color themes
285    Rainbow,
286    RainbowVertical,
287    GradientWarm,
288    GradientCool,
289    GradientOcean,
290    GradientNeon,
291    GradientFire,
292}
293
294/// All color themes in order for cycling.
295const 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    /// Cycle to the next color theme.
314    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    /// Cycle to the previous color theme.
321    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    /// Convert theme to Ratatui Color (for static themes).
332    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            // Dynamic themes return a default color for backward compatibility
342            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    /// Check if this theme requires per-character coloring.
351    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    /// Get color at a specific position for dynamic themes.
365    /// `x` is the horizontal position (column), `y` is the vertical position (row).
366    /// `width` and `height` are the total dimensions for normalization.
367    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), // Orange
373                    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), // Orange
390                    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                // Red -> Orange -> Yellow
405                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                    // Red to Orange
412                    let g = (127.0 * (progress * 2.0)) as u8;
413                    Color::Rgb(255, g, 0)
414                } else {
415                    // Orange to Yellow
416                    let g = 127 + ((128.0 * ((progress - 0.5) * 2.0)) as u8);
417                    Color::Rgb(255, g, 0)
418                }
419            }
420            ColorTheme::GradientCool => {
421                // Blue -> Cyan -> Green
422                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                    // Blue to Cyan
429                    let g = (255.0 * (progress * 2.0)) as u8;
430                    Color::Rgb(0, g, 255)
431                } else {
432                    // Cyan to Green
433                    let b = 255 - ((255.0 * ((progress - 0.5) * 2.0)) as u8);
434                    Color::Rgb(0, 255, b)
435                }
436            }
437            ColorTheme::GradientOcean => {
438                // Dark blue -> Cyan -> Teal
439                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                    // Dark blue to Cyan
446                    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                    // Cyan to Teal
451                    let b = 255 - ((127.0 * ((progress - 0.5) * 2.0)) as u8);
452                    Color::Rgb(100, 255, b)
453                }
454            }
455            ColorTheme::GradientNeon => {
456                // Magenta -> Cyan (synthwave style)
457                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                // Dark red -> Red -> Orange -> Yellow (fire effect)
469                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                    // Dark red to Red
476                    let r = 128 + ((127.0 * (progress * 3.0)) as u8);
477                    Color::Rgb(r, 0, 0)
478                } else if progress < 0.66 {
479                    // Red to Orange
480                    let g = (165.0 * ((progress - 0.33) * 3.0)) as u8;
481                    Color::Rgb(255, g, 0)
482                } else {
483                    // Orange to Yellow
484                    let g = 165 + ((90.0 * ((progress - 0.66) * 3.0)) as u8);
485                    Color::Rgb(255, g, 0)
486                }
487            }
488            // Static themes just return their color
489            _ => self.color(),
490        }
491    }
492
493    /// Get display name for the theme.
494    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
514/// Apply animation transformations to a color.
515pub 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
533/// Shift hue over time.
534fn 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
546/// Pulse brightness using sine wave.
547fn 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    // Apply brightness (minimum 30% to stay visible)
555    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
563/// Wave pattern flowing horizontally.
564fn 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
591/// Apply flash intensity for reactive animation.
592fn apply_reactive(color: Color, flash_intensity: f32) -> Color {
593    let (r, g, b) = color_to_rgb(color);
594
595    // Boost brightness based on flash intensity
596    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
604/// Extract RGB values from a Color.
605fn 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
619/// Convert RGB to HSL.
620fn 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
651/// Convert HSL to RGB.
652fn 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
693/// Check if colon should be visible in the blink cycle.
694/// Returns true during the "on" phase (first 500ms of each second).
695pub fn is_colon_visible(elapsed_ms: u64) -> bool {
696    let phase = (elapsed_ms % 1000) as f32 / 1000.0;
697    phase < 0.5
698}