use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
#[serde(default)]
pub general: GeneralConfig,
#[serde(default)]
pub theme: ThemeConfig,
#[serde(default)]
pub ai: AIConfig,
#[serde(default)]
pub telemetry: TelemetryConfig,
#[serde(default)]
pub shell: ShellConfig,
#[serde(default)]
pub keybindings: KeybindingsConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneralConfig {
#[serde(default)]
pub user_name: Option<String>,
#[serde(default = "default_false")]
pub setup_complete: bool,
#[serde(default = "default_shell")]
pub shell: String,
#[serde(default = "default_history_limit")]
pub history_limit: usize,
#[serde(default = "default_command_timeout")]
pub command_timeout: u64,
#[serde(default = "default_true")]
pub auto_save: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThemeConfig {
#[serde(default = "default_theme")]
pub default_theme: String,
#[serde(default = "default_true")]
pub enable_colors: bool,
#[serde(default = "default_color_depth")]
pub color_depth: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AIConfig {
#[serde(default = "default_false")]
pub enabled: bool,
#[serde(default = "default_ai_provider")]
pub provider: String,
#[serde(default)]
pub api_key: Option<String>,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub endpoint: Option<String>,
#[serde(default = "default_max_tokens")]
pub max_tokens: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelemetryConfig {
#[serde(default = "default_false")]
pub enabled: bool,
#[serde(default)]
pub user_id: Option<String>,
#[serde(default = "default_false")]
pub usage_stats: bool,
#[serde(default = "default_false")]
pub error_reports: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShellConfig {
#[serde(default)]
pub environment: HashMap<String, String>,
#[serde(default)]
pub aliases: HashMap<String, String>,
#[serde(default)]
pub startup_commands: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeybindingsConfig {
#[serde(default)]
pub custom: HashMap<String, String>,
}
impl Config {
pub fn new() -> Self {
Self::default()
}
pub fn load() -> Result<Self> {
let config_path = get_config_file_path()?;
if !config_path.exists() {
let config = Self::default();
config.save()?;
return Ok(config);
}
let config_str = fs::read_to_string(&config_path)
.with_context(|| format!("Failed to read config file: {}", config_path.display()))?;
let mut config: Config = toml::from_str(&config_str)
.with_context(|| format!("Failed to parse config file: {}", config_path.display()))?;
config.apply_env_overrides();
Ok(config)
}
pub fn save(&self) -> Result<()> {
let config_path = get_config_file_path()?;
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create config directory: {}", parent.display()))?;
}
let config_str = toml::to_string_pretty(self)
.context("Failed to serialize config")?;
fs::write(&config_path, config_str)
.with_context(|| format!("Failed to write config file: {}", config_path.display()))?;
Ok(())
}
fn apply_env_overrides(&mut self) {
if let Ok(api_key) = std::env::var("ARCT_AI_API_KEY") {
self.ai.api_key = Some(api_key);
}
if let Ok(provider) = std::env::var("ARCT_AI_PROVIDER") {
self.ai.provider = provider;
}
if let Ok(telemetry) = std::env::var("ARCT_TELEMETRY") {
self.telemetry.enabled = telemetry == "1" || telemetry.to_lowercase() == "true";
}
if let Ok(shell) = std::env::var("ARCT_SHELL") {
self.general.shell = shell;
}
}
pub fn config_path() -> Result<PathBuf> {
get_config_file_path()
}
}
impl Default for Config {
fn default() -> Self {
Self {
general: GeneralConfig::default(),
theme: ThemeConfig::default(),
ai: AIConfig::default(),
telemetry: TelemetryConfig::default(),
shell: ShellConfig::default(),
keybindings: KeybindingsConfig::default(),
}
}
}
impl Default for GeneralConfig {
fn default() -> Self {
Self {
user_name: None,
setup_complete: false,
shell: default_shell(),
history_limit: default_history_limit(),
command_timeout: default_command_timeout(),
auto_save: true,
}
}
}
impl Default for ThemeConfig {
fn default() -> Self {
Self {
default_theme: default_theme(),
enable_colors: true,
color_depth: default_color_depth(),
}
}
}
impl Default for AIConfig {
fn default() -> Self {
Self {
enabled: false,
provider: default_ai_provider(),
api_key: None,
model: None,
endpoint: None,
max_tokens: default_max_tokens(),
}
}
}
impl Default for TelemetryConfig {
fn default() -> Self {
Self {
enabled: false,
user_id: None,
usage_stats: false,
error_reports: false,
}
}
}
impl Default for ShellConfig {
fn default() -> Self {
Self {
environment: HashMap::new(),
aliases: HashMap::new(),
startup_commands: Vec::new(),
}
}
}
impl Default for KeybindingsConfig {
fn default() -> Self {
Self {
custom: HashMap::new(),
}
}
}
pub fn get_config_file_path() -> Result<PathBuf> {
let config_dir = dirs::config_dir()
.context("Could not find config directory")?;
let arct_config_dir = config_dir.join("arct");
if !arct_config_dir.exists() {
fs::create_dir_all(&arct_config_dir)
.with_context(|| format!("Failed to create config directory: {}", arct_config_dir.display()))?;
}
Ok(arct_config_dir.join("config.toml"))
}
pub fn generate_default_config() -> String {
let config = Config::default();
toml::to_string_pretty(&config).unwrap_or_else(|_| String::from("# Failed to generate config"))
}
fn default_shell() -> String {
std::env::var("SHELL")
.unwrap_or_else(|_| "bash".to_string())
.split('/')
.last()
.unwrap_or("bash")
.to_string()
}
fn default_history_limit() -> usize {
1000
}
fn default_command_timeout() -> u64 {
5
}
fn default_theme() -> String {
"Arc Academy Orange".to_string()
}
fn default_color_depth() -> String {
"256".to_string()
}
fn default_ai_provider() -> String {
"anthropic".to_string()
}
fn default_max_tokens() -> usize {
4096
}
fn default_true() -> bool {
true
}
fn default_false() -> bool {
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
assert_eq!(config.general.history_limit, 1000);
assert_eq!(config.theme.default_theme, "Arc Academy Orange");
assert!(!config.ai.enabled);
assert!(!config.telemetry.enabled);
}
#[test]
fn test_serialize_config() {
let config = Config::default();
let toml_str = toml::to_string(&config).unwrap();
assert!(toml_str.contains("[general]"));
assert!(toml_str.contains("[theme]"));
}
#[test]
fn test_deserialize_config() {
let toml_str = r#"
[general]
shell = "zsh"
history_limit = 500
[theme]
default_theme = "Arc Dark"
"#;
let config: Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.general.shell, "zsh");
assert_eq!(config.general.history_limit, 500);
assert_eq!(config.theme.default_theme, "Arc Dark");
}
}