1use std::path::PathBuf;
2use std::time::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}
90
91impl ConfigManager {
92 pub fn new(dir: PathBuf) -> Self {
94 let last_modified = dir
95 .join("config.toml")
96 .metadata()
97 .ok()
98 .and_then(|m| m.modified().ok());
99 let (config, error) = match Config::try_load_from(&dir) {
100 Ok(cfg) => (cfg, None),
101 Err(e) => (Config::default(), Some(e)),
102 };
103 ConfigManager {
104 dir,
105 config,
106 last_modified,
107 dirty: false,
108 error,
109 }
110 }
111
112 pub fn poll(&mut self) {
114 let path = self.dir.join("config.toml");
115 let modified = match path.metadata().ok().and_then(|m| m.modified().ok()) {
116 Some(t) => t,
117 None => return,
118 };
119 let changed = match self.last_modified {
120 Some(last) => modified != last,
121 None => true,
122 };
123 if !changed {
124 return;
125 }
126 self.last_modified = Some(modified);
127 match Config::try_load_from(&self.dir) {
128 Ok(cfg) => {
129 self.config = cfg;
130 self.error = None;
131 }
132 Err(e) => {
133 self.error = Some(e);
134 }
135 }
136 self.dirty = true;
137 }
138
139 pub fn ack(&mut self) {
141 self.dirty = false;
142 }
143
144 pub fn config(&self) -> &Config {
145 &self.config
146 }
147
148 pub fn error(&self) -> Option<&str> {
150 self.error.as_deref()
151 }
152
153 pub fn save_theme(&mut self, theme_name: &str) {
157 self.config.theme = Some(theme_name.to_string());
158 self.config.custom_theme = None;
159 self.persist();
160 }
161
162 pub fn save_custom_theme(&mut self, colors: CustomThemeColors) {
164 self.config.custom_theme = Some(colors);
165 self.persist();
166 }
167
168 pub fn clear_custom_theme(&mut self) {
170 if self.config.custom_theme.is_some() {
171 self.config.custom_theme = None;
172 self.persist();
173 }
174 }
175
176 fn persist(&mut self) {
179 if let Err(e) = self.config.save_to(&self.dir) {
180 eprintln!("[santui] Failed to save config: {e}");
181 return;
182 }
183 self.last_modified = self
184 .dir
185 .join("config.toml")
186 .metadata()
187 .ok()
188 .and_then(|m| m.modified().ok());
189 }
190}