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