1use crate::command;
2use crate::config::YamlConfig;
3use crate::constants::{self, cmd, config_key, rmeta_action, time_function, search_flag, shell, NOTE_CATEGORIES, ALL_SECTIONS, ALIAS_PATH_SECTIONS, LIST_ALL};
4use crate::{info, error};
5use colored::Colorize;
6use rustyline::completion::{Completer, Pair};
7use rustyline::error::ReadlineError;
8use rustyline::highlight::Highlighter;
9use rustyline::hint::{Hinter, HistoryHinter};
10use rustyline::history::DefaultHistory;
11use rustyline::validate::Validator;
12use rustyline::highlight::CmdKind;
13use rustyline::{Cmd, CompletionType, Config, Context, EditMode, Editor, EventHandler, KeyCode, KeyEvent, Modifiers};
14use std::borrow::Cow;
15
16struct CopilotCompleter {
20 config: YamlConfig,
21}
22
23impl CopilotCompleter {
24 fn new(config: &YamlConfig) -> Self {
25 Self {
26 config: config.clone(),
27 }
28 }
29
30 fn refresh(&mut self, config: &YamlConfig) {
32 self.config = config.clone();
33 }
34
35 fn all_aliases(&self) -> Vec<String> {
37 let mut aliases = Vec::new();
38 for s in ALIAS_PATH_SECTIONS {
39 if let Some(map) = self.config.get_section(s) {
40 aliases.extend(map.keys().cloned());
41 }
42 }
43 aliases.sort();
44 aliases.dedup();
45 aliases
46 }
47
48 fn all_sections(&self) -> Vec<String> {
50 self.config
51 .all_section_names()
52 .iter()
53 .map(|s| s.to_string())
54 .collect()
55 }
56
57 fn section_keys(&self, section: &str) -> Vec<String> {
59 self.config
60 .get_section(section)
61 .map(|m| m.keys().cloned().collect())
62 .unwrap_or_default()
63 }
64}
65
66#[derive(Clone)]
69#[allow(dead_code)]
70enum ArgHint {
71 Alias,
72 Category,
73 Section,
74 SectionKeys(String), Fixed(Vec<&'static str>),
76 Placeholder(&'static str),
77 FilePath, None,
79}
80
81fn command_completion_rules() -> Vec<(&'static [&'static str], Vec<ArgHint>)> {
83 vec![
84 (cmd::SET, vec![ArgHint::Placeholder("<alias>"), ArgHint::FilePath]),
86 (cmd::REMOVE, vec![ArgHint::Alias]),
87 (cmd::RENAME, vec![ArgHint::Alias, ArgHint::Placeholder("<new_alias>")]),
88 (cmd::MODIFY, vec![ArgHint::Alias, ArgHint::FilePath]),
89 (cmd::NOTE, vec![ArgHint::Alias, ArgHint::Category]),
91 (cmd::DENOTE, vec![ArgHint::Alias, ArgHint::Category]),
92 (cmd::LIST, vec![ArgHint::Fixed({
94 let mut v: Vec<&'static str> = vec!["", LIST_ALL];
95 for s in ALL_SECTIONS { v.push(s); }
96 v
97 })]),
98 (cmd::CONTAIN, vec![ArgHint::Alias, ArgHint::Placeholder("<sections>")]),
100 (cmd::LOG, vec![ArgHint::Fixed(vec![config_key::MODE]), ArgHint::Fixed(vec![config_key::VERBOSE, config_key::CONCISE])]),
102 (cmd::CHANGE, vec![ArgHint::Section, ArgHint::Placeholder("<field>"), ArgHint::Placeholder("<value>")]),
103 (cmd::REPORT, vec![ArgHint::Placeholder("<content>")]),
105 (cmd::REPORTCTL, vec![ArgHint::Fixed(vec![rmeta_action::NEW, rmeta_action::SYNC, rmeta_action::PUSH, rmeta_action::PULL, rmeta_action::SET_URL, rmeta_action::OPEN]), ArgHint::Placeholder("<date|message|url>")]),
106 (cmd::CHECK, vec![ArgHint::Placeholder("<line_count>")]),
107 (cmd::SEARCH, vec![ArgHint::Placeholder("<line_count|all>"), ArgHint::Placeholder("<target>"), ArgHint::Fixed(vec![search_flag::FUZZY_SHORT, search_flag::FUZZY])]),
108 (cmd::CONCAT, vec![ArgHint::Placeholder("<script_name>"), ArgHint::Placeholder("<script_content>")]),
110 (cmd::TIME, vec![ArgHint::Fixed(vec![time_function::COUNTDOWN]), ArgHint::Placeholder("<duration>")]),
112 (cmd::COMPLETION, vec![ArgHint::Fixed(vec!["zsh", "bash"])]),
114 (cmd::VERSION, vec![]),
116 (cmd::HELP, vec![]),
117 (cmd::CLEAR, vec![]),
118 (cmd::EXIT, vec![]),
119 ]
120}
121
122const ALL_NOTE_CATEGORIES: &[&str] = NOTE_CATEGORIES;
124
125impl Completer for CopilotCompleter {
126 type Candidate = Pair;
127
128 fn complete(
129 &self,
130 line: &str,
131 pos: usize,
132 _ctx: &Context<'_>,
133 ) -> rustyline::Result<(usize, Vec<Pair>)> {
134 let line_to_cursor = &line[..pos];
135 let parts: Vec<&str> = line_to_cursor.split_whitespace().collect();
136
137 let trailing_space = line_to_cursor.ends_with(' ');
139 let word_index = if trailing_space {
140 parts.len()
141 } else {
142 parts.len().saturating_sub(1)
143 };
144
145 let current_word = if trailing_space {
146 ""
147 } else {
148 parts.last().copied().unwrap_or("")
149 };
150
151 let start_pos = pos - current_word.len();
152
153 if !parts.is_empty() && (parts[0] == "!" || parts[0].starts_with('!')) {
155 let candidates = complete_file_path(current_word);
157 return Ok((start_pos, candidates));
158 }
159
160 if word_index == 0 {
161 let mut candidates = Vec::new();
163
164 let rules = command_completion_rules();
166 for (names, _) in &rules {
167 for name in *names {
168 if name.starts_with(current_word) {
169 candidates.push(Pair {
170 display: name.to_string(),
171 replacement: name.to_string(),
172 });
173 }
174 }
175 }
176
177 for alias in self.all_aliases() {
179 if alias.starts_with(current_word) && !command::all_command_keywords().contains(&alias.as_str()) {
180 candidates.push(Pair {
181 display: alias.clone(),
182 replacement: alias,
183 });
184 }
185 }
186
187 return Ok((start_pos, candidates));
188 }
189
190 let cmd = parts[0];
192 let rules = command_completion_rules();
193
194 for (names, arg_hints) in &rules {
195 if names.contains(&cmd) {
196 let arg_index = word_index - 1; if arg_index < arg_hints.len() {
198 let candidates = match &arg_hints[arg_index] {
199 ArgHint::Alias => {
200 self.all_aliases()
201 .into_iter()
202 .filter(|a| a.starts_with(current_word))
203 .map(|a| Pair { display: a.clone(), replacement: a })
204 .collect()
205 }
206 ArgHint::Category => {
207 ALL_NOTE_CATEGORIES
208 .iter()
209 .filter(|c| c.starts_with(current_word))
210 .map(|c| Pair { display: c.to_string(), replacement: c.to_string() })
211 .collect()
212 }
213 ArgHint::Section => {
214 self.all_sections()
215 .into_iter()
216 .filter(|s| s.starts_with(current_word))
217 .map(|s| Pair { display: s.clone(), replacement: s })
218 .collect()
219 }
220 ArgHint::SectionKeys(section) => {
221 self.section_keys(section)
222 .into_iter()
223 .filter(|k| k.starts_with(current_word))
224 .map(|k| Pair { display: k.clone(), replacement: k })
225 .collect()
226 }
227 ArgHint::Fixed(options) => {
228 options
229 .iter()
230 .filter(|o| !o.is_empty() && o.starts_with(current_word))
231 .map(|o| Pair { display: o.to_string(), replacement: o.to_string() })
232 .collect()
233 }
234 ArgHint::Placeholder(_) => {
235 vec![]
237 }
238 ArgHint::FilePath => {
239 complete_file_path(current_word)
241 }
242 ArgHint::None => vec![],
243 };
244 return Ok((start_pos, candidates));
245 }
246 break;
247 }
248 }
249
250 if self.config.alias_exists(cmd) {
252 if self.config.contains(constants::section::EDITOR, cmd) {
254 let candidates = complete_file_path(current_word);
255 return Ok((start_pos, candidates));
256 }
257
258 if self.config.contains(constants::section::BROWSER, cmd) {
260 let mut candidates: Vec<Pair> = self.all_aliases()
261 .into_iter()
262 .filter(|a| a.starts_with(current_word))
263 .map(|a| Pair { display: a.clone(), replacement: a })
264 .collect();
265 candidates.extend(complete_file_path(current_word));
267 return Ok((start_pos, candidates));
268 }
269
270 let mut candidates = complete_file_path(current_word);
272 candidates.extend(
273 self.all_aliases()
274 .into_iter()
275 .filter(|a| a.starts_with(current_word))
276 .map(|a| Pair { display: a.clone(), replacement: a })
277 );
278 return Ok((start_pos, candidates));
279 }
280
281 Ok((start_pos, vec![]))
282 }
283}
284
285struct CopilotHinter {
288 history_hinter: HistoryHinter,
289}
290
291impl CopilotHinter {
292 fn new() -> Self {
293 Self {
294 history_hinter: HistoryHinter::new(),
295 }
296 }
297}
298
299impl Hinter for CopilotHinter {
300 type Hint = String;
301
302 fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option<String> {
303 self.history_hinter.hint(line, pos, ctx)
304 }
305}
306
307struct CopilotHighlighter;
310
311impl Highlighter for CopilotHighlighter {
312 fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
313 Cow::Owned(format!("\x1b[90m{}\x1b[0m", hint))
315 }
316
317 fn highlight_char(&self, _line: &str, _pos: usize, _forced: CmdKind) -> bool {
318 true
320 }
321}
322
323struct CopilotHelper {
326 completer: CopilotCompleter,
327 hinter: CopilotHinter,
328 highlighter: CopilotHighlighter,
329}
330
331impl CopilotHelper {
332 fn new(config: &YamlConfig) -> Self {
333 Self {
334 completer: CopilotCompleter::new(config),
335 hinter: CopilotHinter::new(),
336 highlighter: CopilotHighlighter,
337 }
338 }
339
340 fn refresh(&mut self, config: &YamlConfig) {
341 self.completer.refresh(config);
342 }
343}
344
345impl Completer for CopilotHelper {
346 type Candidate = Pair;
347
348 fn complete(
349 &self,
350 line: &str,
351 pos: usize,
352 ctx: &Context<'_>,
353 ) -> rustyline::Result<(usize, Vec<Pair>)> {
354 self.completer.complete(line, pos, ctx)
355 }
356}
357
358impl Hinter for CopilotHelper {
359 type Hint = String;
360
361 fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option<String> {
362 self.hinter.hint(line, pos, ctx)
363 }
364}
365
366impl Highlighter for CopilotHelper {
367 fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
368 self.highlighter.highlight_hint(hint)
369 }
370
371 fn highlight_char(&self, line: &str, pos: usize, forced: CmdKind) -> bool {
372 self.highlighter.highlight_char(line, pos, forced)
373 }
374}
375
376impl Validator for CopilotHelper {}
377
378impl rustyline::Helper for CopilotHelper {}
379
380pub fn run_interactive(config: &mut YamlConfig) {
384 let rl_config = Config::builder()
385 .completion_type(CompletionType::Circular)
386 .edit_mode(EditMode::Emacs)
387 .auto_add_history(false) .build();
389
390 let helper = CopilotHelper::new(config);
391
392 let mut rl: Editor<CopilotHelper, DefaultHistory> =
393 Editor::with_config(rl_config).expect("无法初始化编辑器");
394 rl.set_helper(Some(helper));
395
396 rl.bind_sequence(
398 KeyEvent(KeyCode::Tab, Modifiers::NONE),
399 EventHandler::Simple(Cmd::Complete),
400 );
401
402 let history_path = history_file_path();
404 let _ = rl.load_history(&history_path);
405
406 info!("{}", constants::WELCOME_MESSAGE);
407
408 inject_envs_to_process(config);
410
411 let prompt = format!("{} ", constants::INTERACTIVE_PROMPT.yellow());
412
413 loop {
414 match rl.readline(&prompt) {
415 Ok(line) => {
416 let input = line.trim();
417
418 if input.is_empty() {
419 continue;
420 }
421
422 if input.starts_with(constants::SHELL_PREFIX) {
424 let shell_cmd = &input[1..].trim();
425 if shell_cmd.is_empty() {
426 enter_interactive_shell(config);
428 } else {
429 execute_shell_command(shell_cmd, config);
430 }
431 let _ = rl.add_history_entry(input);
433 println!();
434 continue;
435 }
436
437 let args = parse_input(input);
439 if args.is_empty() {
440 continue;
441 }
442
443 let args: Vec<String> = args.iter().map(|a| expand_env_vars(a)).collect();
445
446 let verbose = config.is_verbose();
447 let start = if verbose {
448 Some(std::time::Instant::now())
449 } else {
450 None
451 };
452
453 let is_report_cmd = !args.is_empty() && cmd::REPORT.contains(&args[0].as_str());
455 if !is_report_cmd {
456 let _ = rl.add_history_entry(input);
457 }
458
459 execute_interactive_command(&args, config);
460
461 if let Some(start) = start {
462 let elapsed = start.elapsed();
463 crate::debug_log!(config, "duration: {} ms", elapsed.as_millis());
464 }
465
466 if let Some(helper) = rl.helper_mut() {
468 helper.refresh(config);
469 }
470 inject_envs_to_process(config);
472
473 println!();
474 }
475 Err(ReadlineError::Interrupted) => {
476 info!("\nProgram interrupted. Use 'exit' to quit.");
478 }
479 Err(ReadlineError::Eof) => {
480 info!("\nGoodbye! 👋");
482 break;
483 }
484 Err(err) => {
485 error!("读取输入失败: {:?}", err);
486 break;
487 }
488 }
489 }
490
491 let _ = rl.save_history(&history_path);
493}
494
495fn history_file_path() -> std::path::PathBuf {
497 let data_dir = crate::config::YamlConfig::data_dir();
498 let _ = std::fs::create_dir_all(&data_dir);
500 data_dir.join(constants::HISTORY_FILE)
501}
502
503fn parse_input(input: &str) -> Vec<String> {
506 let mut args = Vec::new();
507 let mut current = String::new();
508 let mut in_quotes = false;
509
510 for ch in input.chars() {
511 match ch {
512 '"' => {
513 in_quotes = !in_quotes;
514 }
515 ' ' if !in_quotes => {
516 if !current.is_empty() {
517 args.push(current.clone());
518 current.clear();
519 }
520 }
521 _ => {
522 current.push(ch);
523 }
524 }
525 }
526
527 if !current.is_empty() {
528 args.push(current);
529 }
530
531 args
532}
533
534enum ParseResult {
536 Matched(crate::cli::SubCmd),
538 Handled,
540 NotFound,
542}
543
544fn execute_interactive_command(args: &[String], config: &mut YamlConfig) {
547 if args.is_empty() {
548 return;
549 }
550
551 let cmd_str = &args[0];
552
553 if cmd::EXIT.contains(&cmd_str.as_str()) {
555 command::system::handle_exit();
556 return;
557 }
558
559 match parse_interactive_command(args) {
561 ParseResult::Matched(subcmd) => {
562 command::dispatch(subcmd, config);
563 }
564 ParseResult::Handled => {
565 }
567 ParseResult::NotFound => {
568 command::open::handle_open(args, config);
570 }
571 }
572}
573
574fn parse_interactive_command(args: &[String]) -> ParseResult {
576 use crate::cli::SubCmd;
577
578 if args.is_empty() {
579 return ParseResult::NotFound;
580 }
581
582 let cmd = args[0].as_str();
583 let rest = &args[1..];
584
585 let is = |names: &[&str]| names.contains(&cmd);
587
588 if is(cmd::SET) {
589 if rest.is_empty() {
590 crate::usage!("set <alias> <path>");
591 return ParseResult::Handled;
592 }
593 ParseResult::Matched(SubCmd::Set {
594 alias: rest[0].clone(),
595 path: rest[1..].to_vec(),
596 })
597 } else if is(cmd::REMOVE) {
598 match rest.first() {
599 Some(alias) => ParseResult::Matched(SubCmd::Remove { alias: alias.clone() }),
600 None => { crate::usage!("rm <alias>"); ParseResult::Handled }
601 }
602 } else if is(cmd::RENAME) {
603 if rest.len() < 2 {
604 crate::usage!("rename <alias> <new_alias>");
605 return ParseResult::Handled;
606 }
607 ParseResult::Matched(SubCmd::Rename {
608 alias: rest[0].clone(),
609 new_alias: rest[1].clone(),
610 })
611 } else if is(cmd::MODIFY) {
612 if rest.is_empty() {
613 crate::usage!("mf <alias> <new_path>");
614 return ParseResult::Handled;
615 }
616 ParseResult::Matched(SubCmd::Modify {
617 alias: rest[0].clone(),
618 path: rest[1..].to_vec(),
619 })
620
621 } else if is(cmd::NOTE) {
623 if rest.len() < 2 {
624 crate::usage!("note <alias> <category>");
625 return ParseResult::Handled;
626 }
627 ParseResult::Matched(SubCmd::Note {
628 alias: rest[0].clone(),
629 category: rest[1].clone(),
630 })
631 } else if is(cmd::DENOTE) {
632 if rest.len() < 2 {
633 crate::usage!("denote <alias> <category>");
634 return ParseResult::Handled;
635 }
636 ParseResult::Matched(SubCmd::Denote {
637 alias: rest[0].clone(),
638 category: rest[1].clone(),
639 })
640
641 } else if is(cmd::LIST) {
643 ParseResult::Matched(SubCmd::List {
644 part: rest.first().cloned(),
645 })
646
647 } else if is(cmd::CONTAIN) {
649 if rest.is_empty() {
650 crate::usage!("contain <alias> [sections]");
651 return ParseResult::Handled;
652 }
653 ParseResult::Matched(SubCmd::Contain {
654 alias: rest[0].clone(),
655 containers: rest.get(1).cloned(),
656 })
657
658 } else if is(cmd::LOG) {
660 if rest.len() < 2 {
661 crate::usage!("log mode <verbose|concise>");
662 return ParseResult::Handled;
663 }
664 ParseResult::Matched(SubCmd::Log {
665 key: rest[0].clone(),
666 value: rest[1].clone(),
667 })
668 } else if is(cmd::CHANGE) {
669 if rest.len() < 3 {
670 crate::usage!("change <part> <field> <value>");
671 return ParseResult::Handled;
672 }
673 ParseResult::Matched(SubCmd::Change {
674 part: rest[0].clone(),
675 field: rest[1].clone(),
676 value: rest[2].clone(),
677 })
678 } else if is(cmd::CLEAR) {
679 ParseResult::Matched(SubCmd::Clear)
680
681 } else if is(cmd::REPORT) {
683 ParseResult::Matched(SubCmd::Report {
684 content: rest.to_vec(),
685 })
686 } else if is(cmd::REPORTCTL) {
687 if rest.is_empty() {
688 crate::usage!("reportctl <new|sync|push|pull|set-url> [date|message|url]");
689 return ParseResult::Handled;
690 }
691 ParseResult::Matched(SubCmd::Reportctl {
692 action: rest[0].clone(),
693 arg: rest.get(1).cloned(),
694 })
695 } else if is(cmd::CHECK) {
696 ParseResult::Matched(SubCmd::Check {
697 line_count: rest.first().cloned(),
698 })
699 } else if is(cmd::SEARCH) {
700 if rest.len() < 2 {
701 crate::usage!("search <line_count|all> <target> [-f|-fuzzy]");
702 return ParseResult::Handled;
703 }
704 ParseResult::Matched(SubCmd::Search {
705 line_count: rest[0].clone(),
706 target: rest[1].clone(),
707 fuzzy: rest.get(2).cloned(),
708 })
709
710 } else if is(cmd::CONCAT) {
712 if rest.is_empty() {
713 crate::usage!("concat <script_name> [\"<script_content>\"]");
714 return ParseResult::Handled;
715 }
716 ParseResult::Matched(SubCmd::Concat {
717 name: rest[0].clone(),
718 content: if rest.len() > 1 { rest[1..].to_vec() } else { vec![] },
719 })
720
721 } else if is(cmd::TIME) {
723 if rest.len() < 2 {
724 crate::usage!("time countdown <duration>");
725 return ParseResult::Handled;
726 }
727 ParseResult::Matched(SubCmd::Time {
728 function: rest[0].clone(),
729 arg: rest[1].clone(),
730 })
731
732 } else if is(cmd::VERSION) {
734 ParseResult::Matched(SubCmd::Version)
735 } else if is(cmd::HELP) {
736 ParseResult::Matched(SubCmd::Help)
737 } else if is(cmd::COMPLETION) {
738 ParseResult::Matched(SubCmd::Completion {
739 shell: rest.first().cloned(),
740 })
741
742 } else {
744 ParseResult::NotFound
745 }
746}
747
748fn complete_file_path(partial: &str) -> Vec<Pair> {
751 let mut candidates = Vec::new();
752
753 let expanded = if partial.starts_with('~') {
755 if let Some(home) = dirs::home_dir() {
756 partial.replacen('~', &home.to_string_lossy(), 1)
757 } else {
758 partial.to_string()
759 }
760 } else {
761 partial.to_string()
762 };
763
764 let (dir_path, file_prefix) = if expanded.ends_with('/') || expanded.ends_with(std::path::MAIN_SEPARATOR) {
766 (std::path::Path::new(&expanded).to_path_buf(), String::new())
767 } else {
768 let p = std::path::Path::new(&expanded);
769 let parent = p.parent().unwrap_or(std::path::Path::new(".")).to_path_buf();
770 let fp = p.file_name().map(|s| s.to_string_lossy().to_string()).unwrap_or_default();
771 (parent, fp)
772 };
773
774 if let Ok(entries) = std::fs::read_dir(&dir_path) {
775 for entry in entries.flatten() {
776 let name = entry.file_name().to_string_lossy().to_string();
777
778 if name.starts_with('.') && !file_prefix.starts_with('.') {
780 continue;
781 }
782
783 if name.starts_with(&file_prefix) {
784 let is_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
785
786 let full_replacement = if partial.ends_with('/') || partial.ends_with(std::path::MAIN_SEPARATOR) {
789 format!("{}{}{}", partial, name, if is_dir { "/" } else { "" })
790 } else if partial.contains('/') || partial.contains(std::path::MAIN_SEPARATOR) {
791 let last_sep = partial.rfind('/').or_else(|| partial.rfind(std::path::MAIN_SEPARATOR)).unwrap();
793 format!("{}/{}{}", &partial[..last_sep], name, if is_dir { "/" } else { "" })
794 } else {
795 format!("{}{}", name, if is_dir { "/" } else { "" })
796 };
797
798 let display_name = format!("{}{}", name, if is_dir { "/" } else { "" });
799
800 candidates.push(Pair {
801 display: display_name,
802 replacement: full_replacement,
803 });
804 }
805 }
806 }
807
808 candidates.sort_by(|a, b| a.display.cmp(&b.display));
810 candidates
811}
812
813fn enter_interactive_shell(config: &YamlConfig) {
817 let os = std::env::consts::OS;
818
819 let shell_path = if os == shell::WINDOWS_OS {
820 shell::WINDOWS_CMD.to_string()
821 } else {
822 std::env::var("SHELL").unwrap_or_else(|_| shell::BASH_PATH.to_string())
824 };
825
826 info!("进入 shell 模式 ({}), 输入 exit 返回 copilot", shell_path);
827
828 let mut command = std::process::Command::new(&shell_path);
829
830 for (key, value) in config.collect_alias_envs() {
832 command.env(&key, &value);
833 }
834
835 let mut cleanup_path: Option<std::path::PathBuf> = None;
837
838 if os != shell::WINDOWS_OS {
841 let is_zsh = shell_path.contains("zsh");
842 let is_bash = shell_path.contains("bash");
843
844 if is_zsh {
845 let pid = std::process::id();
848 let tmp_dir = std::path::PathBuf::from(format!("/tmp/j_shell_zsh_{}", pid));
849 let _ = std::fs::create_dir_all(&tmp_dir);
850
851 let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
852 let zshrc_content = format!(
853 "# j shell 临时配置 - 自动生成,退出后自动清理\n\
854 # 恢复 ZDOTDIR 为用户 home 目录,让后续 source 正常工作\n\
855 export ZDOTDIR=\"{home}\"\n\
856 # 加载用户原始 .zshrc(保留所有配置、alias、插件等)\n\
857 if [ -f \"{home}/.zshrc\" ]; then\n\
858 source \"{home}/.zshrc\"\n\
859 fi\n\
860 # 在用户配置加载完成后覆盖 PROMPT,确保不被 oh-my-zsh 等覆盖\n\
861 PROMPT='%F{{green}}shell%f (%F{{cyan}}%~%f) %F{{green}}>%f '\n",
862 home = home,
863 );
864
865 let zshrc_path = tmp_dir.join(".zshrc");
866 if let Err(e) = std::fs::write(&zshrc_path, &zshrc_content) {
867 error!("创建临时 .zshrc 失败: {}", e);
868 command.env("PROMPT", "%F{green}shell%f (%F{cyan}%~%f) %F{green}>%f ");
870 } else {
871 command.env("ZDOTDIR", tmp_dir.to_str().unwrap_or("/tmp"));
872 cleanup_path = Some(tmp_dir);
873 }
874 } else if is_bash {
875 let pid = std::process::id();
877 let tmp_rc = std::path::PathBuf::from(format!("/tmp/j_shell_bashrc_{}", pid));
878
879 let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
880 let bashrc_content = format!(
881 "# j shell 临时配置 - 自动生成,退出后自动清理\n\
882 # 加载用户原始 .bashrc(保留所有配置、alias 等)\n\
883 if [ -f \"{home}/.bashrc\" ]; then\n\
884 source \"{home}/.bashrc\"\n\
885 fi\n\
886 # 在用户配置加载完成后覆盖 PS1
887 PS1='\\[\\033[32m\\]shell\\[\\033[0m\\] (\\[\\033[36m\\]\\w\\[\\033[0m\\]) \\[\\033[32m\\]>\\[\\033[0m\\] '\n",
888 home = home,
889 );
890
891 if let Err(e) = std::fs::write(&tmp_rc, &bashrc_content) {
892 error!("创建临时 bashrc 失败: {}", e);
893 command.env("PS1", "\\[\\033[32m\\]shell\\[\\033[0m\\] (\\[\\033[36m\\]\\w\\[\\033[0m\\]) \\[\\033[32m\\]>\\[\\033[0m\\] ");
894 } else {
895 command.arg("--rcfile");
896 command.arg(tmp_rc.to_str().unwrap_or("/tmp/j_shell_bashrc"));
897 cleanup_path = Some(tmp_rc);
898 }
899 } else {
900 command.env("PS1", "\x1b[32mshell\x1b[0m (\x1b[36m\\w\x1b[0m) \x1b[32m>\x1b[0m ");
902 command.env("PROMPT", "\x1b[32mshell\x1b[0m (\x1b[36m%~\x1b[0m) \x1b[32m>\x1b[0m ");
903 }
904 }
905
906 command
908 .stdin(std::process::Stdio::inherit())
909 .stdout(std::process::Stdio::inherit())
910 .stderr(std::process::Stdio::inherit());
911
912 match command.status() {
913 Ok(status) => {
914 if !status.success() {
915 if let Some(code) = status.code() {
916 error!("shell 退出码: {}", code);
917 }
918 }
919 }
920 Err(e) => {
921 error!("启动 shell 失败: {}", e);
922 }
923 }
924
925 if let Some(path) = cleanup_path {
927 if path.is_dir() {
928 let _ = std::fs::remove_dir_all(&path);
929 } else {
930 let _ = std::fs::remove_file(&path);
931 }
932 }
933
934 info!("{}", "已返回 copilot 交互模式 🚀".green());
935}
936
937fn execute_shell_command(cmd: &str, config: &YamlConfig) {
940 if cmd.is_empty() {
941 return;
942 }
943
944 let os = std::env::consts::OS;
945 let mut command = if os == shell::WINDOWS_OS {
946 let mut c = std::process::Command::new(shell::WINDOWS_CMD);
947 c.args([shell::WINDOWS_CMD_FLAG, cmd]);
948 c
949 } else {
950 let mut c = std::process::Command::new(shell::BASH_PATH);
951 c.args([shell::BASH_CMD_FLAG, cmd]);
952 c
953 };
954
955 for (key, value) in config.collect_alias_envs() {
957 command.env(&key, &value);
958 }
959
960 let result = command.status();
961
962 match result {
963 Ok(status) => {
964 if !status.success() {
965 if let Some(code) = status.code() {
966 error!("命令退出码: {}", code);
967 }
968 }
969 }
970 Err(e) => {
971 error!("执行命令失败: {}", e);
972 }
973 }
974}
975
976fn inject_envs_to_process(config: &YamlConfig) {
979 for (key, value) in config.collect_alias_envs() {
980 unsafe {
982 std::env::set_var(&key, &value);
983 }
984 }
985}
986
987fn expand_env_vars(input: &str) -> String {
990 let mut result = String::with_capacity(input.len());
991 let chars: Vec<char> = input.chars().collect();
992 let len = chars.len();
993 let mut i = 0;
994
995 while i < len {
996 if chars[i] == '$' && i + 1 < len {
997 if chars[i + 1] == '{' {
999 if let Some(end) = chars[i + 2..].iter().position(|&c| c == '}') {
1000 let var_name: String = chars[i + 2..i + 2 + end].iter().collect();
1001 if let Ok(val) = std::env::var(&var_name) {
1002 result.push_str(&val);
1003 } else {
1004 result.push_str(&input[i..i + 3 + end]);
1006 }
1007 i = i + 3 + end;
1008 continue;
1009 }
1010 }
1011 let start = i + 1;
1013 let mut end = start;
1014 while end < len && (chars[end].is_alphanumeric() || chars[end] == '_') {
1015 end += 1;
1016 }
1017 if end > start {
1018 let var_name: String = chars[start..end].iter().collect();
1019 if let Ok(val) = std::env::var(&var_name) {
1020 result.push_str(&val);
1021 } else {
1022 let original: String = chars[i..end].iter().collect();
1024 result.push_str(&original);
1025 }
1026 i = end;
1027 continue;
1028 }
1029 }
1030 result.push(chars[i]);
1031 i += 1;
1032 }
1033
1034 result
1035}