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