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 pub command: Option<String>,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(default)]
22pub struct TerminalConfig {
23 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 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 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}