Skip to main content

worktree_io/
config.rs

1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6#[serde(default)]
7pub struct Config {
8    pub editor: EditorConfig,
9    pub terminal: TerminalConfig,
10    pub open: OpenConfig,
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(default)]
15pub struct EditorConfig {
16    /// Command to launch the editor, e.g. "code ." or "nvim ."
17    pub command: Option<String>,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(default)]
22pub struct TerminalConfig {
23    /// Command to launch a terminal in the workspace dir; None uses platform default
24    pub command: Option<String>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(default)]
29pub struct OpenConfig {
30    pub editor: bool,
31    pub explorer: bool,
32    pub terminal: bool,
33}
34
35impl Default for Config {
36    fn default() -> Self {
37        Self {
38            editor: EditorConfig::default(),
39            terminal: TerminalConfig::default(),
40            open: OpenConfig::default(),
41        }
42    }
43}
44
45impl Default for EditorConfig {
46    fn default() -> Self {
47        Self { command: None }
48    }
49}
50
51impl Default for TerminalConfig {
52    fn default() -> Self {
53        Self { command: None }
54    }
55}
56
57impl Default for OpenConfig {
58    fn default() -> Self {
59        Self {
60            editor: false,
61            explorer: false,
62            terminal: true,
63        }
64    }
65}
66
67impl Config {
68    pub fn path() -> Result<PathBuf> {
69        let config_dir = dirs::config_dir()
70            .context("Could not determine config directory")?;
71        Ok(config_dir.join("worktree").join("config.toml"))
72    }
73
74    pub fn load() -> Result<Self> {
75        let path = Self::path()?;
76        if !path.exists() {
77            return Ok(Self::default());
78        }
79        let content = std::fs::read_to_string(&path)
80            .with_context(|| format!("Failed to read config from {}", path.display()))?;
81        let config: Self = toml::from_str(&content)
82            .with_context(|| format!("Failed to parse config at {}", path.display()))?;
83        Ok(config)
84    }
85
86    pub fn save(&self) -> Result<()> {
87        let path = Self::path()?;
88        if let Some(parent) = path.parent() {
89            std::fs::create_dir_all(parent)
90                .with_context(|| format!("Failed to create config dir {}", parent.display()))?;
91        }
92        let content = toml::to_string_pretty(self)
93            .context("Failed to serialize config")?;
94        std::fs::write(&path, content)
95            .with_context(|| format!("Failed to write config to {}", path.display()))?;
96        Ok(())
97    }
98
99    /// Get a config value by dot-separated key path
100    pub fn get_value(&self, key: &str) -> Result<String> {
101        match key {
102            "editor.command" => Ok(self.editor.command.clone().unwrap_or_default()),
103            "terminal.command" => Ok(self.terminal.command.clone().unwrap_or_default()),
104            "open.editor" => Ok(self.open.editor.to_string()),
105            "open.explorer" => Ok(self.open.explorer.to_string()),
106            "open.terminal" => Ok(self.open.terminal.to_string()),
107            _ => anyhow::bail!("Unknown config key: {key}"),
108        }
109    }
110
111    /// Set a config value by dot-separated key path
112    pub fn set_value(&mut self, key: &str, value: &str) -> Result<()> {
113        match key {
114            "editor.command" => {
115                self.editor.command = if value.is_empty() { None } else { Some(value.to_string()) };
116            }
117            "terminal.command" => {
118                self.terminal.command = if value.is_empty() { None } else { Some(value.to_string()) };
119            }
120            "open.editor" => {
121                self.open.editor = value.parse::<bool>()
122                    .with_context(|| format!("Invalid boolean value: {value}"))?;
123            }
124            "open.explorer" => {
125                self.open.explorer = value.parse::<bool>()
126                    .with_context(|| format!("Invalid boolean value: {value}"))?;
127            }
128            "open.terminal" => {
129                self.open.terminal = value.parse::<bool>()
130                    .with_context(|| format!("Invalid boolean value: {value}"))?;
131            }
132            _ => anyhow::bail!("Unknown config key: {key}"),
133        }
134        Ok(())
135    }
136}