use std::path::{Path, PathBuf};
use std::fs;
use serde::{Deserialize, Serialize};
use crate::error::{Result, ToriiError};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ToriiConfig {
pub user: UserConfig,
pub snapshot: SnapshotConfig,
pub mirror: MirrorConfig,
pub git: GitConfig,
pub ui: UiConfig,
#[serde(default)]
pub auth: AuthConfig,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct AuthConfig {
pub github_token: Option<String>,
pub gitlab_token: Option<String>,
pub gitea_token: Option<String>,
pub forgejo_token: Option<String>,
pub codeberg_token: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UserConfig {
pub name: Option<String>,
pub email: Option<String>,
pub editor: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SnapshotConfig {
pub auto_enabled: bool,
pub auto_interval_minutes: u32,
pub retention_days: u32,
pub max_snapshots: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct MirrorConfig {
pub autofetch_enabled: bool,
pub autofetch_interval_minutes: u32,
pub default_protocol: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GitConfig {
pub default_branch: String,
pub sign_commits: bool,
pub gpg_key: Option<String>,
pub pull_rebase: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UiConfig {
pub colors: bool,
pub emoji: bool,
pub verbose: bool,
pub date_format: String,
}
impl Default for ToriiConfig {
fn default() -> Self {
Self {
user: UserConfig {
name: None,
email: None,
editor: std::env::var("EDITOR").ok(),
},
snapshot: SnapshotConfig {
auto_enabled: false,
auto_interval_minutes: 30,
retention_days: 30,
max_snapshots: Some(100),
},
mirror: MirrorConfig {
autofetch_enabled: false,
autofetch_interval_minutes: 30,
default_protocol: "ssh".to_string(),
},
git: GitConfig {
default_branch: "main".to_string(),
sign_commits: false,
gpg_key: None,
pull_rebase: false,
},
ui: UiConfig {
colors: true,
emoji: true,
verbose: false,
date_format: "%Y-%m-%d %H:%M".to_string(),
},
auth: AuthConfig::default(),
}
}
}
impl ToriiConfig {
fn global_config_path() -> Result<PathBuf> {
let config_dir = dirs::config_dir()
.ok_or_else(|| ToriiError::InvalidConfig("Could not determine config directory for this platform".to_string()))?
.join("torii");
fs::create_dir_all(&config_dir)?;
Ok(config_dir.join("config.toml"))
}
fn local_config_path<P: AsRef<Path>>(repo_path: P) -> Result<PathBuf> {
let torii_dir = repo_path.as_ref().join(".torii");
fs::create_dir_all(&torii_dir)?;
Ok(torii_dir.join("config.toml"))
}
pub fn load_global() -> Result<Self> {
let config_path = Self::global_config_path()?;
if !config_path.exists() {
return Ok(Self::default());
}
let config_str = fs::read_to_string(&config_path)?;
let config: ToriiConfig = toml::from_str(&config_str)
.map_err(|e| ToriiError::InvalidConfig(format!("Failed to parse config: {}", e)))?;
Ok(config)
}
pub fn load_local<P: AsRef<Path>>(repo_path: P) -> Result<Self> {
let mut config = Self::load_global()?;
let local_path = Self::local_config_path(&repo_path)?;
if local_path.exists() {
let local_str = fs::read_to_string(&local_path)?;
let local_config: ToriiConfig = toml::from_str(&local_str)
.map_err(|e| ToriiError::InvalidConfig(format!("Failed to parse local config: {}", e)))?;
config = Self::merge(config, local_config);
}
Ok(config)
}
pub fn save_global(&self) -> Result<()> {
let config_path = Self::global_config_path()?;
let config_str = toml::to_string_pretty(self)
.map_err(|e| ToriiError::InvalidConfig(format!("Failed to serialize config: {}", e)))?;
fs::write(&config_path, config_str)?;
Ok(())
}
pub fn save_local<P: AsRef<Path>>(&self, repo_path: P) -> Result<()> {
let config_path = Self::local_config_path(repo_path)?;
let config_str = toml::to_string_pretty(self)
.map_err(|e| ToriiError::InvalidConfig(format!("Failed to serialize config: {}", e)))?;
fs::write(&config_path, config_str)?;
Ok(())
}
fn merge(mut base: Self, overlay: Self) -> Self {
if overlay.user.name.is_some() {
base.user.name = overlay.user.name;
}
if overlay.user.email.is_some() {
base.user.email = overlay.user.email;
}
if overlay.user.editor.is_some() {
base.user.editor = overlay.user.editor;
}
base.snapshot = overlay.snapshot;
base.mirror = overlay.mirror;
base.git = overlay.git;
base.ui = overlay.ui;
if overlay.auth.github_token.is_some() { base.auth.github_token = overlay.auth.github_token; }
if overlay.auth.gitlab_token.is_some() { base.auth.gitlab_token = overlay.auth.gitlab_token; }
if overlay.auth.gitea_token.is_some() { base.auth.gitea_token = overlay.auth.gitea_token; }
if overlay.auth.forgejo_token.is_some() { base.auth.forgejo_token = overlay.auth.forgejo_token; }
if overlay.auth.codeberg_token.is_some() { base.auth.codeberg_token = overlay.auth.codeberg_token; }
base
}
pub fn get(&self, key: &str) -> Option<String> {
let parts: Vec<&str> = key.split('.').collect();
if parts.len() != 2 {
return None;
}
match (parts[0], parts[1]) {
("user", "name") => self.user.name.clone(),
("user", "email") => self.user.email.clone(),
("user", "editor") => self.user.editor.clone(),
("snapshot", "auto_enabled") => Some(self.snapshot.auto_enabled.to_string()),
("snapshot", "auto_interval_minutes") => Some(self.snapshot.auto_interval_minutes.to_string()),
("snapshot", "retention_days") => Some(self.snapshot.retention_days.to_string()),
("snapshot", "max_snapshots") => self.snapshot.max_snapshots.map(|v| v.to_string()),
("mirror", "autofetch_enabled") => Some(self.mirror.autofetch_enabled.to_string()),
("mirror", "autofetch_interval_minutes") => Some(self.mirror.autofetch_interval_minutes.to_string()),
("mirror", "default_protocol") => Some(self.mirror.default_protocol.clone()),
("git", "default_branch") => Some(self.git.default_branch.clone()),
("git", "sign_commits") => Some(self.git.sign_commits.to_string()),
("git", "gpg_key") => self.git.gpg_key.clone(),
("git", "pull_rebase") => Some(self.git.pull_rebase.to_string()),
("ui", "colors") => Some(self.ui.colors.to_string()),
("ui", "emoji") => Some(self.ui.emoji.to_string()),
("ui", "verbose") => Some(self.ui.verbose.to_string()),
("ui", "date_format") => Some(self.ui.date_format.clone()),
("auth", "github_token") => self.auth.github_token.clone().map(|_| "[set]".to_string()),
("auth", "gitlab_token") => self.auth.gitlab_token.clone().map(|_| "[set]".to_string()),
("auth", "gitea_token") => self.auth.gitea_token.clone().map(|_| "[set]".to_string()),
("auth", "forgejo_token") => self.auth.forgejo_token.clone().map(|_| "[set]".to_string()),
("auth", "codeberg_token") => self.auth.codeberg_token.clone().map(|_| "[set]".to_string()),
_ => None,
}
}
pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
let parts: Vec<&str> = key.split('.').collect();
if parts.len() != 2 {
return Err(ToriiError::InvalidConfig(format!("Invalid config key: {}", key)));
}
match (parts[0], parts[1]) {
("user", "name") => self.user.name = Some(value.to_string()),
("user", "email") => self.user.email = Some(value.to_string()),
("user", "editor") => self.user.editor = Some(value.to_string()),
("snapshot", "auto_enabled") => {
self.snapshot.auto_enabled = value.parse()
.map_err(|_| ToriiError::InvalidConfig("Value must be true or false".to_string()))?;
}
("snapshot", "auto_interval_minutes") => {
self.snapshot.auto_interval_minutes = value.parse()
.map_err(|_| ToriiError::InvalidConfig("Value must be a number".to_string()))?;
}
("snapshot", "retention_days") => {
self.snapshot.retention_days = value.parse()
.map_err(|_| ToriiError::InvalidConfig("Value must be a number".to_string()))?;
}
("snapshot", "max_snapshots") => {
self.snapshot.max_snapshots = Some(value.parse()
.map_err(|_| ToriiError::InvalidConfig("Value must be a number".to_string()))?);
}
("mirror", "autofetch_enabled") => {
self.mirror.autofetch_enabled = value.parse()
.map_err(|_| ToriiError::InvalidConfig("Value must be true or false".to_string()))?;
}
("mirror", "autofetch_interval_minutes") => {
self.mirror.autofetch_interval_minutes = value.parse()
.map_err(|_| ToriiError::InvalidConfig("Value must be a number".to_string()))?;
}
("mirror", "default_protocol") => {
if value != "ssh" && value != "https" {
return Err(ToriiError::InvalidConfig("Protocol must be 'ssh' or 'https'".to_string()));
}
self.mirror.default_protocol = value.to_string();
}
("git", "default_branch") => self.git.default_branch = value.to_string(),
("git", "sign_commits") => {
self.git.sign_commits = value.parse()
.map_err(|_| ToriiError::InvalidConfig("Value must be true or false".to_string()))?;
}
("git", "gpg_key") => self.git.gpg_key = Some(value.to_string()),
("git", "pull_rebase") => {
self.git.pull_rebase = value.parse()
.map_err(|_| ToriiError::InvalidConfig("Value must be true or false".to_string()))?;
}
("ui", "colors") => {
self.ui.colors = value.parse()
.map_err(|_| ToriiError::InvalidConfig("Value must be true or false".to_string()))?;
}
("ui", "emoji") => {
self.ui.emoji = value.parse()
.map_err(|_| ToriiError::InvalidConfig("Value must be true or false".to_string()))?;
}
("ui", "verbose") => {
self.ui.verbose = value.parse()
.map_err(|_| ToriiError::InvalidConfig("Value must be true or false".to_string()))?;
}
("ui", "date_format") => self.ui.date_format = value.to_string(),
("auth", "github_token") => self.auth.github_token = Some(value.to_string()),
("auth", "gitlab_token") => self.auth.gitlab_token = Some(value.to_string()),
("auth", "gitea_token") => self.auth.gitea_token = Some(value.to_string()),
("auth", "forgejo_token") => self.auth.forgejo_token = Some(value.to_string()),
("auth", "codeberg_token") => self.auth.codeberg_token = Some(value.to_string()),
_ => return Err(ToriiError::InvalidConfig(format!("Unknown config key: {}", key))),
}
Ok(())
}
pub fn list(&self) -> Vec<(String, String)> {
let mut items = Vec::new();
if let Some(name) = &self.user.name {
items.push(("user.name".to_string(), name.clone()));
}
if let Some(email) = &self.user.email {
items.push(("user.email".to_string(), email.clone()));
}
if let Some(editor) = &self.user.editor {
items.push(("user.editor".to_string(), editor.clone()));
}
items.push(("snapshot.auto_enabled".to_string(), self.snapshot.auto_enabled.to_string()));
items.push(("snapshot.auto_interval_minutes".to_string(), self.snapshot.auto_interval_minutes.to_string()));
items.push(("snapshot.retention_days".to_string(), self.snapshot.retention_days.to_string()));
if let Some(max) = self.snapshot.max_snapshots {
items.push(("snapshot.max_snapshots".to_string(), max.to_string()));
}
items.push(("mirror.autofetch_enabled".to_string(), self.mirror.autofetch_enabled.to_string()));
items.push(("mirror.autofetch_interval_minutes".to_string(), self.mirror.autofetch_interval_minutes.to_string()));
items.push(("mirror.default_protocol".to_string(), self.mirror.default_protocol.clone()));
items.push(("git.default_branch".to_string(), self.git.default_branch.clone()));
items.push(("git.sign_commits".to_string(), self.git.sign_commits.to_string()));
if let Some(key) = &self.git.gpg_key {
items.push(("git.gpg_key".to_string(), key.clone()));
}
items.push(("git.pull_rebase".to_string(), self.git.pull_rebase.to_string()));
items.push(("ui.colors".to_string(), self.ui.colors.to_string()));
items.push(("ui.emoji".to_string(), self.ui.emoji.to_string()));
items.push(("ui.verbose".to_string(), self.ui.verbose.to_string()));
items.push(("ui.date_format".to_string(), self.ui.date_format.clone()));
if self.auth.github_token.is_some() { items.push(("auth.github_token".to_string(), "[set]".to_string())); }
if self.auth.gitlab_token.is_some() { items.push(("auth.gitlab_token".to_string(), "[set]".to_string())); }
if self.auth.gitea_token.is_some() { items.push(("auth.gitea_token".to_string(), "[set]".to_string())); }
if self.auth.forgejo_token.is_some() { items.push(("auth.forgejo_token".to_string(), "[set]".to_string())); }
if self.auth.codeberg_token.is_some() { items.push(("auth.codeberg_token".to_string(), "[set]".to_string())); }
items
}
}