1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Settings {
13 #[serde(default)]
15 pub model: Option<String>,
16
17 #[serde(default)]
19 pub scripting_mode: Option<String>,
20
21 #[serde(default = "default_web_search_mode")]
23 pub web_search_mode: String,
24
25 #[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 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 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 pub fn set_model(&mut self, model: Option<String>) -> Result<()> {
103 self.model = model;
104 self.save()
105 }
106
107 pub fn set_scripting_mode(&mut self, mode: String) -> Result<()> {
109 self.scripting_mode = Some(mode);
110 self.save()
111 }
112
113 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 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 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}