use anyhow::{Context, Result};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, fs::File, io::Read, path::Path};
#[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>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct EndpointSecrets {
pub api_token: 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,
}
#[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 Model {
#[serde(default = "default_context_window")]
pub context_window: u64,
}
#[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()
}
}
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 {
}
}
}
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)
}