Skip to main content

j_cli/command/
system.rs

1use crate::config::YamlConfig;
2use crate::constants::{self, CONTAIN_SEARCH_SECTIONS, config_key, section};
3use crate::{error, info, md, usage};
4use colored::Colorize;
5
6/// 编译时嵌入的版本信息模板
7const VERSION_TEMPLATE: &str = include_str!("../../assets/version.md");
8
9/// 处理 version 命令: j version
10pub fn handle_version(config: &YamlConfig) {
11    let mut extra = String::new();
12
13    // 收集自定义版本信息
14    if let Some(version_map) = config.get_section("version") {
15        for (key, value) in version_map {
16            if key == "email" || key == "author" {
17                continue;
18            }
19            extra.push_str(&format!("| {} | {} |\n", key, value));
20        }
21    }
22
23    let text = VERSION_TEMPLATE
24        .replace("{version}", constants::VERSION)
25        .replace("{os}", std::env::consts::OS)
26        .replace("{extra}", &extra);
27    md!("{}", text);
28}
29
30/// 编译时嵌入的帮助文档
31const HELP_TEXT: &str = include_str!("../../assets/help.md");
32
33/// 处理 help 命令: j help
34pub fn handle_help() {
35    md!("{}", HELP_TEXT);
36}
37
38/// 处理 exit 命令
39pub fn handle_exit() {
40    info!("Bye~ See you again 😭");
41    std::process::exit(0);
42}
43
44/// 处理 log 命令: j log mode <verbose|concise>
45pub fn handle_log(key: &str, value: &str, config: &mut YamlConfig) {
46    if key == config_key::MODE {
47        let mode = if value == config_key::VERBOSE {
48            config_key::VERBOSE
49        } else {
50            config_key::CONCISE
51        };
52        config.set_property(section::LOG, config_key::MODE, mode);
53        info!("✅ 日志模式已切换为: {}", mode);
54    } else {
55        usage!("j log mode <verbose|concise>");
56    }
57}
58
59/// 处理 clear 命令: j clear
60pub fn handle_clear() {
61    // 使用 ANSI 转义序列清屏
62    print!("\x1B[2J\x1B[1;1H");
63}
64
65/// 处理 contain 命令: j contain <alias> [containers]
66/// 在指定分类中查找别名
67pub fn handle_contain(alias: &str, containers: Option<&str>, config: &YamlConfig) {
68    let sections: Vec<&str> = match containers {
69        Some(c) => c.split(',').collect(),
70        None => CONTAIN_SEARCH_SECTIONS.to_vec(),
71    };
72
73    let mut found = Vec::new();
74
75    for section in &sections {
76        if config.contains(section, alias) {
77            if let Some(value) = config.get_property(section, alias) {
78                found.push(format!(
79                    "{} {}: {}",
80                    format!("[{}]", section).green(),
81                    alias,
82                    value
83                ));
84            }
85        }
86    }
87
88    if found.is_empty() {
89        info!("nothing found 😢");
90    } else {
91        info!("找到 {} 条结果 😊", found.len().to_string().green());
92        for line in &found {
93            info!("{}", line);
94        }
95    }
96}
97
98/// 处理 change 命令: j change <part> <field> <value>
99/// 直接修改配置文件中的某个字段(如果字段不存在则新增)
100pub fn handle_change(part: &str, field: &str, value: &str, config: &mut YamlConfig) {
101    if config.get_section(part).is_none() {
102        error!("❌ 在配置文件中未找到该 section:{}", part);
103        return;
104    }
105
106    let old_value = config.get_property(part, field).cloned();
107    config.set_property(part, field, value);
108
109    match old_value {
110        Some(old) => {
111            info!(
112                "✅ 已修改 {}.{} 的值为 {},旧值为 {}",
113                part, field, value, old
114            );
115        }
116        None => {
117            info!("✅ 已新增 {}.{} = {}", part, field, value);
118        }
119    }
120    info!(
121        "🚧 此命令可能会导致配置文件属性错乱而使 Copilot 无法正常使用,请确保在您清楚在做什么的情况下使用"
122    );
123}
124
125// ========== completion 命令 ==========
126
127/// 处理 completion 命令: j completion [shell]
128/// 生成 shell 补全脚本,支持 zsh / bash
129pub fn handle_completion(shell_type: Option<&str>, config: &YamlConfig) {
130    let shell = shell_type.unwrap_or("zsh");
131
132    match shell {
133        "zsh" => generate_zsh_completion(config),
134        "bash" => generate_bash_completion(config),
135        _ => {
136            error!("❌ 不支持的 shell 类型: {},可选: zsh, bash", shell);
137            usage!("j completion [zsh|bash]");
138        }
139    }
140}
141
142/// 生成 zsh 补全脚本
143fn generate_zsh_completion(config: &YamlConfig) {
144    // 收集所有别名
145    let mut all_aliases = Vec::new();
146    for s in constants::ALIAS_EXISTS_SECTIONS {
147        if let Some(map) = config.get_section(s) {
148            for key in map.keys() {
149                if !all_aliases.contains(key) {
150                    all_aliases.push(key.clone());
151                }
152            }
153        }
154    }
155    all_aliases.sort();
156
157    // 收集编辑器别名(后续参数需要文件路径补全)
158    let editor_aliases: Vec<String> = config
159        .get_section(section::EDITOR)
160        .map(|m| m.keys().cloned().collect())
161        .unwrap_or_default();
162
163    // 收集浏览器别名
164    let browser_aliases: Vec<String> = config
165        .get_section(section::BROWSER)
166        .map(|m| m.keys().cloned().collect())
167        .unwrap_or_default();
168
169    // 收集内置命令关键字
170    let keywords = constants::cmd::all_keywords();
171
172    // 子命令列表
173    let subcmds = keywords.iter().map(|s| *s).collect::<Vec<_>>();
174    let subcmds_str = subcmds.join(" ");
175
176    // 别名列表
177    let aliases_str = all_aliases.join(" ");
178
179    // 编辑器别名模式匹配
180    let editor_pattern = if editor_aliases.is_empty() {
181        String::new()
182    } else {
183        editor_aliases.join("|")
184    };
185
186    // 浏览器别名模式匹配
187    let browser_pattern = if browser_aliases.is_empty() {
188        String::new()
189    } else {
190        browser_aliases.join("|")
191    };
192
193    // 生成 zsh 补全脚本
194    let mut script = String::new();
195    script.push_str("#compdef j\n");
196    script.push_str("# Zsh completion for j (work-copilot)\n");
197    script.push_str("# 生成方式: eval \"$(j completion zsh)\"\n");
198    script.push_str(
199        "# 或: j completion zsh > ~/.zsh/completions/_j && fpath=(~/.zsh/completions $fpath)\n\n",
200    );
201    script.push_str("_j() {\n");
202    script.push_str("    local curcontext=\"$curcontext\" state line\n");
203    script.push_str("    typeset -A opt_args\n\n");
204
205    // 子命令和别名合并列表
206    script.push_str(&format!("    local -a subcmds=({})\n", subcmds_str));
207    script.push_str(&format!("    local -a aliases=({})\n", aliases_str));
208
209    // 编辑器/浏览器别名列表(用于判断是否需要文件补全)
210    if !editor_pattern.is_empty() {
211        script.push_str(&format!(
212            "    local -a editor_aliases=({})\n",
213            editor_aliases.join(" ")
214        ));
215    }
216
217    script.push_str("\n    _arguments -C \\\n");
218    script.push_str("        '1: :->cmd' \\\n");
219    script.push_str("        '*: :->args'\n\n");
220
221    script.push_str("    case $state in\n");
222    script.push_str("        cmd)\n");
223    script.push_str("            _describe 'command' subcmds\n");
224    script.push_str("            _describe 'alias' aliases\n");
225    script.push_str("            ;;\n");
226    script.push_str("        args)\n");
227    script.push_str("            case $words[2] in\n");
228
229    // set / modify 命令:第二个参数是别名,第三个参数是文件路径
230    script.push_str("                set|s|modify|mf)\n");
231    script.push_str("                    if (( CURRENT == 3 )); then\n");
232    script.push_str("                        _describe 'alias' aliases\n");
233    script.push_str("                    else\n");
234    script.push_str("                        _files\n");
235    script.push_str("                    fi\n");
236    script.push_str("                    ;;\n");
237
238    // remove / rename 命令:补全别名
239    script.push_str("                rm|remove|rename|rn|note|nt|denote|dnt|contain|find)\n");
240    script.push_str("                    _describe 'alias' aliases\n");
241    script.push_str("                    ;;\n");
242
243    // list 命令:补全 section 名
244    let sections_str = constants::ALL_SECTIONS.join(" ");
245    script.push_str(&format!("                ls|list)\n"));
246    script.push_str(&format!(
247        "                    local -a sections=(all {})\n",
248        sections_str
249    ));
250    script.push_str("                    _describe 'section' sections\n");
251    script.push_str("                    ;;\n");
252
253    // reportctl 命令:补全子操作
254    script.push_str("                reportctl|rctl)\n");
255    script
256        .push_str("                    local -a rctl_actions=(new sync push pull set-url open)\n");
257    script.push_str("                    _describe 'action' rctl_actions\n");
258    script.push_str("                    ;;\n");
259
260    // log 命令
261    script.push_str("                log)\n");
262    script.push_str("                    if (( CURRENT == 3 )); then\n");
263    script.push_str("                        local -a log_keys=(mode)\n");
264    script.push_str("                        _describe 'key' log_keys\n");
265    script.push_str("                    else\n");
266    script.push_str("                        local -a log_values=(verbose concise)\n");
267    script.push_str("                        _describe 'value' log_values\n");
268    script.push_str("                    fi\n");
269    script.push_str("                    ;;\n");
270
271    // change 命令:补全 section
272    script.push_str(&format!("                change|chg)\n"));
273    script.push_str(&format!(
274        "                    local -a sections=({})\n",
275        sections_str
276    ));
277    script.push_str("                    _describe 'section' sections\n");
278    script.push_str("                    ;;\n");
279
280    // time 命令
281    script.push_str("                time)\n");
282    script.push_str("                    local -a time_funcs=(countdown)\n");
283    script.push_str("                    _describe 'function' time_funcs\n");
284    script.push_str("                    ;;\n");
285
286    // completion 命令
287    script.push_str("                completion)\n");
288    script.push_str("                    local -a shells=(zsh bash)\n");
289    script.push_str("                    _describe 'shell' shells\n");
290    script.push_str("                    ;;\n");
291
292    // 编辑器类别名:文件路径补全
293    if !editor_pattern.is_empty() {
294        script.push_str(&format!("                {})\n", editor_pattern));
295        script.push_str("                    _files\n");
296        script.push_str("                    ;;\n");
297    }
298
299    // 浏览器类别名:别名 + 文件路径补全
300    if !browser_pattern.is_empty() {
301        script.push_str(&format!("                {})\n", browser_pattern));
302        script.push_str("                    _describe 'alias' aliases\n");
303        script.push_str("                    _files\n");
304        script.push_str("                    ;;\n");
305    }
306
307    // 其他别名:文件路径补全 + 别名补全(CLI 工具可能接受文件参数)
308    script.push_str("                *)\n");
309    script.push_str("                    _files\n");
310    script.push_str("                    _describe 'alias' aliases\n");
311    script.push_str("                    ;;\n");
312
313    script.push_str("            esac\n");
314    script.push_str("            ;;\n");
315    script.push_str("    esac\n");
316    script.push_str("}\n\n");
317    script.push_str("_j \"$@\"\n");
318
319    print!("{}", script);
320}
321
322/// 生成 bash 补全脚本
323fn generate_bash_completion(config: &YamlConfig) {
324    // 收集所有别名
325    let mut all_aliases = Vec::new();
326    for s in constants::ALIAS_EXISTS_SECTIONS {
327        if let Some(map) = config.get_section(s) {
328            for key in map.keys() {
329                if !all_aliases.contains(key) {
330                    all_aliases.push(key.clone());
331                }
332            }
333        }
334    }
335    all_aliases.sort();
336
337    let keywords = constants::cmd::all_keywords();
338    let all_completions: Vec<String> = keywords
339        .iter()
340        .map(|s| s.to_string())
341        .chain(all_aliases.iter().cloned())
342        .collect();
343
344    // 收集编辑器别名
345    let editor_aliases: Vec<String> = config
346        .get_section(section::EDITOR)
347        .map(|m| m.keys().cloned().collect())
348        .unwrap_or_default();
349
350    let mut script = String::new();
351    script.push_str("# Bash completion for j (work-copilot)\n");
352    script.push_str("# 生成方式: eval \"$(j completion bash)\"\n");
353    script.push_str("# 或: j completion bash > /etc/bash_completion.d/j\n\n");
354    script.push_str("_j_completion() {\n");
355    script.push_str("    local cur prev words cword\n");
356    script.push_str("    _init_completion || return\n\n");
357
358    script.push_str(&format!(
359        "    local commands=\"{}\"\n",
360        all_completions.join(" ")
361    ));
362    script.push_str(&format!(
363        "    local aliases=\"{}\"\n",
364        all_aliases.join(" ")
365    ));
366
367    if !editor_aliases.is_empty() {
368        script.push_str(&format!(
369            "    local editor_aliases=\"{}\"\n",
370            editor_aliases.join(" ")
371        ));
372    }
373
374    script.push_str("\n    if [[ $cword -eq 1 ]]; then\n");
375    script.push_str("        COMPREPLY=( $(compgen -W \"$commands\" -- \"$cur\") )\n");
376    script.push_str("        return\n");
377    script.push_str("    fi\n\n");
378
379    script.push_str("    case \"${words[1]}\" in\n");
380    script.push_str("        set|s|modify|mf)\n");
381    script.push_str("            if [[ $cword -eq 2 ]]; then\n");
382    script.push_str("                COMPREPLY=( $(compgen -W \"$aliases\" -- \"$cur\") )\n");
383    script.push_str("            else\n");
384    script.push_str("                _filedir\n");
385    script.push_str("            fi\n");
386    script.push_str("            ;;\n");
387    script.push_str("        rm|remove|rename|rn|note|nt|denote|dnt|contain|find)\n");
388    script.push_str("            COMPREPLY=( $(compgen -W \"$aliases\" -- \"$cur\") )\n");
389    script.push_str("            ;;\n");
390    script.push_str("        reportctl|rctl)\n");
391    script.push_str(
392        "            COMPREPLY=( $(compgen -W \"new sync push pull set-url open\" -- \"$cur\") )\n",
393    );
394    script.push_str("            ;;\n");
395
396    // 编辑器别名:文件路径补全
397    if !editor_aliases.is_empty() {
398        for alias in &editor_aliases {
399            script.push_str(&format!("        {})\n", alias));
400            script.push_str("            _filedir\n");
401            script.push_str("            ;;\n");
402        }
403    }
404
405    script.push_str("        *)\n");
406    script.push_str("            _filedir\n");
407    script.push_str("            COMPREPLY+=( $(compgen -W \"$aliases\" -- \"$cur\") )\n");
408    script.push_str("            ;;\n");
409    script.push_str("    esac\n");
410    script.push_str("}\n\n");
411    script.push_str("complete -F _j_completion j\n");
412
413    print!("{}", script);
414}