1use crate::constants::{self, section, config_key};
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.version.insert("name".into(), constants::APP_NAME.into());
155 config.version.insert("version".into(), constants::VERSION.into());
156 config.version.insert("author".into(), constants::AUTHOR.into());
157 config.version.insert("email".into(), constants::EMAIL.into());
158
159 config.log.insert(config_key::MODE.into(), config_key::CONCISE.into());
161
162 config.setting.insert(config_key::SEARCH_ENGINE.into(), constants::DEFAULT_SEARCH_ENGINE.into());
164
165 config
166 }
167
168 pub fn is_verbose(&self) -> bool {
170 self.log.get(config_key::MODE).map_or(false, |m| m == config_key::VERBOSE)
171 }
172
173 pub fn get_section(&self, s: &str) -> Option<&BTreeMap<String, String>> {
177 match s {
178 section::PATH => Some(&self.path),
179 section::INNER_URL => Some(&self.inner_url),
180 section::OUTER_URL => Some(&self.outer_url),
181 section::EDITOR => Some(&self.editor),
182 section::BROWSER => Some(&self.browser),
183 section::VPN => Some(&self.vpn),
184 section::SCRIPT => Some(&self.script),
185 section::VERSION => Some(&self.version),
186 section::SETTING => Some(&self.setting),
187 section::LOG => Some(&self.log),
188 section::REPORT => Some(&self.report),
189 _ => None,
190 }
191 }
192
193 pub fn get_section_mut(&mut self, s: &str) -> Option<&mut BTreeMap<String, String>> {
195 match s {
196 section::PATH => Some(&mut self.path),
197 section::INNER_URL => Some(&mut self.inner_url),
198 section::OUTER_URL => Some(&mut self.outer_url),
199 section::EDITOR => Some(&mut self.editor),
200 section::BROWSER => Some(&mut self.browser),
201 section::VPN => Some(&mut self.vpn),
202 section::SCRIPT => Some(&mut self.script),
203 section::VERSION => Some(&mut self.version),
204 section::SETTING => Some(&mut self.setting),
205 section::LOG => Some(&mut self.log),
206 section::REPORT => Some(&mut self.report),
207 _ => None,
208 }
209 }
210
211 pub fn contains(&self, section: &str, key: &str) -> bool {
213 self.get_section(section)
214 .map_or(false, |m| m.contains_key(key))
215 }
216
217 pub fn get_property(&self, section: &str, key: &str) -> Option<&String> {
219 self.get_section(section).and_then(|m| m.get(key))
220 }
221
222 pub fn set_property(&mut self, section: &str, key: &str, value: &str) {
224 if let Some(map) = self.get_section_mut(section) {
225 map.insert(key.to_string(), value.to_string());
226 self.save();
227 }
228 }
229
230 pub fn remove_property(&mut self, section: &str, key: &str) {
232 if let Some(map) = self.get_section_mut(section) {
233 map.remove(key);
234 self.save();
235 }
236 }
237
238 pub fn rename_property(&mut self, section: &str, old_key: &str, new_key: &str) {
240 if let Some(map) = self.get_section_mut(section) {
241 if let Some(value) = map.remove(old_key) {
242 map.insert(new_key.to_string(), value);
243 self.save();
244 }
245 }
246 }
247
248 pub fn all_section_names(&self) -> &'static [&'static str] {
250 constants::ALL_SECTIONS
251 }
252
253 pub fn alias_exists(&self, alias: &str) -> bool {
255 constants::ALIAS_EXISTS_SECTIONS
256 .iter()
257 .any(|s| self.contains(s, alias))
258 }
259
260 pub fn get_path_by_alias(&self, alias: &str) -> Option<&String> {
262 constants::ALIAS_PATH_SECTIONS
263 .iter()
264 .find_map(|s| self.get_property(s, alias))
265 }
266
267 pub fn collect_alias_envs(&self) -> Vec<(String, String)> {
272 let sections = &[section::PATH, section::INNER_URL, section::OUTER_URL, section::SCRIPT];
273 let mut envs = Vec::new();
274 let mut seen = std::collections::HashSet::new();
275
276 for &sec in sections {
277 if let Some(map) = self.get_section(sec) {
278 for (alias, value) in map {
279 let env_key = format!("J_{}", alias.replace('-', "_").to_uppercase());
281 if seen.insert(env_key.clone()) {
283 envs.push((env_key, value.clone()));
284 }
285 }
286 }
287 }
288
289 envs
290 }
291}