diff --git a/crates/spaceshd/src/config.rs b/crates/spaceshd/src/config.rs index 3197e5f..3a251b6 100644 --- a/crates/spaceshd/src/config.rs +++ b/crates/spaceshd/src/config.rs @@ -4,14 +4,34 @@ //! partial config never breaks startup. use std::path::Path; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Default, Deserialize)] +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct TerminalConfig { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub font_family: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub font_size: Option, +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct AppearanceConfig { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub theme: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub accent: Option, +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] pub struct Config { /// Shell launched for plain (no-command) panels. When unset, the daemon /// auto-detects the user's login shell. - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub default_shell: Option, + #[serde(default)] + pub terminal: TerminalConfig, + #[serde(default)] + pub appearance: AppearanceConfig, } impl Config { @@ -27,6 +47,23 @@ impl Config { Err(_) => Self::default(), } } + + /// Persist to `~/.spacesh/config.toml`. + pub fn save(&self) -> std::io::Result<()> { + let dir = crate::lifecycle::spacesh_dir() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?; + self.save_to(&dir.join("config.toml")) + } + + /// Persist to an arbitrary path. Creates the parent directory if needed. + pub fn save_to(&self, path: &Path) -> std::io::Result<()> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let s = toml::to_string_pretty(self) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?; + std::fs::write(path, s) + } } /// Resolve the shell to spawn for a plain panel. @@ -98,4 +135,41 @@ mod tests { std::env::remove_var("SPACESH_SHELL"); assert_eq!(s, "/tmp/fake-shell"); } + + #[test] + fn parses_terminal_and_appearance() { + let dir = std::env::temp_dir().join(format!("spacesh-cfg-sections-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("config.toml"); + std::fs::write(&path, + "default_shell = \"/bin/zsh\"\n[terminal]\nfont_family = \"Menlo\"\nfont_size = 15\n[appearance]\ntheme = \"light\"\naccent = \"teal\"\n").unwrap(); + let c = Config::from_path(&path); + assert_eq!(c.terminal.font_family.as_deref(), Some("Menlo")); + assert_eq!(c.terminal.font_size, Some(15)); + assert_eq!(c.appearance.theme.as_deref(), Some("light")); + assert_eq!(c.appearance.accent.as_deref(), Some("teal")); + let _ = std::fs::remove_file(&path); + } + + #[test] + fn missing_sections_default() { + let c = Config::from_path(Path::new("/no/such/cfg.toml")); + assert!(c.terminal.font_family.is_none()); + assert!(c.appearance.theme.is_none()); + } + + #[test] + fn save_then_reload_round_trips() { + let dir = std::env::temp_dir().join(format!("spacesh-cfg-save-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("config.toml"); + let mut c = Config::default(); + c.terminal.font_size = Some(14); + c.appearance.accent = Some("purple".into()); + c.save_to(&path).unwrap(); + let back = Config::from_path(&path); + assert_eq!(back.terminal.font_size, Some(14)); + assert_eq!(back.appearance.accent.as_deref(), Some("purple")); + let _ = std::fs::remove_file(&path); + } }