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}