bitbucket_cli/config/
settings.rs1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::PathBuf;
5
6const APP_NAME: &str = "bitbucket-cli";
7const CONFIG_FILE: &str = "config.toml";
8
9pub mod xdg {
27 use super::*;
28
29 pub fn config_dir() -> Result<PathBuf> {
33 #[cfg(unix)]
35 if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
36 if !xdg_config.is_empty() {
37 return Ok(PathBuf::from(xdg_config).join(APP_NAME));
38 }
39 }
40
41 dirs::config_dir()
42 .map(|p| p.join(APP_NAME))
43 .context("Could not determine config directory")
44 }
45
46 pub fn data_dir() -> Result<PathBuf> {
50 #[cfg(unix)]
51 if let Ok(xdg_data) = std::env::var("XDG_DATA_HOME") {
52 if !xdg_data.is_empty() {
53 return Ok(PathBuf::from(xdg_data).join(APP_NAME));
54 }
55 }
56
57 dirs::data_dir()
58 .map(|p| p.join(APP_NAME))
59 .context("Could not determine data directory")
60 }
61
62 pub fn cache_dir() -> Result<PathBuf> {
66 #[cfg(unix)]
67 if let Ok(xdg_cache) = std::env::var("XDG_CACHE_HOME") {
68 if !xdg_cache.is_empty() {
69 return Ok(PathBuf::from(xdg_cache).join(APP_NAME));
70 }
71 }
72
73 dirs::cache_dir()
74 .map(|p| p.join(APP_NAME))
75 .context("Could not determine cache directory")
76 }
77
78 pub fn state_dir() -> Result<PathBuf> {
83 #[cfg(unix)]
84 if let Ok(xdg_state) = std::env::var("XDG_STATE_HOME") {
85 if !xdg_state.is_empty() {
86 return Ok(PathBuf::from(xdg_state).join(APP_NAME));
87 }
88 }
89
90 dirs::state_dir()
92 .or_else(dirs::data_dir)
93 .map(|p| p.join(APP_NAME))
94 .context("Could not determine state directory")
95 }
96
97 pub fn ensure_dir(path: &PathBuf) -> Result<()> {
99 if !path.exists() {
100 fs::create_dir_all(path)
101 .with_context(|| format!("Failed to create directory: {:?}", path))?;
102 }
103 Ok(())
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, Default)]
108pub struct Config {
109 #[serde(default)]
110 pub auth: AuthConfig,
111 #[serde(default)]
112 pub defaults: DefaultsConfig,
113 #[serde(default)]
114 pub display: DisplayConfig,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, Default)]
118pub struct AuthConfig {
119 pub username: Option<String>,
120 pub default_workspace: Option<String>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct DefaultsConfig {
125 pub workspace: Option<String>,
126 pub repository: Option<String>,
127 pub branch: Option<String>,
128}
129
130impl Default for DefaultsConfig {
131 fn default() -> Self {
132 Self {
133 workspace: None,
134 repository: None,
135 branch: Some("main".to_string()),
136 }
137 }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct DisplayConfig {
142 pub color: bool,
143 pub pager: bool,
144 pub date_format: String,
145}
146
147impl Default for DisplayConfig {
148 fn default() -> Self {
149 Self {
150 color: true,
151 pager: true,
152 date_format: "%Y-%m-%d %H:%M".to_string(),
153 }
154 }
155}
156
157impl Config {
158 pub fn config_dir() -> Result<PathBuf> {
163 xdg::config_dir()
164 }
165
166 pub fn config_path() -> Result<PathBuf> {
168 Ok(Self::config_dir()?.join(CONFIG_FILE))
169 }
170
171 pub fn data_dir() -> Result<PathBuf> {
176 xdg::data_dir()
177 }
178
179 pub fn cache_dir() -> Result<PathBuf> {
184 xdg::cache_dir()
185 }
186
187 pub fn state_dir() -> Result<PathBuf> {
192 xdg::state_dir()
193 }
194
195 pub fn load() -> Result<Self> {
197 let config_path = Self::config_path()?;
198
199 if !config_path.exists() {
200 return Ok(Self::default());
201 }
202
203 let contents = fs::read_to_string(&config_path)
204 .with_context(|| format!("Failed to read config file: {:?}", config_path))?;
205
206 let config: Config = toml::from_str(&contents)
207 .with_context(|| format!("Failed to parse config file: {:?}", config_path))?;
208
209 Ok(config)
210 }
211
212 pub fn save(&self) -> Result<()> {
214 let config_dir = Self::config_dir()?;
215 let config_path = Self::config_path()?;
216
217 xdg::ensure_dir(&config_dir)?;
219
220 let contents = toml::to_string_pretty(self).context("Failed to serialize config")?;
221
222 fs::write(&config_path, contents)
223 .with_context(|| format!("Failed to write config file: {:?}", config_path))?;
224
225 Ok(())
226 }
227
228 pub fn set_username(&mut self, username: &str) {
230 self.auth.username = Some(username.to_string());
231 }
232
233 pub fn username(&self) -> Option<&str> {
235 self.auth.username.as_deref()
236 }
237
238 pub fn set_default_workspace(&mut self, workspace: &str) {
240 self.defaults.workspace = Some(workspace.to_string());
241 }
242
243 pub fn default_workspace(&self) -> Option<&str> {
245 self.defaults.workspace.as_deref()
246 }
247
248 pub fn clear_auth(&mut self) {
250 self.auth.username = None;
251 self.auth.default_workspace = None;
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn test_default_config() {
261 let config = Config::default();
262 assert!(config.auth.username.is_none());
263 assert!(config.display.color);
264 }
265
266 #[test]
267 fn test_config_serialization() {
268 let config = Config::default();
269 let serialized = toml::to_string(&config).unwrap();
270 let deserialized: Config = toml::from_str(&serialized).unwrap();
271 assert_eq!(config.display.color, deserialized.display.color);
272 }
273
274 #[test]
275 fn test_xdg_directories() {
276 let config_dir = xdg::config_dir().unwrap();
278 let data_dir = xdg::data_dir().unwrap();
279 let cache_dir = xdg::cache_dir().unwrap();
280 let state_dir = xdg::state_dir().unwrap();
281
282 assert!(config_dir.ends_with("bitbucket-cli"));
284 assert!(data_dir.ends_with("bitbucket-cli"));
285 assert!(cache_dir.ends_with("bitbucket-cli"));
286 assert!(state_dir.ends_with("bitbucket-cli"));
287 }
288
289 #[test]
290 #[cfg(unix)]
291 fn test_xdg_env_override() {
292 use std::env;
293
294 let orig_config = env::var("XDG_CONFIG_HOME").ok();
296
297 unsafe {
299 env::set_var("XDG_CONFIG_HOME", "/tmp/test-xdg-config");
301 }
302
303 let config_dir = xdg::config_dir().unwrap();
304 assert_eq!(
305 config_dir,
306 PathBuf::from("/tmp/test-xdg-config/bitbucket-cli")
307 );
308
309 unsafe {
311 match orig_config {
312 Some(val) => env::set_var("XDG_CONFIG_HOME", val),
313 None => env::remove_var("XDG_CONFIG_HOME"),
314 }
315 }
316 }
317}