Skip to main content

wipe_core/
config.rs

1//! User-global configuration (`<config>/wipe/config.json`).
2//!
3//! Distinct from a board's `settings.json` (which is per-project and git-tracked),
4//! this file holds the *defaults* a user picks once during onboarding - preferred
5//! port, exposure, whether to auto-serve, how much starter content a new board
6//! gets, where to install the agent skill, and UI styling - so later
7//! `wipe init` / `wipe serve` runs don't have to ask again.
8
9use std::path::PathBuf;
10
11use serde::{Deserialize, Serialize};
12
13use crate::model::{Exposure, Starter};
14
15/// Machine-wide user preferences. Every field is optional; an absent field means
16/// "use the built-in default".
17#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
18pub struct GlobalConfig {
19    /// Default daemon port for new boards.
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub default_port: Option<u16>,
22    /// Default exposure for new boards.
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub default_expose: Option<Exposure>,
25    /// Default: shut the daemon down when idle (no overhead when not viewed).
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub autoserve: Option<bool>,
28    /// Default idle timeout in seconds.
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub idle_timeout_secs: Option<u64>,
31    /// How much content a fresh board is seeded with.
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub starter: Option<Starter>,
34    /// Preferred agent-skill install convention: `claude` or `agents`.
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub skill_target: Option<String>,
37    /// Whether to install the skill user-globally (vs project-scoped) by default.
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub skill_global: Option<bool>,
40    /// Preferred UI accent color (token or hex), surfaced to the board UI.
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub ui_accent: Option<String>,
43    /// Preferred UI theme: `light`, `dark`, or `system`.
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub ui_theme: Option<String>,
46}
47
48impl GlobalConfig {
49    /// Path to `config.json`. Honors `$WIPE_CONFIG_DIR` (useful for isolating
50    /// tests and for pinning config in CI), else the user's platform config dir.
51    pub fn path() -> Option<PathBuf> {
52        if let Ok(dir) = std::env::var("WIPE_CONFIG_DIR") {
53            if !dir.trim().is_empty() {
54                return Some(PathBuf::from(dir).join("config.json"));
55            }
56        }
57        directories::ProjectDirs::from("dev", "wipe", "wipe")
58            .map(|d| d.config_dir().join("config.json"))
59    }
60
61    /// Load the config, returning defaults if the file is missing or unreadable.
62    pub fn load() -> Self {
63        Self::path()
64            .and_then(|p| std::fs::read(p).ok())
65            .and_then(|b| serde_json::from_slice(&b).ok())
66            .unwrap_or_default()
67    }
68
69    /// Persist the config (pretty JSON + trailing newline). Best-effort: creates
70    /// the config directory if needed.
71    pub fn save(&self) -> std::io::Result<()> {
72        if let Some(path) = Self::path() {
73            if let Some(dir) = path.parent() {
74                std::fs::create_dir_all(dir)?;
75            }
76            let mut s = serde_json::to_string_pretty(self).unwrap_or_default();
77            s.push('\n');
78            std::fs::write(path, s)?;
79        }
80        Ok(())
81    }
82}