1use std::path::PathBuf;
2use std::time::{Duration, SystemTime};
3
4#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
6pub struct Config {
7 pub theme: Option<String>,
9 pub custom_theme: Option<CustomThemeColors>,
11 #[serde(default)]
13 pub keybindings: Option<KeyBindings>,
14 #[serde(default)]
16 pub plugins: Option<PluginConfig>,
17}
18
19#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
23pub struct CustomThemeColors {
24 pub name: Option<String>,
25 pub accent: Option<String>,
26 pub highlight: Option<String>,
27 pub logo: Option<String>,
28 pub text: Option<String>,
29 pub text_muted: Option<String>,
30 pub background: Option<String>,
31 pub background_panel: Option<String>,
32 pub background_overlay: Option<String>,
33 pub border: Option<String>,
34 pub success: Option<String>,
35 pub error: Option<String>,
36 pub inverted_text: Option<String>,
37}
38
39#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
41pub struct KeyBindings {}
42
43#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
45pub struct PluginConfig {}
46
47impl Config {
48 pub fn load_from(dir: &std::path::Path) -> Self {
51 Self::try_load_from(dir).unwrap_or_else(|_| Config::default())
52 }
53
54 pub fn try_load_from(dir: &std::path::Path) -> Result<Self, String> {
57 let path = dir.join("config.toml");
58 if !path.exists() {
59 return Err("config.toml not found".into());
60 }
61 let content = std::fs::read_to_string(&path)
62 .map_err(|e| format!("Failed to read config.toml: {e}"))?;
63 toml::from_str(&content).map_err(|e| format!("Failed to parse config.toml: {e}"))
64 }
65
66 pub fn save_to(&self, dir: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
68 let path = dir.join("config.toml");
69 let content = toml::to_string_pretty(self)?;
70 std::fs::write(&path, content)?;
71 Ok(())
72 }
73}
74
75#[derive(Debug)]
81pub struct ConfigManager {
82 dir: PathBuf,
83 config: Config,
84 last_modified: Option<SystemTime>,
85 pub dirty: bool,
87 error: Option<String>,
89 tick_rate: Duration,
91 poll_skip: u32,
93}
94
95impl ConfigManager {
96 pub fn new(dir: PathBuf) -> Self {
98 let last_modified = dir
99 .join("config.toml")
100 .metadata()
101 .ok()
102 .and_then(|m| m.modified().ok());
103 let (config, error) = match Config::try_load_from(&dir) {
104 Ok(cfg) => (cfg, None),
105 Err(e) => (Config::default(), Some(e)),
106 };
107 ConfigManager {
108 dir,
109 config,
110 last_modified,
111 dirty: false,
112 error,
113 tick_rate: Duration::from_millis(100),
114 poll_skip: 0,
115 }
116 }
117
118 pub fn poll(&mut self) {
120 self.poll_skip = self.poll_skip.saturating_sub(1);
121 if self.poll_skip > 0 {
122 return;
123 }
124 self.poll_skip = 30;
125 let path = self.dir.join("config.toml");
126 let modified = match path.metadata().ok().and_then(|m| m.modified().ok()) {
127 Some(t) => t,
128 None => return,
129 };
130 let changed = match self.last_modified {
131 Some(last) => modified != last,
132 None => true,
133 };
134 if !changed {
135 return;
136 }
137 self.last_modified = Some(modified);
138 match Config::try_load_from(&self.dir) {
139 Ok(cfg) => {
140 self.config = cfg;
141 self.error = None;
142 self.dirty = true;
143 }
144 Err(e) => {
145 self.error = Some(e);
146 }
147 }
148 }
149
150 pub fn ack(&mut self) {
152 self.dirty = false;
153 }
154
155 pub fn config(&self) -> &Config {
156 &self.config
157 }
158
159 pub fn error(&self) -> Option<&str> {
161 self.error.as_deref()
162 }
163
164 pub fn save_theme(&mut self, theme_name: &str) {
168 self.config.theme = Some(theme_name.to_string());
169 self.config.custom_theme = None;
170 self.persist();
171 }
172
173 pub fn save_custom_theme(&mut self, colors: CustomThemeColors) {
175 self.config.custom_theme = Some(colors);
176 self.persist();
177 }
178
179 pub fn tick_rate(&self) -> Duration {
180 self.tick_rate
181 }
182
183 pub fn set_tick_rate(&mut self, duration: Duration) {
184 self.tick_rate = duration;
185 }
186
187 pub fn clear_custom_theme(&mut self) {
189 if self.config.custom_theme.is_some() {
190 self.config.custom_theme = None;
191 self.persist();
192 }
193 }
194
195 fn persist(&mut self) {
198 if let Err(e) = self.config.save_to(&self.dir) {
199 log::error!("[santui] Failed to save config: {e}");
200 return;
201 }
202 self.last_modified = self
203 .dir
204 .join("config.toml")
205 .metadata()
206 .ok()
207 .and_then(|m| m.modified().ok());
208 }
209}