Skip to main content

speed_reader_core/
config.rs

1use serde::{Deserialize, Serialize};
2
3/// Модель конфигурации SpeedReader.
4/// Requirement: 4.1, 4.2, 4.3, Design: ConfigModel section
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub struct ThemeColors {
8    pub bg: String,
9    pub text: String,
10    pub accent: String,
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14pub struct Theme {
15    pub light: ThemeColors,
16    pub dark: ThemeColors,
17}
18
19impl Default for Theme {
20    fn default() -> Self {
21        Self {
22            light: ThemeColors {
23                bg: "#FFFFFF".into(),
24                text: "#1A1A1A".into(),
25                accent: "#E53935".into(),
26            },
27            dark: ThemeColors {
28                bg: "#1A1A1A".into(),
29                text: "#F5F5F5".into(),
30                accent: "#FF5252".into(),
31            },
32        }
33    }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
37pub enum ThemeMode {
38    Light,
39    Dark,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
43pub struct ConfigModel {
44    pub wpm: u32,
45    pub theme_mode: ThemeMode,
46    pub font_size: f32,
47    pub skip_amount: u32,
48    pub speed_step: u32,
49    pub theme: Theme,
50    pub editor_cmd: String,
51}
52
53impl Default for ConfigModel {
54    fn default() -> Self {
55        Self {
56            wpm: 300,
57            theme_mode: ThemeMode::Dark,
58            font_size: 48.0,
59            skip_amount: 5,
60            speed_step: 10,
61            theme: Theme::default(),
62            editor_cmd: "zed".into(),
63        }
64    }
65}
66
67impl ConfigModel {
68    pub fn validate(&self) -> Result<(), Vec<String>> {
69        let mut errors = Vec::new();
70        if self.wpm < 50 || self.wpm > 1000 {
71            errors.push(format!("WPM {} out of range [50, 1000]", self.wpm));
72        }
73        if self.font_size < 12.0 || self.font_size > 200.0 {
74            errors.push(format!("font_size {} out of range [12, 200]", self.font_size));
75        }
76        if self.skip_amount == 0 {
77            errors.push("skip_amount must be > 0".into());
78        }
79        if self.speed_step == 0 {
80            errors.push("speed_step must be > 0".into());
81        }
82        if self.editor_cmd.is_empty() {
83            errors.push("editor_cmd must not be empty".into());
84        }
85        if errors.is_empty() {
86            Ok(())
87        } else {
88            Err(errors)
89        }
90    }
91
92
93    pub fn current_colors(&self) -> &ThemeColors {
94        match self.theme_mode {
95            ThemeMode::Light => &self.theme.light,
96            ThemeMode::Dark => &self.theme.dark,
97        }
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_default_valid() {
107        let config = ConfigModel::default();
108        assert!(config.validate().is_ok());
109    }
110
111    #[test]
112    fn test_wpm_out_of_range() {
113        let mut config = ConfigModel::default();
114        config.wpm = 10;
115        assert!(config.validate().is_err());
116        config.wpm = 2000;
117        assert!(config.validate().is_err());
118    }
119
120    #[test]
121    fn test_font_size_out_of_range() {
122        let mut config = ConfigModel::default();
123        config.font_size = 8.0;
124        assert!(config.validate().is_err());
125    }
126
127    #[test]
128    fn test_current_colors_dark() {
129        let config = ConfigModel::default();
130        let colors = config.current_colors();
131        assert_eq!(colors.bg, "#1A1A1A");
132    }
133
134    #[test]
135    fn test_current_colors_light() {
136        let mut config = ConfigModel::default();
137        config.theme_mode = ThemeMode::Light;
138        let colors = config.current_colors();
139        assert_eq!(colors.bg, "#FFFFFF");
140    }
141
142    #[test]
143    fn test_serde_roundtrip() {
144        let config = ConfigModel::default();
145        let json = serde_json::to_string(&config).unwrap();
146        let deserialized: ConfigModel = serde_json::from_str(&json).unwrap();
147        assert_eq!(config, deserialized);
148    }
149}