Skip to main content

j_cli/config/
yaml_config.rs

1use crate::constants::{self, section, config_key};
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.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        // 日志模式
160        config.log.insert(config_key::MODE.into(), config_key::CONCISE.into());
161
162        // 默认搜索引擎
163        config.setting.insert(config_key::SEARCH_ENGINE.into(), constants::DEFAULT_SEARCH_ENGINE.into());
164
165        config
166    }
167
168    /// 是否是 verbose 模式
169    pub fn is_verbose(&self) -> bool {
170        self.log.get(config_key::MODE).map_or(false, |m| m == config_key::VERBOSE)
171    }
172
173    // ========== 根据 section 名称获取对应的 map ==========
174
175    /// 获取指定 section 的不可变引用
176    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    /// 获取指定 section 的可变引用
194    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    /// 检查某个 section 中是否包含指定的 key
212    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    /// 获取某个 section 中指定 key 的值
218    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    /// 设置某个 section 中的键值对并保存
223    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    /// 删除某个 section 中的键并保存
231    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    /// 重命名某个 section 中的键
239    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    /// 获取所有已知的 section 名称
249    pub fn all_section_names(&self) -> &'static [&'static str] {
250        constants::ALL_SECTIONS
251    }
252
253    /// 判断别名是否存在于任何 section 中(用于 open 命令判断)
254    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    /// 根据别名获取路径(依次从 path、inner_url、outer_url 中查找)
261    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    /// 收集所有别名路径,用于注入脚本执行时的环境变量
268    /// 返回 Vec<(env_key, value)>,env_key 格式为 J_<ALIAS_UPPER>
269    /// 别名中的 `-` 会转换为 `_`,且全部大写
270    /// 覆盖 section: path, inner_url, outer_url, script
271    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                    // 将别名转为环境变量名: J_<ALIAS_UPPER>,`-` 转 `_`
280                    let env_key = format!("J_{}", alias.replace('-', "_").to_uppercase());
281                    // 同名别名只取优先级高的(path > inner_url > outer_url > script)
282                    if seen.insert(env_key.clone()) {
283                        envs.push((env_key, value.clone()));
284                    }
285                }
286            }
287        }
288
289        envs
290    }
291}