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
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 (
167 cmd::CONCAT,
168 vec![
169 ArgHint::Placeholder("<script_name>"),
170 ArgHint::Placeholder("<script_content>"),
171 ],
172 ),
173 (
175 cmd::TIME,
176 vec![
177 ArgHint::Fixed(vec![time_function::COUNTDOWN]),
178 ArgHint::Placeholder("<duration>"),
179 ],
180 ),
181 (cmd::COMPLETION, vec![ArgHint::Fixed(vec!["zsh", "bash"])]),
183 (cmd::VERSION, vec![]),
185 (cmd::HELP, vec![]),
186 (cmd::CLEAR, vec![]),
187 (cmd::EXIT, vec![]),
188 ]
189}
190
191const 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 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 if !parts.is_empty() && (parts[0] == "!" || parts[0].starts_with('!')) {
224 let candidates = complete_file_path(current_word);
226 return Ok((start_pos, candidates));
227 }
228
229 if word_index == 0 {
230 let mut candidates = Vec::new();
232
233 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 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 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; 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 vec![]
316 }
317 ArgHint::FilePath => {
318 complete_file_path(current_word)
320 }
321 ArgHint::None => vec![],
322 };
323 return Ok((start_pos, candidates));
324 }
325 break;
326 }
327 }
328
329 if self.config.alias_exists(cmd) {
331 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 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 candidates.extend(complete_file_path(current_word));
350 return Ok((start_pos, candidates));
351 }
352
353 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
371struct 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
393struct CopilotHighlighter;
396
397impl Highlighter for CopilotHighlighter {
398 fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
399 Cow::Owned(format!("\x1b[90m{}\x1b[0m", hint))
401 }
402
403 fn highlight_char(&self, _line: &str, _pos: usize, _forced: CmdKind) -> bool {
404 true
406 }
407}
408
409struct 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
466pub 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) .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 rl.bind_sequence(
484 KeyEvent(KeyCode::Tab, Modifiers::NONE),
485 EventHandler::Simple(Cmd::Complete),
486 );
487
488 let history_path = history_file_path();
490 let _ = rl.load_history(&history_path);
491
492 info!("{}", constants::WELCOME_MESSAGE);
493
494 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 if input.starts_with(constants::SHELL_PREFIX) {
510 let shell_cmd = &input[1..].trim();
511 if shell_cmd.is_empty() {
512 enter_interactive_shell(config);
514 } else {
515 execute_shell_command(shell_cmd, config);
516 }
517 let _ = rl.add_history_entry(input);
519 println!();
520 continue;
521 }
522
523 let args = parse_input(input);
525 if args.is_empty() {
526 continue;
527 }
528
529 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 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 if let Some(helper) = rl.helper_mut() {
554 helper.refresh(config);
555 }
556 inject_envs_to_process(config);
558
559 println!();
560 }
561 Err(ReadlineError::Interrupted) => {
562 info!("\nProgram interrupted. Use 'exit' to quit.");
564 }
565 Err(ReadlineError::Eof) => {
566 info!("\nGoodbye! 👋");
568 break;
569 }
570 Err(err) => {
571 error!("读取输入失败: {:?}", err);
572 break;
573 }
574 }
575 }
576
577 let _ = rl.save_history(&history_path);
579}
580
581fn history_file_path() -> std::path::PathBuf {
583 let data_dir = crate::config::YamlConfig::data_dir();
584 let _ = std::fs::create_dir_all(&data_dir);
586 data_dir.join(constants::HISTORY_FILE)
587}
588
589fn 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
620enum ParseResult {
622 Matched(crate::cli::SubCmd),
624 Handled,
626 NotFound,
628}
629
630fn 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 if cmd::EXIT.contains(&cmd_str.as_str()) {
641 command::system::handle_exit();
642 return;
643 }
644
645 match parse_interactive_command(args) {
647 ParseResult::Matched(subcmd) => {
648 command::dispatch(subcmd, config);
649 }
650 ParseResult::Handled => {
651 }
653 ParseResult::NotFound => {
654 command::open::handle_open(args, config);
656 }
657 }
658}
659
660fn 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 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 } 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 } else if is(cmd::LIST) {
734 ParseResult::Matched(SubCmd::List {
735 part: rest.first().cloned(),
736 })
737
738 } 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 } 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 } 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 } else if is(cmd::TODO) {
803 ParseResult::Matched(SubCmd::Todo {
804 content: rest.to_vec(),
805 })
806
807 } else if is(cmd::CHAT) {
809 ParseResult::Matched(SubCmd::Chat {
810 content: rest.to_vec(),
811 })
812
813 } 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 } 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 } 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 } else {
851 ParseResult::NotFound
852 }
853}
854
855fn complete_file_path(partial: &str) -> Vec<Pair> {
858 let mut candidates = Vec::new();
859
860 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 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 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 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 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 candidates.sort_by(|a, b| a.display.cmp(&b.display));
933 candidates
934}
935
936fn 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 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 for (key, value) in config.collect_alias_envs() {
955 command.env(&key, &value);
956 }
957
958 let mut cleanup_path: Option<std::path::PathBuf> = None;
960
961 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 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 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 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 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 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 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
1066fn 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 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
1105fn inject_envs_to_process(config: &YamlConfig) {
1108 for (key, value) in config.collect_alias_envs() {
1109 unsafe {
1111 std::env::set_var(&key, &value);
1112 }
1113 }
1114}
1115
1116fn 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 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 result.push_str(&input[i..i + 3 + end]);
1135 }
1136 i = i + 3 + end;
1137 continue;
1138 }
1139 }
1140 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 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}