use crate::constants::DEFAULT_OLLAMA_PORT;
use crate::prompts;
use anyhow::{Context, Result};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
#[serde(default)]
pub last_used_model: Option<String>,
#[serde(default)]
pub default_model: ModelSettings,
#[serde(default)]
pub ollama: OllamaConfig,
#[serde(default)]
pub openai: OpenAIConfig,
#[serde(default)]
pub anthropic: AnthropicConfig,
#[serde(default)]
pub ui: UIConfig,
#[serde(default)]
pub context: ContextConfig,
#[serde(default)]
pub mode: ModeConfig,
#[serde(default)]
pub behavior: BehaviorConfig,
#[serde(default)]
pub non_interactive: NonInteractiveConfig,
}
impl Default for Config {
fn default() -> Self {
Self {
last_used_model: None,
default_model: ModelSettings::default(),
ollama: OllamaConfig::default(),
openai: OpenAIConfig::default(),
anthropic: AnthropicConfig::default(),
ui: UIConfig::default(),
context: ContextConfig::default(),
mode: ModeConfig::default(),
behavior: BehaviorConfig::default(),
non_interactive: NonInteractiveConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ModelSettings {
pub provider: String,
pub name: String,
pub temperature: f32,
pub max_tokens: usize,
pub system_prompt: Option<String>,
}
impl ModelSettings {
pub fn default_system_prompt() -> String {
prompts::get_system_prompt()
}
}
impl Default for ModelSettings {
fn default() -> Self {
Self {
provider: String::new(),
name: String::new(),
temperature: 0.7,
max_tokens: 4096,
system_prompt: Some(Self::default_system_prompt()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct OllamaConfig {
pub host: String,
pub port: u16,
pub cloud_api_key: Option<String>,
pub num_gpu: Option<i32>,
pub num_thread: Option<i32>,
pub num_ctx: Option<i32>,
pub numa: Option<bool>,
}
impl Default for OllamaConfig {
fn default() -> Self {
Self {
host: String::from("localhost"),
port: DEFAULT_OLLAMA_PORT,
cloud_api_key: None,
num_gpu: None, num_thread: None, num_ctx: None, numa: None, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct OpenAIConfig {
pub api_key_env: String,
pub organization: Option<String>,
}
impl Default for OpenAIConfig {
fn default() -> Self {
Self {
api_key_env: String::from("OPENAI_API_KEY"),
organization: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct AnthropicConfig {
pub api_key_env: String,
}
impl Default for AnthropicConfig {
fn default() -> Self {
Self {
api_key_env: String::from("ANTHROPIC_API_KEY"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct UIConfig {
pub theme: String,
pub syntax_theme: String,
pub show_line_numbers: bool,
pub show_sidebar: bool,
}
impl Default for UIConfig {
fn default() -> Self {
Self {
theme: String::from("dark"),
syntax_theme: String::from("monokai"),
show_line_numbers: true,
show_sidebar: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ContextConfig {
pub max_file_size: usize,
pub max_files: usize,
pub max_context_tokens: usize,
pub include_patterns: Vec<String>,
pub exclude_patterns: Vec<String>,
}
impl Default for ContextConfig {
fn default() -> Self {
Self {
max_file_size: 1024 * 1024, max_files: 100,
max_context_tokens: 50000,
include_patterns: vec![],
exclude_patterns: vec![String::from("*.log"), String::from("*.tmp")],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ModeConfig {
pub default_mode: String,
pub remember_mode: bool,
pub auto_commit_on_accept: bool,
pub require_destructive_confirmation: bool,
}
impl Default for ModeConfig {
fn default() -> Self {
Self {
default_mode: String::from("normal"),
remember_mode: false,
auto_commit_on_accept: false,
require_destructive_confirmation: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct BehaviorConfig {
pub auto_install_models: bool,
pub backend: String,
}
impl Default for BehaviorConfig {
fn default() -> Self {
Self {
auto_install_models: true,
backend: String::from("auto"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct NonInteractiveConfig {
pub output_format: String,
pub max_tokens: usize,
pub no_execute: bool,
}
impl Default for NonInteractiveConfig {
fn default() -> Self {
Self {
output_format: String::from("text"),
max_tokens: 4096,
no_execute: false,
}
}
}
pub fn load_config() -> Result<Config> {
let config_path = get_config_path()?;
if config_path.exists() {
let toml_str = std::fs::read_to_string(&config_path)
.with_context(|| format!("Failed to read {}", config_path.display()))?;
let config: Config = toml::from_str(&toml_str)
.with_context(|| format!("Failed to parse {}. Run 'mermaid init' to regenerate.", config_path.display()))?;
Ok(config)
} else {
Ok(Config::default())
}
}
pub fn get_config_path() -> Result<PathBuf> {
Ok(get_config_dir()?.join("config.toml"))
}
pub fn get_config_dir() -> Result<PathBuf> {
if let Some(proj_dirs) = ProjectDirs::from("", "", "mermaid") {
let config_dir = proj_dirs.config_dir();
std::fs::create_dir_all(config_dir)?;
Ok(config_dir.to_path_buf())
} else {
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.context("Could not determine home directory")?;
let config_dir = PathBuf::from(home).join(".config").join("mermaid");
std::fs::create_dir_all(&config_dir)?;
Ok(config_dir)
}
}
pub fn save_config(config: &Config, path: Option<PathBuf>) -> Result<()> {
let path = if let Some(p) = path {
p
} else {
get_config_dir()?.join("config.toml")
};
let toml_string = toml::to_string_pretty(config)?;
std::fs::write(&path, toml_string)
.with_context(|| format!("Failed to write config to {}", path.display()))?;
Ok(())
}
pub fn init_config() -> Result<()> {
let config_file = get_config_path()?;
if config_file.exists() {
println!("Configuration already exists at: {}", config_file.display());
} else {
let default_config = Config::default();
save_config(&default_config, Some(config_file.clone()))?;
println!("Created configuration at: {}", config_file.display());
}
Ok(())
}
pub fn persist_last_model(model: &str) -> Result<()> {
let mut config = load_config().unwrap_or_default();
config.last_used_model = Some(model.to_string());
save_config(&config, None)
}