use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
const APP_NAME: &str = "bitbucket-cli";
const CONFIG_FILE: &str = "config.toml";
pub mod xdg {
use super::*;
pub fn config_dir() -> Result<PathBuf> {
#[cfg(unix)]
if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
if !xdg_config.is_empty() {
return Ok(PathBuf::from(xdg_config).join(APP_NAME));
}
}
dirs::config_dir()
.map(|p| p.join(APP_NAME))
.context("Could not determine config directory")
}
pub fn data_dir() -> Result<PathBuf> {
#[cfg(unix)]
if let Ok(xdg_data) = std::env::var("XDG_DATA_HOME") {
if !xdg_data.is_empty() {
return Ok(PathBuf::from(xdg_data).join(APP_NAME));
}
}
dirs::data_dir()
.map(|p| p.join(APP_NAME))
.context("Could not determine data directory")
}
pub fn cache_dir() -> Result<PathBuf> {
#[cfg(unix)]
if let Ok(xdg_cache) = std::env::var("XDG_CACHE_HOME") {
if !xdg_cache.is_empty() {
return Ok(PathBuf::from(xdg_cache).join(APP_NAME));
}
}
dirs::cache_dir()
.map(|p| p.join(APP_NAME))
.context("Could not determine cache directory")
}
pub fn state_dir() -> Result<PathBuf> {
#[cfg(unix)]
if let Ok(xdg_state) = std::env::var("XDG_STATE_HOME") {
if !xdg_state.is_empty() {
return Ok(PathBuf::from(xdg_state).join(APP_NAME));
}
}
dirs::state_dir()
.or_else(dirs::data_dir)
.map(|p| p.join(APP_NAME))
.context("Could not determine state directory")
}
pub fn ensure_dir(path: &PathBuf) -> Result<()> {
if !path.exists() {
fs::create_dir_all(path)
.with_context(|| format!("Failed to create directory: {:?}", path))?;
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
#[serde(default)]
pub auth: AuthConfig,
#[serde(default)]
pub defaults: DefaultsConfig,
#[serde(default)]
pub display: DisplayConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AuthConfig {
pub username: Option<String>,
pub default_workspace: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DefaultsConfig {
pub workspace: Option<String>,
pub repository: Option<String>,
pub branch: Option<String>,
}
impl Default for DefaultsConfig {
fn default() -> Self {
Self {
workspace: None,
repository: None,
branch: Some("main".to_string()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisplayConfig {
pub color: bool,
pub pager: bool,
pub date_format: String,
}
impl Default for DisplayConfig {
fn default() -> Self {
Self {
color: true,
pager: true,
date_format: "%Y-%m-%d %H:%M".to_string(),
}
}
}
impl Config {
pub fn config_dir() -> Result<PathBuf> {
xdg::config_dir()
}
pub fn config_path() -> Result<PathBuf> {
Ok(Self::config_dir()?.join(CONFIG_FILE))
}
pub fn data_dir() -> Result<PathBuf> {
xdg::data_dir()
}
pub fn cache_dir() -> Result<PathBuf> {
xdg::cache_dir()
}
pub fn state_dir() -> Result<PathBuf> {
xdg::state_dir()
}
pub fn load() -> Result<Self> {
let config_path = Self::config_path()?;
if !config_path.exists() {
return Ok(Self::default());
}
let contents = fs::read_to_string(&config_path)
.with_context(|| format!("Failed to read config file: {:?}", config_path))?;
let config: Config = toml::from_str(&contents)
.with_context(|| format!("Failed to parse config file: {:?}", config_path))?;
Ok(config)
}
pub fn save(&self) -> Result<()> {
let config_dir = Self::config_dir()?;
let config_path = Self::config_path()?;
xdg::ensure_dir(&config_dir)?;
let contents = toml::to_string_pretty(self).context("Failed to serialize config")?;
fs::write(&config_path, contents)
.with_context(|| format!("Failed to write config file: {:?}", config_path))?;
Ok(())
}
pub fn set_username(&mut self, username: &str) {
self.auth.username = Some(username.to_string());
}
pub fn username(&self) -> Option<&str> {
self.auth.username.as_deref()
}
pub fn set_default_workspace(&mut self, workspace: &str) {
self.defaults.workspace = Some(workspace.to_string());
}
pub fn default_workspace(&self) -> Option<&str> {
self.defaults.workspace.as_deref()
}
pub fn clear_auth(&mut self) {
self.auth.username = None;
self.auth.default_workspace = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
assert!(config.auth.username.is_none());
assert!(config.display.color);
}
#[test]
fn test_config_serialization() {
let config = Config::default();
let serialized = toml::to_string(&config).unwrap();
let deserialized: Config = toml::from_str(&serialized).unwrap();
assert_eq!(config.display.color, deserialized.display.color);
}
#[test]
fn test_xdg_directories() {
let config_dir = xdg::config_dir().unwrap();
let data_dir = xdg::data_dir().unwrap();
let cache_dir = xdg::cache_dir().unwrap();
let state_dir = xdg::state_dir().unwrap();
assert!(config_dir.ends_with("bitbucket-cli"));
assert!(data_dir.ends_with("bitbucket-cli"));
assert!(cache_dir.ends_with("bitbucket-cli"));
assert!(state_dir.ends_with("bitbucket-cli"));
}
#[test]
#[cfg(unix)]
fn test_xdg_env_override() {
use std::env;
let orig_config = env::var("XDG_CONFIG_HOME").ok();
unsafe {
env::set_var("XDG_CONFIG_HOME", "/tmp/test-xdg-config");
}
let config_dir = xdg::config_dir().unwrap();
assert_eq!(
config_dir,
PathBuf::from("/tmp/test-xdg-config/bitbucket-cli")
);
unsafe {
match orig_config {
Some(val) => env::set_var("XDG_CONFIG_HOME", val),
None => env::remove_var("XDG_CONFIG_HOME"),
}
}
}
}