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 (
165 cmd::CONCAT,
166 vec![
167 ArgHint::Placeholder("<script_name>"),
168 ArgHint::Placeholder("<script_content>"),
169 ],
170 ),
171 (
173 cmd::TIME,
174 vec![
175 ArgHint::Fixed(vec![time_function::COUNTDOWN]),
176 ArgHint::Placeholder("<duration>"),
177 ],
178 ),
179 (cmd::COMPLETION, vec![ArgHint::Fixed(vec!["zsh", "bash"])]),
181 (cmd::VERSION, vec![]),
183 (cmd::HELP, vec![]),
184 (cmd::CLEAR, vec![]),
185 (cmd::EXIT, vec![]),
186 ]
187}
188
189const ALL_NOTE_CATEGORIES: &[&str] = NOTE_CATEGORIES;
191
192impl Completer for CopilotCompleter {
193 type Candidate = Pair;
194
195 fn complete(
196 &self,
197 line: &str,
198 pos: usize,
199 _ctx: &Context<'_>,
200 ) -> rustyline::Result<(usize, Vec<Pair>)> {
201 let line_to_cursor = &line[..pos];
202 let parts: Vec<&str> = line_to_cursor.split_whitespace().collect();
203
204 let trailing_space = line_to_cursor.ends_with(' ');
206 let word_index = if trailing_space {
207 parts.len()
208 } else {
209 parts.len().saturating_sub(1)
210 };
211
212 let current_word = if trailing_space {
213 ""
214 } else {
215 parts.last().copied().unwrap_or("")
216 };
217
218 let start_pos = pos - current_word.len();
219
220 if !parts.is_empty() && (parts[0] == "!" || parts[0].starts_with('!')) {
222 let candidates = complete_file_path(current_word);
224 return Ok((start_pos, candidates));
225 }
226
227 if word_index == 0 {
228 let mut candidates = Vec::new();
230
231 let rules = command_completion_rules();
233 for (names, _) in &rules {
234 for name in *names {
235 if name.starts_with(current_word) {
236 candidates.push(Pair {
237 display: name.to_string(),
238 replacement: name.to_string(),
239 });
240 }
241 }
242 }
243
244 for alias in self.all_aliases() {
246 if alias.starts_with(current_word)
247 && !command::all_command_keywords().contains(&alias.as_str())
248 {
249 candidates.push(Pair {
250 display: alias.clone(),
251 replacement: alias,
252 });
253 }
254 }
255
256 return Ok((start_pos, candidates));
257 }
258
259 let cmd = parts[0];
261 let rules = command_completion_rules();
262
263 for (names, arg_hints) in &rules {
264 if names.contains(&cmd) {
265 let arg_index = word_index - 1; if arg_index < arg_hints.len() {
267 let candidates = match &arg_hints[arg_index] {
268 ArgHint::Alias => self
269 .all_aliases()
270 .into_iter()
271 .filter(|a| a.starts_with(current_word))
272 .map(|a| Pair {
273 display: a.clone(),
274 replacement: a,
275 })
276 .collect(),
277 ArgHint::Category => ALL_NOTE_CATEGORIES
278 .iter()
279 .filter(|c| c.starts_with(current_word))
280 .map(|c| Pair {
281 display: c.to_string(),
282 replacement: c.to_string(),
283 })
284 .collect(),
285 ArgHint::Section => self
286 .all_sections()
287 .into_iter()
288 .filter(|s| s.starts_with(current_word))
289 .map(|s| Pair {
290 display: s.clone(),
291 replacement: s,
292 })
293 .collect(),
294 ArgHint::SectionKeys(section) => self
295 .section_keys(section)
296 .into_iter()
297 .filter(|k| k.starts_with(current_word))
298 .map(|k| Pair {
299 display: k.clone(),
300 replacement: k,
301 })
302 .collect(),
303 ArgHint::Fixed(options) => options
304 .iter()
305 .filter(|o| !o.is_empty() && o.starts_with(current_word))
306 .map(|o| Pair {
307 display: o.to_string(),
308 replacement: o.to_string(),
309 })
310 .collect(),
311 ArgHint::Placeholder(_) => {
312 vec![]
314 }
315 ArgHint::FilePath => {
316 complete_file_path(current_word)
318 }
319 ArgHint::None => vec![],
320 };
321 return Ok((start_pos, candidates));
322 }
323 break;
324 }
325 }
326
327 if self.config.alias_exists(cmd) {
329 if self.config.contains(constants::section::EDITOR, cmd) {
331 let candidates = complete_file_path(current_word);
332 return Ok((start_pos, candidates));
333 }
334
335 if self.config.contains(constants::section::BROWSER, cmd) {
337 let mut candidates: Vec<Pair> = self
338 .all_aliases()
339 .into_iter()
340 .filter(|a| a.starts_with(current_word))
341 .map(|a| Pair {
342 display: a.clone(),
343 replacement: a,
344 })
345 .collect();
346 candidates.extend(complete_file_path(current_word));
348 return Ok((start_pos, candidates));
349 }
350
351 let mut candidates = complete_file_path(current_word);
353 candidates.extend(
354 self.all_aliases()
355 .into_iter()
356 .filter(|a| a.starts_with(current_word))
357 .map(|a| Pair {
358 display: a.clone(),
359 replacement: a,
360 }),
361 );
362 return Ok((start_pos, candidates));
363 }
364
365 Ok((start_pos, vec![]))
366 }
367}
368
369struct CopilotHinter {
372 history_hinter: HistoryHinter,
373}
374
375impl CopilotHinter {
376 fn new() -> Self {
377 Self {
378 history_hinter: HistoryHinter::new(),
379 }
380 }
381}
382
383impl Hinter for CopilotHinter {
384 type Hint = String;
385
386 fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option<String> {
387 self.history_hinter.hint(line, pos, ctx)
388 }
389}
390
391struct CopilotHighlighter;
394
395impl Highlighter for CopilotHighlighter {
396 fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
397 Cow::Owned(format!("\x1b[90m{}\x1b[0m", hint))
399 }
400
401 fn highlight_char(&self, _line: &str, _pos: usize, _forced: CmdKind) -> bool {
402 true
404 }
405}
406
407struct CopilotHelper {
410 completer: CopilotCompleter,
411 hinter: CopilotHinter,
412 highlighter: CopilotHighlighter,
413}
414
415impl CopilotHelper {
416 fn new(config: &YamlConfig) -> Self {
417 Self {
418 completer: CopilotCompleter::new(config),
419 hinter: CopilotHinter::new(),
420 highlighter: CopilotHighlighter,
421 }
422 }
423
424 fn refresh(&mut self, config: &YamlConfig) {
425 self.completer.refresh(config);
426 }
427}
428
429impl Completer for CopilotHelper {
430 type Candidate = Pair;
431
432 fn complete(
433 &self,
434 line: &str,
435 pos: usize,
436 ctx: &Context<'_>,
437 ) -> rustyline::Result<(usize, Vec<Pair>)> {
438 self.completer.complete(line, pos, ctx)
439 }
440}
441
442impl Hinter for CopilotHelper {
443 type Hint = String;
444
445 fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option<String> {
446 self.hinter.hint(line, pos, ctx)
447 }
448}
449
450impl Highlighter for CopilotHelper {
451 fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
452 self.highlighter.highlight_hint(hint)
453 }
454
455 fn highlight_char(&self, line: &str, pos: usize, forced: CmdKind) -> bool {
456 self.highlighter.highlight_char(line, pos, forced)
457 }
458}
459
460impl Validator for CopilotHelper {}
461
462impl rustyline::Helper for CopilotHelper {}
463
464pub fn run_interactive(config: &mut YamlConfig) {
468 let rl_config = Config::builder()
469 .completion_type(CompletionType::Circular)
470 .edit_mode(EditMode::Emacs)
471 .auto_add_history(false) .build();
473
474 let helper = CopilotHelper::new(config);
475
476 let mut rl: Editor<CopilotHelper, DefaultHistory> =
477 Editor::with_config(rl_config).expect("无法初始化编辑器");
478 rl.set_helper(Some(helper));
479
480 rl.bind_sequence(
482 KeyEvent(KeyCode::Tab, Modifiers::NONE),
483 EventHandler::Simple(Cmd::Complete),
484 );
485
486 let history_path = history_file_path();
488 let _ = rl.load_history(&history_path);
489
490 info!("{}", constants::WELCOME_MESSAGE);
491
492 inject_envs_to_process(config);
494
495 let prompt = format!("{} ", constants::INTERACTIVE_PROMPT.yellow());
496
497 loop {
498 match rl.readline(&prompt) {
499 Ok(line) => {
500 let input = line.trim();
501
502 if input.is_empty() {
503 continue;
504 }
505
506 if input.starts_with(constants::SHELL_PREFIX) {
508 let shell_cmd = &input[1..].trim();
509 if shell_cmd.is_empty() {
510 enter_interactive_shell(config);
512 } else {
513 execute_shell_command(shell_cmd, config);
514 }
515 let _ = rl.add_history_entry(input);
517 println!();
518 continue;
519 }
520
521 let args = parse_input(input);
523 if args.is_empty() {
524 continue;
525 }
526
527 let args: Vec<String> = args.iter().map(|a| expand_env_vars(a)).collect();
529
530 let verbose = config.is_verbose();
531 let start = if verbose {
532 Some(std::time::Instant::now())
533 } else {
534 None
535 };
536
537 let is_report_cmd = !args.is_empty() && cmd::REPORT.contains(&args[0].as_str());
539 if !is_report_cmd {
540 let _ = rl.add_history_entry(input);
541 }
542
543 execute_interactive_command(&args, config);
544
545 if let Some(start) = start {
546 let elapsed = start.elapsed();
547 crate::debug_log!(config, "duration: {} ms", elapsed.as_millis());
548 }
549
550 if let Some(helper) = rl.helper_mut() {
552 helper.refresh(config);
553 }
554 inject_envs_to_process(config);
556
557 println!();
558 }
559 Err(ReadlineError::Interrupted) => {
560 info!("\nProgram interrupted. Use 'exit' to quit.");
562 }
563 Err(ReadlineError::Eof) => {
564 info!("\nGoodbye! 👋");
566 break;
567 }
568 Err(err) => {
569 error!("读取输入失败: {:?}", err);
570 break;
571 }
572 }
573 }
574
575 let _ = rl.save_history(&history_path);
577}
578
579fn history_file_path() -> std::path::PathBuf {
581 let data_dir = crate::config::YamlConfig::data_dir();
582 let _ = std::fs::create_dir_all(&data_dir);
584 data_dir.join(constants::HISTORY_FILE)
585}
586
587fn parse_input(input: &str) -> Vec<String> {
590 let mut args = Vec::new();
591 let mut current = String::new();
592 let mut in_quotes = false;
593
594 for ch in input.chars() {
595 match ch {
596 '"' => {
597 in_quotes = !in_quotes;
598 }
599 ' ' if !in_quotes => {
600 if !current.is_empty() {
601 args.push(current.clone());
602 current.clear();
603 }
604 }
605 _ => {
606 current.push(ch);
607 }
608 }
609 }
610
611 if !current.is_empty() {
612 args.push(current);
613 }
614
615 args
616}
617
618enum ParseResult {
620 Matched(crate::cli::SubCmd),
622 Handled,
624 NotFound,
626}
627
628fn execute_interactive_command(args: &[String], config: &mut YamlConfig) {
631 if args.is_empty() {
632 return;
633 }
634
635 let cmd_str = &args[0];
636
637 if cmd::EXIT.contains(&cmd_str.as_str()) {
639 command::system::handle_exit();
640 return;
641 }
642
643 match parse_interactive_command(args) {
645 ParseResult::Matched(subcmd) => {
646 command::dispatch(subcmd, config);
647 }
648 ParseResult::Handled => {
649 }
651 ParseResult::NotFound => {
652 command::open::handle_open(args, config);
654 }
655 }
656}
657
658fn parse_interactive_command(args: &[String]) -> ParseResult {
660 use crate::cli::SubCmd;
661
662 if args.is_empty() {
663 return ParseResult::NotFound;
664 }
665
666 let cmd = args[0].as_str();
667 let rest = &args[1..];
668
669 let is = |names: &[&str]| names.contains(&cmd);
671
672 if is(cmd::SET) {
673 if rest.is_empty() {
674 crate::usage!("set <alias> <path>");
675 return ParseResult::Handled;
676 }
677 ParseResult::Matched(SubCmd::Set {
678 alias: rest[0].clone(),
679 path: rest[1..].to_vec(),
680 })
681 } else if is(cmd::REMOVE) {
682 match rest.first() {
683 Some(alias) => ParseResult::Matched(SubCmd::Remove {
684 alias: alias.clone(),
685 }),
686 None => {
687 crate::usage!("rm <alias>");
688 ParseResult::Handled
689 }
690 }
691 } else if is(cmd::RENAME) {
692 if rest.len() < 2 {
693 crate::usage!("rename <alias> <new_alias>");
694 return ParseResult::Handled;
695 }
696 ParseResult::Matched(SubCmd::Rename {
697 alias: rest[0].clone(),
698 new_alias: rest[1].clone(),
699 })
700 } else if is(cmd::MODIFY) {
701 if rest.is_empty() {
702 crate::usage!("mf <alias> <new_path>");
703 return ParseResult::Handled;
704 }
705 ParseResult::Matched(SubCmd::Modify {
706 alias: rest[0].clone(),
707 path: rest[1..].to_vec(),
708 })
709
710 } else if is(cmd::NOTE) {
712 if rest.len() < 2 {
713 crate::usage!("note <alias> <category>");
714 return ParseResult::Handled;
715 }
716 ParseResult::Matched(SubCmd::Note {
717 alias: rest[0].clone(),
718 category: rest[1].clone(),
719 })
720 } else if is(cmd::DENOTE) {
721 if rest.len() < 2 {
722 crate::usage!("denote <alias> <category>");
723 return ParseResult::Handled;
724 }
725 ParseResult::Matched(SubCmd::Denote {
726 alias: rest[0].clone(),
727 category: rest[1].clone(),
728 })
729
730 } else if is(cmd::LIST) {
732 ParseResult::Matched(SubCmd::List {
733 part: rest.first().cloned(),
734 })
735
736 } else if is(cmd::CONTAIN) {
738 if rest.is_empty() {
739 crate::usage!("contain <alias> [sections]");
740 return ParseResult::Handled;
741 }
742 ParseResult::Matched(SubCmd::Contain {
743 alias: rest[0].clone(),
744 containers: rest.get(1).cloned(),
745 })
746
747 } else if is(cmd::LOG) {
749 if rest.len() < 2 {
750 crate::usage!("log mode <verbose|concise>");
751 return ParseResult::Handled;
752 }
753 ParseResult::Matched(SubCmd::Log {
754 key: rest[0].clone(),
755 value: rest[1].clone(),
756 })
757 } else if is(cmd::CHANGE) {
758 if rest.len() < 3 {
759 crate::usage!("change <part> <field> <value>");
760 return ParseResult::Handled;
761 }
762 ParseResult::Matched(SubCmd::Change {
763 part: rest[0].clone(),
764 field: rest[1].clone(),
765 value: rest[2].clone(),
766 })
767 } else if is(cmd::CLEAR) {
768 ParseResult::Matched(SubCmd::Clear)
769
770 } else if is(cmd::REPORT) {
772 ParseResult::Matched(SubCmd::Report {
773 content: rest.to_vec(),
774 })
775 } else if is(cmd::REPORTCTL) {
776 if rest.is_empty() {
777 crate::usage!("reportctl <new|sync|push|pull|set-url> [date|message|url]");
778 return ParseResult::Handled;
779 }
780 ParseResult::Matched(SubCmd::Reportctl {
781 action: rest[0].clone(),
782 arg: rest.get(1).cloned(),
783 })
784 } else if is(cmd::CHECK) {
785 ParseResult::Matched(SubCmd::Check {
786 line_count: rest.first().cloned(),
787 })
788 } else if is(cmd::SEARCH) {
789 if rest.len() < 2 {
790 crate::usage!("search <line_count|all> <target> [-f|-fuzzy]");
791 return ParseResult::Handled;
792 }
793 ParseResult::Matched(SubCmd::Search {
794 line_count: rest[0].clone(),
795 target: rest[1].clone(),
796 fuzzy: rest.get(2).cloned(),
797 })
798
799 } else if is(cmd::TODO) {
801 ParseResult::Matched(SubCmd::Todo {
802 content: rest.to_vec(),
803 })
804
805 } else if is(cmd::CONCAT) {
807 if rest.is_empty() {
808 crate::usage!("concat <script_name> [\"<script_content>\"]");
809 return ParseResult::Handled;
810 }
811 ParseResult::Matched(SubCmd::Concat {
812 name: rest[0].clone(),
813 content: if rest.len() > 1 {
814 rest[1..].to_vec()
815 } else {
816 vec![]
817 },
818 })
819
820 } else if is(cmd::TIME) {
822 if rest.len() < 2 {
823 crate::usage!("time countdown <duration>");
824 return ParseResult::Handled;
825 }
826 ParseResult::Matched(SubCmd::Time {
827 function: rest[0].clone(),
828 arg: rest[1].clone(),
829 })
830
831 } else if is(cmd::VERSION) {
833 ParseResult::Matched(SubCmd::Version)
834 } else if is(cmd::HELP) {
835 ParseResult::Matched(SubCmd::Help)
836 } else if is(cmd::COMPLETION) {
837 ParseResult::Matched(SubCmd::Completion {
838 shell: rest.first().cloned(),
839 })
840
841 } else {
843 ParseResult::NotFound
844 }
845}
846
847fn complete_file_path(partial: &str) -> Vec<Pair> {
850 let mut candidates = Vec::new();
851
852 let expanded = if partial.starts_with('~') {
854 if let Some(home) = dirs::home_dir() {
855 partial.replacen('~', &home.to_string_lossy(), 1)
856 } else {
857 partial.to_string()
858 }
859 } else {
860 partial.to_string()
861 };
862
863 let (dir_path, file_prefix) =
865 if expanded.ends_with('/') || expanded.ends_with(std::path::MAIN_SEPARATOR) {
866 (std::path::Path::new(&expanded).to_path_buf(), String::new())
867 } else {
868 let p = std::path::Path::new(&expanded);
869 let parent = p
870 .parent()
871 .unwrap_or(std::path::Path::new("."))
872 .to_path_buf();
873 let fp = p
874 .file_name()
875 .map(|s| s.to_string_lossy().to_string())
876 .unwrap_or_default();
877 (parent, fp)
878 };
879
880 if let Ok(entries) = std::fs::read_dir(&dir_path) {
881 for entry in entries.flatten() {
882 let name = entry.file_name().to_string_lossy().to_string();
883
884 if name.starts_with('.') && !file_prefix.starts_with('.') {
886 continue;
887 }
888
889 if name.starts_with(&file_prefix) {
890 let is_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
891
892 let full_replacement =
895 if partial.ends_with('/') || partial.ends_with(std::path::MAIN_SEPARATOR) {
896 format!("{}{}{}", partial, name, if is_dir { "/" } else { "" })
897 } else if partial.contains('/') || partial.contains(std::path::MAIN_SEPARATOR) {
898 let last_sep = partial
900 .rfind('/')
901 .or_else(|| partial.rfind(std::path::MAIN_SEPARATOR))
902 .unwrap();
903 format!(
904 "{}/{}{}",
905 &partial[..last_sep],
906 name,
907 if is_dir { "/" } else { "" }
908 )
909 } else {
910 format!("{}{}", name, if is_dir { "/" } else { "" })
911 };
912
913 let display_name = format!("{}{}", name, if is_dir { "/" } else { "" });
914
915 candidates.push(Pair {
916 display: display_name,
917 replacement: full_replacement,
918 });
919 }
920 }
921 }
922
923 candidates.sort_by(|a, b| a.display.cmp(&b.display));
925 candidates
926}
927
928fn enter_interactive_shell(config: &YamlConfig) {
932 let os = std::env::consts::OS;
933
934 let shell_path = if os == shell::WINDOWS_OS {
935 shell::WINDOWS_CMD.to_string()
936 } else {
937 std::env::var("SHELL").unwrap_or_else(|_| shell::BASH_PATH.to_string())
939 };
940
941 info!("进入 shell 模式 ({}), 输入 exit 返回 copilot", shell_path);
942
943 let mut command = std::process::Command::new(&shell_path);
944
945 for (key, value) in config.collect_alias_envs() {
947 command.env(&key, &value);
948 }
949
950 let mut cleanup_path: Option<std::path::PathBuf> = None;
952
953 if os != shell::WINDOWS_OS {
956 let is_zsh = shell_path.contains("zsh");
957 let is_bash = shell_path.contains("bash");
958
959 if is_zsh {
960 let pid = std::process::id();
963 let tmp_dir = std::path::PathBuf::from(format!("/tmp/j_shell_zsh_{}", pid));
964 let _ = std::fs::create_dir_all(&tmp_dir);
965
966 let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
967 let zshrc_content = format!(
968 "# j shell 临时配置 - 自动生成,退出后自动清理\n\
969 # 恢复 ZDOTDIR 为用户 home 目录,让后续 source 正常工作\n\
970 export ZDOTDIR=\"{home}\"\n\
971 # 加载用户原始 .zshrc(保留所有配置、alias、插件等)\n\
972 if [ -f \"{home}/.zshrc\" ]; then\n\
973 source \"{home}/.zshrc\"\n\
974 fi\n\
975 # 在用户配置加载完成后覆盖 PROMPT,确保不被 oh-my-zsh 等覆盖\n\
976 PROMPT='%F{{green}}shell%f (%F{{cyan}}%~%f) %F{{green}}>%f '\n",
977 home = home,
978 );
979
980 let zshrc_path = tmp_dir.join(".zshrc");
981 if let Err(e) = std::fs::write(&zshrc_path, &zshrc_content) {
982 error!("创建临时 .zshrc 失败: {}", e);
983 command.env("PROMPT", "%F{green}shell%f (%F{cyan}%~%f) %F{green}>%f ");
985 } else {
986 command.env("ZDOTDIR", tmp_dir.to_str().unwrap_or("/tmp"));
987 cleanup_path = Some(tmp_dir);
988 }
989 } else if is_bash {
990 let pid = std::process::id();
992 let tmp_rc = std::path::PathBuf::from(format!("/tmp/j_shell_bashrc_{}", pid));
993
994 let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
995 let bashrc_content = format!(
996 "# j shell 临时配置 - 自动生成,退出后自动清理\n\
997 # 加载用户原始 .bashrc(保留所有配置、alias 等)\n\
998 if [ -f \"{home}/.bashrc\" ]; then\n\
999 source \"{home}/.bashrc\"\n\
1000 fi\n\
1001 # 在用户配置加载完成后覆盖 PS1
1002 PS1='\\[\\033[32m\\]shell\\[\\033[0m\\] (\\[\\033[36m\\]\\w\\[\\033[0m\\]) \\[\\033[32m\\]>\\[\\033[0m\\] '\n",
1003 home = home,
1004 );
1005
1006 if let Err(e) = std::fs::write(&tmp_rc, &bashrc_content) {
1007 error!("创建临时 bashrc 失败: {}", e);
1008 command.env("PS1", "\\[\\033[32m\\]shell\\[\\033[0m\\] (\\[\\033[36m\\]\\w\\[\\033[0m\\]) \\[\\033[32m\\]>\\[\\033[0m\\] ");
1009 } else {
1010 command.arg("--rcfile");
1011 command.arg(tmp_rc.to_str().unwrap_or("/tmp/j_shell_bashrc"));
1012 cleanup_path = Some(tmp_rc);
1013 }
1014 } else {
1015 command.env(
1017 "PS1",
1018 "\x1b[32mshell\x1b[0m (\x1b[36m\\w\x1b[0m) \x1b[32m>\x1b[0m ",
1019 );
1020 command.env(
1021 "PROMPT",
1022 "\x1b[32mshell\x1b[0m (\x1b[36m%~\x1b[0m) \x1b[32m>\x1b[0m ",
1023 );
1024 }
1025 }
1026
1027 command
1029 .stdin(std::process::Stdio::inherit())
1030 .stdout(std::process::Stdio::inherit())
1031 .stderr(std::process::Stdio::inherit());
1032
1033 match command.status() {
1034 Ok(status) => {
1035 if !status.success() {
1036 if let Some(code) = status.code() {
1037 error!("shell 退出码: {}", code);
1038 }
1039 }
1040 }
1041 Err(e) => {
1042 error!("启动 shell 失败: {}", e);
1043 }
1044 }
1045
1046 if let Some(path) = cleanup_path {
1048 if path.is_dir() {
1049 let _ = std::fs::remove_dir_all(&path);
1050 } else {
1051 let _ = std::fs::remove_file(&path);
1052 }
1053 }
1054
1055 info!("{}", "已返回 copilot 交互模式 🚀".green());
1056}
1057
1058fn execute_shell_command(cmd: &str, config: &YamlConfig) {
1061 if cmd.is_empty() {
1062 return;
1063 }
1064
1065 let os = std::env::consts::OS;
1066 let mut command = if os == shell::WINDOWS_OS {
1067 let mut c = std::process::Command::new(shell::WINDOWS_CMD);
1068 c.args([shell::WINDOWS_CMD_FLAG, cmd]);
1069 c
1070 } else {
1071 let mut c = std::process::Command::new(shell::BASH_PATH);
1072 c.args([shell::BASH_CMD_FLAG, cmd]);
1073 c
1074 };
1075
1076 for (key, value) in config.collect_alias_envs() {
1078 command.env(&key, &value);
1079 }
1080
1081 let result = command.status();
1082
1083 match result {
1084 Ok(status) => {
1085 if !status.success() {
1086 if let Some(code) = status.code() {
1087 error!("命令退出码: {}", code);
1088 }
1089 }
1090 }
1091 Err(e) => {
1092 error!("执行命令失败: {}", e);
1093 }
1094 }
1095}
1096
1097fn inject_envs_to_process(config: &YamlConfig) {
1100 for (key, value) in config.collect_alias_envs() {
1101 unsafe {
1103 std::env::set_var(&key, &value);
1104 }
1105 }
1106}
1107
1108fn expand_env_vars(input: &str) -> String {
1111 let mut result = String::with_capacity(input.len());
1112 let chars: Vec<char> = input.chars().collect();
1113 let len = chars.len();
1114 let mut i = 0;
1115
1116 while i < len {
1117 if chars[i] == '$' && i + 1 < len {
1118 if chars[i + 1] == '{' {
1120 if let Some(end) = chars[i + 2..].iter().position(|&c| c == '}') {
1121 let var_name: String = chars[i + 2..i + 2 + end].iter().collect();
1122 if let Ok(val) = std::env::var(&var_name) {
1123 result.push_str(&val);
1124 } else {
1125 result.push_str(&input[i..i + 3 + end]);
1127 }
1128 i = i + 3 + end;
1129 continue;
1130 }
1131 }
1132 let start = i + 1;
1134 let mut end = start;
1135 while end < len && (chars[end].is_alphanumeric() || chars[end] == '_') {
1136 end += 1;
1137 }
1138 if end > start {
1139 let var_name: String = chars[start..end].iter().collect();
1140 if let Ok(val) = std::env::var(&var_name) {
1141 result.push_str(&val);
1142 } else {
1143 let original: String = chars[i..end].iter().collect();
1145 result.push_str(&original);
1146 }
1147 i = end;
1148 continue;
1149 }
1150 }
1151 result.push(chars[i]);
1152 i += 1;
1153 }
1154
1155 result
1156}