Skip to main content

netget/
settings.rs

1//! Application settings management
2
3use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use std::path::{Path, PathBuf};
6use tracing::{debug, warn};
7
8use crate::state::app_state::WebSearchMode;
9
10/// Application settings
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Settings {
13    /// Ollama model name (None = auto-select from available models)
14    #[serde(default)]
15    pub model: Option<String>,
16
17    /// Scripting mode (llm, python, javascript, go)
18    #[serde(default)]
19    pub scripting_mode: Option<String>,
20
21    /// Web search mode (on, off, ask)
22    #[serde(default = "default_web_search_mode")]
23    pub web_search_mode: String,
24
25    /// Legacy field for migration (deprecated)
26    #[serde(skip_serializing, default)]
27    web_search_enabled: Option<bool>,
28}
29
30fn default_web_search_mode() -> String {
31    "on".to_string()
32}
33
34impl Default for Settings {
35    fn default() -> Self {
36        Self {
37            model: None,
38            scripting_mode: None,
39            web_search_mode: default_web_search_mode(),
40            web_search_enabled: None,
41        }
42    }
43}
44
45impl Settings {
46    /// The canonical settings path; an existing legacy file is read until the
47    /// next explicit save safely migrates it into the shared config directory.
48    pub fn settings_path() -> Option<PathBuf> {
49        dirs::home_dir().map(|home| Self::path_at(&home))
50    }
51
52    fn path_at(home: &Path) -> PathBuf {
53        let root = home.join(".netget");
54        if root.is_file() {
55            root
56        } else if !root.exists() && home.join(".netget-legacy.json").is_file() {
57            home.join(".netget-legacy.json")
58        } else {
59            root.join("settings.json")
60        }
61    }
62
63    pub fn load() -> Self {
64        let result = dirs::home_dir()
65            .context("Cannot find home directory")
66            .and_then(|home| Self::load_at(&home));
67        result.unwrap_or_else(|error| {
68            warn!("Failed to load settings: {error:#}; using defaults");
69            Self::default()
70        })
71    }
72
73    /// Explicit home path permits migration tests without mutating process HOME.
74    pub fn load_at(home: &Path) -> Result<Self> {
75        let path = Self::path_at(home);
76        if !path.exists() {
77            return Ok(Self::default());
78        }
79        let contents = crate::utils::file_io::read_text(&path, 1024 * 1024)?;
80        let mut settings: Self = serde_json::from_str(&contents).context("parse settings")?;
81        if let Some(enabled) = settings.web_search_enabled.take() {
82            if settings.web_search_mode == default_web_search_mode() {
83                settings.web_search_mode = if enabled { "on" } else { "off" }.into();
84            }
85        }
86        Ok(settings)
87    }
88
89    pub fn save(&self) -> Result<()> {
90        self.save_at(&dirs::home_dir().context("Cannot find home directory")?)
91    }
92
93    pub fn save_at(&self, home: &Path) -> Result<()> {
94        let directory = crate::utils::file_io::ensure_config_directory(home)?;
95        let contents = serde_json::to_vec_pretty(self).context("serialize settings")?;
96        crate::utils::file_io::write_atomic(&directory.join("settings.json"), &contents)?;
97        debug!("Saved settings in {}", directory.display());
98        Ok(())
99    }
100
101    /// Update model and save
102    pub fn set_model(&mut self, model: Option<String>) -> Result<()> {
103        self.model = model;
104        self.save()
105    }
106
107    /// Update scripting mode and save
108    pub fn set_scripting_mode(&mut self, mode: String) -> Result<()> {
109        self.scripting_mode = Some(mode);
110        self.save()
111    }
112
113    /// Get web search mode
114    pub fn get_web_search_mode(&self) -> WebSearchMode {
115        self.web_search_mode.parse().unwrap_or_else(|e| {
116            warn!(
117                "Invalid web search mode in settings: '{}' ({}), using default",
118                self.web_search_mode, e
119            );
120            WebSearchMode::On
121        })
122    }
123
124    /// Update web search mode and save
125    pub fn set_web_search_mode(&mut self, mode: WebSearchMode) -> Result<()> {
126        self.web_search_mode = mode.to_string();
127        self.save()
128    }
129
130    /// Parse saved scripting mode
131    pub fn parse_scripting_mode(&self) -> Option<crate::state::app_state::ScriptingMode> {
132        self.scripting_mode
133            .as_ref()
134            .and_then(|mode_str| match mode_str.to_lowercase().as_str() {
135                "llm" => Some(crate::state::app_state::ScriptingMode::Off),
136                "python" => Some(crate::state::app_state::ScriptingMode::Python),
137                "javascript" => Some(crate::state::app_state::ScriptingMode::JavaScript),
138                "go" => Some(crate::state::app_state::ScriptingMode::Go),
139                _ => {
140                    warn!(
141                        "Invalid scripting mode in settings: '{}', ignoring",
142                        mode_str
143                    );
144                    None
145                }
146            })
147    }
148}