speed_reader_core/
config.rs1use serde::{Deserialize, Serialize};
2
3#[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}