mermaid_cli/app/
config.rs1use crate::constants::{DEFAULT_MAX_TOKENS, DEFAULT_OLLAMA_PORT, DEFAULT_TEMPERATURE};
2use anyhow::{Context, Result};
3use directories::ProjectDirs;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::PathBuf;
7
8#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10pub struct Config {
11 #[serde(default)]
13 pub last_used_model: Option<String>,
14
15 #[serde(default)]
17 pub default_model: ModelSettings,
18
19 #[serde(default)]
21 pub ollama: OllamaConfig,
22
23 #[serde(default)]
25 pub non_interactive: NonInteractiveConfig,
26
27 #[serde(default)]
29 pub mcp_servers: HashMap<String, McpServerConfig>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct McpServerConfig {
35 pub command: String,
37 #[serde(default)]
39 pub args: Vec<String>,
40 #[serde(default)]
42 pub env: HashMap<String, String>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47#[serde(default)]
48pub struct ModelSettings {
49 pub provider: String,
51 pub name: String,
53 pub temperature: f32,
55 pub max_tokens: usize,
57}
58
59impl Default for ModelSettings {
60 fn default() -> Self {
61 Self {
62 provider: String::new(),
63 name: String::new(),
64 temperature: DEFAULT_TEMPERATURE,
65 max_tokens: DEFAULT_MAX_TOKENS,
66 }
67 }
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72#[serde(default)]
73pub struct OllamaConfig {
74 pub host: String,
76 pub port: u16,
78 pub cloud_api_key: Option<String>,
82 pub num_gpu: Option<i32>,
85 pub num_thread: Option<i32>,
88 pub num_ctx: Option<i32>,
91 pub numa: Option<bool>,
93}
94
95impl Default for OllamaConfig {
96 fn default() -> Self {
97 Self {
98 host: String::from("localhost"),
99 port: DEFAULT_OLLAMA_PORT,
100 cloud_api_key: None,
101 num_gpu: None, num_thread: None, num_ctx: None, numa: None, }
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111#[serde(default)]
112pub struct NonInteractiveConfig {
113 pub output_format: String,
115 pub max_tokens: usize,
117 pub no_execute: bool,
119}
120
121impl Default for NonInteractiveConfig {
122 fn default() -> Self {
123 Self {
124 output_format: String::from("text"),
125 max_tokens: DEFAULT_MAX_TOKENS,
126 no_execute: false,
127 }
128 }
129}
130
131pub fn load_config() -> Result<Config> {
134 let config_path = get_config_path()?;
135
136 if config_path.exists() {
137 let toml_str = std::fs::read_to_string(&config_path)
138 .with_context(|| format!("Failed to read {}", config_path.display()))?;
139 let config: Config = toml::from_str(&toml_str).with_context(|| {
140 format!(
141 "Failed to parse {}. Run 'mermaid init' to regenerate.",
142 config_path.display()
143 )
144 })?;
145 Ok(config)
146 } else {
147 Ok(Config::default())
148 }
149}
150
151pub fn get_config_path() -> Result<PathBuf> {
153 Ok(get_config_dir()?.join("config.toml"))
154}
155
156pub fn get_config_dir() -> Result<PathBuf> {
158 if let Some(proj_dirs) = ProjectDirs::from("", "", "mermaid") {
159 let config_dir = proj_dirs.config_dir();
160 std::fs::create_dir_all(config_dir)?;
161 Ok(config_dir.to_path_buf())
162 } else {
163 let home = std::env::var("HOME")
165 .or_else(|_| std::env::var("USERPROFILE"))
166 .context("Could not determine home directory")?;
167 let config_dir = PathBuf::from(home).join(".config").join("mermaid");
168 std::fs::create_dir_all(&config_dir)?;
169 Ok(config_dir)
170 }
171}
172
173pub fn save_config(config: &Config, path: Option<PathBuf>) -> Result<()> {
175 let path = if let Some(p) = path {
176 p
177 } else {
178 get_config_dir()?.join("config.toml")
179 };
180
181 let toml_string = toml::to_string_pretty(config)?;
182 std::fs::write(&path, toml_string)
183 .with_context(|| format!("Failed to write config to {}", path.display()))?;
184
185 Ok(())
186}
187
188pub fn init_config() -> Result<()> {
190 let config_file = get_config_path()?;
191
192 if config_file.exists() {
193 println!("Configuration already exists at: {}", config_file.display());
194 } else {
195 let default_config = Config::default();
196 save_config(&default_config, Some(config_file.clone()))?;
197 println!("Created configuration at: {}", config_file.display());
198 }
199
200 Ok(())
201}
202
203pub fn persist_last_model(model: &str) -> Result<()> {
205 let mut config = load_config().unwrap_or_default();
206 config.last_used_model = Some(model.to_string());
207 save_config(&config, None)
208}
209
210pub async fn resolve_model_id(cli_model: Option<&str>, config: &Config) -> anyhow::Result<String> {
212 if let Some(model) = cli_model {
213 return Ok(model.to_string());
214 }
215 if let Some(last_model) = &config.last_used_model {
216 return Ok(last_model.clone());
217 }
218 if !config.default_model.provider.is_empty() && !config.default_model.name.is_empty() {
219 return Ok(format!(
220 "{}/{}",
221 config.default_model.provider, config.default_model.name
222 ));
223 }
224 let available = crate::ollama::require_any_model().await?;
225 Ok(format!("ollama/{}", available[0]))
226}