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