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
22struct CopilotCompleter {
26 config: YamlConfig,
27}
28
29impl CopilotCompleter {
30 fn new(config: &YamlConfig) -> Self {
31 Self {
32 config: config.clone(),
33 }
34 }
35
36 fn refresh(&mut self, config: &YamlConfig) {
38 self.config = config.clone();
39 }
40
41 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 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 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#[derive(Clone)]
75#[allow(dead_code)]
76enum ArgHint {
77 Alias,
78 Category,
79 Section,
80 SectionKeys(String), Fixed(Vec<&'static str>),
82 Placeholder(&'static str),
83 FilePath, None,
85}
86
87fn command_completion_rules() -> Vec<(&'static [&'static str], Vec<ArgHint>)> {
89 vec![
90 (
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 (cmd::NOTE, vec![ArgHint::Alias, ArgHint::Category]),
103 (cmd::DENOTE, vec![ArgHint::Alias, ArgHint::Category]),
104 (
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 (
117 cmd::CONTAIN,
118 vec![ArgHint::Alias, ArgHint::Placeholder("<sections>")],
119 ),
120 (
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 (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 (cmd::TODO, vec![ArgHint::Placeholder("<content>")]),
163 (cmd::CHAT, vec![ArgHint::Placeholder("<message>")]),
165 (cmd::VOICE, vec![ArgHint::Fixed(vec![vc::ACTION_DOWNLOAD])]),
167 (
169 cmd::CONCAT,
170 vec![
171 ArgHint::Placeholder("<script_name>"),
172 ArgHint::Placeholder("<script_content>"),
173 ],
174 ),
175 (
177 cmd::TIME,
178 vec![
179 ArgHint::Fixed(vec![time_function::COUNTDOWN]),
180 ArgHint::Placeholder("<duration>"),
181 ],
182 ),
183 (cmd::COMPLETION, vec![ArgHint::Fixed(vec!["zsh", "bash"])]),
185 (cmd::VERSION, vec![]),
187 (cmd::HELP, vec![]),
188 (cmd::CLEAR, vec![]),
189 (cmd::EXIT, vec![]),
190 ]
191}
192
193const 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 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 if !parts.is_empty() && (parts[0] == "!" || parts[0].starts_with('!')) {
226 let candidates = complete_file_path(current_word);
228 return Ok((start_pos, candidates));
229 }
230
231 if word_index == 0 {
232 let mut candidates = Vec::new();
234
235 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 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 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; 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 vec![]
318 }
319 ArgHint::FilePath => {
320 complete_file_path(current_word)
322 }
323 ArgHint::None => vec![],
324 };
325 return Ok((start_pos, candidates));
326 }
327 break;
328 }
329 }
330
331 if self.config.alias_exists(cmd) {
333 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 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 candidates.extend(complete_file_path(current_word));
352 return Ok((start_pos, candidates));
353 }
354
355 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
373struct 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
395struct CopilotHighlighter;
398
399impl Highlighter for CopilotHighlighter {
400 fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
401 Cow::Owned(format!("\x1b[90m{}\x1b[0m", hint))
403 }
404
405 fn highlight_char(&self, _line: &str, _pos: usize, _forced: CmdKind) -> bool {
406 true
408 }
409}
410
411struct 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
468pub 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) .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 rl.bind_sequence(
486 KeyEvent(KeyCode::Tab, Modifiers::NONE),
487 EventHandler::Simple(Cmd::Complete),
488 );
489
490 let history_path = history_file_path();
492 let _ = rl.load_history(&history_path);
493
494 info!("{}", constants::WELCOME_MESSAGE);
495
496 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 if input.starts_with(constants::SHELL_PREFIX) {
512 let shell_cmd = &input[1..].trim();
513 if shell_cmd.is_empty() {
514 enter_interactive_shell(config);
516 } else {
517 execute_shell_command(shell_cmd, config);
518 }
519 let _ = rl.add_history_entry(input);
521 println!();
522 continue;
523 }
524
525 let args = parse_input(input);
527 if args.is_empty() {
528 continue;
529 }
530
531 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 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 if let Some(helper) = rl.helper_mut() {
556 helper.refresh(config);
557 }
558 inject_envs_to_process(config);
560
561 println!();
562 }
563 Err(ReadlineError::Interrupted) => {
564 info!("\nProgram interrupted. Use 'exit' to quit.");
566 }
567 Err(ReadlineError::Eof) => {
568 info!("\nGoodbye! 👋");
570 break;
571 }
572 Err(err) => {
573 error!("读取输入失败: {:?}", err);
574 break;
575 }
576 }
577 }
578
579 let _ = rl.save_history(&history_path);
581}
582
583fn history_file_path() -> std::path::PathBuf {
585 let data_dir = crate::config::YamlConfig::data_dir();
586 let _ = std::fs::create_dir_all(&data_dir);
588 data_dir.join(constants::HISTORY_FILE)
589}
590
591fn 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
622enum ParseResult {
624 Matched(crate::cli::SubCmd),
626 Handled,
628 NotFound,
630}
631
632fn 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 if cmd::EXIT.contains(&cmd_str.as_str()) {
643 command::system::handle_exit();
644 return;
645 }
646
647 match parse_interactive_command(args) {
649 ParseResult::Matched(subcmd) => {
650 command::dispatch(subcmd, config);
651 }
652 ParseResult::Handled => {
653 }
655 ParseResult::NotFound => {
656 command::open::handle_open(args, config);
658 }
659 }
660}
661
662fn 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 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 } 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 } else if is(cmd::LIST) {
736 ParseResult::Matched(SubCmd::List {
737 part: rest.first().cloned(),
738 })
739
740 } 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 } 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 } 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 } else if is(cmd::TODO) {
805 ParseResult::Matched(SubCmd::Todo {
806 content: rest.to_vec(),
807 })
808
809 } else if is(cmd::CHAT) {
811 ParseResult::Matched(SubCmd::Chat {
812 content: rest.to_vec(),
813 })
814
815 } 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 } 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 } 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 } 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 } else {
864 ParseResult::NotFound
865 }
866}
867
868fn complete_file_path(partial: &str) -> Vec<Pair> {
871 let mut candidates = Vec::new();
872
873 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 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 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 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 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 candidates.sort_by(|a, b| a.display.cmp(&b.display));
946 candidates
947}
948
949fn 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 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 for (key, value) in config.collect_alias_envs() {
968 command.env(&key, &value);
969 }
970
971 let mut cleanup_path: Option<std::path::PathBuf> = None;
973
974 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 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 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 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 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 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 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
1079fn 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 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
1118fn inject_envs_to_process(config: &YamlConfig) {
1121 for (key, value) in config.collect_alias_envs() {
1122 unsafe {
1124 std::env::set_var(&key, &value);
1125 }
1126 }
1127}
1128
1129fn 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 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 result.push_str(&input[i..i + 3 + end]);
1148 }
1149 i = i + 3 + end;
1150 continue;
1151 }
1152 }
1153 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 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}