1use crate::cli::display_utils::{
2 TextAlignment, format_token_for_display, get_terminal_width, pad_text_to_width,
3 text_display_width,
4};
5use crate::config::EnvironmentConfig;
6use crate::config::types::{ConfigStorage, Configuration};
7use anyhow::{Context, Result};
8use colored::*;
9use crossterm::{
10 event::{self, Event, KeyCode, KeyEvent, KeyEventKind},
11 execute, terminal,
12};
13use std::io::{self, Write};
14use std::process::Command;
15
16pub(crate) fn char_display_width(c: char) -> usize {
19 match c as u32 {
20 0x00..=0x7F => 1,
21 0x80..=0x2FF => 1,
22 0x2190..=0x21FF => 2,
23 0x3000..=0x303F => 2,
24 0x3040..=0x309F => 2,
25 0x30A0..=0x30FF => 2,
26 0x4E00..=0x9FFF => 2,
27 0xAC00..=0xD7AF => 2,
28 0x3400..=0x4DBF => 2,
29 0xFF01..=0xFF60 => 2,
30 _ => 1,
31 }
32}
33
34pub(crate) fn truncate_text_to_width(text: &str, available_width: usize) -> (String, usize) {
36 let mut current_width = 0;
37 let truncated: String = text
38 .chars()
39 .take_while(|&c| {
40 let char_width = char_display_width(c);
41 if current_width + char_width <= available_width {
42 current_width += char_width;
43 true
44 } else {
45 false
46 }
47 })
48 .collect();
49 let truncated_width = text_display_width(&truncated);
50 (truncated, truncated_width)
51}
52
53pub(crate) fn cleanup_terminal(stdout: &mut io::Stdout) {
55 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
56 let _ = terminal::disable_raw_mode();
57}
58
59pub(crate) struct BorderDrawing {
61 pub unicode_supported: bool,
63}
64
65impl BorderDrawing {
66 pub(crate) fn new() -> Self {
68 let unicode_supported = Self::detect_unicode_support();
69 Self { unicode_supported }
70 }
71
72 fn detect_unicode_support() -> bool {
74 if let Ok(term) = std::env::var("TERM") {
76 if term.contains("xterm") || term.contains("screen") || term == "tmux-256color" {
78 return true;
79 }
80 }
81
82 if let Ok(lang) = std::env::var("LANG")
84 && (lang.contains("UTF-8") || lang.contains("utf8"))
85 {
86 return true;
87 }
88
89 true
92 }
93
94 pub(crate) fn draw_top_border(&self, title: &str, width: usize) -> String {
96 if self.unicode_supported {
97 let title_padded = format!(" {title} ");
98 let title_len = text_display_width(&title_padded);
99
100 if title_len >= width.saturating_sub(2) {
101 format!("╔{}╗", "═".repeat(width.saturating_sub(2)))
103 } else {
104 let inner_width = width.saturating_sub(2); let padding_total = inner_width.saturating_sub(title_len);
106 let padding_left = padding_total / 2;
107 let padding_right = padding_total - padding_left;
108 format!(
109 "╔{}{}{}╗",
110 "═".repeat(padding_left),
111 title_padded,
112 "═".repeat(padding_right)
113 )
114 }
115 } else {
116 let title_padded = format!(" {title} ");
118 let title_len = title_padded.len();
119
120 if title_len >= width.saturating_sub(2) {
121 format!("+{}+", "-".repeat(width.saturating_sub(2)))
122 } else {
123 let inner_width = width.saturating_sub(2);
124 let padding_total = inner_width.saturating_sub(title_len);
125 let padding_left = padding_total / 2;
126 let padding_right = padding_total - padding_left;
127 format!(
128 "+{}{}{}+",
129 "-".repeat(padding_left),
130 title_padded,
131 "-".repeat(padding_right)
132 )
133 }
134 }
135 }
136
137 pub(crate) fn draw_middle_line(&self, text: &str, width: usize) -> String {
139 let text_len = text_display_width(text);
140 let available_width = width.saturating_sub(4);
142
143 let (left_border, right_border) = if self.unicode_supported {
144 ("║", "║")
145 } else {
146 ("|", "|")
147 };
148
149 if text_len > available_width {
150 let (truncated, truncated_width) = truncate_text_to_width(text, available_width);
152 let padding_spaces = available_width.saturating_sub(truncated_width);
153 format!(
154 "{left_border} {}{} {right_border}",
155 truncated,
156 " ".repeat(padding_spaces)
157 )
158 } else {
159 let padded_text = pad_text_to_width(text, available_width, TextAlignment::Left, ' ');
160 format!("{left_border} {padded_text} {right_border}")
161 }
162 }
163
164 pub(crate) fn draw_bottom_border(&self, width: usize) -> String {
166 if self.unicode_supported {
167 format!("╚{}╝", "═".repeat(width - 2))
168 } else {
169 format!("+{}+", "-".repeat(width - 2))
170 }
171 }
172}
173
174pub fn handle_current_command() -> Result<()> {
184 let storage = ConfigStorage::load()?;
185
186 println!("\n{}", "Current Configuration:".green().bold());
187 println!("Environment variable mode: configurations are set per-command execution");
188 println!("Select a configuration from the menu below to launch Claude");
189 println!("Select 'cc' to launch Claude with default settings");
190
191 let raw_mode_enabled = terminal::enable_raw_mode().is_ok();
193
194 if raw_mode_enabled {
195 let mut stdout = io::stdout();
196 if execute!(
197 stdout,
198 terminal::EnterAlternateScreen,
199 terminal::Clear(terminal::ClearType::All)
200 )
201 .is_ok()
202 {
203 let result = handle_main_menu_interactive(&mut stdout, &storage);
205
206 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
208 let _ = terminal::disable_raw_mode();
209
210 return result;
211 } else {
212 let _ = terminal::disable_raw_mode();
214 }
215 }
216
217 handle_main_menu_simple(&storage)
219}
220
221fn handle_main_menu_interactive(stdout: &mut io::Stdout, storage: &ConfigStorage) -> Result<()> {
223 let menu_items = [
224 "Execute claude --dangerously-skip-permissions",
225 "Switch configuration",
226 "Exit",
227 ];
228 let mut selected_index = 0;
229
230 loop {
231 execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
233 execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
234
235 let border = BorderDrawing::new();
237 const MAIN_MENU_WIDTH: usize = 68;
238
239 println!(
240 "\r{}",
241 border.draw_top_border("Main Menu", MAIN_MENU_WIDTH).green()
242 );
243 println!(
244 "\r{}",
245 border
246 .draw_middle_line(
247 "↑↓/jk导航,1-9快选,E-编辑,R-官方,Q-退出,Enter确认,Esc取消",
248 MAIN_MENU_WIDTH
249 )
250 .green()
251 );
252 println!("\r{}", border.draw_bottom_border(MAIN_MENU_WIDTH).green());
253 println!();
254
255 for (index, item) in menu_items.iter().enumerate() {
257 if index == selected_index {
258 println!("\r> {} {}", "●".blue().bold(), item.blue().bold());
259 } else {
260 println!("\r {} {}", "○".dimmed(), item.dimmed());
261 }
262 }
263
264 stdout.flush()?;
266
267 let event = match event::read() {
269 Ok(event) => event,
270 Err(e) => {
271 cleanup_terminal(stdout);
273 return Err(e.into());
274 }
275 };
276
277 match event {
278 Event::Key(KeyEvent {
279 code,
280 kind: KeyEventKind::Press,
281 ..
282 }) => {
283 match code {
284 KeyCode::Up => {
285 selected_index = selected_index.saturating_sub(1);
286 }
287 KeyCode::Down if selected_index < menu_items.len() - 1 => {
288 selected_index += 1;
289 }
290 KeyCode::Down => {}
291 KeyCode::Enter => {
292 cleanup_terminal(stdout);
294
295 return handle_main_menu_action(selected_index, storage);
296 }
297 KeyCode::Esc => {
298 cleanup_terminal(stdout);
300
301 println!("\nExiting...");
302 return Ok(());
303 }
304 _ => {}
305 }
306 }
307 Event::Key(_) => {} _ => {}
309 }
310 }
311}
312
313fn handle_main_menu_simple(storage: &ConfigStorage) -> Result<()> {
315 loop {
316 println!("\n{}", "Available Actions:".blue().bold());
317 println!("1. Execute claude --dangerously-skip-permissions");
318 println!("2. Switch configuration");
319 println!("3. Exit");
320
321 print!("\nPlease select an option (1-3): ");
322 io::stdout().flush().context("Failed to flush stdout")?;
323
324 let mut input = String::new();
325 io::stdin()
326 .read_line(&mut input)
327 .context("Failed to read input")?;
328
329 let choice = input.trim();
330
331 match choice {
332 "1" => return handle_main_menu_action(0, storage),
333 "2" => return handle_main_menu_action(1, storage),
334 "3" => return handle_main_menu_action(2, storage),
335 _ => {
336 println!("Invalid option. Please select 1-3.");
337 }
338 }
339 }
340}
341
342fn handle_main_menu_action(selected_index: usize, storage: &ConfigStorage) -> Result<()> {
344 match selected_index {
345 0 => {
346 println!("\nExecuting: claude --dangerously-skip-permissions");
347 execute_claude_command(true)?;
348 }
349 1 => {
350 handle_interactive_selection(storage)?;
352 }
353 2 => {
354 println!("Exiting...");
355 }
356 _ => {
357 println!("Invalid selection");
358 }
359 }
360 Ok(())
361}
362
363pub fn handle_interactive_selection(storage: &ConfigStorage) -> Result<()> {
371 if storage.configurations.is_empty() {
372 println!("No configurations available. Use 'add' command to create configurations first.");
373 return Ok(());
374 }
375
376 let mut configs: Vec<Configuration> = storage.configurations.values().cloned().collect();
377 configs.sort_by(|a, b| a.alias_name.cmp(&b.alias_name));
378
379 let mut selected_index = 0;
380
381 let raw_mode_enabled = terminal::enable_raw_mode().is_ok();
383
384 if raw_mode_enabled {
385 let mut stdout = io::stdout();
386 if execute!(
387 stdout,
388 terminal::EnterAlternateScreen,
389 terminal::Clear(terminal::ClearType::All)
390 )
391 .is_ok()
392 {
393 let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
395 let result = handle_full_interactive_menu(
396 &mut stdout,
397 &mut configs,
398 &mut selected_index,
399 storage,
400 storage_mode,
401 );
402
403 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
405 let _ = terminal::disable_raw_mode();
406
407 return result;
408 } else {
409 let _ = terminal::disable_raw_mode();
411 }
412 }
413
414 handle_simple_interactive_menu(&configs.iter().collect::<Vec<_>>(), storage)
416}
417
418fn handle_full_interactive_menu(
420 stdout: &mut io::Stdout,
421 configs: &mut Vec<Configuration>,
422 selected_index: &mut usize,
423 storage: &ConfigStorage,
424 storage_mode: crate::config::types::StorageMode,
425) -> Result<()> {
426 if configs.is_empty() {
428 println!("\r{}", "No configurations available".yellow());
429 println!(
430 "\r{}",
431 "Use 'cc-switch add <alias> <token> <url>' to add configurations first.".dimmed()
432 );
433 println!("\r{}", "Press any key to continue...".dimmed());
434 let _ = event::read(); return Ok(());
436 }
437
438 const PAGE_SIZE: usize = 9; let total_pages = if configs.len() <= PAGE_SIZE {
442 1
443 } else {
444 configs.len().div_ceil(PAGE_SIZE)
445 };
446 let mut current_page = 0;
447
448 loop {
449 let start_idx = current_page * PAGE_SIZE;
451 let end_idx = std::cmp::min(start_idx + PAGE_SIZE, configs.len());
452 let page_configs = &configs[start_idx..end_idx];
453
454 execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
456 execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
457
458 let border = BorderDrawing::new();
460 const CONFIG_MENU_WIDTH: usize = 80;
463
464 println!(
465 "\r{}",
466 border
467 .draw_top_border("Select Configuration", CONFIG_MENU_WIDTH)
468 .green()
469 );
470 if total_pages > 1 {
471 println!(
472 "\r{}",
473 border
474 .draw_middle_line(
475 &format!("第 {} 页,共 {} 页", current_page + 1, total_pages),
476 CONFIG_MENU_WIDTH
477 )
478 .green()
479 );
480 println!(
481 "\r{}",
482 border
483 .draw_middle_line(
484 "↑↓/jk导航,1-9快选,E-编辑,N/P翻页,R-官方,Q-退出,Enter确认",
485 CONFIG_MENU_WIDTH
486 )
487 .green()
488 );
489 } else {
490 println!(
491 "\r{}",
492 border
493 .draw_middle_line(
494 "↑↓/jk导航,1-9快选,E-编辑,R-官方,Q-退出,Enter确认,Esc取消",
495 CONFIG_MENU_WIDTH
496 )
497 .green()
498 );
499 }
500 println!("\r{}", border.draw_bottom_border(CONFIG_MENU_WIDTH).green());
501 println!();
502
503 let official_index = 0;
505 if *selected_index == official_index {
506 println!(
507 "\r> {} {} {}",
508 "●".red().bold(),
509 "[R]".red().bold(),
510 "official".red().bold()
511 );
512 println!("\r Use official Claude API (no custom configuration)");
513 println!();
514 } else {
515 println!("\r {} {} {}", "○".red(), "[R]".red(), "official".red());
516 }
517
518 for (page_index, config) in page_configs.iter().enumerate() {
520 let actual_config_index = start_idx + page_index;
521 let display_number = page_index + 1; let actual_index = actual_config_index + 1; let number_label = format!("[{display_number}]");
524
525 if *selected_index == actual_index {
526 println!(
527 "\r> {} {} {}",
528 "●".blue().bold(),
529 number_label.blue().bold(),
530 config.alias_name.blue().bold()
531 );
532
533 let details = format_config_details(config, "\r ", false);
535 for detail_line in details {
536 println!("{detail_line}");
537 }
538 println!();
539 } else {
540 println!(
541 "\r {} {} {}",
542 "○".dimmed(),
543 number_label.dimmed(),
544 config.alias_name.dimmed()
545 );
546 }
547 }
548
549 let exit_index = configs.len() + 1;
551 if *selected_index == exit_index {
552 println!(
553 "\r> {} {} {}",
554 "●".yellow().bold(),
555 "[Q]".yellow().bold(),
556 "Exit".yellow().bold()
557 );
558 println!("\r Exit without making changes");
559 println!();
560 } else {
561 println!(
562 "\r {} {} {}",
563 "○".dimmed(),
564 "[Q]".dimmed(),
565 "Exit".dimmed()
566 );
567 }
568
569 if total_pages > 1 {
571 println!(
572 "\r{}",
573 format!(
574 "Page Navigation: [N]ext, [P]revious (第 {} 页,共 {} 页)",
575 current_page + 1,
576 total_pages
577 )
578 .dimmed()
579 );
580 }
581
582 stdout.flush()?;
584
585 let event = match event::read() {
587 Ok(event) => event,
588 Err(e) => {
589 cleanup_terminal(stdout);
591 return Err(e.into());
592 }
593 };
594
595 match event {
596 Event::Key(KeyEvent {
597 code,
598 kind: KeyEventKind::Press,
599 ..
600 }) => match code {
601 KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => {
602 *selected_index = selected_index.saturating_sub(1);
603 }
604 KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J')
605 if *selected_index < configs.len() + 1 =>
606 {
607 *selected_index += 1;
608 }
609 KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => {}
610 KeyCode::PageDown | KeyCode::Char('n') | KeyCode::Char('N')
611 if total_pages > 1 && current_page < total_pages - 1 =>
612 {
613 current_page += 1;
614 let new_page_start_idx = current_page * PAGE_SIZE;
615 *selected_index = new_page_start_idx + 1;
616 }
617 KeyCode::PageDown | KeyCode::Char('n') | KeyCode::Char('N') => {}
618 KeyCode::PageUp | KeyCode::Char('p') | KeyCode::Char('P')
619 if total_pages > 1 && current_page > 0 =>
620 {
621 current_page -= 1;
622 let new_page_start_idx = current_page * PAGE_SIZE;
623 *selected_index = new_page_start_idx + 1;
624 }
625 KeyCode::PageUp | KeyCode::Char('p') | KeyCode::Char('P') => {}
626 KeyCode::Enter => {
627 cleanup_terminal(stdout);
629
630 return handle_selection_action(
631 &configs.iter().collect::<Vec<_>>(),
632 *selected_index,
633 storage,
634 storage_mode,
635 );
636 }
637 KeyCode::Esc => {
638 cleanup_terminal(stdout);
640
641 println!("\nSelection cancelled");
642 return Ok(());
643 }
644 KeyCode::Char(c) if c.is_ascii_digit() => {
645 let digit = c.to_digit(10).unwrap() as usize;
646 if digit >= 1 && digit <= page_configs.len() {
648 let actual_config_index = start_idx + (digit - 1);
649 let selection_index = actual_config_index + 1; cleanup_terminal(stdout);
653
654 return handle_selection_action(
655 &configs.iter().collect::<Vec<_>>(),
656 selection_index,
657 storage,
658 storage_mode,
659 );
660 }
661 }
663 KeyCode::Char('r') | KeyCode::Char('R') => {
664 cleanup_terminal(stdout);
666
667 return handle_selection_action(
668 &configs.iter().collect::<Vec<_>>(),
669 0,
670 storage,
671 storage_mode,
672 );
673 }
674 KeyCode::Char('e') | KeyCode::Char('E')
675 if *selected_index > 0 && *selected_index <= configs.len() =>
676 {
677 cleanup_terminal(stdout);
678 let config_index = *selected_index - 1;
679 let edit_result = handle_config_edit(&configs[config_index]);
680 if execute!(
681 stdout,
682 terminal::EnterAlternateScreen,
683 terminal::Clear(terminal::ClearType::All)
684 )
685 .is_ok()
686 && terminal::enable_raw_mode().is_ok()
687 {
688 match edit_result {
689 Ok(_) => {
690 if let Ok(reloaded_storage) = ConfigStorage::load() {
691 *configs =
692 reloaded_storage.configurations.values().cloned().collect();
693 configs.sort_by(|a, b| a.alias_name.cmp(&b.alias_name));
694 if *selected_index > configs.len() + 1 {
695 *selected_index = configs.len() + 1;
696 }
697 }
698 continue;
699 }
700 Err(e) => {
701 if e.downcast_ref::<EditModeError>()
702 == Some(&EditModeError::ReturnToMenu)
703 {
704 continue;
705 }
706 cleanup_terminal(stdout);
707 return Err(e);
708 }
709 }
710 }
711 }
712 KeyCode::Char('e') | KeyCode::Char('E') => {}
713 KeyCode::Char('q') | KeyCode::Char('Q') => {
714 cleanup_terminal(stdout);
716
717 return handle_selection_action(
718 &configs.iter().collect::<Vec<_>>(),
719 configs.len() + 1,
720 storage,
721 storage_mode,
722 );
723 }
724 _ => {}
725 },
726 Event::Key(_) => {} _ => {}
728 }
729 }
730}
731
732fn handle_simple_interactive_menu(
734 configs: &[&Configuration],
735 storage: &ConfigStorage,
736) -> Result<()> {
737 const PAGE_SIZE: usize = 9; if configs.len() <= PAGE_SIZE {
741 return handle_simple_single_page_menu(configs, storage);
742 }
743
744 let total_pages = configs.len().div_ceil(PAGE_SIZE);
746 let mut current_page = 0;
747
748 loop {
749 let start_idx = current_page * PAGE_SIZE;
751 let end_idx = std::cmp::min(start_idx + PAGE_SIZE, configs.len());
752 let page_configs = &configs[start_idx..end_idx];
753
754 println!("\n{}", "Available Configurations:".blue().bold());
755 if total_pages > 1 {
756 println!("第 {} 页,共 {} 页", current_page + 1, total_pages);
757 println!("使用 'n' 下一页, 'p' 上一页, 'r' 官方配置, 'q' 退出");
758 }
759 println!();
760
761 println!("{} {}", "[r]".red().bold(), "official".red());
763 println!(" Use official Claude API (no custom configuration)");
764 println!();
765
766 for (page_index, config) in page_configs.iter().enumerate() {
768 let display_number = page_index + 1;
769
770 println!(
771 "{}. {}",
772 format!("[{display_number}]").green().bold(),
773 config.alias_name.green()
774 );
775
776 let details = format_config_details(config, " ", true);
778 for detail_line in details {
779 println!("{detail_line}");
780 }
781 println!();
782 }
783
784 println!("{} {}", "[q]".yellow().bold(), "Exit".yellow());
786
787 if total_pages > 1 {
788 println!(
789 "\n页面导航: [n]下页, [p]上页 | 配置选择: [1-{}] | [e]编辑 | [r]官方 | [q]退出",
790 page_configs.len()
791 );
792 }
793
794 print!("\n请输入选择: ");
795 io::stdout().flush()?;
796
797 let mut input = String::new();
798 io::stdin().read_line(&mut input)?;
799 let choice = input.trim().to_lowercase();
800
801 match choice.as_str() {
802 "r" => {
803 println!("Using official Claude configuration");
805
806 let mut settings = crate::config::types::ClaudeSettings::load(
808 storage.get_claude_settings_dir().map(|s| s.as_str()),
809 )?;
810 settings.remove_anthropic_env();
811 settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
812
813 return launch_claude_with_env(EnvironmentConfig::empty(), None, None, false);
814 }
815 "e" => {
816 println!("编辑功能在交互式菜单中可用");
819 }
820 "q" => {
821 println!("Exiting...");
822 return Ok(());
823 }
824 "n" if total_pages > 1 && current_page < total_pages - 1 => {
825 current_page += 1;
826 continue;
827 }
828 "p" if total_pages > 1 && current_page > 0 => {
829 current_page -= 1;
830 continue;
831 }
832 digit_str => {
833 if let Ok(digit) = digit_str.parse::<usize>()
834 && digit >= 1
835 && digit <= page_configs.len()
836 {
837 let actual_config_index = start_idx + (digit - 1);
838 let selection_index = actual_config_index + 1; let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
840 return handle_selection_action(
841 configs,
842 selection_index,
843 storage,
844 storage_mode,
845 );
846 }
847 println!("无效选择,请重新输入");
848 }
849 }
850 }
851}
852
853fn handle_simple_single_page_menu(
855 configs: &[&Configuration],
856 storage: &ConfigStorage,
857) -> Result<()> {
858 println!("\n{}", "Available Configurations:".blue().bold());
859
860 println!("1. {}", "official".red());
862 println!(" Use official Claude API (no custom configuration)");
863 println!();
864
865 for (index, config) in configs.iter().enumerate() {
866 println!(
867 "{}. {}",
868 index + 2, config.alias_name.green()
870 );
871
872 let details = format_config_details(config, " ", true);
874 for detail_line in details {
875 println!("{detail_line}");
876 }
877 println!();
878 }
879
880 println!("{}. {}", configs.len() + 2, "Exit".yellow());
881
882 print!("\nSelect configuration (1-{}): ", configs.len() + 2);
883 io::stdout().flush()?;
884
885 let mut input = String::new();
886 io::stdin().read_line(&mut input)?;
887
888 match input.trim().parse::<usize>() {
889 Ok(1) => {
890 println!("Using official Claude configuration");
892
893 let mut settings = crate::config::types::ClaudeSettings::load(
895 storage.get_claude_settings_dir().map(|s| s.as_str()),
896 )?;
897 settings.remove_anthropic_env();
898 settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
899
900 launch_claude_with_env(EnvironmentConfig::empty(), None, None, false)
901 }
902 Ok(num) if num >= 2 && num <= configs.len() + 1 => {
903 let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
904 handle_selection_action(configs, num - 1, storage, storage_mode) }
906 Ok(num) if num == configs.len() + 2 => {
907 println!("Exiting...");
908 Ok(())
909 }
910 _ => {
911 println!("Invalid selection");
912 Ok(())
913 }
914 }
915}
916
917fn handle_selection_action(
919 configs: &[&Configuration],
920 selected_index: usize,
921 storage: &ConfigStorage,
922 storage_mode: crate::config::types::StorageMode,
923) -> Result<()> {
924 if selected_index == 0 {
925 println!("\nUsing official Claude configuration");
927
928 let mut settings = crate::config::types::ClaudeSettings::load(
930 storage.get_claude_settings_dir().map(|s| s.as_str()),
931 )?;
932 settings.remove_anthropic_env();
933 settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
934
935 launch_claude_with_env(EnvironmentConfig::empty(), None, None, false)
936 } else if selected_index <= configs.len() {
937 let config_index = selected_index - 1; let selected_config = configs[config_index].clone();
940 let env_config = EnvironmentConfig::from_config(&selected_config);
941
942 println!(
943 "\nSwitched to configuration '{}'",
944 selected_config.alias_name.green().bold()
945 );
946
947 let details = format_config_details(&selected_config, "", false);
949 for detail_line in details {
950 println!("{detail_line}");
951 }
952
953 let mut settings = crate::config::types::ClaudeSettings::load(
955 storage.get_claude_settings_dir().map(|s| s.as_str()),
956 )?;
957 settings.switch_to_config_with_mode(
958 &selected_config,
959 storage_mode,
960 storage.get_claude_settings_dir().map(|s| s.as_str()),
961 )?;
962
963 launch_claude_with_env(env_config, None, None, false)
964 } else {
965 println!("\nExiting...");
967 Ok(())
968 }
969}
970
971pub fn launch_claude_with_env(
973 env_config: EnvironmentConfig,
974 prompt: Option<&str>,
975 resume: Option<&str>,
976 continue_session: bool,
977) -> Result<()> {
978 println!("\nLaunching Claude CLI...");
979
980 for (key, value) in env_config.as_env_tuples() {
982 unsafe {
983 std::env::set_var(&key, &value);
984 }
985 }
986
987 #[cfg(unix)]
989 {
990 use std::os::unix::process::CommandExt;
991 let mut command = Command::new("claude");
992 command.arg("--dangerously-skip-permissions");
993 if let Some(session_id) = resume {
994 command.args(["--resume", session_id]);
995 }
996 if continue_session {
997 command.arg("--continue");
998 }
999 if let Some(p) = prompt {
1000 command.arg(p);
1001 }
1002 let error = command.exec();
1003 anyhow::bail!("Failed to exec claude: {}", error);
1005 }
1006
1007 #[cfg(not(unix))]
1009 {
1010 use std::process::Stdio;
1011 let mut command = Command::new("claude");
1012 command.arg("--dangerously-skip-permissions");
1013 if let Some(session_id) = resume {
1014 command.args(["--resume", session_id]);
1015 }
1016 if continue_session {
1017 command.arg("--continue");
1018 }
1019 if let Some(p) = prompt {
1020 command.arg(p);
1021 }
1022 command
1023 .stdin(Stdio::inherit())
1024 .stdout(Stdio::inherit())
1025 .stderr(Stdio::inherit());
1026
1027 let mut child = command.spawn().context(
1028 "Failed to launch Claude CLI. Make sure 'claude' command is available in PATH",
1029 )?;
1030
1031 let status = child.wait()?;
1032
1033 if !status.success() {
1034 anyhow::bail!("Claude CLI exited with error status: {}", status);
1035 }
1036 }
1037}
1038
1039fn execute_claude_command(skip_permissions: bool) -> Result<()> {
1044 println!("Launching Claude CLI...");
1045
1046 #[cfg(unix)]
1048 {
1049 use std::os::unix::process::CommandExt;
1050 let mut command = Command::new("claude");
1051 if skip_permissions {
1052 command.arg("--dangerously-skip-permissions");
1053 }
1054
1055 let error = command.exec();
1056 anyhow::bail!("Failed to exec claude: {}", error);
1058 }
1059
1060 #[cfg(not(unix))]
1062 {
1063 use std::process::Stdio;
1064 let mut command = Command::new("claude");
1065 if skip_permissions {
1066 command.arg("--dangerously-skip-permissions");
1067 }
1068
1069 command
1070 .stdin(Stdio::inherit())
1071 .stdout(Stdio::inherit())
1072 .stderr(Stdio::inherit());
1073
1074 let mut child = command.spawn().context(
1075 "Failed to launch Claude CLI. Make sure 'claude' command is available in PATH",
1076 )?;
1077
1078 let status = child
1079 .wait()
1080 .context("Failed to wait for Claude CLI process")?;
1081
1082 if !status.success() {
1083 anyhow::bail!("Claude CLI exited with error status: {}", status);
1084 }
1085 }
1086}
1087
1088pub fn read_input(prompt: &str) -> Result<String> {
1096 print!("{prompt}");
1097 io::stdout().flush().context("Failed to flush stdout")?;
1098 let mut input = String::new();
1099 io::stdin()
1100 .read_line(&mut input)
1101 .context("Failed to read input")?;
1102 Ok(input.trim().to_string())
1103}
1104
1105pub fn read_sensitive_input(prompt: &str) -> Result<String> {
1113 print!("{prompt}");
1114 io::stdout().flush().context("Failed to flush stdout")?;
1115 let mut input = String::new();
1116 io::stdin()
1117 .read_line(&mut input)
1118 .context("Failed to read input")?;
1119 Ok(input.trim().to_string())
1120}
1121
1122fn format_config_details(config: &Configuration, indent: &str, _compact: bool) -> Vec<String> {
1135 let mut lines = Vec::new();
1136
1137 let terminal_width = get_terminal_width();
1139 let _available_width = terminal_width.saturating_sub(text_display_width(indent) + 8);
1140
1141 let token_label = "Token:";
1143 let url_label = "URL:";
1144 let model_label = "Model:";
1145 let small_model_label = "Small Fast Model:";
1146 let max_thinking_tokens_label = "Max Thinking Tokens:";
1147 let api_timeout_ms_label = "API Timeout (ms):";
1148 let disable_nonessential_traffic_label = "Disable Nonessential Traffic:";
1149 let default_sonnet_model_label = "Default Sonnet Model:";
1150 let default_opus_model_label = "Default Opus Model:";
1151 let default_haiku_model_label = "Default Haiku Model:";
1152 let subagent_model_label = "Subagent Model:";
1153 let disable_nonstreaming_fallback_label = "Disable Nonstreaming Fallback:";
1154 let effort_level_label = "Effort Level:";
1155
1156 let max_label_width = [
1158 token_label,
1159 url_label,
1160 model_label,
1161 small_model_label,
1162 max_thinking_tokens_label,
1163 api_timeout_ms_label,
1164 disable_nonessential_traffic_label,
1165 default_sonnet_model_label,
1166 default_opus_model_label,
1167 default_haiku_model_label,
1168 subagent_model_label,
1169 disable_nonstreaming_fallback_label,
1170 effort_level_label,
1171 ]
1172 .iter()
1173 .map(|label| text_display_width(label))
1174 .max()
1175 .unwrap_or(0);
1176
1177 let token_line = format!(
1179 "{}{} {}",
1180 indent,
1181 pad_text_to_width(token_label, max_label_width, TextAlignment::Left, ' '),
1182 format_token_for_display(&config.token).dimmed()
1183 );
1184 lines.push(token_line);
1185
1186 let url_line = format!(
1188 "{}{} {}",
1189 indent,
1190 pad_text_to_width(url_label, max_label_width, TextAlignment::Left, ' '),
1191 config.url.cyan()
1192 );
1193 lines.push(url_line);
1194
1195 if let Some(model) = &config.model {
1197 let model_line = format!(
1198 "{}{} {}",
1199 indent,
1200 pad_text_to_width(model_label, max_label_width, TextAlignment::Left, ' '),
1201 model.yellow()
1202 );
1203 lines.push(model_line);
1204 }
1205
1206 if let Some(small_fast_model) = &config.small_fast_model {
1208 let small_model_line = format!(
1209 "{}{} {}",
1210 indent,
1211 pad_text_to_width(small_model_label, max_label_width, TextAlignment::Left, ' '),
1212 small_fast_model.yellow()
1213 );
1214 lines.push(small_model_line);
1215 }
1216
1217 if let Some(max_thinking_tokens) = config.max_thinking_tokens {
1219 let tokens_line = format!(
1220 "{}{} {}",
1221 indent,
1222 pad_text_to_width(
1223 max_thinking_tokens_label,
1224 max_label_width,
1225 TextAlignment::Left,
1226 ' '
1227 ),
1228 format!("{}", max_thinking_tokens).yellow()
1229 );
1230 lines.push(tokens_line);
1231 }
1232
1233 if let Some(api_timeout_ms) = config.api_timeout_ms {
1235 let timeout_line = format!(
1236 "{}{} {}",
1237 indent,
1238 pad_text_to_width(
1239 api_timeout_ms_label,
1240 max_label_width,
1241 TextAlignment::Left,
1242 ' '
1243 ),
1244 format!("{}", api_timeout_ms).yellow()
1245 );
1246 lines.push(timeout_line);
1247 }
1248
1249 if let Some(disable_flag) = config.claude_code_disable_nonessential_traffic {
1251 let flag_line = format!(
1252 "{}{} {}",
1253 indent,
1254 pad_text_to_width(
1255 disable_nonessential_traffic_label,
1256 max_label_width,
1257 TextAlignment::Left,
1258 ' '
1259 ),
1260 format!("{}", disable_flag).yellow()
1261 );
1262 lines.push(flag_line);
1263 }
1264
1265 if let Some(sonnet_model) = &config.anthropic_default_sonnet_model {
1267 let sonnet_line = format!(
1268 "{}{} {}",
1269 indent,
1270 pad_text_to_width(
1271 default_sonnet_model_label,
1272 max_label_width,
1273 TextAlignment::Left,
1274 ' '
1275 ),
1276 sonnet_model.yellow()
1277 );
1278 lines.push(sonnet_line);
1279 }
1280
1281 if let Some(opus_model) = &config.anthropic_default_opus_model {
1283 let opus_line = format!(
1284 "{}{} {}",
1285 indent,
1286 pad_text_to_width(
1287 default_opus_model_label,
1288 max_label_width,
1289 TextAlignment::Left,
1290 ' '
1291 ),
1292 opus_model.yellow()
1293 );
1294 lines.push(opus_line);
1295 }
1296
1297 if let Some(haiku_model) = &config.anthropic_default_haiku_model {
1299 let haiku_line = format!(
1300 "{}{} {}",
1301 indent,
1302 pad_text_to_width(
1303 default_haiku_model_label,
1304 max_label_width,
1305 TextAlignment::Left,
1306 ' '
1307 ),
1308 haiku_model.yellow()
1309 );
1310 lines.push(haiku_line);
1311 }
1312
1313 if let Some(subagent_model) = &config.claude_code_subagent_model {
1315 let subagent_line = format!(
1316 "{}{} {}",
1317 indent,
1318 pad_text_to_width(
1319 subagent_model_label,
1320 max_label_width,
1321 TextAlignment::Left,
1322 ' '
1323 ),
1324 subagent_model.yellow()
1325 );
1326 lines.push(subagent_line);
1327 }
1328
1329 if let Some(disable_flag) = config.claude_code_disable_nonstreaming_fallback {
1331 let flag_line = format!(
1332 "{}{} {}",
1333 indent,
1334 pad_text_to_width(
1335 disable_nonstreaming_fallback_label,
1336 max_label_width,
1337 TextAlignment::Left,
1338 ' '
1339 ),
1340 format!("{}", disable_flag).yellow()
1341 );
1342 lines.push(flag_line);
1343 }
1344
1345 if let Some(effort_level) = &config.claude_code_effort_level {
1347 let effort_line = format!(
1348 "{}{} {}",
1349 indent,
1350 pad_text_to_width(
1351 effort_level_label,
1352 max_label_width,
1353 TextAlignment::Left,
1354 ' '
1355 ),
1356 effort_level.yellow()
1357 );
1358 lines.push(effort_line);
1359 }
1360
1361 lines
1362}
1363
1364#[cfg(test)]
1365mod border_drawing_tests {
1366 use super::*;
1367
1368 #[test]
1369 fn test_border_drawing_unicode_support() {
1370 let _border = BorderDrawing::new();
1371 }
1373
1374 #[test]
1375 fn test_border_drawing_top_border() {
1376 let border = BorderDrawing {
1377 unicode_supported: true,
1378 };
1379 let result = border.draw_top_border("Test", 20);
1380 assert!(!result.is_empty());
1381 assert!(result.contains("Test"));
1382 }
1383
1384 #[test]
1385 fn test_border_drawing_ascii_fallback() {
1386 let border = BorderDrawing {
1387 unicode_supported: false,
1388 };
1389 let result = border.draw_top_border("Test", 20);
1390 assert!(!result.is_empty());
1391 assert!(result.contains("Test"));
1392 assert!(result.contains("+"));
1393 assert!(result.contains("-"));
1394 }
1395
1396 #[test]
1397 fn test_border_drawing_middle_line() {
1398 let border = BorderDrawing {
1399 unicode_supported: true,
1400 };
1401 let result = border.draw_middle_line("Test message", 30);
1402 assert!(!result.is_empty());
1403 assert!(result.contains("Test message"));
1404 }
1405
1406 #[test]
1407 fn test_border_drawing_bottom_border() {
1408 let border = BorderDrawing {
1409 unicode_supported: true,
1410 };
1411 let result = border.draw_bottom_border(20);
1412 assert!(!result.is_empty());
1413 }
1414
1415 #[test]
1416 fn test_border_drawing_width_consistency() {
1417 let border = BorderDrawing {
1418 unicode_supported: true,
1419 };
1420 let width = 30;
1421 let top = border.draw_top_border("Title", width);
1422 let middle = border.draw_middle_line("Content", width);
1423 let bottom = border.draw_bottom_border(width);
1424
1425 assert!(top.chars().count() >= width - 2);
1427 assert!(middle.chars().count() >= width - 2);
1428 assert!(bottom.chars().count() >= width - 2);
1429 }
1430}
1431
1432#[cfg(test)]
1433mod pagination_tests {
1434
1435 #[test]
1437 fn test_pagination_calculation() {
1438 const PAGE_SIZE: usize = 9;
1439
1440 assert_eq!(1_usize.div_ceil(PAGE_SIZE), 1); assert_eq!(9_usize.div_ceil(PAGE_SIZE), 1); assert_eq!(10_usize.div_ceil(PAGE_SIZE), 2); assert_eq!(18_usize.div_ceil(PAGE_SIZE), 2); assert_eq!(19_usize.div_ceil(PAGE_SIZE), 3); assert_eq!(27_usize.div_ceil(PAGE_SIZE), 3); assert_eq!(28_usize.div_ceil(PAGE_SIZE), 4); }
1451
1452 #[test]
1454 fn test_page_range_calculation() {
1455 const PAGE_SIZE: usize = 9;
1456
1457 let current_page = 0;
1459 let start_idx = current_page * PAGE_SIZE; let end_idx = std::cmp::min(start_idx + PAGE_SIZE, 15); assert_eq!(start_idx, 0);
1462 assert_eq!(end_idx, 9);
1463 assert_eq!(end_idx - start_idx, 9); let current_page = 1;
1467 let start_idx = current_page * PAGE_SIZE; let end_idx = std::cmp::min(start_idx + PAGE_SIZE, 15); assert_eq!(start_idx, 9);
1470 assert_eq!(end_idx, 15);
1471 assert_eq!(end_idx - start_idx, 6); let current_page = 0;
1475 let start_idx = current_page * PAGE_SIZE; let end_idx = std::cmp::min(start_idx + PAGE_SIZE, PAGE_SIZE); assert_eq!(start_idx, 0);
1478 assert_eq!(end_idx, 9);
1479 assert_eq!(end_idx - start_idx, 9); }
1481
1482 #[test]
1484 fn test_digit_mapping_to_config_index() {
1485 const PAGE_SIZE: usize = 9;
1486
1487 let current_page = 0;
1489 let start_idx = current_page * PAGE_SIZE; let digit = 1;
1493 let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 0);
1495
1496 let digit = 9;
1498 let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 8);
1500
1501 let current_page = 1;
1503 let start_idx = current_page * PAGE_SIZE; let digit = 1;
1507 let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 9);
1509
1510 let digit = 5;
1512 let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 13);
1514 }
1515
1516 #[test]
1518 fn test_selection_index_conversion() {
1519 const PAGE_SIZE: usize = 9;
1526
1527 let current_page = 0;
1529 let start_idx = current_page * PAGE_SIZE; let digit = 1;
1531 let actual_config_index = start_idx + (digit - 1); let selection_index = actual_config_index + 1; assert_eq!(selection_index, 1);
1534
1535 let current_page = 1;
1537 let start_idx = current_page * PAGE_SIZE; let digit = 1;
1539 let actual_config_index = start_idx + (digit - 1); let selection_index = actual_config_index + 1; assert_eq!(selection_index, 10);
1542 }
1543
1544 #[test]
1546 fn test_page_navigation_bounds() {
1547 const PAGE_SIZE: usize = 9;
1548 let total_configs: usize = 25; let total_pages = total_configs.div_ceil(PAGE_SIZE); assert_eq!(total_pages, 3);
1551
1552 let mut current_page = 0;
1554 if current_page > 0 {
1555 current_page -= 1;
1556 }
1557 assert_eq!(current_page, 0); let mut current_page = total_pages - 1; if current_page < total_pages - 1 {
1562 current_page += 1;
1563 }
1564 assert_eq!(current_page, 2); let mut current_page = 1;
1568
1569 if current_page < total_pages - 1 {
1571 current_page += 1;
1572 }
1573 assert_eq!(current_page, 2);
1574
1575 if current_page > 0 {
1577 current_page = current_page.saturating_sub(1);
1578 }
1579 assert_eq!(current_page, 1);
1580 }
1581
1582 #[test]
1584 fn test_digit_key_boundary_conditions() {
1585 const PAGE_SIZE: usize = 9;
1586
1587 let digit = 0;
1589 assert!(digit < 1, "Digit 0 should be less than 1 and ignored");
1590
1591 let configs_len = 5; let page_configs_len = std::cmp::min(PAGE_SIZE, configs_len); let digit = 9; assert!(
1596 digit > page_configs_len,
1597 "Digit 9 should be beyond available configs (5) and ignored"
1598 );
1599
1600 for digit in 1..=page_configs_len {
1602 assert!(
1603 digit >= 1 && digit <= page_configs_len,
1604 "Digit {} should be valid",
1605 digit
1606 );
1607 }
1608 }
1609
1610 #[test]
1612 fn test_empty_configs_handling() {
1613 let empty_configs: Vec<String> = Vec::new();
1614 assert!(
1615 empty_configs.is_empty(),
1616 "Empty config list should be properly detected"
1617 );
1618
1619 let configs_len = empty_configs.len(); assert_eq!(configs_len, 0, "Empty configs should have length 0");
1622
1623 }
1626
1627 #[test]
1629 fn test_page_navigation_boundaries() {
1630 const PAGE_SIZE: usize = 9;
1631 let total_configs: usize = 20; let total_pages = total_configs.div_ceil(PAGE_SIZE); let mut current_page = 0;
1636 let original_page = current_page;
1637
1638 if current_page > 0 {
1640 current_page -= 1;
1641 }
1642 assert_eq!(
1643 current_page, original_page,
1644 "First page should not navigate to previous"
1645 );
1646
1647 let mut current_page = total_pages - 1; let original_page = current_page;
1650
1651 if current_page < total_pages - 1 {
1653 current_page += 1;
1654 }
1655 assert_eq!(
1656 current_page, original_page,
1657 "Last page should not navigate to next"
1658 );
1659
1660 let mut current_page = 1; if current_page < total_pages - 1 {
1665 current_page += 1;
1666 }
1667 assert_eq!(current_page, 2, "Should navigate to next page");
1668
1669 if current_page > 0 {
1671 current_page = current_page.saturating_sub(1);
1672 }
1673 assert_eq!(current_page, 1, "Should navigate to previous page");
1674 }
1675
1676 #[test]
1678 fn test_j_key_navigation() {
1679 let mut selected_index: usize = 0;
1680 let configs_len = 5; if selected_index < configs_len + 1 {
1685 selected_index += 1;
1686 }
1687 assert_eq!(selected_index, 1, "j key should move selection down by one");
1688
1689 selected_index = configs_len + 1;
1691 let original_index = selected_index;
1692 if selected_index < configs_len + 1 {
1693 selected_index += 1;
1694 }
1695 assert_eq!(
1696 selected_index, original_index,
1697 "j key should not move beyond bottom boundary"
1698 );
1699 }
1700
1701 #[test]
1703 fn test_k_key_navigation() {
1704 let mut selected_index: usize = 5;
1705
1706 selected_index = selected_index.saturating_sub(1);
1709 assert_eq!(selected_index, 4, "k key should move selection up by one");
1710
1711 selected_index = 0;
1713 let original_index = selected_index;
1714 selected_index = selected_index.saturating_sub(1);
1715 assert_eq!(
1716 selected_index, original_index,
1717 "k key should not move beyond top boundary"
1718 );
1719 }
1720
1721 #[test]
1723 fn test_jk_key_boundary_conditions() {
1724 const CONFIGS_LEN: usize = 5;
1725
1726 let mut selected_index: usize = CONFIGS_LEN + 1; let original_index = selected_index;
1729 if selected_index < CONFIGS_LEN + 1 {
1730 selected_index += 1; }
1732 assert_eq!(
1733 selected_index, original_index,
1734 "j key should respect bottom boundary like Down arrow"
1735 );
1736
1737 let mut selected_index: usize = 0; let original_index = selected_index;
1740 selected_index = selected_index.saturating_sub(1); assert_eq!(
1742 selected_index, original_index,
1743 "k key should respect top boundary like Up arrow"
1744 );
1745 }
1746}
1747
1748#[derive(Debug, PartialEq)]
1750pub(crate) enum EditModeError {
1751 ReturnToMenu,
1752}
1753
1754impl std::fmt::Display for EditModeError {
1755 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1756 match self {
1757 EditModeError::ReturnToMenu => write!(f, "return_to_menu"),
1758 }
1759 }
1760}
1761
1762impl std::error::Error for EditModeError {}
1763
1764fn handle_config_edit(config: &Configuration) -> Result<()> {
1766 println!("\n{}", "配置编辑模式".green().bold());
1767 println!("{}", "===================".green());
1768 println!("正在编辑配置: {}", config.alias_name.cyan().bold());
1769 println!();
1770
1771 let mut editing_config = config.clone();
1773 let original_alias = config.alias_name.clone();
1774
1775 loop {
1776 display_edit_menu(&editing_config);
1778
1779 println!("\n{}", "提示: 可使用大小写字母".dimmed());
1781 print!("请选择要编辑的字段 (1-9, A-E), 或输入 S 保存, Q 返回上一级菜单: ");
1782 io::stdout().flush()?;
1783
1784 let mut input = String::new();
1785 io::stdin().read_line(&mut input)?;
1786 let input = input.trim();
1787
1788 match input {
1790 "1" => edit_field_alias(&mut editing_config)?,
1791 "2" => edit_field_token(&mut editing_config)?,
1792 "3" => edit_field_url(&mut editing_config)?,
1793 "4" => edit_field_model(&mut editing_config)?,
1794 "5" => edit_field_small_fast_model(&mut editing_config)?,
1795 "6" => edit_field_max_thinking_tokens(&mut editing_config)?,
1796 "7" => edit_field_api_timeout_ms(&mut editing_config)?,
1797 "8" => edit_field_claude_code_disable_nonessential_traffic(&mut editing_config)?,
1798 "9" => edit_field_anthropic_default_sonnet_model(&mut editing_config)?,
1799 "10" | "a" | "A" => edit_field_anthropic_default_opus_model(&mut editing_config)?,
1800 "11" | "b" | "B" => edit_field_anthropic_default_haiku_model(&mut editing_config)?,
1801 "12" | "c" | "C" => edit_field_claude_code_subagent_model(&mut editing_config)?,
1802 "13" | "d" | "D" => {
1803 edit_field_claude_code_disable_nonstreaming_fallback(&mut editing_config)?
1804 }
1805 "14" | "e" | "E" => edit_field_claude_code_effort_level(&mut editing_config)?,
1806 "s" | "S" => {
1807 return save_configuration_changes(&original_alias, &editing_config);
1809 }
1810 "q" | "Q" => {
1811 println!("\n{}", "返回上一级菜单".blue());
1812 return Err(EditModeError::ReturnToMenu.into());
1813 }
1814 _ => {
1815 println!("{}", "无效选择,请重试".red());
1816 }
1817 }
1818 }
1819}
1820
1821fn display_edit_menu(config: &Configuration) {
1823 println!("\n{}", "当前配置值:".blue().bold());
1824 println!("{}", "─────────────────────────".blue());
1825
1826 println!("1. 别名 (alias_name): {}", config.alias_name.green());
1827
1828 println!(
1829 "2. 令牌 (ANTHROPIC_AUTH_TOKEN): {}",
1830 format_token_for_display(&config.token).green()
1831 );
1832
1833 println!("3. URL (ANTHROPIC_BASE_URL): {}", config.url.green());
1834
1835 println!(
1836 "4. 模型 (ANTHROPIC_MODEL): {}",
1837 config.model.as_deref().unwrap_or("[未设置]").green()
1838 );
1839
1840 println!(
1841 "5. 快速模型 (ANTHROPIC_SMALL_FAST_MODEL): {}",
1842 config
1843 .small_fast_model
1844 .as_deref()
1845 .unwrap_or("[未设置]")
1846 .green()
1847 );
1848
1849 println!(
1850 "6. 最大思考令牌数 (ANTHROPIC_MAX_THINKING_TOKENS): {}",
1851 config
1852 .max_thinking_tokens
1853 .map(|t| t.to_string())
1854 .unwrap_or("[未设置]".to_string())
1855 .green()
1856 );
1857
1858 println!(
1859 "7. API超时时间 (API_TIMEOUT_MS): {}",
1860 config
1861 .api_timeout_ms
1862 .map(|t| t.to_string())
1863 .unwrap_or("[未设置]".to_string())
1864 .green()
1865 );
1866
1867 println!(
1868 "8. 禁用非必要流量 (CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC): {}",
1869 config
1870 .claude_code_disable_nonessential_traffic
1871 .map(|t| t.to_string())
1872 .unwrap_or("[未设置]".to_string())
1873 .green()
1874 );
1875
1876 println!(
1877 "9. 默认 Sonnet 模型 (ANTHROPIC_DEFAULT_SONNET_MODEL): {}",
1878 config
1879 .anthropic_default_sonnet_model
1880 .as_deref()
1881 .unwrap_or("[未设置]")
1882 .green()
1883 );
1884
1885 println!(
1886 "A. 默认 Opus 模型 (ANTHROPIC_DEFAULT_OPUS_MODEL): {}",
1887 config
1888 .anthropic_default_opus_model
1889 .as_deref()
1890 .unwrap_or("[未设置]")
1891 .green()
1892 );
1893
1894 println!(
1895 "B. 默认 Haiku 模型 (ANTHROPIC_DEFAULT_HAIKU_MODEL): {}",
1896 config
1897 .anthropic_default_haiku_model
1898 .as_deref()
1899 .unwrap_or("[未设置]")
1900 .green()
1901 );
1902
1903 println!(
1904 "C. 子代理模型 (CLAUDE_CODE_SUBAGENT_MODEL): {}",
1905 config
1906 .claude_code_subagent_model
1907 .as_deref()
1908 .unwrap_or("[未设置]")
1909 .green()
1910 );
1911
1912 println!(
1913 "D. 禁用非流式回退 (CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK): {}",
1914 config
1915 .claude_code_disable_nonstreaming_fallback
1916 .map(|t| t.to_string())
1917 .unwrap_or("[未设置]".to_string())
1918 .green()
1919 );
1920
1921 println!(
1922 "E. 努力级别 (CLAUDE_CODE_EFFORT_LEVEL): {}",
1923 config
1924 .claude_code_effort_level
1925 .as_deref()
1926 .unwrap_or("[未设置]")
1927 .green()
1928 );
1929
1930 println!("{}", "─────────────────────────".blue());
1931 println!(
1932 "S. {} | Q. {}",
1933 "保存更改".green().bold(),
1934 "返回上一级菜单".blue()
1935 );
1936}
1937
1938pub(crate) fn edit_string_field(
1940 field_name: &str,
1941 current_value: &str,
1942 validator: impl Fn(&str) -> Result<()>,
1943) -> Result<Option<String>> {
1944 println!("\n编辑{field_name}:");
1945 println!("当前值: {}", current_value.cyan());
1946 print!("新值 (回车保持不变): ");
1947 io::stdout().flush()?;
1948
1949 let mut input = String::new();
1950 io::stdin().read_line(&mut input)?;
1951 let input = input.trim();
1952
1953 if !input.is_empty() {
1954 validator(input)?;
1955 println!("{field_name}已更新为: {}", input.green());
1956 Ok(Some(input.to_string()))
1957 } else {
1958 Ok(None)
1959 }
1960}
1961
1962pub(crate) type OptionalStringResult = Result<Option<Option<String>>>;
1964
1965pub(crate) fn edit_optional_string_field(
1967 field_name: &str,
1968 current_value: Option<&str>,
1969) -> OptionalStringResult {
1970 println!("\n编辑{field_name}:");
1971 println!("当前值: {}", current_value.unwrap_or("[未设置]").cyan());
1972 print!("新值 (回车保持不变,输入空格清除): ");
1973 io::stdout().flush()?;
1974
1975 let mut input = String::new();
1976 io::stdin().read_line(&mut input)?;
1977 let input = input.trim();
1978
1979 if !input.is_empty() {
1980 if input == " " {
1981 println!("{}", format!("{field_name}已清除").green());
1982 Ok(Some(None))
1983 } else {
1984 println!("{field_name}已更新为: {}", input.green());
1985 Ok(Some(Some(input.to_string())))
1986 }
1987 } else {
1988 Ok(None)
1989 }
1990}
1991
1992type OptionalU32Result = Result<Option<Option<u32>>>;
1994
1995fn edit_optional_u32_field(field_name: &str, current_value: Option<u32>) -> OptionalU32Result {
1997 println!("\n编辑{field_name}:");
1998 println!(
1999 "当前值: {}",
2000 current_value
2001 .map(|t| t.to_string())
2002 .unwrap_or("[未设置]".to_string())
2003 .cyan()
2004 );
2005 print!("新值 (回车保持不变,输入 0 清除): ");
2006 io::stdout().flush()?;
2007
2008 let mut input = String::new();
2009 io::stdin().read_line(&mut input)?;
2010 let input = input.trim();
2011
2012 if !input.is_empty() {
2013 if input == "0" {
2014 println!("{}", format!("{field_name}已清除").green());
2015 Ok(Some(None))
2016 } else if let Ok(value) = input.parse::<u32>() {
2017 println!("{field_name}已更新为: {}", value.to_string().green());
2018 Ok(Some(Some(value)))
2019 } else {
2020 println!("{}", "错误: 请输入有效的数字".red());
2021 Ok(None)
2022 }
2023 } else {
2024 Ok(None)
2025 }
2026}
2027
2028fn edit_field_alias(config: &mut Configuration) -> Result<()> {
2030 let validator = |input: &str| -> Result<()> {
2031 if input.contains(char::is_whitespace) {
2032 anyhow::bail!("错误: 别名不能包含空白字符");
2033 }
2034 if input == "cc" {
2035 anyhow::bail!("错误: 'cc' 是保留名称");
2036 }
2037 if input == "official" {
2038 anyhow::bail!("错误: 'official' 是保留名称");
2039 }
2040 Ok(())
2041 };
2042
2043 match edit_string_field("别名", &config.alias_name, validator) {
2044 Ok(Some(new_value)) => config.alias_name = new_value,
2045 Ok(None) => {}
2046 Err(e) => println!("{}", e.to_string().red()),
2047 }
2048 Ok(())
2049}
2050
2051fn edit_field_token(config: &mut Configuration) -> Result<()> {
2053 let no_validator = |_: &str| -> Result<()> { Ok(()) };
2054 if let Some(new_value) = edit_string_field(
2055 "令牌",
2056 &format_token_for_display(&config.token),
2057 no_validator,
2058 )? {
2059 config.token = new_value;
2060 println!("{}", "令牌已更新".green());
2061 }
2062 Ok(())
2063}
2064
2065fn edit_field_url(config: &mut Configuration) -> Result<()> {
2067 let no_validator = |_: &str| -> Result<()> { Ok(()) };
2068 if let Some(new_value) = edit_string_field("URL", &config.url, no_validator)? {
2069 config.url = new_value;
2070 }
2071 Ok(())
2072}
2073
2074fn edit_field_model(config: &mut Configuration) -> Result<()> {
2076 if let Some(result) = edit_optional_string_field("模型", config.model.as_deref())? {
2077 config.model = result;
2078 }
2079 Ok(())
2080}
2081
2082fn edit_field_small_fast_model(config: &mut Configuration) -> Result<()> {
2084 if let Some(result) =
2085 edit_optional_string_field("快速模型", config.small_fast_model.as_deref())?
2086 {
2087 config.small_fast_model = result;
2088 }
2089 Ok(())
2090}
2091
2092fn edit_field_max_thinking_tokens(config: &mut Configuration) -> Result<()> {
2094 if let Some(result) = edit_optional_u32_field("最大思考令牌数", config.max_thinking_tokens)?
2095 {
2096 config.max_thinking_tokens = result;
2097 }
2098 Ok(())
2099}
2100
2101fn edit_field_api_timeout_ms(config: &mut Configuration) -> Result<()> {
2103 if let Some(result) = edit_optional_u32_field("API超时时间 (毫秒)", config.api_timeout_ms)?
2104 {
2105 config.api_timeout_ms = result;
2106 }
2107 Ok(())
2108}
2109
2110fn edit_field_claude_code_disable_nonessential_traffic(config: &mut Configuration) -> Result<()> {
2112 if let Some(result) = edit_optional_u32_field(
2113 "禁用非必要流量标志",
2114 config.claude_code_disable_nonessential_traffic,
2115 )? {
2116 config.claude_code_disable_nonessential_traffic = result;
2117 }
2118 Ok(())
2119}
2120
2121fn edit_field_anthropic_default_sonnet_model(config: &mut Configuration) -> Result<()> {
2123 if let Some(result) = edit_optional_string_field(
2124 "默认 Sonnet 模型",
2125 config.anthropic_default_sonnet_model.as_deref(),
2126 )? {
2127 config.anthropic_default_sonnet_model = result;
2128 }
2129 Ok(())
2130}
2131
2132fn edit_field_anthropic_default_opus_model(config: &mut Configuration) -> Result<()> {
2134 if let Some(result) = edit_optional_string_field(
2135 "默认 Opus 模型",
2136 config.anthropic_default_opus_model.as_deref(),
2137 )? {
2138 config.anthropic_default_opus_model = result;
2139 }
2140 Ok(())
2141}
2142
2143fn edit_field_anthropic_default_haiku_model(config: &mut Configuration) -> Result<()> {
2145 if let Some(result) = edit_optional_string_field(
2146 "默认 Haiku 模型",
2147 config.anthropic_default_haiku_model.as_deref(),
2148 )? {
2149 config.anthropic_default_haiku_model = result;
2150 }
2151 Ok(())
2152}
2153
2154fn edit_field_claude_code_subagent_model(config: &mut Configuration) -> Result<()> {
2156 if let Some(result) =
2157 edit_optional_string_field("子代理模型", config.claude_code_subagent_model.as_deref())?
2158 {
2159 config.claude_code_subagent_model = result;
2160 }
2161 Ok(())
2162}
2163
2164fn edit_field_claude_code_disable_nonstreaming_fallback(config: &mut Configuration) -> Result<()> {
2166 if let Some(result) = edit_optional_u32_field(
2167 "禁用非流式回退标志",
2168 config.claude_code_disable_nonstreaming_fallback,
2169 )? {
2170 config.claude_code_disable_nonstreaming_fallback = result;
2171 }
2172 Ok(())
2173}
2174
2175fn edit_field_claude_code_effort_level(config: &mut Configuration) -> Result<()> {
2177 if let Some(result) =
2178 edit_optional_string_field("努力级别", config.claude_code_effort_level.as_deref())?
2179 {
2180 config.claude_code_effort_level = result;
2181 }
2182 Ok(())
2183}
2184
2185fn save_configuration_changes(original_alias: &str, new_config: &Configuration) -> Result<()> {
2187 let mut storage = ConfigStorage::load()?;
2189
2190 if original_alias != new_config.alias_name
2192 && storage.get_configuration(&new_config.alias_name).is_some()
2193 {
2194 println!("\n{}", "别名冲突!".red().bold());
2195 println!("配置 '{}' 已存在", new_config.alias_name.yellow());
2196 print!("是否覆盖现有配置? (y/N): ");
2197 io::stdout().flush()?;
2198
2199 let mut input = String::new();
2200 io::stdin().read_line(&mut input)?;
2201 let input = input.trim().to_lowercase();
2202
2203 if input != "y" && input != "yes" {
2204 println!("{}", "编辑已取消".yellow());
2205 return Ok(());
2206 }
2207 }
2208
2209 storage.update_configuration(original_alias, new_config.clone())?;
2211 storage.save()?;
2212
2213 println!("\n{}", "配置已成功保存!".green().bold());
2214
2215 Ok(())
2216}