Skip to main content

j_cli/
interactive.rs

1use crate::command;
2use crate::config::YamlConfig;
3use crate::constants::{self, cmd, config_key, rmeta_action, time_function, search_flag, shell, NOTE_CATEGORIES, ALL_SECTIONS, ALIAS_PATH_SECTIONS, LIST_ALL};
4use crate::{info, error};
5use colored::Colorize;
6use rustyline::completion::{Completer, Pair};
7use rustyline::error::ReadlineError;
8use rustyline::highlight::Highlighter;
9use rustyline::hint::{Hinter, HistoryHinter};
10use rustyline::history::DefaultHistory;
11use rustyline::validate::Validator;
12use rustyline::highlight::CmdKind;
13use rustyline::{Cmd, CompletionType, Config, Context, EditMode, Editor, EventHandler, KeyCode, KeyEvent, Modifiers};
14use std::borrow::Cow;
15
16// ========== 补全器定义 ==========
17
18/// 自定义补全器:根据上下文提供命令、别名、分类等补全
19struct CopilotCompleter {
20    config: YamlConfig,
21}
22
23impl CopilotCompleter {
24    fn new(config: &YamlConfig) -> Self {
25        Self {
26            config: config.clone(),
27        }
28    }
29
30    /// 刷新配置(别名可能在交互过程中发生变化)
31    fn refresh(&mut self, config: &YamlConfig) {
32        self.config = config.clone();
33    }
34
35    /// 获取所有别名列表(用于补全)
36    fn all_aliases(&self) -> Vec<String> {
37        let mut aliases = Vec::new();
38        for s in ALIAS_PATH_SECTIONS {
39            if let Some(map) = self.config.get_section(s) {
40                aliases.extend(map.keys().cloned());
41            }
42        }
43        aliases.sort();
44        aliases.dedup();
45        aliases
46    }
47
48    /// 所有 section 名称(用于 ls / change 等补全)
49    fn all_sections(&self) -> Vec<String> {
50        self.config
51            .all_section_names()
52            .iter()
53            .map(|s| s.to_string())
54            .collect()
55    }
56
57    /// 指定 section 下的所有 key(用于 change 第三个参数补全)
58    fn section_keys(&self, section: &str) -> Vec<String> {
59        self.config
60            .get_section(section)
61            .map(|m| m.keys().cloned().collect())
62            .unwrap_or_default()
63    }
64}
65
66/// 命令定义:(命令名列表, 参数位置补全策略)
67/// 参数位置策略: Alias = 别名补全, Category = 分类补全, Section = section补全, File = 文件路径提示, Fixed = 固定选项
68#[derive(Clone)]
69#[allow(dead_code)]
70enum ArgHint {
71    Alias,
72    Category,
73    Section,
74    SectionKeys(String), // 依赖上一个参数的 section 名
75    Fixed(Vec<&'static str>),
76    Placeholder(&'static str),
77    FilePath, // 文件系统路径补全
78    None,
79}
80
81/// 获取命令的补全规则定义
82fn command_completion_rules() -> Vec<(&'static [&'static str], Vec<ArgHint>)> {
83    vec![
84        // 别名管理
85        (cmd::SET, vec![ArgHint::Placeholder("<alias>"), ArgHint::FilePath]),
86        (cmd::REMOVE, vec![ArgHint::Alias]),
87        (cmd::RENAME, vec![ArgHint::Alias, ArgHint::Placeholder("<new_alias>")]),
88        (cmd::MODIFY, vec![ArgHint::Alias, ArgHint::FilePath]),
89        // 分类
90        (cmd::NOTE, vec![ArgHint::Alias, ArgHint::Category]),
91        (cmd::DENOTE, vec![ArgHint::Alias, ArgHint::Category]),
92        // 列表
93        (cmd::LIST, vec![ArgHint::Fixed({
94            let mut v: Vec<&'static str> = vec!["", LIST_ALL];
95            for s in ALL_SECTIONS { v.push(s); }
96            v
97        })]),
98        // 查找
99        (cmd::CONTAIN, vec![ArgHint::Alias, ArgHint::Placeholder("<sections>")]),
100        // 系统设置
101        (cmd::LOG, vec![ArgHint::Fixed(vec![config_key::MODE]), ArgHint::Fixed(vec![config_key::VERBOSE, config_key::CONCISE])]),
102        (cmd::CHANGE, vec![ArgHint::Section, ArgHint::Placeholder("<field>"), ArgHint::Placeholder("<value>")]),
103        // 日报系统
104        (cmd::REPORT, vec![ArgHint::Placeholder("<content>")]),
105        (cmd::REPORTCTL, vec![ArgHint::Fixed(vec![rmeta_action::NEW, rmeta_action::SYNC, rmeta_action::PUSH, rmeta_action::PULL, rmeta_action::SET_URL, rmeta_action::OPEN]), ArgHint::Placeholder("<date|message|url>")]),
106        (cmd::CHECK, vec![ArgHint::Placeholder("<line_count>")]),
107        (cmd::SEARCH, vec![ArgHint::Placeholder("<line_count|all>"), ArgHint::Placeholder("<target>"), ArgHint::Fixed(vec![search_flag::FUZZY_SHORT, search_flag::FUZZY])]),
108        // 脚本
109        (cmd::CONCAT, vec![ArgHint::Placeholder("<script_name>"), ArgHint::Placeholder("<script_content>")]),
110        // 倒计时
111        (cmd::TIME, vec![ArgHint::Fixed(vec![time_function::COUNTDOWN]), ArgHint::Placeholder("<duration>")]),
112        // shell 补全
113        (cmd::COMPLETION, vec![ArgHint::Fixed(vec!["zsh", "bash"])]),
114        // 系统信息
115        (cmd::VERSION, vec![]),
116        (cmd::HELP, vec![]),
117        (cmd::CLEAR, vec![]),
118        (cmd::EXIT, vec![]),
119    ]
120}
121
122/// 分类常量(引用全局常量)
123const ALL_NOTE_CATEGORIES: &[&str] = NOTE_CATEGORIES;
124
125impl Completer for CopilotCompleter {
126    type Candidate = Pair;
127
128    fn complete(
129        &self,
130        line: &str,
131        pos: usize,
132        _ctx: &Context<'_>,
133    ) -> rustyline::Result<(usize, Vec<Pair>)> {
134        let line_to_cursor = &line[..pos];
135        let parts: Vec<&str> = line_to_cursor.split_whitespace().collect();
136
137        // 判断光标处是否在空格之后(即准备输入新 token)
138        let trailing_space = line_to_cursor.ends_with(' ');
139        let word_index = if trailing_space {
140            parts.len()
141        } else {
142            parts.len().saturating_sub(1)
143        };
144
145        let current_word = if trailing_space {
146            ""
147        } else {
148            parts.last().copied().unwrap_or("")
149        };
150
151        let start_pos = pos - current_word.len();
152
153        // Shell 命令(! 前缀):对所有参数提供文件路径补全
154        if !parts.is_empty() && (parts[0] == "!" || parts[0].starts_with('!')) {
155            // ! 后面的所有参数都支持文件路径补全
156            let candidates = complete_file_path(current_word);
157            return Ok((start_pos, candidates));
158        }
159
160        if word_index == 0 {
161            // 第一个词:补全命令名 + 别名
162            let mut candidates = Vec::new();
163
164            // 内置命令
165            let rules = command_completion_rules();
166            for (names, _) in &rules {
167                for name in *names {
168                    if name.starts_with(current_word) {
169                        candidates.push(Pair {
170                            display: name.to_string(),
171                            replacement: name.to_string(),
172                        });
173                    }
174                }
175            }
176
177            // 别名(用于 j <alias> 直接打开)
178            for alias in self.all_aliases() {
179                if alias.starts_with(current_word) && !command::all_command_keywords().contains(&alias.as_str()) {
180                    candidates.push(Pair {
181                        display: alias.clone(),
182                        replacement: alias,
183                    });
184                }
185            }
186
187            return Ok((start_pos, candidates));
188        }
189
190        // 后续参数:根据第一个词确定补全策略
191        let cmd = parts[0];
192        let rules = command_completion_rules();
193
194        for (names, arg_hints) in &rules {
195            if names.contains(&cmd) {
196                let arg_index = word_index - 1; // 减去命令本身
197                if arg_index < arg_hints.len() {
198                    let candidates = match &arg_hints[arg_index] {
199                        ArgHint::Alias => {
200                            self.all_aliases()
201                                .into_iter()
202                                .filter(|a| a.starts_with(current_word))
203                                .map(|a| Pair { display: a.clone(), replacement: a })
204                                .collect()
205                        }
206                        ArgHint::Category => {
207                            ALL_NOTE_CATEGORIES
208                                .iter()
209                                .filter(|c| c.starts_with(current_word))
210                                .map(|c| Pair { display: c.to_string(), replacement: c.to_string() })
211                                .collect()
212                        }
213                        ArgHint::Section => {
214                            self.all_sections()
215                                .into_iter()
216                                .filter(|s| s.starts_with(current_word))
217                                .map(|s| Pair { display: s.clone(), replacement: s })
218                                .collect()
219                        }
220                        ArgHint::SectionKeys(section) => {
221                            self.section_keys(section)
222                                .into_iter()
223                                .filter(|k| k.starts_with(current_word))
224                                .map(|k| Pair { display: k.clone(), replacement: k })
225                                .collect()
226                        }
227                        ArgHint::Fixed(options) => {
228                            options
229                                .iter()
230                                .filter(|o| !o.is_empty() && o.starts_with(current_word))
231                                .map(|o| Pair { display: o.to_string(), replacement: o.to_string() })
232                                .collect()
233                        }
234                        ArgHint::Placeholder(_) => {
235                            // placeholder 不提供候选项
236                            vec![]
237                        }
238                        ArgHint::FilePath => {
239                            // 文件系统路径补全
240                            complete_file_path(current_word)
241                        }
242                        ArgHint::None => vec![],
243                    };
244                    return Ok((start_pos, candidates));
245                }
246                break;
247            }
248        }
249
250        // 如果第一个词是别名(非命令),根据别名类型智能补全后续参数
251        if self.config.alias_exists(cmd) {
252            // 编辑器类别名:后续参数补全文件路径(如 vscode ./src<Tab>)
253            if self.config.contains(constants::section::EDITOR, cmd) {
254                let candidates = complete_file_path(current_word);
255                return Ok((start_pos, candidates));
256            }
257
258            // 浏览器类别名:后续参数补全 URL 别名 + 文件路径
259            if self.config.contains(constants::section::BROWSER, cmd) {
260                let mut candidates: Vec<Pair> = self.all_aliases()
261                    .into_iter()
262                    .filter(|a| a.starts_with(current_word))
263                    .map(|a| Pair { display: a.clone(), replacement: a })
264                    .collect();
265                // 也支持文件路径补全(浏览器打开本地文件)
266                candidates.extend(complete_file_path(current_word));
267                return Ok((start_pos, candidates));
268            }
269
270            // 其他别名(如 CLI 工具):后续参数补全文件路径 + 别名
271            let mut candidates = complete_file_path(current_word);
272            candidates.extend(
273                self.all_aliases()
274                    .into_iter()
275                    .filter(|a| a.starts_with(current_word))
276                    .map(|a| Pair { display: a.clone(), replacement: a })
277            );
278            return Ok((start_pos, candidates));
279        }
280
281        Ok((start_pos, vec![]))
282    }
283}
284
285// ========== Hinter:基于历史的自动建议 ==========
286
287struct CopilotHinter {
288    history_hinter: HistoryHinter,
289}
290
291impl CopilotHinter {
292    fn new() -> Self {
293        Self {
294            history_hinter: HistoryHinter::new(),
295        }
296    }
297}
298
299impl Hinter for CopilotHinter {
300    type Hint = String;
301
302    fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option<String> {
303        self.history_hinter.hint(line, pos, ctx)
304    }
305}
306
307// ========== Highlighter:提示文字灰色显示 ==========
308
309struct CopilotHighlighter;
310
311impl Highlighter for CopilotHighlighter {
312    fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
313        // 灰色显示 hint
314        Cow::Owned(format!("\x1b[90m{}\x1b[0m", hint))
315    }
316
317    fn highlight_char(&self, _line: &str, _pos: usize, _forced: CmdKind) -> bool {
318        // 返回 true 让 highlight_hint 生效
319        true
320    }
321}
322
323// ========== 组合 Helper ==========
324
325struct CopilotHelper {
326    completer: CopilotCompleter,
327    hinter: CopilotHinter,
328    highlighter: CopilotHighlighter,
329}
330
331impl CopilotHelper {
332    fn new(config: &YamlConfig) -> Self {
333        Self {
334            completer: CopilotCompleter::new(config),
335            hinter: CopilotHinter::new(),
336            highlighter: CopilotHighlighter,
337        }
338    }
339
340    fn refresh(&mut self, config: &YamlConfig) {
341        self.completer.refresh(config);
342    }
343}
344
345impl Completer for CopilotHelper {
346    type Candidate = Pair;
347
348    fn complete(
349        &self,
350        line: &str,
351        pos: usize,
352        ctx: &Context<'_>,
353    ) -> rustyline::Result<(usize, Vec<Pair>)> {
354        self.completer.complete(line, pos, ctx)
355    }
356}
357
358impl Hinter for CopilotHelper {
359    type Hint = String;
360
361    fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option<String> {
362        self.hinter.hint(line, pos, ctx)
363    }
364}
365
366impl Highlighter for CopilotHelper {
367    fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
368        self.highlighter.highlight_hint(hint)
369    }
370
371    fn highlight_char(&self, line: &str, pos: usize, forced: CmdKind) -> bool {
372        self.highlighter.highlight_char(line, pos, forced)
373    }
374}
375
376impl Validator for CopilotHelper {}
377
378impl rustyline::Helper for CopilotHelper {}
379
380// ========== 交互模式入口 ==========
381
382/// 启动交互模式
383pub fn run_interactive(config: &mut YamlConfig) {
384    let rl_config = Config::builder()
385        .completion_type(CompletionType::Circular)
386        .edit_mode(EditMode::Emacs)
387        .auto_add_history(false) // 手动控制历史记录,report 内容不入历史(隐私保护)
388        .build();
389
390    let helper = CopilotHelper::new(config);
391
392    let mut rl: Editor<CopilotHelper, DefaultHistory> =
393        Editor::with_config(rl_config).expect("无法初始化编辑器");
394    rl.set_helper(Some(helper));
395
396    // Tab 键绑定到补全
397    rl.bind_sequence(
398        KeyEvent(KeyCode::Tab, Modifiers::NONE),
399        EventHandler::Simple(Cmd::Complete),
400    );
401
402    // 加载历史记录
403    let history_path = history_file_path();
404    let _ = rl.load_history(&history_path);
405
406    info!("{}", constants::WELCOME_MESSAGE);
407
408    // 进入交互模式时,将所有别名路径注入为当前进程的环境变量
409    inject_envs_to_process(config);
410
411    let prompt = format!("{} ", constants::INTERACTIVE_PROMPT.yellow());
412
413    loop {
414        match rl.readline(&prompt) {
415            Ok(line) => {
416                let input = line.trim();
417
418                if input.is_empty() {
419                    continue;
420                }
421
422                // Shell 命令前缀开头:执行 shell 命令
423                if input.starts_with(constants::SHELL_PREFIX) {
424                    let shell_cmd = &input[1..].trim();
425                    if shell_cmd.is_empty() {
426                        // 无命令:进入交互式 shell(状态延续,直到 exit 退出)
427                        enter_interactive_shell(config);
428                    } else {
429                        execute_shell_command(shell_cmd, config);
430                    }
431                    // Shell 命令记录到历史
432                    let _ = rl.add_history_entry(input);
433                    println!();
434                    continue;
435                }
436
437                // 解析并执行 copilot 命令
438                let args = parse_input(input);
439                if args.is_empty() {
440                    continue;
441                }
442
443                // 展开参数中的环境变量引用(如 $J_HELLO → 实际路径)
444                let args: Vec<String> = args.iter().map(|a| expand_env_vars(a)).collect();
445
446                let verbose = config.is_verbose();
447                let start = if verbose {
448                    Some(std::time::Instant::now())
449                } else {
450                    None
451                };
452
453                // report 内容不记入历史(隐私保护),其他命令正常记录
454                let is_report_cmd = !args.is_empty() && cmd::REPORT.contains(&args[0].as_str());
455                if !is_report_cmd {
456                    let _ = rl.add_history_entry(input);
457                }
458
459                execute_interactive_command(&args, config);
460
461                if let Some(start) = start {
462                    let elapsed = start.elapsed();
463                    crate::debug_log!(config, "duration: {} ms", elapsed.as_millis());
464                }
465
466                // 每次命令执行后刷新补全器中的配置(别名可能已变化)
467                if let Some(helper) = rl.helper_mut() {
468                    helper.refresh(config);
469                }
470                // 刷新进程环境变量(别名可能已增删改)
471                inject_envs_to_process(config);
472
473                println!();
474            }
475            Err(ReadlineError::Interrupted) => {
476                // Ctrl+C
477                info!("\nProgram interrupted. Use 'exit' to quit.");
478            }
479            Err(ReadlineError::Eof) => {
480                // Ctrl+D
481                info!("\nGoodbye! 👋");
482                break;
483            }
484            Err(err) => {
485                error!("读取输入失败: {:?}", err);
486                break;
487            }
488        }
489    }
490
491    // 保存历史记录
492    let _ = rl.save_history(&history_path);
493}
494
495/// 获取历史文件路径: ~/.jdata/history.txt
496fn history_file_path() -> std::path::PathBuf {
497    let data_dir = crate::config::YamlConfig::data_dir();
498    // 确保目录存在
499    let _ = std::fs::create_dir_all(&data_dir);
500    data_dir.join(constants::HISTORY_FILE)
501}
502
503/// 解析用户输入为参数列表
504/// 支持双引号包裹带空格的参数,与 Java 版保持一致
505fn parse_input(input: &str) -> Vec<String> {
506    let mut args = Vec::new();
507    let mut current = String::new();
508    let mut in_quotes = false;
509
510    for ch in input.chars() {
511        match ch {
512            '"' => {
513                in_quotes = !in_quotes;
514            }
515            ' ' if !in_quotes => {
516                if !current.is_empty() {
517                    args.push(current.clone());
518                    current.clear();
519                }
520            }
521            _ => {
522                current.push(ch);
523            }
524        }
525    }
526
527    if !current.is_empty() {
528        args.push(current);
529    }
530
531    args
532}
533
534/// 交互命令解析结果(三态)
535enum ParseResult {
536    /// 成功解析为内置命令
537    Matched(crate::cli::SubCmd),
538    /// 是内置命令但参数不足,已打印 usage 提示
539    Handled,
540    /// 不是内置命令
541    NotFound,
542}
543
544/// 在交互模式下执行命令
545/// 与快捷模式不同,这里从解析后的 args 来分发命令
546fn execute_interactive_command(args: &[String], config: &mut YamlConfig) {
547    if args.is_empty() {
548        return;
549    }
550
551    let cmd_str = &args[0];
552
553    // 检查是否是退出命令
554    if cmd::EXIT.contains(&cmd_str.as_str()) {
555        command::system::handle_exit();
556        return;
557    }
558
559    // 尝试解析为内置命令
560    match parse_interactive_command(args) {
561        ParseResult::Matched(subcmd) => {
562            command::dispatch(subcmd, config);
563        }
564        ParseResult::Handled => {
565            // 内置命令参数不足,已打印 usage,无需额外处理
566        }
567        ParseResult::NotFound => {
568            // 不是内置命令,尝试作为别名打开
569            command::open::handle_open(args, config);
570        }
571    }
572}
573
574/// 从交互模式输入的参数解析出 SubCmd
575fn parse_interactive_command(args: &[String]) -> ParseResult {
576    use crate::cli::SubCmd;
577
578    if args.is_empty() {
579        return ParseResult::NotFound;
580    }
581
582    let cmd = args[0].as_str();
583    let rest = &args[1..];
584
585    // 使用闭包简化命令匹配:判断 cmd 是否在某个命令常量组中
586    let is = |names: &[&str]| names.contains(&cmd);
587
588    if is(cmd::SET) {
589        if rest.is_empty() {
590            crate::usage!("set <alias> <path>");
591            return ParseResult::Handled;
592        }
593        ParseResult::Matched(SubCmd::Set {
594            alias: rest[0].clone(),
595            path: rest[1..].to_vec(),
596        })
597    } else if is(cmd::REMOVE) {
598        match rest.first() {
599            Some(alias) => ParseResult::Matched(SubCmd::Remove { alias: alias.clone() }),
600            None => { crate::usage!("rm <alias>"); ParseResult::Handled }
601        }
602    } else if is(cmd::RENAME) {
603        if rest.len() < 2 {
604            crate::usage!("rename <alias> <new_alias>");
605            return ParseResult::Handled;
606        }
607        ParseResult::Matched(SubCmd::Rename {
608            alias: rest[0].clone(),
609            new_alias: rest[1].clone(),
610        })
611    } else if is(cmd::MODIFY) {
612        if rest.is_empty() {
613            crate::usage!("mf <alias> <new_path>");
614            return ParseResult::Handled;
615        }
616        ParseResult::Matched(SubCmd::Modify {
617            alias: rest[0].clone(),
618            path: rest[1..].to_vec(),
619        })
620
621    // 分类标记
622    } else if is(cmd::NOTE) {
623        if rest.len() < 2 {
624            crate::usage!("note <alias> <category>");
625            return ParseResult::Handled;
626        }
627        ParseResult::Matched(SubCmd::Note {
628            alias: rest[0].clone(),
629            category: rest[1].clone(),
630        })
631    } else if is(cmd::DENOTE) {
632        if rest.len() < 2 {
633            crate::usage!("denote <alias> <category>");
634            return ParseResult::Handled;
635        }
636        ParseResult::Matched(SubCmd::Denote {
637            alias: rest[0].clone(),
638            category: rest[1].clone(),
639        })
640
641    // 列表
642    } else if is(cmd::LIST) {
643        ParseResult::Matched(SubCmd::List {
644            part: rest.first().cloned(),
645        })
646
647    // 查找
648    } else if is(cmd::CONTAIN) {
649        if rest.is_empty() {
650            crate::usage!("contain <alias> [sections]");
651            return ParseResult::Handled;
652        }
653        ParseResult::Matched(SubCmd::Contain {
654            alias: rest[0].clone(),
655            containers: rest.get(1).cloned(),
656        })
657
658    // 系统设置
659    } else if is(cmd::LOG) {
660        if rest.len() < 2 {
661            crate::usage!("log mode <verbose|concise>");
662            return ParseResult::Handled;
663        }
664        ParseResult::Matched(SubCmd::Log {
665            key: rest[0].clone(),
666            value: rest[1].clone(),
667        })
668    } else if is(cmd::CHANGE) {
669        if rest.len() < 3 {
670            crate::usage!("change <part> <field> <value>");
671            return ParseResult::Handled;
672        }
673        ParseResult::Matched(SubCmd::Change {
674            part: rest[0].clone(),
675            field: rest[1].clone(),
676            value: rest[2].clone(),
677        })
678    } else if is(cmd::CLEAR) {
679        ParseResult::Matched(SubCmd::Clear)
680
681    // 日报系统
682    } else if is(cmd::REPORT) {
683        ParseResult::Matched(SubCmd::Report {
684            content: rest.to_vec(),
685        })
686    } else if is(cmd::REPORTCTL) {
687        if rest.is_empty() {
688            crate::usage!("reportctl <new|sync|push|pull|set-url> [date|message|url]");
689            return ParseResult::Handled;
690        }
691        ParseResult::Matched(SubCmd::Reportctl {
692            action: rest[0].clone(),
693            arg: rest.get(1).cloned(),
694        })
695    } else if is(cmd::CHECK) {
696        ParseResult::Matched(SubCmd::Check {
697            line_count: rest.first().cloned(),
698        })
699    } else if is(cmd::SEARCH) {
700        if rest.len() < 2 {
701            crate::usage!("search <line_count|all> <target> [-f|-fuzzy]");
702            return ParseResult::Handled;
703        }
704        ParseResult::Matched(SubCmd::Search {
705            line_count: rest[0].clone(),
706            target: rest[1].clone(),
707            fuzzy: rest.get(2).cloned(),
708        })
709
710    // 脚本创建
711    } else if is(cmd::CONCAT) {
712        if rest.is_empty() {
713            crate::usage!("concat <script_name> [\"<script_content>\"]");
714            return ParseResult::Handled;
715        }
716        ParseResult::Matched(SubCmd::Concat {
717            name: rest[0].clone(),
718            content: if rest.len() > 1 { rest[1..].to_vec() } else { vec![] },
719        })
720
721    // 倒计时
722    } else if is(cmd::TIME) {
723        if rest.len() < 2 {
724            crate::usage!("time countdown <duration>");
725            return ParseResult::Handled;
726        }
727        ParseResult::Matched(SubCmd::Time {
728            function: rest[0].clone(),
729            arg: rest[1].clone(),
730        })
731
732    // 系统信息
733    } else if is(cmd::VERSION) {
734        ParseResult::Matched(SubCmd::Version)
735    } else if is(cmd::HELP) {
736        ParseResult::Matched(SubCmd::Help)
737    } else if is(cmd::COMPLETION) {
738        ParseResult::Matched(SubCmd::Completion {
739            shell: rest.first().cloned(),
740        })
741
742    // 未匹配到内置命令
743    } else {
744        ParseResult::NotFound
745    }
746}
747
748/// 文件系统路径补全
749/// 根据用户已输入的部分路径,列出匹配的文件和目录
750fn complete_file_path(partial: &str) -> Vec<Pair> {
751    let mut candidates = Vec::new();
752
753    // 展开 ~ 为 home 目录
754    let expanded = if partial.starts_with('~') {
755        if let Some(home) = dirs::home_dir() {
756            partial.replacen('~', &home.to_string_lossy(), 1)
757        } else {
758            partial.to_string()
759        }
760    } else {
761        partial.to_string()
762    };
763
764    // 解析目录路径和文件名前缀
765    let (dir_path, file_prefix) = if expanded.ends_with('/') || expanded.ends_with(std::path::MAIN_SEPARATOR) {
766        (std::path::Path::new(&expanded).to_path_buf(), String::new())
767    } else {
768        let p = std::path::Path::new(&expanded);
769        let parent = p.parent().unwrap_or(std::path::Path::new(".")).to_path_buf();
770        let fp = p.file_name().map(|s| s.to_string_lossy().to_string()).unwrap_or_default();
771        (parent, fp)
772    };
773
774    if let Ok(entries) = std::fs::read_dir(&dir_path) {
775        for entry in entries.flatten() {
776            let name = entry.file_name().to_string_lossy().to_string();
777
778            // 跳过隐藏文件(除非用户已经输入了 .)
779            if name.starts_with('.') && !file_prefix.starts_with('.') {
780                continue;
781            }
782
783            if name.starts_with(&file_prefix) {
784                let is_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
785
786                // 构建完整路径用于替换
787                // 保留用户输入的原始前缀风格(如 ~ 或绝对路径)
788                let full_replacement = if partial.ends_with('/') || partial.ends_with(std::path::MAIN_SEPARATOR) {
789                    format!("{}{}{}", partial, name, if is_dir { "/" } else { "" })
790                } else if partial.contains('/') || partial.contains(std::path::MAIN_SEPARATOR) {
791                    // 替换最后一段
792                    let last_sep = partial.rfind('/').or_else(|| partial.rfind(std::path::MAIN_SEPARATOR)).unwrap();
793                    format!("{}/{}{}", &partial[..last_sep], name, if is_dir { "/" } else { "" })
794                } else {
795                    format!("{}{}", name, if is_dir { "/" } else { "" })
796                };
797
798                let display_name = format!("{}{}", name, if is_dir { "/" } else { "" });
799
800                candidates.push(Pair {
801                    display: display_name,
802                    replacement: full_replacement,
803                });
804            }
805        }
806    }
807
808    // 按名称排序,目录优先
809    candidates.sort_by(|a, b| a.display.cmp(&b.display));
810    candidates
811}
812
813/// 进入交互式 shell 子进程
814/// 启动用户默认 shell(从 $SHELL 环境变量获取),以交互模式运行
815/// 在 shell 中可以自由执行命令,cd 等状态会延续,直到 exit 退出回到 copilot
816fn enter_interactive_shell(config: &YamlConfig) {
817    let os = std::env::consts::OS;
818
819    let shell_path = if os == shell::WINDOWS_OS {
820        shell::WINDOWS_CMD.to_string()
821    } else {
822        // 优先使用用户默认 shell,fallback 到 /bin/bash
823        std::env::var("SHELL").unwrap_or_else(|_| shell::BASH_PATH.to_string())
824    };
825
826    info!("进入 shell 模式 ({}), 输入 exit 返回 copilot", shell_path);
827
828    let mut command = std::process::Command::new(&shell_path);
829
830    // 注入别名环境变量
831    for (key, value) in config.collect_alias_envs() {
832        command.env(&key, &value);
833    }
834
835    // 用于记录需要清理的临时目录/文件
836    let mut cleanup_path: Option<std::path::PathBuf> = None;
837
838    // 自定义 shell 提示符为 "shell > "
839    // 通过临时 rc 文件注入,先 source 用户配置再覆盖 PROMPT,确保不被 oh-my-zsh 等覆盖
840    if os != shell::WINDOWS_OS {
841        let is_zsh = shell_path.contains("zsh");
842        let is_bash = shell_path.contains("bash");
843
844        if is_zsh {
845            // zsh 方案:创建临时 ZDOTDIR 目录,写入自定义 .zshrc
846            // 在自定义 .zshrc 中先恢复 ZDOTDIR 并 source 用户原始配置,再覆盖 PROMPT
847            let pid = std::process::id();
848            let tmp_dir = std::path::PathBuf::from(format!("/tmp/j_shell_zsh_{}", pid));
849            let _ = std::fs::create_dir_all(&tmp_dir);
850
851            let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
852            let zshrc_content = format!(
853                "# j shell 临时配置 - 自动生成,退出后自动清理\n\
854                 # 恢复 ZDOTDIR 为用户 home 目录,让后续 source 正常工作\n\
855                 export ZDOTDIR=\"{home}\"\n\
856                 # 加载用户原始 .zshrc(保留所有配置、alias、插件等)\n\
857                 if [ -f \"{home}/.zshrc\" ]; then\n\
858                   source \"{home}/.zshrc\"\n\
859                 fi\n\
860                 # 在用户配置加载完成后覆盖 PROMPT,确保不被 oh-my-zsh 等覆盖\n\
861                 PROMPT='%F{{green}}shell%f (%F{{cyan}}%~%f) %F{{green}}>%f '\n",
862                home = home,
863            );
864
865            let zshrc_path = tmp_dir.join(".zshrc");
866            if let Err(e) = std::fs::write(&zshrc_path, &zshrc_content) {
867                error!("创建临时 .zshrc 失败: {}", e);
868                // fallback: 直接设置环境变量(可能被覆盖)
869                command.env("PROMPT", "%F{green}shell%f (%F{cyan}%~%f) %F{green}>%f ");
870            } else {
871                command.env("ZDOTDIR", tmp_dir.to_str().unwrap_or("/tmp"));
872                cleanup_path = Some(tmp_dir);
873            }
874        } else if is_bash {
875            // bash 方案:创建临时 rc 文件,用 --rcfile 加载
876            let pid = std::process::id();
877            let tmp_rc = std::path::PathBuf::from(format!("/tmp/j_shell_bashrc_{}", pid));
878
879            let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
880            let bashrc_content = format!(
881                "# j shell 临时配置 - 自动生成,退出后自动清理\n\
882                 # 加载用户原始 .bashrc(保留所有配置、alias 等)\n\
883                 if [ -f \"{home}/.bashrc\" ]; then\n\
884                   source \"{home}/.bashrc\"\n\
885                 fi\n\
886                 # 在用户配置加载完成后覆盖 PS1
887                 PS1='\\[\\033[32m\\]shell\\[\\033[0m\\] (\\[\\033[36m\\]\\w\\[\\033[0m\\]) \\[\\033[32m\\]>\\[\\033[0m\\] '\n",
888                home = home,
889            );
890
891            if let Err(e) = std::fs::write(&tmp_rc, &bashrc_content) {
892                error!("创建临时 bashrc 失败: {}", e);
893                command.env("PS1", "\\[\\033[32m\\]shell\\[\\033[0m\\] (\\[\\033[36m\\]\\w\\[\\033[0m\\]) \\[\\033[32m\\]>\\[\\033[0m\\] ");
894            } else {
895                command.arg("--rcfile");
896                command.arg(tmp_rc.to_str().unwrap_or("/tmp/j_shell_bashrc"));
897                cleanup_path = Some(tmp_rc);
898            }
899        } else {
900            // 其他 shell:fallback 到直接设置环境变量
901            command.env("PS1", "\x1b[32mshell\x1b[0m (\x1b[36m\\w\x1b[0m) \x1b[32m>\x1b[0m ");
902            command.env("PROMPT", "\x1b[32mshell\x1b[0m (\x1b[36m%~\x1b[0m) \x1b[32m>\x1b[0m ");
903        }
904    }
905
906    // 继承 stdin/stdout/stderr,让 shell 完全接管终端
907    command
908        .stdin(std::process::Stdio::inherit())
909        .stdout(std::process::Stdio::inherit())
910        .stderr(std::process::Stdio::inherit());
911
912    match command.status() {
913        Ok(status) => {
914            if !status.success() {
915                if let Some(code) = status.code() {
916                    error!("shell 退出码: {}", code);
917                }
918            }
919        }
920        Err(e) => {
921            error!("启动 shell 失败: {}", e);
922        }
923    }
924
925    // 清理临时文件/目录
926    if let Some(path) = cleanup_path {
927        if path.is_dir() {
928            let _ = std::fs::remove_dir_all(&path);
929        } else {
930            let _ = std::fs::remove_file(&path);
931        }
932    }
933
934    info!("{}", "已返回 copilot 交互模式 🚀".green());
935}
936
937/// 执行 shell 命令(交互模式下 ! 前缀触发)
938/// 自动注入所有别名路径为环境变量(J_<ALIAS_UPPER>)
939fn execute_shell_command(cmd: &str, config: &YamlConfig) {
940    if cmd.is_empty() {
941        return;
942    }
943
944    let os = std::env::consts::OS;
945    let mut command = if os == shell::WINDOWS_OS {
946        let mut c = std::process::Command::new(shell::WINDOWS_CMD);
947        c.args([shell::WINDOWS_CMD_FLAG, cmd]);
948        c
949    } else {
950        let mut c = std::process::Command::new(shell::BASH_PATH);
951        c.args([shell::BASH_CMD_FLAG, cmd]);
952        c
953    };
954
955    // 注入别名环境变量
956    for (key, value) in config.collect_alias_envs() {
957        command.env(&key, &value);
958    }
959
960    let result = command.status();
961
962    match result {
963        Ok(status) => {
964            if !status.success() {
965                if let Some(code) = status.code() {
966                    error!("命令退出码: {}", code);
967                }
968            }
969        }
970        Err(e) => {
971            error!("执行命令失败: {}", e);
972        }
973    }
974}
975
976/// 将所有别名路径注入为当前进程的环境变量
977/// 这样在交互模式下,参数中的 $J_XXX 可以被正确展开
978fn inject_envs_to_process(config: &YamlConfig) {
979    for (key, value) in config.collect_alias_envs() {
980        // SAFETY: 交互模式为单线程,set_var 不会引起数据竞争
981        unsafe {
982            std::env::set_var(&key, &value);
983        }
984    }
985}
986
987/// 展开字符串中的环境变量引用
988/// 支持 $VAR_NAME 和 ${VAR_NAME} 两种格式
989fn expand_env_vars(input: &str) -> String {
990    let mut result = String::with_capacity(input.len());
991    let chars: Vec<char> = input.chars().collect();
992    let len = chars.len();
993    let mut i = 0;
994
995    while i < len {
996        if chars[i] == '$' && i + 1 < len {
997            // ${VAR_NAME} 格式
998            if chars[i + 1] == '{' {
999                if let Some(end) = chars[i + 2..].iter().position(|&c| c == '}') {
1000                    let var_name: String = chars[i + 2..i + 2 + end].iter().collect();
1001                    if let Ok(val) = std::env::var(&var_name) {
1002                        result.push_str(&val);
1003                    } else {
1004                        // 环境变量不存在,保留原文
1005                        result.push_str(&input[i..i + 3 + end]);
1006                    }
1007                    i = i + 3 + end;
1008                    continue;
1009                }
1010            }
1011            // $VAR_NAME 格式(变量名由字母、数字、下划线组成)
1012            let start = i + 1;
1013            let mut end = start;
1014            while end < len && (chars[end].is_alphanumeric() || chars[end] == '_') {
1015                end += 1;
1016            }
1017            if end > start {
1018                let var_name: String = chars[start..end].iter().collect();
1019                if let Ok(val) = std::env::var(&var_name) {
1020                    result.push_str(&val);
1021                } else {
1022                    // 环境变量不存在,保留原文
1023                    let original: String = chars[i..end].iter().collect();
1024                    result.push_str(&original);
1025                }
1026                i = end;
1027                continue;
1028            }
1029        }
1030        result.push(chars[i]);
1031        i += 1;
1032    }
1033
1034    result
1035}