use std::path::PathBuf;
use std::time::{Duration, SystemTime};
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
pub struct Config {
pub theme: Option<String>,
pub custom_theme: Option<CustomThemeColors>,
#[serde(default)]
pub keybindings: Option<KeyBindings>,
#[serde(default)]
pub plugins: Option<PluginConfig>,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct CustomThemeColors {
pub name: Option<String>,
pub accent: Option<String>,
pub highlight: Option<String>,
pub logo: Option<String>,
pub text: Option<String>,
pub text_muted: Option<String>,
pub background: Option<String>,
pub background_panel: Option<String>,
pub background_overlay: Option<String>,
pub border: Option<String>,
pub success: Option<String>,
pub error: Option<String>,
pub inverted_text: Option<String>,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct KeyBindings {}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct PluginConfig {}
impl Config {
pub fn load_from(dir: &std::path::Path) -> Self {
Self::try_load_from(dir).unwrap_or_else(|_| Config::default())
}
pub fn try_load_from(dir: &std::path::Path) -> Result<Self, String> {
let path = dir.join("config.toml");
if !path.exists() {
return Err("config.toml not found".into());
}
let content = std::fs::read_to_string(&path)
.map_err(|e| format!("Failed to read config.toml: {e}"))?;
toml::from_str(&content).map_err(|e| format!("Failed to parse config.toml: {e}"))
}
pub fn save_to(&self, dir: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
let path = dir.join("config.toml");
let content = toml::to_string_pretty(self)?;
std::fs::write(&path, content)?;
Ok(())
}
}
#[derive(Debug)]
pub struct ConfigManager {
dir: PathBuf,
config: Config,
last_modified: Option<SystemTime>,
pub dirty: bool,
error: Option<String>,
tick_rate: Duration,
}
impl ConfigManager {
pub fn new(dir: PathBuf) -> Self {
let last_modified = dir
.join("config.toml")
.metadata()
.ok()
.and_then(|m| m.modified().ok());
let (config, error) = match Config::try_load_from(&dir) {
Ok(cfg) => (cfg, None),
Err(e) => (Config::default(), Some(e)),
};
ConfigManager {
dir,
config,
last_modified,
dirty: false,
error,
tick_rate: Duration::from_millis(100),
}
}
pub fn poll(&mut self) {
let path = self.dir.join("config.toml");
let modified = match path.metadata().ok().and_then(|m| m.modified().ok()) {
Some(t) => t,
None => return,
};
let changed = match self.last_modified {
Some(last) => modified != last,
None => true,
};
if !changed {
return;
}
self.last_modified = Some(modified);
match Config::try_load_from(&self.dir) {
Ok(cfg) => {
self.config = cfg;
self.error = None;
}
Err(e) => {
self.error = Some(e);
}
}
self.dirty = true;
}
pub fn ack(&mut self) {
self.dirty = false;
}
pub fn config(&self) -> &Config {
&self.config
}
pub fn error(&self) -> Option<&str> {
self.error.as_deref()
}
pub fn save_theme(&mut self, theme_name: &str) {
self.config.theme = Some(theme_name.to_string());
self.config.custom_theme = None;
self.persist();
}
pub fn save_custom_theme(&mut self, colors: CustomThemeColors) {
self.config.custom_theme = Some(colors);
self.persist();
}
pub fn tick_rate(&self) -> Duration {
self.tick_rate
}
pub fn set_tick_rate(&mut self, duration: Duration) {
self.tick_rate = duration;
}
pub fn clear_custom_theme(&mut self) {
if self.config.custom_theme.is_some() {
self.config.custom_theme = None;
self.persist();
}
}
fn persist(&mut self) {
if let Err(e) = self.config.save_to(&self.dir) {
log::error!("[santui] Failed to save config: {e}");
return;
}
self.last_modified = self
.dir
.join("config.toml")
.metadata()
.ok()
.and_then(|m| m.modified().ok());
}
}