Skip to main content

j_cli/command/
system.rs

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