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 => {
288 if selected_index < menu_items.len() - 1 {
289 selected_index += 1;
290 }
291 }
292 KeyCode::Enter => {
293 cleanup_terminal(stdout);
295
296 return handle_main_menu_action(selected_index, storage);
297 }
298 KeyCode::Esc => {
299 cleanup_terminal(stdout);
301
302 println!("\nExiting...");
303 return Ok(());
304 }
305 _ => {}
306 }
307 }
308 Event::Key(_) => {} _ => {}
310 }
311 }
312}
313
314fn handle_main_menu_simple(storage: &ConfigStorage) -> Result<()> {
316 loop {
317 println!("\n{}", "Available Actions:".blue().bold());
318 println!("1. Execute claude --dangerously-skip-permissions");
319 println!("2. Switch configuration");
320 println!("3. Exit");
321
322 print!("\nPlease select an option (1-3): ");
323 io::stdout().flush().context("Failed to flush stdout")?;
324
325 let mut input = String::new();
326 io::stdin()
327 .read_line(&mut input)
328 .context("Failed to read input")?;
329
330 let choice = input.trim();
331
332 match choice {
333 "1" => return handle_main_menu_action(0, storage),
334 "2" => return handle_main_menu_action(1, storage),
335 "3" => return handle_main_menu_action(2, storage),
336 _ => {
337 println!("Invalid option. Please select 1-3.");
338 }
339 }
340 }
341}
342
343fn handle_main_menu_action(selected_index: usize, storage: &ConfigStorage) -> Result<()> {
345 match selected_index {
346 0 => {
347 println!("\nExecuting: claude --dangerously-skip-permissions");
348 execute_claude_command(true)?;
349 }
350 1 => {
351 handle_interactive_selection(storage)?;
353 }
354 2 => {
355 println!("Exiting...");
356 }
357 _ => {
358 println!("Invalid selection");
359 }
360 }
361 Ok(())
362}
363
364pub fn handle_interactive_selection(storage: &ConfigStorage) -> Result<()> {
372 if storage.configurations.is_empty() {
373 println!("No configurations available. Use 'add' command to create configurations first.");
374 return Ok(());
375 }
376
377 let mut configs: Vec<Configuration> = storage.configurations.values().cloned().collect();
378 configs.sort_by(|a, b| a.alias_name.cmp(&b.alias_name));
379
380 let mut selected_index = 0;
381
382 let raw_mode_enabled = terminal::enable_raw_mode().is_ok();
384
385 if raw_mode_enabled {
386 let mut stdout = io::stdout();
387 if execute!(
388 stdout,
389 terminal::EnterAlternateScreen,
390 terminal::Clear(terminal::ClearType::All)
391 )
392 .is_ok()
393 {
394 let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
396 let result = handle_full_interactive_menu(
397 &mut stdout,
398 &mut configs,
399 &mut selected_index,
400 storage,
401 storage_mode,
402 );
403
404 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
406 let _ = terminal::disable_raw_mode();
407
408 return result;
409 } else {
410 let _ = terminal::disable_raw_mode();
412 }
413 }
414
415 handle_simple_interactive_menu(&configs.iter().collect::<Vec<_>>(), storage)
417}
418
419fn handle_full_interactive_menu(
421 stdout: &mut io::Stdout,
422 configs: &mut Vec<Configuration>,
423 selected_index: &mut usize,
424 storage: &ConfigStorage,
425 storage_mode: crate::config::types::StorageMode,
426) -> Result<()> {
427 if configs.is_empty() {
429 println!("\r{}", "No configurations available".yellow());
430 println!(
431 "\r{}",
432 "Use 'cc-switch add <alias> <token> <url>' to add configurations first.".dimmed()
433 );
434 println!("\r{}", "Press any key to continue...".dimmed());
435 let _ = event::read(); return Ok(());
437 }
438
439 const PAGE_SIZE: usize = 9; let total_pages = if configs.len() <= PAGE_SIZE {
443 1
444 } else {
445 configs.len().div_ceil(PAGE_SIZE)
446 };
447 let mut current_page = 0;
448
449 loop {
450 let start_idx = current_page * PAGE_SIZE;
452 let end_idx = std::cmp::min(start_idx + PAGE_SIZE, configs.len());
453 let page_configs = &configs[start_idx..end_idx];
454
455 execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
457 execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
458
459 let border = BorderDrawing::new();
461 const CONFIG_MENU_WIDTH: usize = 80;
464
465 println!(
466 "\r{}",
467 border
468 .draw_top_border("Select Configuration", CONFIG_MENU_WIDTH)
469 .green()
470 );
471 if total_pages > 1 {
472 println!(
473 "\r{}",
474 border
475 .draw_middle_line(
476 &format!("第 {} 页,共 {} 页", current_page + 1, total_pages),
477 CONFIG_MENU_WIDTH
478 )
479 .green()
480 );
481 println!(
482 "\r{}",
483 border
484 .draw_middle_line(
485 "↑↓/jk导航,1-9快选,E-编辑,N/P翻页,R-官方,Q-退出,Enter确认",
486 CONFIG_MENU_WIDTH
487 )
488 .green()
489 );
490 } else {
491 println!(
492 "\r{}",
493 border
494 .draw_middle_line(
495 "↑↓/jk导航,1-9快选,E-编辑,R-官方,Q-退出,Enter确认,Esc取消",
496 CONFIG_MENU_WIDTH
497 )
498 .green()
499 );
500 }
501 println!("\r{}", border.draw_bottom_border(CONFIG_MENU_WIDTH).green());
502 println!();
503
504 let official_index = 0;
506 if *selected_index == official_index {
507 println!(
508 "\r> {} {} {}",
509 "●".red().bold(),
510 "[R]".red().bold(),
511 "official".red().bold()
512 );
513 println!("\r Use official Claude API (no custom configuration)");
514 println!();
515 } else {
516 println!("\r {} {} {}", "○".red(), "[R]".red(), "official".red());
517 }
518
519 for (page_index, config) in page_configs.iter().enumerate() {
521 let actual_config_index = start_idx + page_index;
522 let display_number = page_index + 1; let actual_index = actual_config_index + 1; let number_label = format!("[{display_number}]");
525
526 if *selected_index == actual_index {
527 println!(
528 "\r> {} {} {}",
529 "●".blue().bold(),
530 number_label.blue().bold(),
531 config.alias_name.blue().bold()
532 );
533
534 let details = format_config_details(config, "\r ", false);
536 for detail_line in details {
537 println!("{detail_line}");
538 }
539 println!();
540 } else {
541 println!(
542 "\r {} {} {}",
543 "○".dimmed(),
544 number_label.dimmed(),
545 config.alias_name.dimmed()
546 );
547 }
548 }
549
550 let exit_index = configs.len() + 1;
552 if *selected_index == exit_index {
553 println!(
554 "\r> {} {} {}",
555 "●".yellow().bold(),
556 "[Q]".yellow().bold(),
557 "Exit".yellow().bold()
558 );
559 println!("\r Exit without making changes");
560 println!();
561 } else {
562 println!(
563 "\r {} {} {}",
564 "○".dimmed(),
565 "[Q]".dimmed(),
566 "Exit".dimmed()
567 );
568 }
569
570 if total_pages > 1 {
572 println!(
573 "\r{}",
574 format!(
575 "Page Navigation: [N]ext, [P]revious (第 {} 页,共 {} 页)",
576 current_page + 1,
577 total_pages
578 )
579 .dimmed()
580 );
581 }
582
583 stdout.flush()?;
585
586 let event = match event::read() {
588 Ok(event) => event,
589 Err(e) => {
590 cleanup_terminal(stdout);
592 return Err(e.into());
593 }
594 };
595
596 match event {
597 Event::Key(KeyEvent {
598 code,
599 kind: KeyEventKind::Press,
600 ..
601 }) => match code {
602 KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => {
603 *selected_index = selected_index.saturating_sub(1);
604 }
605 KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => {
606 if *selected_index < configs.len() + 1 {
607 *selected_index += 1;
608 }
609 }
610 KeyCode::PageDown | KeyCode::Char('n') | KeyCode::Char('N') => {
611 if total_pages > 1 && current_page < total_pages - 1 {
612 current_page += 1;
613 let new_page_start_idx = current_page * PAGE_SIZE;
615 *selected_index = new_page_start_idx + 1; }
617 }
618 KeyCode::PageUp | KeyCode::Char('p') | KeyCode::Char('P') => {
619 if total_pages > 1 && current_page > 0 {
620 current_page -= 1;
621 let new_page_start_idx = current_page * PAGE_SIZE;
623 *selected_index = new_page_start_idx + 1; }
625 }
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() {
677 cleanup_terminal(stdout);
679
680 let config_index = *selected_index - 1; let edit_result = handle_config_edit(&configs[config_index]);
684
685 if execute!(
687 stdout,
688 terminal::EnterAlternateScreen,
689 terminal::Clear(terminal::ClearType::All)
690 )
691 .is_ok()
692 && terminal::enable_raw_mode().is_ok()
693 {
694 match edit_result {
696 Ok(_) => {
697 if let Ok(reloaded_storage) = ConfigStorage::load() {
699 *configs = reloaded_storage
700 .configurations
701 .values()
702 .cloned()
703 .collect();
704 configs.sort_by(|a, b| a.alias_name.cmp(&b.alias_name));
705 if *selected_index > configs.len() + 1 {
707 *selected_index = configs.len() + 1;
708 }
709 }
710 continue;
712 }
713 Err(e) => {
714 if e.downcast_ref::<EditModeError>()
716 == Some(&EditModeError::ReturnToMenu)
717 {
718 continue;
720 }
721 cleanup_terminal(stdout);
723 return Err(e);
724 }
725 }
726 }
727 }
728 }
730 KeyCode::Char('q') | KeyCode::Char('Q') => {
731 cleanup_terminal(stdout);
733
734 return handle_selection_action(
735 &configs.iter().collect::<Vec<_>>(),
736 configs.len() + 1,
737 storage,
738 storage_mode,
739 );
740 }
741 _ => {}
742 },
743 Event::Key(_) => {} _ => {}
745 }
746 }
747}
748
749fn handle_simple_interactive_menu(
751 configs: &[&Configuration],
752 storage: &ConfigStorage,
753) -> Result<()> {
754 const PAGE_SIZE: usize = 9; if configs.len() <= PAGE_SIZE {
758 return handle_simple_single_page_menu(configs, storage);
759 }
760
761 let total_pages = configs.len().div_ceil(PAGE_SIZE);
763 let mut current_page = 0;
764
765 loop {
766 let start_idx = current_page * PAGE_SIZE;
768 let end_idx = std::cmp::min(start_idx + PAGE_SIZE, configs.len());
769 let page_configs = &configs[start_idx..end_idx];
770
771 println!("\n{}", "Available Configurations:".blue().bold());
772 if total_pages > 1 {
773 println!("第 {} 页,共 {} 页", current_page + 1, total_pages);
774 println!("使用 'n' 下一页, 'p' 上一页, 'r' 官方配置, 'q' 退出");
775 }
776 println!();
777
778 println!("{} {}", "[r]".red().bold(), "official".red());
780 println!(" Use official Claude API (no custom configuration)");
781 println!();
782
783 for (page_index, config) in page_configs.iter().enumerate() {
785 let display_number = page_index + 1;
786
787 println!(
788 "{}. {}",
789 format!("[{display_number}]").green().bold(),
790 config.alias_name.green()
791 );
792
793 let details = format_config_details(config, " ", true);
795 for detail_line in details {
796 println!("{detail_line}");
797 }
798 println!();
799 }
800
801 println!("{} {}", "[q]".yellow().bold(), "Exit".yellow());
803
804 if total_pages > 1 {
805 println!(
806 "\n页面导航: [n]下页, [p]上页 | 配置选择: [1-{}] | [e]编辑 | [r]官方 | [q]退出",
807 page_configs.len()
808 );
809 }
810
811 print!("\n请输入选择: ");
812 io::stdout().flush()?;
813
814 let mut input = String::new();
815 io::stdin().read_line(&mut input)?;
816 let choice = input.trim().to_lowercase();
817
818 match choice.as_str() {
819 "r" => {
820 println!("Using official Claude configuration");
822
823 let mut settings = crate::config::types::ClaudeSettings::load(
825 storage.get_claude_settings_dir().map(|s| s.as_str()),
826 )?;
827 settings.remove_anthropic_env();
828 settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
829
830 return launch_claude_with_env(EnvironmentConfig::empty(), None, None, false);
831 }
832 "e" => {
833 println!("编辑功能在交互式菜单中可用");
836 }
837 "q" => {
838 println!("Exiting...");
839 return Ok(());
840 }
841 "n" if total_pages > 1 && current_page < total_pages - 1 => {
842 current_page += 1;
843 continue;
844 }
845 "p" if total_pages > 1 && current_page > 0 => {
846 current_page -= 1;
847 continue;
848 }
849 digit_str => {
850 if let Ok(digit) = digit_str.parse::<usize>()
851 && digit >= 1
852 && digit <= page_configs.len()
853 {
854 let actual_config_index = start_idx + (digit - 1);
855 let selection_index = actual_config_index + 1; let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
857 return handle_selection_action(
858 configs,
859 selection_index,
860 storage,
861 storage_mode,
862 );
863 }
864 println!("无效选择,请重新输入");
865 }
866 }
867 }
868}
869
870fn handle_simple_single_page_menu(
872 configs: &[&Configuration],
873 storage: &ConfigStorage,
874) -> Result<()> {
875 println!("\n{}", "Available Configurations:".blue().bold());
876
877 println!("1. {}", "official".red());
879 println!(" Use official Claude API (no custom configuration)");
880 println!();
881
882 for (index, config) in configs.iter().enumerate() {
883 println!(
884 "{}. {}",
885 index + 2, config.alias_name.green()
887 );
888
889 let details = format_config_details(config, " ", true);
891 for detail_line in details {
892 println!("{detail_line}");
893 }
894 println!();
895 }
896
897 println!("{}. {}", configs.len() + 2, "Exit".yellow());
898
899 print!("\nSelect configuration (1-{}): ", configs.len() + 2);
900 io::stdout().flush()?;
901
902 let mut input = String::new();
903 io::stdin().read_line(&mut input)?;
904
905 match input.trim().parse::<usize>() {
906 Ok(1) => {
907 println!("Using official Claude configuration");
909
910 let mut settings = crate::config::types::ClaudeSettings::load(
912 storage.get_claude_settings_dir().map(|s| s.as_str()),
913 )?;
914 settings.remove_anthropic_env();
915 settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
916
917 launch_claude_with_env(EnvironmentConfig::empty(), None, None, false)
918 }
919 Ok(num) if num >= 2 && num <= configs.len() + 1 => {
920 let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
921 handle_selection_action(configs, num - 1, storage, storage_mode) }
923 Ok(num) if num == configs.len() + 2 => {
924 println!("Exiting...");
925 Ok(())
926 }
927 _ => {
928 println!("Invalid selection");
929 Ok(())
930 }
931 }
932}
933
934fn handle_selection_action(
936 configs: &[&Configuration],
937 selected_index: usize,
938 storage: &ConfigStorage,
939 storage_mode: crate::config::types::StorageMode,
940) -> Result<()> {
941 if selected_index == 0 {
942 println!("\nUsing official Claude configuration");
944
945 let mut settings = crate::config::types::ClaudeSettings::load(
947 storage.get_claude_settings_dir().map(|s| s.as_str()),
948 )?;
949 settings.remove_anthropic_env();
950 settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
951
952 launch_claude_with_env(EnvironmentConfig::empty(), None, None, false)
953 } else if selected_index <= configs.len() {
954 let config_index = selected_index - 1; let selected_config = configs[config_index].clone();
957 let env_config = EnvironmentConfig::from_config(&selected_config);
958
959 println!(
960 "\nSwitched to configuration '{}'",
961 selected_config.alias_name.green().bold()
962 );
963
964 let details = format_config_details(&selected_config, "", false);
966 for detail_line in details {
967 println!("{detail_line}");
968 }
969
970 let mut settings = crate::config::types::ClaudeSettings::load(
972 storage.get_claude_settings_dir().map(|s| s.as_str()),
973 )?;
974 settings.switch_to_config_with_mode(
975 &selected_config,
976 storage_mode,
977 storage.get_claude_settings_dir().map(|s| s.as_str()),
978 )?;
979
980 launch_claude_with_env(env_config, None, None, false)
981 } else {
982 println!("\nExiting...");
984 Ok(())
985 }
986}
987
988pub fn launch_claude_with_env(
990 env_config: EnvironmentConfig,
991 prompt: Option<&str>,
992 resume: Option<&str>,
993 continue_session: bool,
994) -> Result<()> {
995 println!("\nLaunching Claude CLI...");
996
997 for (key, value) in env_config.as_env_tuples() {
999 unsafe {
1000 std::env::set_var(&key, &value);
1001 }
1002 }
1003
1004 #[cfg(unix)]
1006 {
1007 use std::os::unix::process::CommandExt;
1008 let mut command = Command::new("claude");
1009 command.arg("--dangerously-skip-permissions");
1010 if let Some(session_id) = resume {
1011 command.args(["--resume", session_id]);
1012 }
1013 if continue_session {
1014 command.arg("--continue");
1015 }
1016 if let Some(p) = prompt {
1017 command.arg(p);
1018 }
1019 let error = command.exec();
1020 anyhow::bail!("Failed to exec claude: {}", error);
1022 }
1023
1024 #[cfg(not(unix))]
1026 {
1027 use std::process::Stdio;
1028 let mut command = Command::new("claude");
1029 command.arg("--dangerously-skip-permissions");
1030 if let Some(session_id) = resume {
1031 command.args(["--resume", session_id]);
1032 }
1033 if continue_session {
1034 command.arg("--continue");
1035 }
1036 if let Some(p) = prompt {
1037 command.arg(p);
1038 }
1039 command
1040 .stdin(Stdio::inherit())
1041 .stdout(Stdio::inherit())
1042 .stderr(Stdio::inherit());
1043
1044 let mut child = command.spawn().context(
1045 "Failed to launch Claude CLI. Make sure 'claude' command is available in PATH",
1046 )?;
1047
1048 let status = child.wait()?;
1049
1050 if !status.success() {
1051 anyhow::bail!("Claude CLI exited with error status: {}", status);
1052 }
1053 }
1054}
1055
1056fn execute_claude_command(skip_permissions: bool) -> Result<()> {
1061 println!("Launching Claude CLI...");
1062
1063 #[cfg(unix)]
1065 {
1066 use std::os::unix::process::CommandExt;
1067 let mut command = Command::new("claude");
1068 if skip_permissions {
1069 command.arg("--dangerously-skip-permissions");
1070 }
1071
1072 let error = command.exec();
1073 anyhow::bail!("Failed to exec claude: {}", error);
1075 }
1076
1077 #[cfg(not(unix))]
1079 {
1080 use std::process::Stdio;
1081 let mut command = Command::new("claude");
1082 if skip_permissions {
1083 command.arg("--dangerously-skip-permissions");
1084 }
1085
1086 command
1087 .stdin(Stdio::inherit())
1088 .stdout(Stdio::inherit())
1089 .stderr(Stdio::inherit());
1090
1091 let mut child = command.spawn().context(
1092 "Failed to launch Claude CLI. Make sure 'claude' command is available in PATH",
1093 )?;
1094
1095 let status = child
1096 .wait()
1097 .context("Failed to wait for Claude CLI process")?;
1098
1099 if !status.success() {
1100 anyhow::bail!("Claude CLI exited with error status: {}", status);
1101 }
1102 }
1103}
1104
1105pub fn read_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
1122pub fn read_sensitive_input(prompt: &str) -> Result<String> {
1130 print!("{prompt}");
1131 io::stdout().flush().context("Failed to flush stdout")?;
1132 let mut input = String::new();
1133 io::stdin()
1134 .read_line(&mut input)
1135 .context("Failed to read input")?;
1136 Ok(input.trim().to_string())
1137}
1138
1139fn format_config_details(config: &Configuration, indent: &str, _compact: bool) -> Vec<String> {
1152 let mut lines = Vec::new();
1153
1154 let terminal_width = get_terminal_width();
1156 let _available_width = terminal_width.saturating_sub(text_display_width(indent) + 8);
1157
1158 let token_label = "Token:";
1160 let url_label = "URL:";
1161 let model_label = "Model:";
1162 let small_model_label = "Small Fast Model:";
1163 let max_thinking_tokens_label = "Max Thinking Tokens:";
1164 let api_timeout_ms_label = "API Timeout (ms):";
1165 let disable_nonessential_traffic_label = "Disable Nonessential Traffic:";
1166 let default_sonnet_model_label = "Default Sonnet Model:";
1167 let default_opus_model_label = "Default Opus Model:";
1168 let default_haiku_model_label = "Default Haiku Model:";
1169 let subagent_model_label = "Subagent Model:";
1170 let disable_nonstreaming_fallback_label = "Disable Nonstreaming Fallback:";
1171 let effort_level_label = "Effort Level:";
1172
1173 let max_label_width = [
1175 token_label,
1176 url_label,
1177 model_label,
1178 small_model_label,
1179 max_thinking_tokens_label,
1180 api_timeout_ms_label,
1181 disable_nonessential_traffic_label,
1182 default_sonnet_model_label,
1183 default_opus_model_label,
1184 default_haiku_model_label,
1185 subagent_model_label,
1186 disable_nonstreaming_fallback_label,
1187 effort_level_label,
1188 ]
1189 .iter()
1190 .map(|label| text_display_width(label))
1191 .max()
1192 .unwrap_or(0);
1193
1194 let token_line = format!(
1196 "{}{} {}",
1197 indent,
1198 pad_text_to_width(token_label, max_label_width, TextAlignment::Left, ' '),
1199 format_token_for_display(&config.token).dimmed()
1200 );
1201 lines.push(token_line);
1202
1203 let url_line = format!(
1205 "{}{} {}",
1206 indent,
1207 pad_text_to_width(url_label, max_label_width, TextAlignment::Left, ' '),
1208 config.url.cyan()
1209 );
1210 lines.push(url_line);
1211
1212 if let Some(model) = &config.model {
1214 let model_line = format!(
1215 "{}{} {}",
1216 indent,
1217 pad_text_to_width(model_label, max_label_width, TextAlignment::Left, ' '),
1218 model.yellow()
1219 );
1220 lines.push(model_line);
1221 }
1222
1223 if let Some(small_fast_model) = &config.small_fast_model {
1225 let small_model_line = format!(
1226 "{}{} {}",
1227 indent,
1228 pad_text_to_width(small_model_label, max_label_width, TextAlignment::Left, ' '),
1229 small_fast_model.yellow()
1230 );
1231 lines.push(small_model_line);
1232 }
1233
1234 if let Some(max_thinking_tokens) = config.max_thinking_tokens {
1236 let tokens_line = format!(
1237 "{}{} {}",
1238 indent,
1239 pad_text_to_width(
1240 max_thinking_tokens_label,
1241 max_label_width,
1242 TextAlignment::Left,
1243 ' '
1244 ),
1245 format!("{}", max_thinking_tokens).yellow()
1246 );
1247 lines.push(tokens_line);
1248 }
1249
1250 if let Some(api_timeout_ms) = config.api_timeout_ms {
1252 let timeout_line = format!(
1253 "{}{} {}",
1254 indent,
1255 pad_text_to_width(
1256 api_timeout_ms_label,
1257 max_label_width,
1258 TextAlignment::Left,
1259 ' '
1260 ),
1261 format!("{}", api_timeout_ms).yellow()
1262 );
1263 lines.push(timeout_line);
1264 }
1265
1266 if let Some(disable_flag) = config.claude_code_disable_nonessential_traffic {
1268 let flag_line = format!(
1269 "{}{} {}",
1270 indent,
1271 pad_text_to_width(
1272 disable_nonessential_traffic_label,
1273 max_label_width,
1274 TextAlignment::Left,
1275 ' '
1276 ),
1277 format!("{}", disable_flag).yellow()
1278 );
1279 lines.push(flag_line);
1280 }
1281
1282 if let Some(sonnet_model) = &config.anthropic_default_sonnet_model {
1284 let sonnet_line = format!(
1285 "{}{} {}",
1286 indent,
1287 pad_text_to_width(
1288 default_sonnet_model_label,
1289 max_label_width,
1290 TextAlignment::Left,
1291 ' '
1292 ),
1293 sonnet_model.yellow()
1294 );
1295 lines.push(sonnet_line);
1296 }
1297
1298 if let Some(opus_model) = &config.anthropic_default_opus_model {
1300 let opus_line = format!(
1301 "{}{} {}",
1302 indent,
1303 pad_text_to_width(
1304 default_opus_model_label,
1305 max_label_width,
1306 TextAlignment::Left,
1307 ' '
1308 ),
1309 opus_model.yellow()
1310 );
1311 lines.push(opus_line);
1312 }
1313
1314 if let Some(haiku_model) = &config.anthropic_default_haiku_model {
1316 let haiku_line = format!(
1317 "{}{} {}",
1318 indent,
1319 pad_text_to_width(
1320 default_haiku_model_label,
1321 max_label_width,
1322 TextAlignment::Left,
1323 ' '
1324 ),
1325 haiku_model.yellow()
1326 );
1327 lines.push(haiku_line);
1328 }
1329
1330 if let Some(subagent_model) = &config.claude_code_subagent_model {
1332 let subagent_line = format!(
1333 "{}{} {}",
1334 indent,
1335 pad_text_to_width(
1336 subagent_model_label,
1337 max_label_width,
1338 TextAlignment::Left,
1339 ' '
1340 ),
1341 subagent_model.yellow()
1342 );
1343 lines.push(subagent_line);
1344 }
1345
1346 if let Some(disable_flag) = config.claude_code_disable_nonstreaming_fallback {
1348 let flag_line = format!(
1349 "{}{} {}",
1350 indent,
1351 pad_text_to_width(
1352 disable_nonstreaming_fallback_label,
1353 max_label_width,
1354 TextAlignment::Left,
1355 ' '
1356 ),
1357 format!("{}", disable_flag).yellow()
1358 );
1359 lines.push(flag_line);
1360 }
1361
1362 if let Some(effort_level) = &config.claude_code_effort_level {
1364 let effort_line = format!(
1365 "{}{} {}",
1366 indent,
1367 pad_text_to_width(
1368 effort_level_label,
1369 max_label_width,
1370 TextAlignment::Left,
1371 ' '
1372 ),
1373 effort_level.yellow()
1374 );
1375 lines.push(effort_line);
1376 }
1377
1378 lines
1379}
1380
1381#[cfg(test)]
1382mod border_drawing_tests {
1383 use super::*;
1384
1385 #[test]
1386 fn test_border_drawing_unicode_support() {
1387 let _border = BorderDrawing::new();
1388 }
1390
1391 #[test]
1392 fn test_border_drawing_top_border() {
1393 let border = BorderDrawing {
1394 unicode_supported: true,
1395 };
1396 let result = border.draw_top_border("Test", 20);
1397 assert!(!result.is_empty());
1398 assert!(result.contains("Test"));
1399 }
1400
1401 #[test]
1402 fn test_border_drawing_ascii_fallback() {
1403 let border = BorderDrawing {
1404 unicode_supported: false,
1405 };
1406 let result = border.draw_top_border("Test", 20);
1407 assert!(!result.is_empty());
1408 assert!(result.contains("Test"));
1409 assert!(result.contains("+"));
1410 assert!(result.contains("-"));
1411 }
1412
1413 #[test]
1414 fn test_border_drawing_middle_line() {
1415 let border = BorderDrawing {
1416 unicode_supported: true,
1417 };
1418 let result = border.draw_middle_line("Test message", 30);
1419 assert!(!result.is_empty());
1420 assert!(result.contains("Test message"));
1421 }
1422
1423 #[test]
1424 fn test_border_drawing_bottom_border() {
1425 let border = BorderDrawing {
1426 unicode_supported: true,
1427 };
1428 let result = border.draw_bottom_border(20);
1429 assert!(!result.is_empty());
1430 }
1431
1432 #[test]
1433 fn test_border_drawing_width_consistency() {
1434 let border = BorderDrawing {
1435 unicode_supported: true,
1436 };
1437 let width = 30;
1438 let top = border.draw_top_border("Title", width);
1439 let middle = border.draw_middle_line("Content", width);
1440 let bottom = border.draw_bottom_border(width);
1441
1442 assert!(top.chars().count() >= width - 2);
1444 assert!(middle.chars().count() >= width - 2);
1445 assert!(bottom.chars().count() >= width - 2);
1446 }
1447}
1448
1449#[cfg(test)]
1450mod pagination_tests {
1451
1452 #[test]
1454 fn test_pagination_calculation() {
1455 const PAGE_SIZE: usize = 9;
1456
1457 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); }
1468
1469 #[test]
1471 fn test_page_range_calculation() {
1472 const PAGE_SIZE: usize = 9;
1473
1474 let current_page = 0;
1476 let start_idx = current_page * PAGE_SIZE; let end_idx = std::cmp::min(start_idx + PAGE_SIZE, 15); assert_eq!(start_idx, 0);
1479 assert_eq!(end_idx, 9);
1480 assert_eq!(end_idx - start_idx, 9); let current_page = 1;
1484 let start_idx = current_page * PAGE_SIZE; let end_idx = std::cmp::min(start_idx + PAGE_SIZE, 15); assert_eq!(start_idx, 9);
1487 assert_eq!(end_idx, 15);
1488 assert_eq!(end_idx - start_idx, 6); let current_page = 0;
1492 let start_idx = current_page * PAGE_SIZE; let end_idx = std::cmp::min(start_idx + PAGE_SIZE, PAGE_SIZE); assert_eq!(start_idx, 0);
1495 assert_eq!(end_idx, 9);
1496 assert_eq!(end_idx - start_idx, 9); }
1498
1499 #[test]
1501 fn test_digit_mapping_to_config_index() {
1502 const PAGE_SIZE: usize = 9;
1503
1504 let current_page = 0;
1506 let start_idx = current_page * PAGE_SIZE; let digit = 1;
1510 let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 0);
1512
1513 let digit = 9;
1515 let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 8);
1517
1518 let current_page = 1;
1520 let start_idx = current_page * PAGE_SIZE; let digit = 1;
1524 let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 9);
1526
1527 let digit = 5;
1529 let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 13);
1531 }
1532
1533 #[test]
1535 fn test_selection_index_conversion() {
1536 const PAGE_SIZE: usize = 9;
1543
1544 let current_page = 0;
1546 let start_idx = current_page * PAGE_SIZE; let digit = 1;
1548 let actual_config_index = start_idx + (digit - 1); let selection_index = actual_config_index + 1; assert_eq!(selection_index, 1);
1551
1552 let current_page = 1;
1554 let start_idx = current_page * PAGE_SIZE; let digit = 1;
1556 let actual_config_index = start_idx + (digit - 1); let selection_index = actual_config_index + 1; assert_eq!(selection_index, 10);
1559 }
1560
1561 #[test]
1563 fn test_page_navigation_bounds() {
1564 const PAGE_SIZE: usize = 9;
1565 let total_configs: usize = 25; let total_pages = total_configs.div_ceil(PAGE_SIZE); assert_eq!(total_pages, 3);
1568
1569 let mut current_page = 0;
1571 if current_page > 0 {
1572 current_page -= 1;
1573 }
1574 assert_eq!(current_page, 0); let mut current_page = total_pages - 1; if current_page < total_pages - 1 {
1579 current_page += 1;
1580 }
1581 assert_eq!(current_page, 2); let mut current_page = 1;
1585
1586 if current_page < total_pages - 1 {
1588 current_page += 1;
1589 }
1590 assert_eq!(current_page, 2);
1591
1592 if current_page > 0 {
1594 current_page = current_page.saturating_sub(1);
1595 }
1596 assert_eq!(current_page, 1);
1597 }
1598
1599 #[test]
1601 fn test_digit_key_boundary_conditions() {
1602 const PAGE_SIZE: usize = 9;
1603
1604 let digit = 0;
1606 assert!(digit < 1, "Digit 0 should be less than 1 and ignored");
1607
1608 let configs_len = 5; let page_configs_len = std::cmp::min(PAGE_SIZE, configs_len); let digit = 9; assert!(
1613 digit > page_configs_len,
1614 "Digit 9 should be beyond available configs (5) and ignored"
1615 );
1616
1617 for digit in 1..=page_configs_len {
1619 assert!(
1620 digit >= 1 && digit <= page_configs_len,
1621 "Digit {} should be valid",
1622 digit
1623 );
1624 }
1625 }
1626
1627 #[test]
1629 fn test_empty_configs_handling() {
1630 let empty_configs: Vec<String> = Vec::new();
1631 assert!(
1632 empty_configs.is_empty(),
1633 "Empty config list should be properly detected"
1634 );
1635
1636 let configs_len = empty_configs.len(); assert_eq!(configs_len, 0, "Empty configs should have length 0");
1639
1640 }
1643
1644 #[test]
1646 fn test_page_navigation_boundaries() {
1647 const PAGE_SIZE: usize = 9;
1648 let total_configs: usize = 20; let total_pages = total_configs.div_ceil(PAGE_SIZE); let mut current_page = 0;
1653 let original_page = current_page;
1654
1655 if current_page > 0 {
1657 current_page -= 1;
1658 }
1659 assert_eq!(
1660 current_page, original_page,
1661 "First page should not navigate to previous"
1662 );
1663
1664 let mut current_page = total_pages - 1; let original_page = current_page;
1667
1668 if current_page < total_pages - 1 {
1670 current_page += 1;
1671 }
1672 assert_eq!(
1673 current_page, original_page,
1674 "Last page should not navigate to next"
1675 );
1676
1677 let mut current_page = 1; if current_page < total_pages - 1 {
1682 current_page += 1;
1683 }
1684 assert_eq!(current_page, 2, "Should navigate to next page");
1685
1686 if current_page > 0 {
1688 current_page = current_page.saturating_sub(1);
1689 }
1690 assert_eq!(current_page, 1, "Should navigate to previous page");
1691 }
1692
1693 #[test]
1695 fn test_j_key_navigation() {
1696 let mut selected_index: usize = 0;
1697 let configs_len = 5; if selected_index < configs_len + 1 {
1702 selected_index += 1;
1703 }
1704 assert_eq!(selected_index, 1, "j key should move selection down by one");
1705
1706 selected_index = configs_len + 1;
1708 let original_index = selected_index;
1709 if selected_index < configs_len + 1 {
1710 selected_index += 1;
1711 }
1712 assert_eq!(
1713 selected_index, original_index,
1714 "j key should not move beyond bottom boundary"
1715 );
1716 }
1717
1718 #[test]
1720 fn test_k_key_navigation() {
1721 let mut selected_index: usize = 5;
1722
1723 selected_index = selected_index.saturating_sub(1);
1726 assert_eq!(selected_index, 4, "k key should move selection up by one");
1727
1728 selected_index = 0;
1730 let original_index = selected_index;
1731 selected_index = selected_index.saturating_sub(1);
1732 assert_eq!(
1733 selected_index, original_index,
1734 "k key should not move beyond top boundary"
1735 );
1736 }
1737
1738 #[test]
1740 fn test_jk_key_boundary_conditions() {
1741 const CONFIGS_LEN: usize = 5;
1742
1743 let mut selected_index: usize = CONFIGS_LEN + 1; let original_index = selected_index;
1746 if selected_index < CONFIGS_LEN + 1 {
1747 selected_index += 1; }
1749 assert_eq!(
1750 selected_index, original_index,
1751 "j key should respect bottom boundary like Down arrow"
1752 );
1753
1754 let mut selected_index: usize = 0; let original_index = selected_index;
1757 selected_index = selected_index.saturating_sub(1); assert_eq!(
1759 selected_index, original_index,
1760 "k key should respect top boundary like Up arrow"
1761 );
1762 }
1763}
1764
1765#[derive(Debug, PartialEq)]
1767pub(crate) enum EditModeError {
1768 ReturnToMenu,
1769}
1770
1771impl std::fmt::Display for EditModeError {
1772 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1773 match self {
1774 EditModeError::ReturnToMenu => write!(f, "return_to_menu"),
1775 }
1776 }
1777}
1778
1779impl std::error::Error for EditModeError {}
1780
1781fn handle_config_edit(config: &Configuration) -> Result<()> {
1783 println!("\n{}", "配置编辑模式".green().bold());
1784 println!("{}", "===================".green());
1785 println!("正在编辑配置: {}", config.alias_name.cyan().bold());
1786 println!();
1787
1788 let mut editing_config = config.clone();
1790 let original_alias = config.alias_name.clone();
1791
1792 loop {
1793 display_edit_menu(&editing_config);
1795
1796 println!("\n{}", "提示: 可使用大小写字母".dimmed());
1798 print!("请选择要编辑的字段 (1-9, A-E), 或输入 S 保存, Q 返回上一级菜单: ");
1799 io::stdout().flush()?;
1800
1801 let mut input = String::new();
1802 io::stdin().read_line(&mut input)?;
1803 let input = input.trim();
1804
1805 match input {
1807 "1" => edit_field_alias(&mut editing_config)?,
1808 "2" => edit_field_token(&mut editing_config)?,
1809 "3" => edit_field_url(&mut editing_config)?,
1810 "4" => edit_field_model(&mut editing_config)?,
1811 "5" => edit_field_small_fast_model(&mut editing_config)?,
1812 "6" => edit_field_max_thinking_tokens(&mut editing_config)?,
1813 "7" => edit_field_api_timeout_ms(&mut editing_config)?,
1814 "8" => edit_field_claude_code_disable_nonessential_traffic(&mut editing_config)?,
1815 "9" => edit_field_anthropic_default_sonnet_model(&mut editing_config)?,
1816 "10" | "a" | "A" => edit_field_anthropic_default_opus_model(&mut editing_config)?,
1817 "11" | "b" | "B" => edit_field_anthropic_default_haiku_model(&mut editing_config)?,
1818 "12" | "c" | "C" => edit_field_claude_code_subagent_model(&mut editing_config)?,
1819 "13" | "d" | "D" => {
1820 edit_field_claude_code_disable_nonstreaming_fallback(&mut editing_config)?
1821 }
1822 "14" | "e" | "E" => edit_field_claude_code_effort_level(&mut editing_config)?,
1823 "s" | "S" => {
1824 return save_configuration_changes(&original_alias, &editing_config);
1826 }
1827 "q" | "Q" => {
1828 println!("\n{}", "返回上一级菜单".blue());
1829 return Err(EditModeError::ReturnToMenu.into());
1830 }
1831 _ => {
1832 println!("{}", "无效选择,请重试".red());
1833 }
1834 }
1835 }
1836}
1837
1838fn display_edit_menu(config: &Configuration) {
1840 println!("\n{}", "当前配置值:".blue().bold());
1841 println!("{}", "─────────────────────────".blue());
1842
1843 println!("1. 别名 (alias_name): {}", config.alias_name.green());
1844
1845 println!(
1846 "2. 令牌 (ANTHROPIC_AUTH_TOKEN): {}",
1847 format_token_for_display(&config.token).green()
1848 );
1849
1850 println!("3. URL (ANTHROPIC_BASE_URL): {}", config.url.green());
1851
1852 println!(
1853 "4. 模型 (ANTHROPIC_MODEL): {}",
1854 config.model.as_deref().unwrap_or("[未设置]").green()
1855 );
1856
1857 println!(
1858 "5. 快速模型 (ANTHROPIC_SMALL_FAST_MODEL): {}",
1859 config
1860 .small_fast_model
1861 .as_deref()
1862 .unwrap_or("[未设置]")
1863 .green()
1864 );
1865
1866 println!(
1867 "6. 最大思考令牌数 (ANTHROPIC_MAX_THINKING_TOKENS): {}",
1868 config
1869 .max_thinking_tokens
1870 .map(|t| t.to_string())
1871 .unwrap_or("[未设置]".to_string())
1872 .green()
1873 );
1874
1875 println!(
1876 "7. API超时时间 (API_TIMEOUT_MS): {}",
1877 config
1878 .api_timeout_ms
1879 .map(|t| t.to_string())
1880 .unwrap_or("[未设置]".to_string())
1881 .green()
1882 );
1883
1884 println!(
1885 "8. 禁用非必要流量 (CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC): {}",
1886 config
1887 .claude_code_disable_nonessential_traffic
1888 .map(|t| t.to_string())
1889 .unwrap_or("[未设置]".to_string())
1890 .green()
1891 );
1892
1893 println!(
1894 "9. 默认 Sonnet 模型 (ANTHROPIC_DEFAULT_SONNET_MODEL): {}",
1895 config
1896 .anthropic_default_sonnet_model
1897 .as_deref()
1898 .unwrap_or("[未设置]")
1899 .green()
1900 );
1901
1902 println!(
1903 "A. 默认 Opus 模型 (ANTHROPIC_DEFAULT_OPUS_MODEL): {}",
1904 config
1905 .anthropic_default_opus_model
1906 .as_deref()
1907 .unwrap_or("[未设置]")
1908 .green()
1909 );
1910
1911 println!(
1912 "B. 默认 Haiku 模型 (ANTHROPIC_DEFAULT_HAIKU_MODEL): {}",
1913 config
1914 .anthropic_default_haiku_model
1915 .as_deref()
1916 .unwrap_or("[未设置]")
1917 .green()
1918 );
1919
1920 println!(
1921 "C. 子代理模型 (CLAUDE_CODE_SUBAGENT_MODEL): {}",
1922 config
1923 .claude_code_subagent_model
1924 .as_deref()
1925 .unwrap_or("[未设置]")
1926 .green()
1927 );
1928
1929 println!(
1930 "D. 禁用非流式回退 (CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK): {}",
1931 config
1932 .claude_code_disable_nonstreaming_fallback
1933 .map(|t| t.to_string())
1934 .unwrap_or("[未设置]".to_string())
1935 .green()
1936 );
1937
1938 println!(
1939 "E. 努力级别 (CLAUDE_CODE_EFFORT_LEVEL): {}",
1940 config
1941 .claude_code_effort_level
1942 .as_deref()
1943 .unwrap_or("[未设置]")
1944 .green()
1945 );
1946
1947 println!("{}", "─────────────────────────".blue());
1948 println!(
1949 "S. {} | Q. {}",
1950 "保存更改".green().bold(),
1951 "返回上一级菜单".blue()
1952 );
1953}
1954
1955pub(crate) fn edit_string_field(
1957 field_name: &str,
1958 current_value: &str,
1959 validator: impl Fn(&str) -> Result<()>,
1960) -> Result<Option<String>> {
1961 println!("\n编辑{field_name}:");
1962 println!("当前值: {}", current_value.cyan());
1963 print!("新值 (回车保持不变): ");
1964 io::stdout().flush()?;
1965
1966 let mut input = String::new();
1967 io::stdin().read_line(&mut input)?;
1968 let input = input.trim();
1969
1970 if !input.is_empty() {
1971 validator(input)?;
1972 println!("{field_name}已更新为: {}", input.green());
1973 Ok(Some(input.to_string()))
1974 } else {
1975 Ok(None)
1976 }
1977}
1978
1979pub(crate) type OptionalStringResult = Result<Option<Option<String>>>;
1981
1982pub(crate) fn edit_optional_string_field(
1984 field_name: &str,
1985 current_value: Option<&str>,
1986) -> OptionalStringResult {
1987 println!("\n编辑{field_name}:");
1988 println!("当前值: {}", current_value.unwrap_or("[未设置]").cyan());
1989 print!("新值 (回车保持不变,输入空格清除): ");
1990 io::stdout().flush()?;
1991
1992 let mut input = String::new();
1993 io::stdin().read_line(&mut input)?;
1994 let input = input.trim();
1995
1996 if !input.is_empty() {
1997 if input == " " {
1998 println!("{}", format!("{field_name}已清除").green());
1999 Ok(Some(None))
2000 } else {
2001 println!("{field_name}已更新为: {}", input.green());
2002 Ok(Some(Some(input.to_string())))
2003 }
2004 } else {
2005 Ok(None)
2006 }
2007}
2008
2009type OptionalU32Result = Result<Option<Option<u32>>>;
2011
2012fn edit_optional_u32_field(field_name: &str, current_value: Option<u32>) -> OptionalU32Result {
2014 println!("\n编辑{field_name}:");
2015 println!(
2016 "当前值: {}",
2017 current_value
2018 .map(|t| t.to_string())
2019 .unwrap_or("[未设置]".to_string())
2020 .cyan()
2021 );
2022 print!("新值 (回车保持不变,输入 0 清除): ");
2023 io::stdout().flush()?;
2024
2025 let mut input = String::new();
2026 io::stdin().read_line(&mut input)?;
2027 let input = input.trim();
2028
2029 if !input.is_empty() {
2030 if input == "0" {
2031 println!("{}", format!("{field_name}已清除").green());
2032 Ok(Some(None))
2033 } else if let Ok(value) = input.parse::<u32>() {
2034 println!("{field_name}已更新为: {}", value.to_string().green());
2035 Ok(Some(Some(value)))
2036 } else {
2037 println!("{}", "错误: 请输入有效的数字".red());
2038 Ok(None)
2039 }
2040 } else {
2041 Ok(None)
2042 }
2043}
2044
2045fn edit_field_alias(config: &mut Configuration) -> Result<()> {
2047 let validator = |input: &str| -> Result<()> {
2048 if input.contains(char::is_whitespace) {
2049 anyhow::bail!("错误: 别名不能包含空白字符");
2050 }
2051 if input == "cc" {
2052 anyhow::bail!("错误: 'cc' 是保留名称");
2053 }
2054 if input == "official" {
2055 anyhow::bail!("错误: 'official' 是保留名称");
2056 }
2057 Ok(())
2058 };
2059
2060 match edit_string_field("别名", &config.alias_name, validator) {
2061 Ok(Some(new_value)) => config.alias_name = new_value,
2062 Ok(None) => {}
2063 Err(e) => println!("{}", e.to_string().red()),
2064 }
2065 Ok(())
2066}
2067
2068fn edit_field_token(config: &mut Configuration) -> Result<()> {
2070 let no_validator = |_: &str| -> Result<()> { Ok(()) };
2071 if let Some(new_value) = edit_string_field(
2072 "令牌",
2073 &format_token_for_display(&config.token),
2074 no_validator,
2075 )? {
2076 config.token = new_value;
2077 println!("{}", "令牌已更新".green());
2078 }
2079 Ok(())
2080}
2081
2082fn edit_field_url(config: &mut Configuration) -> Result<()> {
2084 let no_validator = |_: &str| -> Result<()> { Ok(()) };
2085 if let Some(new_value) = edit_string_field("URL", &config.url, no_validator)? {
2086 config.url = new_value;
2087 }
2088 Ok(())
2089}
2090
2091fn edit_field_model(config: &mut Configuration) -> Result<()> {
2093 if let Some(result) = edit_optional_string_field("模型", config.model.as_deref())? {
2094 config.model = result;
2095 }
2096 Ok(())
2097}
2098
2099fn edit_field_small_fast_model(config: &mut Configuration) -> Result<()> {
2101 if let Some(result) =
2102 edit_optional_string_field("快速模型", config.small_fast_model.as_deref())?
2103 {
2104 config.small_fast_model = result;
2105 }
2106 Ok(())
2107}
2108
2109fn edit_field_max_thinking_tokens(config: &mut Configuration) -> Result<()> {
2111 if let Some(result) = edit_optional_u32_field("最大思考令牌数", config.max_thinking_tokens)?
2112 {
2113 config.max_thinking_tokens = result;
2114 }
2115 Ok(())
2116}
2117
2118fn edit_field_api_timeout_ms(config: &mut Configuration) -> Result<()> {
2120 if let Some(result) = edit_optional_u32_field("API超时时间 (毫秒)", config.api_timeout_ms)?
2121 {
2122 config.api_timeout_ms = result;
2123 }
2124 Ok(())
2125}
2126
2127fn edit_field_claude_code_disable_nonessential_traffic(config: &mut Configuration) -> Result<()> {
2129 if let Some(result) = edit_optional_u32_field(
2130 "禁用非必要流量标志",
2131 config.claude_code_disable_nonessential_traffic,
2132 )? {
2133 config.claude_code_disable_nonessential_traffic = result;
2134 }
2135 Ok(())
2136}
2137
2138fn edit_field_anthropic_default_sonnet_model(config: &mut Configuration) -> Result<()> {
2140 if let Some(result) = edit_optional_string_field(
2141 "默认 Sonnet 模型",
2142 config.anthropic_default_sonnet_model.as_deref(),
2143 )? {
2144 config.anthropic_default_sonnet_model = result;
2145 }
2146 Ok(())
2147}
2148
2149fn edit_field_anthropic_default_opus_model(config: &mut Configuration) -> Result<()> {
2151 if let Some(result) = edit_optional_string_field(
2152 "默认 Opus 模型",
2153 config.anthropic_default_opus_model.as_deref(),
2154 )? {
2155 config.anthropic_default_opus_model = result;
2156 }
2157 Ok(())
2158}
2159
2160fn edit_field_anthropic_default_haiku_model(config: &mut Configuration) -> Result<()> {
2162 if let Some(result) = edit_optional_string_field(
2163 "默认 Haiku 模型",
2164 config.anthropic_default_haiku_model.as_deref(),
2165 )? {
2166 config.anthropic_default_haiku_model = result;
2167 }
2168 Ok(())
2169}
2170
2171fn edit_field_claude_code_subagent_model(config: &mut Configuration) -> Result<()> {
2173 if let Some(result) =
2174 edit_optional_string_field("子代理模型", config.claude_code_subagent_model.as_deref())?
2175 {
2176 config.claude_code_subagent_model = result;
2177 }
2178 Ok(())
2179}
2180
2181fn edit_field_claude_code_disable_nonstreaming_fallback(config: &mut Configuration) -> Result<()> {
2183 if let Some(result) = edit_optional_u32_field(
2184 "禁用非流式回退标志",
2185 config.claude_code_disable_nonstreaming_fallback,
2186 )? {
2187 config.claude_code_disable_nonstreaming_fallback = result;
2188 }
2189 Ok(())
2190}
2191
2192fn edit_field_claude_code_effort_level(config: &mut Configuration) -> Result<()> {
2194 if let Some(result) =
2195 edit_optional_string_field("努力级别", config.claude_code_effort_level.as_deref())?
2196 {
2197 config.claude_code_effort_level = result;
2198 }
2199 Ok(())
2200}
2201
2202fn save_configuration_changes(original_alias: &str, new_config: &Configuration) -> Result<()> {
2204 let mut storage = ConfigStorage::load()?;
2206
2207 if original_alias != new_config.alias_name
2209 && storage.get_configuration(&new_config.alias_name).is_some()
2210 {
2211 println!("\n{}", "别名冲突!".red().bold());
2212 println!("配置 '{}' 已存在", new_config.alias_name.yellow());
2213 print!("是否覆盖现有配置? (y/N): ");
2214 io::stdout().flush()?;
2215
2216 let mut input = String::new();
2217 io::stdin().read_line(&mut input)?;
2218 let input = input.trim().to_lowercase();
2219
2220 if input != "y" && input != "yes" {
2221 println!("{}", "编辑已取消".yellow());
2222 return Ok(());
2223 }
2224 }
2225
2226 storage.update_configuration(original_alias, new_config.clone())?;
2228 storage.save()?;
2229
2230 println!("\n{}", "配置已成功保存!".green().bold());
2231
2232 Ok(())
2233}