Skip to main content

j_cli/
interactive.rs

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