1use crate::constants::{self, config_key, section};
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4use std::fs;
5use std::path::PathBuf;
6
7#[derive(Debug, Serialize, Deserialize, Clone, Default)]
10pub struct YamlConfig {
11 #[serde(default)]
12 pub path: BTreeMap<String, String>,
13
14 #[serde(default)]
15 pub inner_url: BTreeMap<String, String>,
16
17 #[serde(default)]
18 pub outer_url: BTreeMap<String, String>,
19
20 #[serde(default)]
21 pub editor: BTreeMap<String, String>,
22
23 #[serde(default)]
24 pub browser: BTreeMap<String, String>,
25
26 #[serde(default)]
27 pub vpn: BTreeMap<String, String>,
28
29 #[serde(default)]
30 pub script: BTreeMap<String, String>,
31
32 #[serde(default)]
33 pub version: BTreeMap<String, String>,
34
35 #[serde(default)]
36 pub setting: BTreeMap<String, String>,
37
38 #[serde(default)]
39 pub log: BTreeMap<String, String>,
40
41 #[serde(default)]
42 pub report: BTreeMap<String, String>,
43
44 #[serde(flatten)]
46 pub extra: BTreeMap<String, serde_yaml::Value>,
47}
48
49impl YamlConfig {
50 pub fn data_dir() -> PathBuf {
52 if let Ok(path) = std::env::var(constants::DATA_PATH_ENV) {
54 return PathBuf::from(path);
55 }
56 dirs::home_dir()
58 .unwrap_or_else(|| PathBuf::from("."))
59 .join(constants::DATA_DIR)
60 }
61
62 fn config_path() -> PathBuf {
64 Self::data_dir().join(constants::CONFIG_FILE)
65 }
66
67 pub fn scripts_dir() -> PathBuf {
69 let dir = Self::data_dir().join(constants::SCRIPTS_DIR);
70 let _ = fs::create_dir_all(&dir);
72 dir
73 }
74
75 pub fn report_dir() -> PathBuf {
77 let dir = Self::data_dir().join(constants::REPORT_DIR);
78 let _ = fs::create_dir_all(&dir);
79 dir
80 }
81
82 pub fn report_file_path(&self) -> PathBuf {
84 if let Some(custom_path) = self.get_property(section::REPORT, config_key::WEEK_REPORT) {
85 if !custom_path.is_empty() {
86 return Self::expand_tilde(custom_path);
87 }
88 }
89 Self::report_dir().join(constants::REPORT_DEFAULT_FILE)
90 }
91
92 fn expand_tilde(path: &str) -> PathBuf {
94 if path.starts_with('~') {
95 if let Some(home) = dirs::home_dir() {
96 if path == "~" {
97 return home;
98 } else if path.starts_with("~/") {
99 return home.join(&path[2..]);
100 }
101 }
102 }
103 PathBuf::from(path)
104 }
105
106 pub fn load() -> Self {
108 let path = Self::config_path();
109 if !path.exists() {
110 let config = Self::default_config();
112 eprintln!("[INFO] 创建默认配置文件: {:?}", path);
113 config.save();
114 return config;
115 }
116
117 let content = fs::read_to_string(&path).unwrap_or_else(|e| {
118 eprintln!("[ERROR] 读取配置文件失败: {}, 路径: {:?}", e, path);
119 String::new()
120 });
121
122 serde_yaml::from_str(&content).unwrap_or_else(|e| {
123 eprintln!("[ERROR] 解析配置文件失败: {}, 路径: {:?}", e, path);
124 Self::default_config()
125 })
126 }
127
128 pub fn save(&self) {
130 let path = Self::config_path();
131
132 if let Some(parent) = path.parent() {
134 fs::create_dir_all(parent).unwrap_or_else(|e| {
135 eprintln!("[ERROR] 创建配置目录失败: {}", e);
136 });
137 }
138
139 let content = serde_yaml::to_string(self).unwrap_or_else(|e| {
140 eprintln!("[ERROR] 序列化配置失败: {}", e);
141 String::new()
142 });
143
144 fs::write(&path, content).unwrap_or_else(|e| {
145 eprintln!("[ERROR] 保存配置文件失败: {}, 路径: {:?}", e, path);
146 });
147 }
148
149 fn default_config() -> Self {
151 let mut config = Self::default();
152
153 config
155 .version
156 .insert("name".into(), constants::APP_NAME.into());
157 config
158 .version
159 .insert("version".into(), constants::VERSION.into());
160 config
161 .version
162 .insert("author".into(), constants::AUTHOR.into());
163 config
164 .version
165 .insert("email".into(), constants::EMAIL.into());
166
167 config
169 .log
170 .insert(config_key::MODE.into(), config_key::CONCISE.into());
171
172 config.setting.insert(
174 config_key::SEARCH_ENGINE.into(),
175 constants::DEFAULT_SEARCH_ENGINE.into(),
176 );
177
178 config
179 }
180
181 pub fn is_verbose(&self) -> bool {
183 self.log
184 .get(config_key::MODE)
185 .map_or(false, |m| m == config_key::VERBOSE)
186 }
187
188 pub fn get_section(&self, s: &str) -> Option<&BTreeMap<String, String>> {
192 match s {
193 section::PATH => Some(&self.path),
194 section::INNER_URL => Some(&self.inner_url),
195 section::OUTER_URL => Some(&self.outer_url),
196 section::EDITOR => Some(&self.editor),
197 section::BROWSER => Some(&self.browser),
198 section::VPN => Some(&self.vpn),
199 section::SCRIPT => Some(&self.script),
200 section::VERSION => Some(&self.version),
201 section::SETTING => Some(&self.setting),
202 section::LOG => Some(&self.log),
203 section::REPORT => Some(&self.report),
204 _ => None,
205 }
206 }
207
208 pub fn get_section_mut(&mut self, s: &str) -> Option<&mut BTreeMap<String, String>> {
210 match s {
211 section::PATH => Some(&mut self.path),
212 section::INNER_URL => Some(&mut self.inner_url),
213 section::OUTER_URL => Some(&mut self.outer_url),
214 section::EDITOR => Some(&mut self.editor),
215 section::BROWSER => Some(&mut self.browser),
216 section::VPN => Some(&mut self.vpn),
217 section::SCRIPT => Some(&mut self.script),
218 section::VERSION => Some(&mut self.version),
219 section::SETTING => Some(&mut self.setting),
220 section::LOG => Some(&mut self.log),
221 section::REPORT => Some(&mut self.report),
222 _ => None,
223 }
224 }
225
226 pub fn contains(&self, section: &str, key: &str) -> bool {
228 self.get_section(section)
229 .map_or(false, |m| m.contains_key(key))
230 }
231
232 pub fn get_property(&self, section: &str, key: &str) -> Option<&String> {
234 self.get_section(section).and_then(|m| m.get(key))
235 }
236
237 pub fn set_property(&mut self, section: &str, key: &str, value: &str) {
239 if let Some(map) = self.get_section_mut(section) {
240 map.insert(key.to_string(), value.to_string());
241 self.save();
242 }
243 }
244
245 pub fn remove_property(&mut self, section: &str, key: &str) {
247 if let Some(map) = self.get_section_mut(section) {
248 map.remove(key);
249 self.save();
250 }
251 }
252
253 pub fn rename_property(&mut self, section: &str, old_key: &str, new_key: &str) {
255 if let Some(map) = self.get_section_mut(section) {
256 if let Some(value) = map.remove(old_key) {
257 map.insert(new_key.to_string(), value);
258 self.save();
259 }
260 }
261 }
262
263 pub fn all_section_names(&self) -> &'static [&'static str] {
265 constants::ALL_SECTIONS
266 }
267
268 pub fn alias_exists(&self, alias: &str) -> bool {
270 constants::ALIAS_EXISTS_SECTIONS
271 .iter()
272 .any(|s| self.contains(s, alias))
273 }
274
275 pub fn get_path_by_alias(&self, alias: &str) -> Option<&String> {
277 constants::ALIAS_PATH_SECTIONS
278 .iter()
279 .find_map(|s| self.get_property(s, alias))
280 }
281
282 pub fn collect_alias_envs(&self) -> Vec<(String, String)> {
287 let sections = &[
288 section::PATH,
289 section::INNER_URL,
290 section::OUTER_URL,
291 section::SCRIPT,
292 ];
293 let mut envs = Vec::new();
294 let mut seen = std::collections::HashSet::new();
295
296 for &sec in sections {
297 if let Some(map) = self.get_section(sec) {
298 for (alias, value) in map {
299 let env_key = format!("J_{}", alias.replace('-', "_").to_uppercase());
301 if seen.insert(env_key.clone()) {
303 envs.push((env_key, value.clone()));
304 }
305 }
306 }
307 }
308
309 envs
310 }
311}