use anyhow::{Context, Result};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, fs::File, io::Read, path::Path};
const API_KEY_PLACEHOLDER: &str = "<API_KEY>";
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub enum EndpointApi {
OpenAICompletions,
#[default]
OpenAIResponses,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Auth {
pub endpoint: BTreeMap<String, EndpointSecrets>,
#[serde(default)]
pub tool: ToolsAuth,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct EndpointSecrets {
pub api_key: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct ToolsAuth {
#[serde(rename = "websearch")]
pub web_search: Option<ToolSecrets>,
#[serde(default)]
pub mcp: BTreeMap<String, ToolSecrets>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct ToolSecrets {
pub api_key: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Config {
pub general: General,
pub endpoint: BTreeMap<String, Endpoint>,
#[serde(default)]
pub model: BTreeMap<String, Model>,
#[serde(default)]
pub symbols: Symbols,
#[serde(default, rename = "tool")]
pub tools: Tools,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct General {
pub primary_endpoint: String,
#[serde(default = "default_max_tokens_per_iteration")]
pub max_tokens: u64,
#[serde(default = "default_max_iterations_per_request")]
pub max_iterations_per_request: usize,
#[serde(default = "default_max_dir_list_results")]
pub max_list_dir_results: usize,
#[serde(default = "default_max_search_text_results")]
pub max_search_text_results: usize,
#[serde(default = "default_max_tool_observations")]
pub max_tool_observations: u64,
#[serde(default = "default_min_reasoning_observations")]
pub min_reasoning_observations: u64,
#[serde(default = "default_view_mode")]
pub view_mode: ViewMode,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Endpoint {
pub base_url: String,
pub default_model: String,
#[serde(skip)]
pub secrets: Option<EndpointSecrets>,
#[serde(default)]
pub api_type: EndpointApi,
#[serde(default)]
pub models: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct McpTool {
pub url: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Model {
#[serde(default = "default_context_window")]
pub context_window: u64,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Tools {
#[serde(rename = "websearch")]
pub web_search: Option<WebSearch>,
#[serde(default)]
pub mcp: BTreeMap<String, McpTool>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Symbols {
#[serde(default = "default_assistant_message_symbol")]
pub assistant_message: String,
#[serde(default = "default_assistant_reasoning_symbol")]
pub assistant_reasoning: String,
#[serde(default = "default_idle_indicator_symbol")]
pub idle_indicator: String,
#[serde(default = "default_active_indicator_symbols")]
pub active_indicators: Vec<String>,
#[serde(default = "default_tool_message_symbol")]
pub tool_message: String,
#[serde(default = "default_message_state_final_symbol")]
pub message_state_final: String,
#[serde(default = "default_message_state_intermediate_symbol")]
pub message_state_intermediate: String,
#[serde(default = "default_notification_debug_symbol")]
pub message_notification_debug: String,
#[serde(default = "default_notification_error_symbol")]
pub message_notification_error: String,
#[serde(default = "default_notification_info_symbol")]
pub message_notification_info: String,
#[serde(default = "default_notification_warning_symbol")]
pub message_notification_warning: String,
#[serde(default = "default_status_failed_symbol")]
pub status_failed: String,
#[serde(default = "default_status_success_symbol")]
pub status_success: String,
#[serde(default = "default_statusbar_leader_symbol")]
pub statusbar_leader: String,
#[serde(default = "default_statusbar_separator_symbol")]
pub statusbar_separator: String,
#[serde(default = "default_statusbar_reverse_separator_symbol")]
pub statusbar_reverse_separator: String,
#[serde(default = "default_system_message_symbol")]
pub system_message: String,
#[serde(default = "default_tokens_input_symbol")]
pub input_tokens: String,
#[serde(default = "default_tokens_output_symbol")]
pub output_tokens: String,
#[serde(default = "default_user_message_symbol")]
pub user_message: String,
}
impl Default for Symbols {
fn default() -> Self {
Self {
assistant_message: default_assistant_message_symbol(),
assistant_reasoning: default_assistant_reasoning_symbol(),
idle_indicator: default_idle_indicator_symbol(),
active_indicators: default_active_indicator_symbols(),
tool_message: default_tool_message_symbol(),
message_state_final: default_message_state_final_symbol(),
message_state_intermediate: default_message_state_intermediate_symbol(),
message_notification_debug: default_notification_debug_symbol(),
message_notification_error: default_notification_error_symbol(),
message_notification_info: default_notification_info_symbol(),
message_notification_warning: default_notification_warning_symbol(),
system_message: default_system_message_symbol(),
status_failed: default_status_failed_symbol(),
status_success: default_status_success_symbol(),
statusbar_leader: default_statusbar_leader_symbol(),
statusbar_separator: default_statusbar_separator_symbol(),
statusbar_reverse_separator: default_statusbar_reverse_separator_symbol(),
input_tokens: default_tokens_input_symbol(),
output_tokens: default_tokens_output_symbol(),
user_message: default_user_message_symbol(),
}
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum ViewMode {
Debug,
Full,
Detailed,
Compact,
Minimal,
}
impl Default for ViewMode {
fn default() -> Self {
default_view_mode()
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct WebSearch {
pub provider: WebSearchProvider,
pub url: String,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum WebSearchProvider {
SearXNG,
Tavily,
}
impl WebSearchProvider {
pub fn display_string(&self) -> String {
match self {
WebSearchProvider::SearXNG => "SearXNG".to_string(),
WebSearchProvider::Tavily => "Tavily".to_string(),
}
}
}
pub fn default_assistant_message_symbol() -> String {
"🤖".to_string()
}
pub fn default_assistant_reasoning_symbol() -> String {
"💭".to_string()
}
pub fn default_idle_indicator_symbol() -> String {
"●".to_string()
}
pub fn default_active_indicator_symbols() -> Vec<String> {
vec![
"⠋".to_string(),
"⠙".to_string(),
"⠹".to_string(),
"⠸".to_string(),
"⠼".to_string(),
"⠴".to_string(),
"⠦".to_string(),
"⠧".to_string(),
"⠇".to_string(),
"⠏".to_string(),
]
}
pub fn default_context_window() -> u64 {
128000
}
pub fn default_max_tokens_per_iteration() -> u64 {
16000
}
fn default_max_iterations_per_request() -> usize {
40
}
fn default_max_dir_list_results() -> usize {
100
}
fn default_max_search_text_results() -> usize {
100
}
fn default_max_tool_observations() -> u64 {
5
}
fn default_min_reasoning_observations() -> u64 {
10
}
pub fn default_message_state_final_symbol() -> String {
"✅".to_string()
}
pub fn default_message_state_intermediate_symbol() -> String {
"🕝".to_string()
}
pub fn default_notification_debug_symbol() -> String {
"🪲".to_string()
}
pub fn default_notification_error_symbol() -> String {
"🛑".to_string()
}
pub fn default_notification_info_symbol() -> String {
"ℹ️".to_string()
}
pub fn default_notification_warning_symbol() -> String {
"⚠️".to_string()
}
pub fn default_tool_message_symbol() -> String {
"🔧".to_string()
}
pub fn default_status_failed_symbol() -> String {
"❌".to_string()
}
pub fn default_status_success_symbol() -> String {
"✅".to_string()
}
pub fn default_statusbar_leader_symbol() -> String {
"".to_string()
}
pub fn default_statusbar_separator_symbol() -> String {
"".to_string()
}
pub fn default_statusbar_reverse_separator_symbol() -> String {
"".to_string()
}
pub fn default_system_message_symbol() -> String {
"⚙️".to_string()
}
pub fn default_tokens_input_symbol() -> String {
"⇑".to_string()
}
pub fn default_tokens_output_symbol() -> String {
"⇓".to_string()
}
pub fn default_user_message_symbol() -> String {
"🧑💻".to_string()
}
fn default_view_mode() -> ViewMode {
ViewMode::Compact
}
pub fn load_config() -> Result<Config> {
if let Some(proj_dirs) = ProjectDirs::from("org", "codemonk", "monothink") {
let config_dir = proj_dirs.config_dir().to_path_buf();
let mut config_file_dir = config_dir.clone();
config_file_dir.push("config.toml");
let config_file_path = config_file_dir.as_path();
let mut auth_file_dir = config_dir.clone();
auth_file_dir.push("auth.toml");
let auth_file_path = auth_file_dir.as_path();
let mut config = parse_config(config_file_path).with_context(|| {
format!(
"Error while parsing config file '{}'.",
config_file_path.to_string_lossy()
)
})?;
let auth = parse_auth(auth_file_path).with_context(|| {
format!(
"Error while parsing auth file '{}'.",
auth_file_path.to_string_lossy()
)
})?;
merge_config_with_auth(&mut config, &auth);
Ok(config)
} else {
panic!("Could not retrieve a valid home directory from the operating system.");
}
}
fn merge_config_with_auth(config: &mut Config, auth: &Auth) {
for (key, val) in &auth.endpoint {
if let Some(endpoint) = config.endpoint.get_mut(key) {
endpoint.secrets = Some(val.to_owned());
} else {
}
}
if let Some(tool) = &mut config.tools.web_search {
if let Some(api_key) = &auth
.tool
.web_search
.as_ref()
.and_then(|secrets| secrets.api_key.clone())
&& tool.url.contains(API_KEY_PLACEHOLDER)
{
tool.url = tool.url.replace(API_KEY_PLACEHOLDER, api_key);
} else if tool.url.contains(API_KEY_PLACEHOLDER) {
println!("Missing API key for web search. Web search not enabled.");
}
}
for (key, val) in &auth.tool.mcp {
if let Some(mcp_tool) = config.tools.mcp.get_mut(key) {
if let Some(api_key) = &val.api_key {
mcp_tool.url = mcp_tool.url.replace(API_KEY_PLACEHOLDER, api_key);
} else {
}
} else {
}
}
}
fn parse_auth(path: &Path) -> Result<Auth> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let auth: Auth = toml::from_str(&contents)?;
Ok(auth)
}
fn parse_config(path: &Path) -> Result<Config> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let config: Config = toml::from_str(&contents)?;
Ok(config)
}