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    /// Start the wipe UI daemon automatically at login (an always-on, lightweight
32    /// viewer). Backed by a per-OS login entry managed by the CLI.
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub autostart: Option<bool>,
35    /// How much content a fresh board is seeded with.
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub starter: Option<Starter>,
38    /// Preferred agent-skill install convention: `claude` or `agents`.
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub skill_target: Option<String>,
41    /// Whether to install the skill user-globally (vs project-scoped) by default.
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub skill_global: Option<bool>,
44    /// Preferred UI accent color (token or hex), surfaced to the board UI.
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub ui_accent: Option<String>,
47    /// Preferred UI theme: `light`, `dark`, or `system`.
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub ui_theme: Option<String>,
50}
51
52impl GlobalConfig {
53    /// Path to `config.json`. Honors `$WIPE_CONFIG_DIR` (useful for isolating
54    /// tests and for pinning config in CI), else the user's platform config dir.
55    pub fn path() -> Option<PathBuf> {
56        if let Ok(dir) = std::env::var("WIPE_CONFIG_DIR") {
57            if !dir.trim().is_empty() {
58                return Some(PathBuf::from(dir).join("config.json"));
59            }
60        }
61        directories::ProjectDirs::from("dev", "wipe", "wipe")
62            .map(|d| d.config_dir().join("config.json"))
63    }
64
65    /// Load the config, returning defaults if the file is missing or unreadable.
66    pub fn load() -> Self {
67        Self::path()
68            .and_then(|p| std::fs::read(p).ok())
69            .and_then(|b| serde_json::from_slice(&b).ok())
70            .unwrap_or_default()
71    }
72
73    /// Persist the config (pretty JSON + trailing newline). Best-effort: creates
74    /// the config directory if needed.
75    pub fn save(&self) -> std::io::Result<()> {
76        if let Some(path) = Self::path() {
77            if let Some(dir) = path.parent() {
78                std::fs::create_dir_all(dir)?;
79            }
80            let mut s = serde_json::to_string_pretty(self).unwrap_or_default();
81            s.push('\n');
82            std::fs::write(path, s)?;
83        }
84        Ok(())
85    }
86}