use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tracing::{debug, warn};
use crate::state::app_state::WebSearchMode;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Settings {
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub scripting_mode: Option<String>,
#[serde(default = "default_web_search_mode")]
pub web_search_mode: String,
#[serde(skip_serializing, default)]
web_search_enabled: Option<bool>,
}
fn default_web_search_mode() -> String {
"on".to_string()
}
impl Default for Settings {
fn default() -> Self {
Self {
model: None,
scripting_mode: None,
web_search_mode: default_web_search_mode(),
web_search_enabled: None,
}
}
}
impl Settings {
pub fn settings_path() -> Option<PathBuf> {
dirs::home_dir().map(|home| Self::path_at(&home))
}
fn path_at(home: &Path) -> PathBuf {
let root = home.join(".netget");
if root.is_file() {
root
} else if !root.exists() && home.join(".netget-legacy.json").is_file() {
home.join(".netget-legacy.json")
} else {
root.join("settings.json")
}
}
pub fn load() -> Self {
let result = dirs::home_dir()
.context("Cannot find home directory")
.and_then(|home| Self::load_at(&home));
result.unwrap_or_else(|error| {
warn!("Failed to load settings: {error:#}; using defaults");
Self::default()
})
}
pub fn load_at(home: &Path) -> Result<Self> {
let path = Self::path_at(home);
if !path.exists() {
return Ok(Self::default());
}
let contents = crate::utils::file_io::read_text(&path, 1024 * 1024)?;
let mut settings: Self = serde_json::from_str(&contents).context("parse settings")?;
if let Some(enabled) = settings.web_search_enabled.take() {
if settings.web_search_mode == default_web_search_mode() {
settings.web_search_mode = if enabled { "on" } else { "off" }.into();
}
}
Ok(settings)
}
pub fn save(&self) -> Result<()> {
self.save_at(&dirs::home_dir().context("Cannot find home directory")?)
}
pub fn save_at(&self, home: &Path) -> Result<()> {
let directory = crate::utils::file_io::ensure_config_directory(home)?;
let contents = serde_json::to_vec_pretty(self).context("serialize settings")?;
crate::utils::file_io::write_atomic(&directory.join("settings.json"), &contents)?;
debug!("Saved settings in {}", directory.display());
Ok(())
}
pub fn set_model(&mut self, model: Option<String>) -> Result<()> {
self.model = model;
self.save()
}
pub fn set_scripting_mode(&mut self, mode: String) -> Result<()> {
self.scripting_mode = Some(mode);
self.save()
}
pub fn get_web_search_mode(&self) -> WebSearchMode {
self.web_search_mode.parse().unwrap_or_else(|e| {
warn!(
"Invalid web search mode in settings: '{}' ({}), using default",
self.web_search_mode, e
);
WebSearchMode::On
})
}
pub fn set_web_search_mode(&mut self, mode: WebSearchMode) -> Result<()> {
self.web_search_mode = mode.to_string();
self.save()
}
pub fn parse_scripting_mode(&self) -> Option<crate::state::app_state::ScriptingMode> {
self.scripting_mode
.as_ref()
.and_then(|mode_str| match mode_str.to_lowercase().as_str() {
"llm" => Some(crate::state::app_state::ScriptingMode::Off),
"python" => Some(crate::state::app_state::ScriptingMode::Python),
"javascript" => Some(crate::state::app_state::ScriptingMode::JavaScript),
"go" => Some(crate::state::app_state::ScriptingMode::Go),
_ => {
warn!(
"Invalid scripting mode in settings: '{}', ignoring",
mode_str
);
None
}
})
}
}