1use crate::server::{ServerConfig, WorkspaceInit};
2use crate::workspace::{PersistHook, WorkspaceFlags, WorkspaceRegistry};
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5use std::sync::{Arc, Mutex};
6
7fn default_true() -> bool {
8 true
9}
10fn default_stable() -> String {
11 "stable".to_string()
12}
13fn default_auto() -> String {
14 "auto".to_string()
15}
16
17#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum PortMode {
20 Auto,
21 #[default]
22 Spec,
23}
24
25#[derive(Debug, Clone, Default, Serialize, Deserialize)]
26pub struct WorkspaceSettings {
27 pub path: String,
28 #[serde(flatten, default)]
29 pub flags: WorkspaceFlags,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33#[serde(default)]
34pub struct AppSettings {
35 pub port_mode: PortMode,
36 pub port: u16,
37 pub host: String,
38 pub theme: String,
39 #[serde(default = "default_auto")]
40 pub language: String,
41 #[serde(default = "default_auto")]
42 pub web_theme: String,
43 #[serde(default = "default_auto")]
44 pub web_language: String,
45 pub db_path: Option<String>,
46 pub workspaces: Vec<WorkspaceSettings>,
47 #[serde(default = "default_true")]
48 pub tray_resident: bool,
49 #[serde(default = "default_true")]
50 pub default_search: bool,
51 #[serde(default = "default_true")]
52 pub default_viewed: bool,
53 #[serde(default)]
54 pub default_live: bool,
55 #[serde(default)]
56 pub default_edit: bool,
57 #[serde(default)]
58 pub default_shared_annotation: bool,
59 #[serde(default)]
60 pub web_styles: std::collections::HashMap<String, String>,
61 #[serde(default)]
62 pub shortcuts: std::collections::HashMap<String, serde_json::Value>,
63 #[serde(default = "default_true")]
64 pub auto_update: bool,
65 #[serde(default = "default_stable")]
66 pub update_channel: String,
67 #[serde(default)]
68 pub window_width: Option<u32>,
69 #[serde(default)]
70 pub window_height: Option<u32>,
71}
72
73impl Default for AppSettings {
74 fn default() -> Self {
75 Self {
76 port_mode: PortMode::Spec,
77 port: 6419,
78 host: "127.0.0.1".to_string(),
79 theme: "auto".to_string(),
80 language: "auto".to_string(),
81 web_theme: "auto".to_string(),
82 web_language: "auto".to_string(),
83 db_path: None,
84 workspaces: vec![],
85 tray_resident: true,
86 default_search: true,
87 default_viewed: true,
88 default_live: false,
89 default_edit: false,
90 default_shared_annotation: false,
91 web_styles: std::collections::HashMap::new(),
92 shortcuts: std::collections::HashMap::new(),
93 auto_update: true,
94 update_channel: "stable".to_string(),
95 window_width: None,
96 window_height: None,
97 }
98 }
99}
100
101impl AppSettings {
102 pub fn settings_path() -> PathBuf {
103 dirs::home_dir()
104 .unwrap()
105 .join(".markon")
106 .join("settings.json")
107 }
108 pub fn load() -> Self {
109 let p = Self::settings_path();
110 if let Ok(c) = std::fs::read_to_string(p) {
111 if let Ok(mut s) = serde_json::from_str::<Self>(&c) {
112 s.normalize();
113 return s;
114 }
115 }
116 Self::default()
117 }
118
119 fn normalize(&mut self) {
124 use std::collections::HashSet;
125 let mut seen = HashSet::new();
126 self.workspaces.retain(|w| seen.insert(w.path.clone()));
127 for field in [
128 &mut self.language,
129 &mut self.web_theme,
130 &mut self.web_language,
131 ] {
132 if field.is_empty() {
133 *field = "auto".to_string();
134 }
135 }
136 }
137 pub fn save(&self) -> Result<(), String> {
138 let p = Self::settings_path();
139 if let Some(parent) = p.parent() {
140 let _ = std::fs::create_dir_all(parent);
141 }
142 let c = serde_json::to_string_pretty(self).unwrap();
143 std::fs::write(p, c).map_err(|e| e.to_string())
144 }
145 pub fn to_server_config(&self, port: u16) -> ServerConfig {
146 let initial_workspaces: Vec<WorkspaceInit> = self
147 .workspaces
148 .iter()
149 .filter(|w| !w.path.is_empty())
150 .map(|w| WorkspaceInit {
151 path: PathBuf::from(&w.path),
152 flags: w.flags,
153 initial_path: None,
154 })
155 .collect();
156 ServerConfig {
157 host: self.host.clone(),
158 port,
159 theme: if self.web_theme == "auto" {
160 self.theme.clone()
161 } else {
162 self.web_theme.clone()
163 },
164 qr: None,
165 open_browser: None,
166 shared_annotation: initial_workspaces.iter().any(|w| w.flags.shared_annotation),
167 salt: None,
168 initial_workspaces,
169 bound_listener: None,
170 registry: None,
171 management_token: None,
172 language: if self.web_language == "auto" {
173 Some(self.language.clone())
174 } else {
175 Some(self.web_language.clone())
176 },
177 styles_css: self.render_styles_css(),
178 shortcuts_json: self.render_shortcuts_json(),
179 }
180 }
181 pub fn effective_web_language(&self) -> Option<String> {
182 let l = if self.web_language == "auto" {
183 &self.language
184 } else {
185 &self.web_language
186 };
187 if l == "auto" || l.is_empty() {
188 None
189 } else {
190 Some(l.clone())
191 }
192 }
193 pub fn persist_hook(settings: Arc<Mutex<AppSettings>>) -> PersistHook {
198 Arc::new(move |reg| {
199 let mut s = settings.lock().unwrap();
200 s.sync_from_registry(reg);
201 let _ = s.save();
202 })
203 }
204
205 pub fn render_shortcuts_json(&self) -> Option<String> {
206 if self.shortcuts.is_empty() {
207 None
208 } else {
209 serde_json::to_string(&self.shortcuts).ok()
210 }
211 }
212 pub fn sync_from_registry(&mut self, registry: &WorkspaceRegistry) {
214 self.workspaces = registry
215 .info_list()
216 .into_iter()
217 .map(|info| WorkspaceSettings {
218 path: info.path,
219 flags: info.flags,
220 })
221 .collect();
222 }
223
224 pub fn render_styles_css(&self) -> Option<String> {
225 if self.web_styles.is_empty() {
226 return None;
227 }
228 Some(
229 self.web_styles
230 .iter()
231 .map(|(k, v)| format!("{k}: {v};"))
232 .collect::<Vec<_>>()
233 .join(" "),
234 )
235 }
236}