Skip to main content

j_cli/command/
system.rs

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