Skip to main content

j_cli/config/
yaml_config.rs

1use crate::constants::{self, config_key, section};
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4use std::fs;
5use std::path::PathBuf;
6
7/// YAML 配置文件的完整结构
8/// 使用 BTreeMap 保持键的有序性,与 Java 版的 LinkedHashMap 行为一致
9#[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    /// 捕获未知的顶级键,保证不丢失任何配置
45    #[serde(flatten)]
46    pub extra: BTreeMap<String, serde_yaml::Value>,
47}
48
49impl YamlConfig {
50    /// 获取数据根目录: ~/.jdata/
51    pub fn data_dir() -> PathBuf {
52        // 优先使用环境变量指定的数据路径
53        if let Ok(path) = std::env::var(constants::DATA_PATH_ENV) {
54            return PathBuf::from(path);
55        }
56        // 默认路径: ~/.jdata/
57        dirs::home_dir()
58            .unwrap_or_else(|| PathBuf::from("."))
59            .join(constants::DATA_DIR)
60    }
61
62    /// 获取配置文件路径: ~/.jdata/config.yaml
63    fn config_path() -> PathBuf {
64        Self::data_dir().join(constants::CONFIG_FILE)
65    }
66
67    /// 获取脚本存储目录: ~/.jdata/scripts/
68    pub fn scripts_dir() -> PathBuf {
69        let dir = Self::data_dir().join(constants::SCRIPTS_DIR);
70        // 确保目录存在
71        let _ = fs::create_dir_all(&dir);
72        dir
73    }
74
75    /// 获取日报目录: ~/.jdata/report/
76    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    /// 获取日报文件路径(优先使用用户配置,否则使用默认路径 ~/.jdata/report/week_report.md)
83    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    /// 展开路径中的 ~ 为用户主目录
93    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    /// 从配置文件加载
107    pub fn load() -> Self {
108        let path = Self::config_path();
109        if !path.exists() {
110            // 配置文件不存在,创建默认配置
111            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    /// 保存配置到文件
129    pub fn save(&self) {
130        let path = Self::config_path();
131
132        // 确保目录存在
133        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    /// 创建默认配置
150    fn default_config() -> Self {
151        let mut config = Self::default();
152
153        // 版本信息
154        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        // 日志模式
168        config
169            .log
170            .insert(config_key::MODE.into(), config_key::CONCISE.into());
171
172        // 默认搜索引擎
173        config.setting.insert(
174            config_key::SEARCH_ENGINE.into(),
175            constants::DEFAULT_SEARCH_ENGINE.into(),
176        );
177
178        config
179    }
180
181    /// 是否是 verbose 模式
182    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    // ========== 根据 section 名称获取对应的 map ==========
189
190    /// 获取指定 section 的不可变引用
191    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    /// 获取指定 section 的可变引用
209    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    /// 检查某个 section 中是否包含指定的 key
227    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    /// 获取某个 section 中指定 key 的值
233    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    /// 设置某个 section 中的键值对并保存
238    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    /// 删除某个 section 中的键并保存
246    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    /// 重命名某个 section 中的键
254    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    /// 获取所有已知的 section 名称
264    pub fn all_section_names(&self) -> &'static [&'static str] {
265        constants::ALL_SECTIONS
266    }
267
268    /// 判断别名是否存在于任何 section 中(用于 open 命令判断)
269    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    /// 根据别名获取路径(依次从 path、inner_url、outer_url 中查找)
276    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    /// 收集所有别名路径,用于注入脚本执行时的环境变量
283    /// 返回 Vec<(env_key, value)>,env_key 格式为 J_<ALIAS_UPPER>
284    /// 别名中的 `-` 会转换为 `_`,且全部大写
285    /// 覆盖 section: path, inner_url, outer_url, script
286    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                    // 将别名转为环境变量名: J_<ALIAS_UPPER>,`-` 转 `_`
300                    let env_key = format!("J_{}", alias.replace('-', "_").to_uppercase());
301                    // 同名别名只取优先级高的(path > inner_url > outer_url > script)
302                    if seen.insert(env_key.clone()) {
303                        envs.push((env_key, value.clone()));
304                    }
305                }
306            }
307        }
308
309        envs
310    }
311}