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;
15use std::thread;
16use std::time::Duration;
17
18struct BorderDrawing {
20 pub unicode_supported: bool,
22}
23
24impl BorderDrawing {
25 fn new() -> Self {
27 let unicode_supported = Self::detect_unicode_support();
28 Self { unicode_supported }
29 }
30
31 fn detect_unicode_support() -> bool {
33 if let Ok(term) = std::env::var("TERM") {
35 if term.contains("xterm") || term.contains("screen") || term == "tmux-256color" {
37 return true;
38 }
39 }
40
41 if let Ok(lang) = std::env::var("LANG")
43 && (lang.contains("UTF-8") || lang.contains("utf8"))
44 {
45 return true;
46 }
47
48 true
51 }
52
53 fn draw_top_border(&self, title: &str, width: usize) -> String {
55 if self.unicode_supported {
56 let title_padded = format!(" {title} ");
57 let title_len = text_display_width(&title_padded);
58
59 if title_len >= width.saturating_sub(2) {
60 format!("╔{}╗", "═".repeat(width.saturating_sub(2)))
62 } else {
63 let inner_width = width.saturating_sub(2); let padding_total = inner_width.saturating_sub(title_len);
65 let padding_left = padding_total / 2;
66 let padding_right = padding_total - padding_left;
67 format!(
68 "╔{}{}{}╗",
69 "═".repeat(padding_left),
70 title_padded,
71 "═".repeat(padding_right)
72 )
73 }
74 } else {
75 let title_padded = format!(" {title} ");
77 let title_len = title_padded.len();
78
79 if title_len >= width.saturating_sub(2) {
80 format!("+{}+", "-".repeat(width.saturating_sub(2)))
81 } else {
82 let inner_width = width.saturating_sub(2);
83 let padding_total = inner_width.saturating_sub(title_len);
84 let padding_left = padding_total / 2;
85 let padding_right = padding_total - padding_left;
86 format!(
87 "+{}{}{}+",
88 "-".repeat(padding_left),
89 title_padded,
90 "-".repeat(padding_right)
91 )
92 }
93 }
94 }
95
96 fn draw_middle_line(&self, text: &str, width: usize) -> String {
98 if self.unicode_supported {
99 let text_len = text_display_width(text);
100 let available_width = width.saturating_sub(4);
103 if text_len > available_width {
104 let mut current_width = 0;
106 let truncated: String = text
107 .chars()
108 .take_while(|&c| {
109 let char_width = match c as u32 {
110 0x00..=0x7F => 1,
111 0x80..=0x2FF => 1,
112 0x2190..=0x21FF => 2,
113 0x3000..=0x303F => 2,
114 0x3040..=0x309F => 2,
115 0x30A0..=0x30FF => 2,
116 0x4E00..=0x9FFF => 2,
117 0xAC00..=0xD7AF => 2,
118 0x3400..=0x4DBF => 2,
119 0xFF01..=0xFF60 => 2,
120 _ => 1,
121 };
122 if current_width + char_width <= available_width {
123 current_width += char_width;
124 true
125 } else {
126 false
127 }
128 })
129 .collect();
130 let truncated_width = text_display_width(&truncated);
132 let padding_spaces = available_width.saturating_sub(truncated_width);
133 format!("║ {}{} ║", truncated, " ".repeat(padding_spaces))
134 } else {
135 let padded_text =
136 pad_text_to_width(text, available_width, TextAlignment::Left, ' ');
137 format!("║ {padded_text} ║")
138 }
139 } else {
140 let text_len = text_display_width(text);
142 let available_width = width.saturating_sub(4);
143 if text_len > available_width {
144 let mut current_width = 0;
146 let truncated: String = text
147 .chars()
148 .take_while(|&c| {
149 let char_width = if (c as u32) <= 0x7F { 1 } else { 2 };
150 if current_width + char_width <= available_width {
151 current_width += char_width;
152 true
153 } else {
154 false
155 }
156 })
157 .collect();
158 let truncated_width = text_display_width(&truncated);
160 let padding_spaces = available_width.saturating_sub(truncated_width);
161 format!("| {}{} |", truncated, " ".repeat(padding_spaces))
162 } else {
163 let padded_text =
164 pad_text_to_width(text, available_width, TextAlignment::Left, ' ');
165 format!("| {padded_text} |")
166 }
167 }
168 }
169
170 fn draw_bottom_border(&self, width: usize) -> String {
172 if self.unicode_supported {
173 format!("╚{}╝", "═".repeat(width - 2))
174 } else {
175 format!("+{}+", "-".repeat(width - 2))
176 }
177 }
178}
179
180pub fn handle_current_command() -> Result<()> {
190 let storage = ConfigStorage::load()?;
191
192 println!("\n{}", "Current Configuration:".green().bold());
193 println!("Environment variable mode: configurations are set per-command execution");
194 println!("Select a configuration from the menu below to launch Claude");
195 println!("Select 'cc' to launch Claude with default settings");
196
197 let raw_mode_enabled = terminal::enable_raw_mode().is_ok();
199
200 if raw_mode_enabled {
201 let mut stdout = io::stdout();
202 if execute!(
203 stdout,
204 terminal::EnterAlternateScreen,
205 terminal::Clear(terminal::ClearType::All)
206 )
207 .is_ok()
208 {
209 let result = handle_main_menu_interactive(&mut stdout, &storage);
211
212 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
214 let _ = terminal::disable_raw_mode();
215
216 return result;
217 } else {
218 let _ = terminal::disable_raw_mode();
220 }
221 }
222
223 handle_main_menu_simple(&storage)
225}
226
227fn handle_main_menu_interactive(stdout: &mut io::Stdout, storage: &ConfigStorage) -> Result<()> {
229 let menu_items = [
230 "Execute claude --dangerously-skip-permissions",
231 "Switch configuration",
232 "Exit",
233 ];
234 let mut selected_index = 0;
235
236 loop {
237 execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
239 execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
240
241 let border = BorderDrawing::new();
243 const MAIN_MENU_WIDTH: usize = 68;
244
245 println!(
246 "\r{}",
247 border.draw_top_border("Main Menu", MAIN_MENU_WIDTH).green()
248 );
249 println!(
250 "\r{}",
251 border
252 .draw_middle_line(
253 "↑↓/jk导航,1-9快选,E-编辑,R-官方,Q-退出,Enter确认,Esc取消",
254 MAIN_MENU_WIDTH
255 )
256 .green()
257 );
258 println!("\r{}", border.draw_bottom_border(MAIN_MENU_WIDTH).green());
259 println!();
260
261 for (index, item) in menu_items.iter().enumerate() {
263 if index == selected_index {
264 println!("\r> {} {}", "●".blue().bold(), item.blue().bold());
265 } else {
266 println!("\r {} {}", "○".dimmed(), item.dimmed());
267 }
268 }
269
270 stdout.flush()?;
272
273 let event = match event::read() {
275 Ok(event) => event,
276 Err(e) => {
277 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
279 let _ = terminal::disable_raw_mode();
280 return Err(e.into());
281 }
282 };
283
284 match event {
285 Event::Key(KeyEvent {
286 code,
287 kind: KeyEventKind::Press,
288 ..
289 }) => {
290 match code {
291 KeyCode::Up => {
292 selected_index = selected_index.saturating_sub(1);
293 }
294 KeyCode::Down => {
295 if selected_index < menu_items.len() - 1 {
296 selected_index += 1;
297 }
298 }
299 KeyCode::Enter => {
300 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
302 let _ = terminal::disable_raw_mode();
303
304 return handle_main_menu_action(selected_index, storage);
305 }
306 KeyCode::Esc => {
307 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
309 let _ = terminal::disable_raw_mode();
310
311 println!("\nExiting...");
312 return Ok(());
313 }
314 _ => {}
315 }
316 }
317 Event::Key(_) => {} _ => {}
319 }
320 }
321}
322
323fn handle_main_menu_simple(storage: &ConfigStorage) -> Result<()> {
325 loop {
326 println!("\n{}", "Available Actions:".blue().bold());
327 println!("1. Execute claude --dangerously-skip-permissions");
328 println!("2. Switch configuration");
329 println!("3. Exit");
330
331 print!("\nPlease select an option (1-3): ");
332 io::stdout().flush().context("Failed to flush stdout")?;
333
334 let mut input = String::new();
335 io::stdin()
336 .read_line(&mut input)
337 .context("Failed to read input")?;
338
339 let choice = input.trim();
340
341 match choice {
342 "1" => return handle_main_menu_action(0, storage),
343 "2" => return handle_main_menu_action(1, storage),
344 "3" => return handle_main_menu_action(2, storage),
345 _ => {
346 println!("Invalid option. Please select 1-3.");
347 }
348 }
349 }
350}
351
352fn handle_main_menu_action(selected_index: usize, storage: &ConfigStorage) -> Result<()> {
354 match selected_index {
355 0 => {
356 println!("\nExecuting: claude --dangerously-skip-permissions");
357 execute_claude_command(true)?;
358 }
359 1 => {
360 handle_interactive_selection(storage)?;
362 }
363 2 => {
364 println!("Exiting...");
365 }
366 _ => {
367 println!("Invalid selection");
368 }
369 }
370 Ok(())
371}
372
373pub fn handle_interactive_selection(storage: &ConfigStorage) -> Result<()> {
381 if storage.configurations.is_empty() {
382 println!("No configurations available. Use 'add' command to create configurations first.");
383 return Ok(());
384 }
385
386 let mut configs: Vec<&Configuration> = storage.configurations.values().collect();
387 configs.sort_by(|a, b| a.alias_name.cmp(&b.alias_name));
388
389 let mut selected_index = 0;
390
391 let raw_mode_enabled = terminal::enable_raw_mode().is_ok();
393
394 if raw_mode_enabled {
395 let mut stdout = io::stdout();
396 if execute!(
397 stdout,
398 terminal::EnterAlternateScreen,
399 terminal::Clear(terminal::ClearType::All)
400 )
401 .is_ok()
402 {
403 let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
405 let result = handle_full_interactive_menu(
406 &mut stdout,
407 &configs,
408 &mut selected_index,
409 storage,
410 storage_mode,
411 );
412
413 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
415 let _ = terminal::disable_raw_mode();
416
417 return result;
418 } else {
419 let _ = terminal::disable_raw_mode();
421 }
422 }
423
424 handle_simple_interactive_menu(&configs, storage)
426}
427
428fn handle_full_interactive_menu(
430 stdout: &mut io::Stdout,
431 configs: &[&Configuration],
432 selected_index: &mut usize,
433 storage: &ConfigStorage,
434 storage_mode: crate::config::types::StorageMode,
435) -> Result<()> {
436 if configs.is_empty() {
438 println!("\r{}", "No configurations available".yellow());
439 println!(
440 "\r{}",
441 "Use 'cc-switch add <alias> <token> <url>' to add configurations first.".dimmed()
442 );
443 println!("\r{}", "Press any key to continue...".dimmed());
444 let _ = event::read(); return Ok(());
446 }
447
448 const PAGE_SIZE: usize = 9; let total_pages = if configs.len() <= PAGE_SIZE {
452 1
453 } else {
454 configs.len().div_ceil(PAGE_SIZE)
455 };
456 let mut current_page = 0;
457
458 loop {
459 let start_idx = current_page * PAGE_SIZE;
461 let end_idx = std::cmp::min(start_idx + PAGE_SIZE, configs.len());
462 let page_configs = &configs[start_idx..end_idx];
463
464 execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
466 execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
467
468 let border = BorderDrawing::new();
470 const CONFIG_MENU_WIDTH: usize = 80;
473
474 println!(
475 "\r{}",
476 border
477 .draw_top_border("Select Configuration", CONFIG_MENU_WIDTH)
478 .green()
479 );
480 if total_pages > 1 {
481 println!(
482 "\r{}",
483 border
484 .draw_middle_line(
485 &format!("第 {} 页,共 {} 页", current_page + 1, total_pages),
486 CONFIG_MENU_WIDTH
487 )
488 .green()
489 );
490 println!(
491 "\r{}",
492 border
493 .draw_middle_line(
494 "↑↓/jk导航,1-9快选,E-编辑,N/P翻页,R-官方,Q-退出,Enter确认",
495 CONFIG_MENU_WIDTH
496 )
497 .green()
498 );
499 } else {
500 println!(
501 "\r{}",
502 border
503 .draw_middle_line(
504 "↑↓/jk导航,1-9快选,E-编辑,R-官方,Q-退出,Enter确认,Esc取消",
505 CONFIG_MENU_WIDTH
506 )
507 .green()
508 );
509 }
510 println!("\r{}", border.draw_bottom_border(CONFIG_MENU_WIDTH).green());
511 println!();
512
513 let official_index = 0;
515 if *selected_index == official_index {
516 println!(
517 "\r> {} {} {}",
518 "●".red().bold(),
519 "[R]".red().bold(),
520 "official".red().bold()
521 );
522 println!("\r Use official Claude API (no custom configuration)");
523 println!();
524 } else {
525 println!("\r {} {} {}", "○".red(), "[R]".red(), "official".red());
526 }
527
528 for (page_index, config) in page_configs.iter().enumerate() {
530 let actual_config_index = start_idx + page_index;
531 let display_number = page_index + 1; let actual_index = actual_config_index + 1; let number_label = format!("[{display_number}]");
534
535 if *selected_index == actual_index {
536 println!(
537 "\r> {} {} {}",
538 "●".blue().bold(),
539 number_label.blue().bold(),
540 config.alias_name.blue().bold()
541 );
542
543 let details = format_config_details(config, "\r ", false);
545 for detail_line in details {
546 println!("{detail_line}");
547 }
548 println!();
549 } else {
550 println!(
551 "\r {} {} {}",
552 "○".dimmed(),
553 number_label.dimmed(),
554 config.alias_name.dimmed()
555 );
556 }
557 }
558
559 let exit_index = configs.len() + 1;
561 if *selected_index == exit_index {
562 println!(
563 "\r> {} {} {}",
564 "●".yellow().bold(),
565 "[Q]".yellow().bold(),
566 "Exit".yellow().bold()
567 );
568 println!("\r Exit without making changes");
569 println!();
570 } else {
571 println!(
572 "\r {} {} {}",
573 "○".dimmed(),
574 "[Q]".dimmed(),
575 "Exit".dimmed()
576 );
577 }
578
579 if total_pages > 1 {
581 println!(
582 "\r{}",
583 format!(
584 "Page Navigation: [N]ext, [P]revious (第 {} 页,共 {} 页)",
585 current_page + 1,
586 total_pages
587 )
588 .dimmed()
589 );
590 }
591
592 stdout.flush()?;
594
595 let event = match event::read() {
597 Ok(event) => event,
598 Err(e) => {
599 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
601 let _ = terminal::disable_raw_mode();
602 return Err(e.into());
603 }
604 };
605
606 match event {
607 Event::Key(KeyEvent {
608 code,
609 kind: KeyEventKind::Press,
610 ..
611 }) => match code {
612 KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => {
613 *selected_index = selected_index.saturating_sub(1);
614 }
615 KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => {
616 if *selected_index < configs.len() + 1 {
617 *selected_index += 1;
618 }
619 }
620 KeyCode::PageDown | KeyCode::Char('n') | KeyCode::Char('N') => {
621 if total_pages > 1 && current_page < total_pages - 1 {
622 current_page += 1;
623 let new_page_start_idx = current_page * PAGE_SIZE;
625 *selected_index = new_page_start_idx + 1; }
627 }
628 KeyCode::PageUp | KeyCode::Char('p') | KeyCode::Char('P') => {
629 if total_pages > 1 && current_page > 0 {
630 current_page -= 1;
631 let new_page_start_idx = current_page * PAGE_SIZE;
633 *selected_index = new_page_start_idx + 1; }
635 }
636 KeyCode::Enter => {
637 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
639 let _ = terminal::disable_raw_mode();
640
641 return handle_selection_action(
642 configs,
643 *selected_index,
644 storage,
645 storage_mode,
646 );
647 }
648 KeyCode::Esc => {
649 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
651 let _ = terminal::disable_raw_mode();
652
653 println!("\nSelection cancelled");
654 return Ok(());
655 }
656 KeyCode::Char(c) if c.is_ascii_digit() => {
657 let digit = c.to_digit(10).unwrap() as usize;
658 if digit >= 1 && digit <= page_configs.len() {
660 let actual_config_index = start_idx + (digit - 1);
661 let selection_index = actual_config_index + 1; let _ = execute!(stdout, terminal::LeaveAlternateScreen);
665 let _ = terminal::disable_raw_mode();
666
667 return handle_selection_action(
668 configs,
669 selection_index,
670 storage,
671 storage_mode,
672 );
673 }
674 }
676 KeyCode::Char('r') | KeyCode::Char('R') => {
677 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
679 let _ = terminal::disable_raw_mode();
680
681 return handle_selection_action(configs, 0, storage, storage_mode);
682 }
683 KeyCode::Char('e') | KeyCode::Char('E') => {
684 if *selected_index > 0 && *selected_index <= configs.len() {
686 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
688 let _ = terminal::disable_raw_mode();
689
690 let config_index = *selected_index - 1; if let Err(e) = handle_config_edit(configs[config_index]) {
694 if e.downcast_ref::<EditModeError>()
696 == Some(&EditModeError::ReturnToMenu)
697 {
698 if execute!(
700 stdout,
701 terminal::EnterAlternateScreen,
702 terminal::Clear(terminal::ClearType::All)
703 )
704 .is_ok()
705 && terminal::enable_raw_mode().is_ok()
706 {
707 continue;
709 }
710 }
711 return Err(e);
713 }
714 }
715 }
717 KeyCode::Char('q') | KeyCode::Char('Q') => {
718 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
720 let _ = terminal::disable_raw_mode();
721
722 return handle_selection_action(
723 configs,
724 configs.len() + 1,
725 storage,
726 storage_mode,
727 );
728 }
729 _ => {}
730 },
731 Event::Key(_) => {} _ => {}
733 }
734 }
735}
736
737fn handle_simple_interactive_menu(
739 configs: &[&Configuration],
740 storage: &ConfigStorage,
741) -> Result<()> {
742 const PAGE_SIZE: usize = 9; if configs.len() <= PAGE_SIZE {
746 return handle_simple_single_page_menu(configs, storage);
747 }
748
749 let total_pages = configs.len().div_ceil(PAGE_SIZE);
751 let mut current_page = 0;
752
753 loop {
754 let start_idx = current_page * PAGE_SIZE;
756 let end_idx = std::cmp::min(start_idx + PAGE_SIZE, configs.len());
757 let page_configs = &configs[start_idx..end_idx];
758
759 println!("\n{}", "Available Configurations:".blue().bold());
760 if total_pages > 1 {
761 println!("第 {} 页,共 {} 页", current_page + 1, total_pages);
762 println!("使用 'n' 下一页, 'p' 上一页, 'r' 官方配置, 'q' 退出");
763 }
764 println!();
765
766 println!("{} {}", "[r]".red().bold(), "official".red());
768 println!(" Use official Claude API (no custom configuration)");
769 println!();
770
771 for (page_index, config) in page_configs.iter().enumerate() {
773 let display_number = page_index + 1;
774
775 println!(
776 "{}. {}",
777 format!("[{display_number}]").green().bold(),
778 config.alias_name.green()
779 );
780
781 let details = format_config_details(config, " ", true);
783 for detail_line in details {
784 println!("{detail_line}");
785 }
786 println!();
787 }
788
789 println!("{} {}", "[q]".yellow().bold(), "Exit".yellow());
791
792 if total_pages > 1 {
793 println!(
794 "\n页面导航: [n]下页, [p]上页 | 配置选择: [1-{}] | [e]编辑 | [r]官方 | [q]退出",
795 page_configs.len()
796 );
797 }
798
799 print!("\n请输入选择: ");
800 io::stdout().flush()?;
801
802 let mut input = String::new();
803 io::stdin().read_line(&mut input)?;
804 let choice = input.trim().to_lowercase();
805
806 match choice.as_str() {
807 "r" => {
808 println!("Using official Claude configuration");
810
811 let mut settings = crate::config::types::ClaudeSettings::load(
813 storage.get_claude_settings_dir().map(|s| s.as_str()),
814 )?;
815 settings.remove_anthropic_env();
816 settings.remove_anthropic_config_mode();
817 settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
818
819 return launch_claude_with_env(EnvironmentConfig::empty());
820 }
821 "e" => {
822 println!("编辑功能在交互式菜单中可用");
825 }
826 "q" => {
827 println!("Exiting...");
828 return Ok(());
829 }
830 "n" if total_pages > 1 && current_page < total_pages - 1 => {
831 current_page += 1;
832 continue;
833 }
834 "p" if total_pages > 1 && current_page > 0 => {
835 current_page -= 1;
836 continue;
837 }
838 digit_str => {
839 if let Ok(digit) = digit_str.parse::<usize>()
840 && digit >= 1
841 && digit <= page_configs.len()
842 {
843 let actual_config_index = start_idx + (digit - 1);
844 let selection_index = actual_config_index + 1; let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
846 return handle_selection_action(
847 configs,
848 selection_index,
849 storage,
850 storage_mode,
851 );
852 }
853 println!("无效选择,请重新输入");
854 }
855 }
856 }
857}
858
859fn handle_simple_single_page_menu(
861 configs: &[&Configuration],
862 storage: &ConfigStorage,
863) -> Result<()> {
864 println!("\n{}", "Available Configurations:".blue().bold());
865
866 println!("1. {}", "official".red());
868 println!(" Use official Claude API (no custom configuration)");
869 println!();
870
871 for (index, config) in configs.iter().enumerate() {
872 println!(
873 "{}. {}",
874 index + 2, config.alias_name.green()
876 );
877
878 let details = format_config_details(config, " ", true);
880 for detail_line in details {
881 println!("{detail_line}");
882 }
883 println!();
884 }
885
886 println!("{}. {}", configs.len() + 2, "Exit".yellow());
887
888 print!("\nSelect configuration (1-{}): ", configs.len() + 2);
889 io::stdout().flush()?;
890
891 let mut input = String::new();
892 io::stdin().read_line(&mut input)?;
893
894 match input.trim().parse::<usize>() {
895 Ok(1) => {
896 println!("Using official Claude configuration");
898
899 let mut settings = crate::config::types::ClaudeSettings::load(
901 storage.get_claude_settings_dir().map(|s| s.as_str()),
902 )?;
903 settings.remove_anthropic_env();
904 settings.remove_anthropic_config_mode();
905 settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
906
907 launch_claude_with_env(EnvironmentConfig::empty())
908 }
909 Ok(num) if num >= 2 && num <= configs.len() + 1 => {
910 let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
911 handle_selection_action(configs, num - 1, storage, storage_mode) }
913 Ok(num) if num == configs.len() + 2 => {
914 println!("Exiting...");
915 Ok(())
916 }
917 _ => {
918 println!("Invalid selection");
919 Ok(())
920 }
921 }
922}
923
924fn handle_selection_action(
926 configs: &[&Configuration],
927 selected_index: usize,
928 storage: &ConfigStorage,
929 storage_mode: crate::config::types::StorageMode,
930) -> Result<()> {
931 if selected_index == 0 {
932 println!("\nUsing official Claude configuration");
934
935 let mut settings = crate::config::types::ClaudeSettings::load(
937 storage.get_claude_settings_dir().map(|s| s.as_str()),
938 )?;
939 settings.remove_anthropic_env();
940 settings.remove_anthropic_config_mode();
941 settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
942
943 launch_claude_with_env(EnvironmentConfig::empty())
944 } else if selected_index <= configs.len() {
945 let config_index = selected_index - 1; let selected_config = configs[config_index].clone();
948 let env_config = EnvironmentConfig::from_config(&selected_config);
949
950 println!(
951 "\nSwitched to configuration '{}'",
952 selected_config.alias_name.green().bold()
953 );
954
955 let details = format_config_details(&selected_config, "", false);
957 for detail_line in details {
958 println!("{detail_line}");
959 }
960
961 let mut settings = crate::config::types::ClaudeSettings::load(
963 storage.get_claude_settings_dir().map(|s| s.as_str()),
964 )?;
965 settings.switch_to_config_with_mode(
966 &selected_config,
967 storage_mode,
968 storage.get_claude_settings_dir().map(|s| s.as_str()),
969 )?;
970
971 launch_claude_with_env(env_config)
972 } else {
973 println!("\nExiting...");
975 Ok(())
976 }
977}
978
979fn launch_claude_with_env(env_config: EnvironmentConfig) -> Result<()> {
981 println!("\nWaiting 0.5 seconds before launching Claude...");
982 thread::sleep(Duration::from_millis(500));
983
984 println!("Launching Claude CLI...");
985
986 for (key, value) in env_config.as_env_tuples() {
988 unsafe {
989 std::env::set_var(&key, &value);
990 }
991 }
992
993 #[cfg(unix)]
995 {
996 use std::os::unix::process::CommandExt;
997 let error = Command::new("claude")
998 .arg("--dangerously-skip-permissions")
999 .exec();
1000 anyhow::bail!("Failed to exec claude: {}", error);
1002 }
1003
1004 #[cfg(not(unix))]
1006 {
1007 use std::process::Stdio;
1008 let mut child = Command::new("claude")
1009 .arg("--dangerously-skip-permissions")
1010 .stdin(Stdio::inherit())
1011 .stdout(Stdio::inherit())
1012 .stderr(Stdio::inherit())
1013 .spawn()
1014 .context(
1015 "Failed to launch Claude CLI. Make sure 'claude' command is available in PATH",
1016 )?;
1017
1018 let status = child.wait()?;
1019
1020 if !status.success() {
1021 anyhow::bail!("Claude CLI exited with error status: {}", status);
1022 }
1023 }
1024}
1025
1026fn execute_claude_command(skip_permissions: bool) -> Result<()> {
1031 println!("Launching Claude CLI...");
1032
1033 #[cfg(unix)]
1035 {
1036 use std::os::unix::process::CommandExt;
1037 let mut command = Command::new("claude");
1038 if skip_permissions {
1039 command.arg("--dangerously-skip-permissions");
1040 }
1041
1042 let error = command.exec();
1043 anyhow::bail!("Failed to exec claude: {}", error);
1045 }
1046
1047 #[cfg(not(unix))]
1049 {
1050 use std::process::Stdio;
1051 let mut command = Command::new("claude");
1052 if skip_permissions {
1053 command.arg("--dangerously-skip-permissions");
1054 }
1055
1056 command
1057 .stdin(Stdio::inherit())
1058 .stdout(Stdio::inherit())
1059 .stderr(Stdio::inherit());
1060
1061 let mut child = command.spawn().context(
1062 "Failed to launch Claude CLI. Make sure 'claude' command is available in PATH",
1063 )?;
1064
1065 let status = child
1066 .wait()
1067 .context("Failed to wait for Claude CLI process")?;
1068
1069 if !status.success() {
1070 anyhow::bail!("Claude CLI exited with error status: {}", status);
1071 }
1072 }
1073}
1074
1075pub fn read_input(prompt: &str) -> Result<String> {
1083 print!("{prompt}");
1084 io::stdout().flush().context("Failed to flush stdout")?;
1085 let mut input = String::new();
1086 io::stdin()
1087 .read_line(&mut input)
1088 .context("Failed to read input")?;
1089 Ok(input.trim().to_string())
1090}
1091
1092pub fn read_sensitive_input(prompt: &str) -> Result<String> {
1100 print!("{prompt}");
1101 io::stdout().flush().context("Failed to flush stdout")?;
1102 let mut input = String::new();
1103 io::stdin()
1104 .read_line(&mut input)
1105 .context("Failed to read input")?;
1106 Ok(input.trim().to_string())
1107}
1108
1109fn format_config_details(config: &Configuration, indent: &str, _compact: bool) -> Vec<String> {
1122 let mut lines = Vec::new();
1123
1124 let terminal_width = get_terminal_width();
1126 let _available_width = terminal_width.saturating_sub(text_display_width(indent) + 8);
1127
1128 let token_label = "Token:";
1130 let url_label = "URL:";
1131 let model_label = "Model:";
1132 let small_model_label = "Small Fast Model:";
1133 let max_thinking_tokens_label = "Max Thinking Tokens:";
1134 let api_timeout_ms_label = "API Timeout (ms):";
1135 let disable_nonessential_traffic_label = "Disable Nonessential Traffic:";
1136 let default_sonnet_model_label = "Default Sonnet Model:";
1137 let default_opus_model_label = "Default Opus Model:";
1138 let default_haiku_model_label = "Default Haiku Model:";
1139
1140 let max_label_width = [
1142 token_label,
1143 url_label,
1144 model_label,
1145 small_model_label,
1146 max_thinking_tokens_label,
1147 api_timeout_ms_label,
1148 disable_nonessential_traffic_label,
1149 default_sonnet_model_label,
1150 default_opus_model_label,
1151 default_haiku_model_label,
1152 ]
1153 .iter()
1154 .map(|label| text_display_width(label))
1155 .max()
1156 .unwrap_or(0);
1157
1158 let token_line = format!(
1160 "{}{} {}",
1161 indent,
1162 pad_text_to_width(token_label, max_label_width, TextAlignment::Left, ' '),
1163 format_token_for_display(&config.token).dimmed()
1164 );
1165 lines.push(token_line);
1166
1167 let url_line = format!(
1169 "{}{} {}",
1170 indent,
1171 pad_text_to_width(url_label, max_label_width, TextAlignment::Left, ' '),
1172 config.url.cyan()
1173 );
1174 lines.push(url_line);
1175
1176 if let Some(model) = &config.model {
1178 let model_line = format!(
1179 "{}{} {}",
1180 indent,
1181 pad_text_to_width(model_label, max_label_width, TextAlignment::Left, ' '),
1182 model.yellow()
1183 );
1184 lines.push(model_line);
1185 }
1186
1187 if let Some(small_fast_model) = &config.small_fast_model {
1189 let small_model_line = format!(
1190 "{}{} {}",
1191 indent,
1192 pad_text_to_width(small_model_label, max_label_width, TextAlignment::Left, ' '),
1193 small_fast_model.yellow()
1194 );
1195 lines.push(small_model_line);
1196 }
1197
1198 if let Some(max_thinking_tokens) = config.max_thinking_tokens {
1200 let tokens_line = format!(
1201 "{}{} {}",
1202 indent,
1203 pad_text_to_width(
1204 max_thinking_tokens_label,
1205 max_label_width,
1206 TextAlignment::Left,
1207 ' '
1208 ),
1209 format!("{}", max_thinking_tokens).yellow()
1210 );
1211 lines.push(tokens_line);
1212 }
1213
1214 if let Some(api_timeout_ms) = config.api_timeout_ms {
1216 let timeout_line = format!(
1217 "{}{} {}",
1218 indent,
1219 pad_text_to_width(
1220 api_timeout_ms_label,
1221 max_label_width,
1222 TextAlignment::Left,
1223 ' '
1224 ),
1225 format!("{}", api_timeout_ms).yellow()
1226 );
1227 lines.push(timeout_line);
1228 }
1229
1230 if let Some(disable_flag) = config.claude_code_disable_nonessential_traffic {
1232 let flag_line = format!(
1233 "{}{} {}",
1234 indent,
1235 pad_text_to_width(
1236 disable_nonessential_traffic_label,
1237 max_label_width,
1238 TextAlignment::Left,
1239 ' '
1240 ),
1241 format!("{}", disable_flag).yellow()
1242 );
1243 lines.push(flag_line);
1244 }
1245
1246 if let Some(sonnet_model) = &config.anthropic_default_sonnet_model {
1248 let sonnet_line = format!(
1249 "{}{} {}",
1250 indent,
1251 pad_text_to_width(
1252 default_sonnet_model_label,
1253 max_label_width,
1254 TextAlignment::Left,
1255 ' '
1256 ),
1257 sonnet_model.yellow()
1258 );
1259 lines.push(sonnet_line);
1260 }
1261
1262 if let Some(opus_model) = &config.anthropic_default_opus_model {
1264 let opus_line = format!(
1265 "{}{} {}",
1266 indent,
1267 pad_text_to_width(
1268 default_opus_model_label,
1269 max_label_width,
1270 TextAlignment::Left,
1271 ' '
1272 ),
1273 opus_model.yellow()
1274 );
1275 lines.push(opus_line);
1276 }
1277
1278 if let Some(haiku_model) = &config.anthropic_default_haiku_model {
1280 let haiku_line = format!(
1281 "{}{} {}",
1282 indent,
1283 pad_text_to_width(
1284 default_haiku_model_label,
1285 max_label_width,
1286 TextAlignment::Left,
1287 ' '
1288 ),
1289 haiku_model.yellow()
1290 );
1291 lines.push(haiku_line);
1292 }
1293
1294 lines
1295}
1296
1297#[cfg(test)]
1298mod border_drawing_tests {
1299 use super::*;
1300
1301 #[test]
1302 fn test_border_drawing_unicode_support() {
1303 let _border = BorderDrawing::new();
1304 }
1306
1307 #[test]
1308 fn test_border_drawing_top_border() {
1309 let border = BorderDrawing {
1310 unicode_supported: true,
1311 };
1312 let result = border.draw_top_border("Test", 20);
1313 assert!(!result.is_empty());
1314 assert!(result.contains("Test"));
1315 }
1316
1317 #[test]
1318 fn test_border_drawing_ascii_fallback() {
1319 let border = BorderDrawing {
1320 unicode_supported: false,
1321 };
1322 let result = border.draw_top_border("Test", 20);
1323 assert!(!result.is_empty());
1324 assert!(result.contains("Test"));
1325 assert!(result.contains("+"));
1326 assert!(result.contains("-"));
1327 }
1328
1329 #[test]
1330 fn test_border_drawing_middle_line() {
1331 let border = BorderDrawing {
1332 unicode_supported: true,
1333 };
1334 let result = border.draw_middle_line("Test message", 30);
1335 assert!(!result.is_empty());
1336 assert!(result.contains("Test message"));
1337 }
1338
1339 #[test]
1340 fn test_border_drawing_bottom_border() {
1341 let border = BorderDrawing {
1342 unicode_supported: true,
1343 };
1344 let result = border.draw_bottom_border(20);
1345 assert!(!result.is_empty());
1346 }
1347
1348 #[test]
1349 fn test_border_drawing_width_consistency() {
1350 let border = BorderDrawing {
1351 unicode_supported: true,
1352 };
1353 let width = 30;
1354 let top = border.draw_top_border("Title", width);
1355 let middle = border.draw_middle_line("Content", width);
1356 let bottom = border.draw_bottom_border(width);
1357
1358 assert!(top.chars().count() >= width - 2);
1360 assert!(middle.chars().count() >= width - 2);
1361 assert!(bottom.chars().count() >= width - 2);
1362 }
1363}
1364
1365#[cfg(test)]
1366mod pagination_tests {
1367
1368 #[test]
1370 fn test_pagination_calculation() {
1371 const PAGE_SIZE: usize = 9;
1372
1373 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); }
1384
1385 #[test]
1387 fn test_page_range_calculation() {
1388 const PAGE_SIZE: usize = 9;
1389
1390 let current_page = 0;
1392 let start_idx = current_page * PAGE_SIZE; let end_idx = std::cmp::min(start_idx + PAGE_SIZE, 15); assert_eq!(start_idx, 0);
1395 assert_eq!(end_idx, 9);
1396 assert_eq!(end_idx - start_idx, 9); let current_page = 1;
1400 let start_idx = current_page * PAGE_SIZE; let end_idx = std::cmp::min(start_idx + PAGE_SIZE, 15); assert_eq!(start_idx, 9);
1403 assert_eq!(end_idx, 15);
1404 assert_eq!(end_idx - start_idx, 6); let current_page = 0;
1408 let start_idx = current_page * PAGE_SIZE; let end_idx = std::cmp::min(start_idx + PAGE_SIZE, PAGE_SIZE); assert_eq!(start_idx, 0);
1411 assert_eq!(end_idx, 9);
1412 assert_eq!(end_idx - start_idx, 9); }
1414
1415 #[test]
1417 fn test_digit_mapping_to_config_index() {
1418 const PAGE_SIZE: usize = 9;
1419
1420 let current_page = 0;
1422 let start_idx = current_page * PAGE_SIZE; let digit = 1;
1426 let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 0);
1428
1429 let digit = 9;
1431 let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 8);
1433
1434 let current_page = 1;
1436 let start_idx = current_page * PAGE_SIZE; let digit = 1;
1440 let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 9);
1442
1443 let digit = 5;
1445 let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 13);
1447 }
1448
1449 #[test]
1451 fn test_selection_index_conversion() {
1452 const PAGE_SIZE: usize = 9;
1459
1460 let current_page = 0;
1462 let start_idx = current_page * PAGE_SIZE; let digit = 1;
1464 let actual_config_index = start_idx + (digit - 1); let selection_index = actual_config_index + 1; assert_eq!(selection_index, 1);
1467
1468 let current_page = 1;
1470 let start_idx = current_page * PAGE_SIZE; let digit = 1;
1472 let actual_config_index = start_idx + (digit - 1); let selection_index = actual_config_index + 1; assert_eq!(selection_index, 10);
1475 }
1476
1477 #[test]
1479 fn test_page_navigation_bounds() {
1480 const PAGE_SIZE: usize = 9;
1481 let total_configs: usize = 25; let total_pages = total_configs.div_ceil(PAGE_SIZE); assert_eq!(total_pages, 3);
1484
1485 let mut current_page = 0;
1487 if current_page > 0 {
1488 current_page -= 1;
1489 }
1490 assert_eq!(current_page, 0); let mut current_page = total_pages - 1; if current_page < total_pages - 1 {
1495 current_page += 1;
1496 }
1497 assert_eq!(current_page, 2); let mut current_page = 1;
1501
1502 if current_page < total_pages - 1 {
1504 current_page += 1;
1505 }
1506 assert_eq!(current_page, 2);
1507
1508 if current_page > 0 {
1510 current_page = current_page.saturating_sub(1);
1511 }
1512 assert_eq!(current_page, 1);
1513 }
1514
1515 #[test]
1517 fn test_digit_key_boundary_conditions() {
1518 const PAGE_SIZE: usize = 9;
1519
1520 let digit = 0;
1522 assert!(digit < 1, "Digit 0 should be less than 1 and ignored");
1523
1524 let configs_len = 5; let page_configs_len = std::cmp::min(PAGE_SIZE, configs_len); let digit = 9; assert!(
1529 digit > page_configs_len,
1530 "Digit 9 should be beyond available configs (5) and ignored"
1531 );
1532
1533 for digit in 1..=page_configs_len {
1535 assert!(
1536 digit >= 1 && digit <= page_configs_len,
1537 "Digit {} should be valid",
1538 digit
1539 );
1540 }
1541 }
1542
1543 #[test]
1545 fn test_empty_configs_handling() {
1546 let empty_configs: Vec<String> = Vec::new();
1547 assert!(
1548 empty_configs.is_empty(),
1549 "Empty config list should be properly detected"
1550 );
1551
1552 let configs_len = empty_configs.len(); assert_eq!(configs_len, 0, "Empty configs should have length 0");
1555
1556 }
1559
1560 #[test]
1562 fn test_page_navigation_boundaries() {
1563 const PAGE_SIZE: usize = 9;
1564 let total_configs: usize = 20; let total_pages = total_configs.div_ceil(PAGE_SIZE); let mut current_page = 0;
1569 let original_page = current_page;
1570
1571 if current_page > 0 {
1573 current_page -= 1;
1574 }
1575 assert_eq!(
1576 current_page, original_page,
1577 "First page should not navigate to previous"
1578 );
1579
1580 let mut current_page = total_pages - 1; let original_page = current_page;
1583
1584 if current_page < total_pages - 1 {
1586 current_page += 1;
1587 }
1588 assert_eq!(
1589 current_page, original_page,
1590 "Last page should not navigate to next"
1591 );
1592
1593 let mut current_page = 1; if current_page < total_pages - 1 {
1598 current_page += 1;
1599 }
1600 assert_eq!(current_page, 2, "Should navigate to next page");
1601
1602 if current_page > 0 {
1604 current_page = current_page.saturating_sub(1);
1605 }
1606 assert_eq!(current_page, 1, "Should navigate to previous page");
1607 }
1608
1609 #[test]
1611 fn test_j_key_navigation() {
1612 let mut selected_index: usize = 0;
1613 let configs_len = 5; if selected_index < configs_len + 1 {
1618 selected_index += 1;
1619 }
1620 assert_eq!(selected_index, 1, "j key should move selection down by one");
1621
1622 selected_index = configs_len + 1;
1624 let original_index = selected_index;
1625 if selected_index < configs_len + 1 {
1626 selected_index += 1;
1627 }
1628 assert_eq!(
1629 selected_index, original_index,
1630 "j key should not move beyond bottom boundary"
1631 );
1632 }
1633
1634 #[test]
1636 fn test_k_key_navigation() {
1637 let mut selected_index: usize = 5;
1638
1639 selected_index = selected_index.saturating_sub(1);
1642 assert_eq!(selected_index, 4, "k key should move selection up by one");
1643
1644 selected_index = 0;
1646 let original_index = selected_index;
1647 selected_index = selected_index.saturating_sub(1);
1648 assert_eq!(
1649 selected_index, original_index,
1650 "k key should not move beyond top boundary"
1651 );
1652 }
1653
1654 #[test]
1656 fn test_jk_key_boundary_conditions() {
1657 const CONFIGS_LEN: usize = 5;
1658
1659 let mut selected_index: usize = CONFIGS_LEN + 1; let original_index = selected_index;
1662 if selected_index < CONFIGS_LEN + 1 {
1663 selected_index += 1; }
1665 assert_eq!(
1666 selected_index, original_index,
1667 "j key should respect bottom boundary like Down arrow"
1668 );
1669
1670 let mut selected_index: usize = 0; let original_index = selected_index;
1673 selected_index = selected_index.saturating_sub(1); assert_eq!(
1675 selected_index, original_index,
1676 "k key should respect top boundary like Up arrow"
1677 );
1678 }
1679}
1680
1681#[derive(Debug, PartialEq)]
1683enum EditModeError {
1684 ReturnToMenu,
1685}
1686
1687impl std::fmt::Display for EditModeError {
1688 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1689 match self {
1690 EditModeError::ReturnToMenu => write!(f, "return_to_menu"),
1691 }
1692 }
1693}
1694
1695impl std::error::Error for EditModeError {}
1696
1697fn handle_config_edit(config: &Configuration) -> Result<()> {
1699 println!("\n{}", "配置编辑模式".green().bold());
1700 println!("{}", "===================".green());
1701 println!("正在编辑配置: {}", config.alias_name.cyan().bold());
1702 println!();
1703
1704 let mut editing_config = config.clone();
1706 let original_alias = config.alias_name.clone();
1707
1708 loop {
1709 display_edit_menu(&editing_config);
1711
1712 println!("\n{}", "提示: 可使用大小写字母".dimmed());
1714 print!("请选择要编辑的字段 (1-9, A-B), 或输入 S 保存, Q 返回上一级菜单: ");
1715 io::stdout().flush()?;
1716
1717 let mut input = String::new();
1718 io::stdin().read_line(&mut input)?;
1719 let input = input.trim();
1720
1721 match input {
1723 "1" => edit_field_alias(&mut editing_config)?,
1724 "2" => edit_field_token(&mut editing_config)?,
1725 "3" => edit_field_url(&mut editing_config)?,
1726 "4" => edit_field_model(&mut editing_config)?,
1727 "5" => edit_field_small_fast_model(&mut editing_config)?,
1728 "6" => edit_field_max_thinking_tokens(&mut editing_config)?,
1729 "7" => edit_field_api_timeout_ms(&mut editing_config)?,
1730 "8" => edit_field_claude_code_disable_nonessential_traffic(&mut editing_config)?,
1731 "9" => edit_field_anthropic_default_sonnet_model(&mut editing_config)?,
1732 "10" | "a" | "A" => edit_field_anthropic_default_opus_model(&mut editing_config)?,
1733 "11" | "b" | "B" => edit_field_anthropic_default_haiku_model(&mut editing_config)?,
1734 "s" | "S" => {
1735 return save_configuration_changes(&original_alias, &editing_config);
1737 }
1738 "q" | "Q" => {
1739 println!("\n{}", "返回上一级菜单".blue());
1740 return Err(EditModeError::ReturnToMenu.into());
1741 }
1742 _ => {
1743 println!("{}", "无效选择,请重试".red());
1744 }
1745 }
1746 }
1747}
1748
1749fn display_edit_menu(config: &Configuration) {
1751 println!("\n{}", "当前配置值:".blue().bold());
1752 println!("{}", "─────────────────────────".blue());
1753
1754 println!("1. 别名 (alias_name): {}", config.alias_name.green());
1755
1756 println!(
1757 "2. 令牌 (ANTHROPIC_AUTH_TOKEN): {}",
1758 format_token_for_display(&config.token).green()
1759 );
1760
1761 println!("3. URL (ANTHROPIC_BASE_URL): {}", config.url.green());
1762
1763 println!(
1764 "4. 模型 (ANTHROPIC_MODEL): {}",
1765 config.model.as_deref().unwrap_or("[未设置]").green()
1766 );
1767
1768 println!(
1769 "5. 快速模型 (ANTHROPIC_SMALL_FAST_MODEL): {}",
1770 config
1771 .small_fast_model
1772 .as_deref()
1773 .unwrap_or("[未设置]")
1774 .green()
1775 );
1776
1777 println!(
1778 "6. 最大思考令牌数 (ANTHROPIC_MAX_THINKING_TOKENS): {}",
1779 config
1780 .max_thinking_tokens
1781 .map(|t| t.to_string())
1782 .unwrap_or("[未设置]".to_string())
1783 .green()
1784 );
1785
1786 println!(
1787 "7. API超时时间 (API_TIMEOUT_MS): {}",
1788 config
1789 .api_timeout_ms
1790 .map(|t| t.to_string())
1791 .unwrap_or("[未设置]".to_string())
1792 .green()
1793 );
1794
1795 println!(
1796 "8. 禁用非必要流量 (CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC): {}",
1797 config
1798 .claude_code_disable_nonessential_traffic
1799 .map(|t| t.to_string())
1800 .unwrap_or("[未设置]".to_string())
1801 .green()
1802 );
1803
1804 println!(
1805 "9. 默认 Sonnet 模型 (ANTHROPIC_DEFAULT_SONNET_MODEL): {}",
1806 config
1807 .anthropic_default_sonnet_model
1808 .as_deref()
1809 .unwrap_or("[未设置]")
1810 .green()
1811 );
1812
1813 println!(
1814 "A. 默认 Opus 模型 (ANTHROPIC_DEFAULT_OPUS_MODEL): {}",
1815 config
1816 .anthropic_default_opus_model
1817 .as_deref()
1818 .unwrap_or("[未设置]")
1819 .green()
1820 );
1821
1822 println!(
1823 "B. 默认 Haiku 模型 (ANTHROPIC_DEFAULT_HAIKU_MODEL): {}",
1824 config
1825 .anthropic_default_haiku_model
1826 .as_deref()
1827 .unwrap_or("[未设置]")
1828 .green()
1829 );
1830
1831 println!("{}", "─────────────────────────".blue());
1832 println!(
1833 "S. {} | Q. {}",
1834 "保存更改".green().bold(),
1835 "返回上一级菜单".blue()
1836 );
1837}
1838
1839fn edit_field_alias(config: &mut Configuration) -> Result<()> {
1841 println!("\n编辑别名:");
1842 println!("当前值: {}", config.alias_name.cyan());
1843 print!("新值 (回车保持不变): ");
1844 io::stdout().flush()?;
1845
1846 let mut input = String::new();
1847 io::stdin().read_line(&mut input)?;
1848 let input = input.trim();
1849
1850 if !input.is_empty() {
1851 if input.contains(char::is_whitespace) {
1853 println!("{}", "错误: 别名不能包含空白字符".red());
1854 return Ok(());
1855 }
1856 if input == "cc" {
1857 println!("{}", "错误: 'cc' 是保留名称".red());
1858 return Ok(());
1859 }
1860
1861 config.alias_name = input.to_string();
1862 println!("别名已更新为: {}", input.green());
1863 }
1864 Ok(())
1865}
1866
1867fn edit_field_token(config: &mut Configuration) -> Result<()> {
1869 println!("\n编辑令牌:");
1870 println!("当前值: {}", format_token_for_display(&config.token).cyan());
1871 print!("新值 (回车保持不变): ");
1872 io::stdout().flush()?;
1873
1874 let mut input = String::new();
1875 io::stdin().read_line(&mut input)?;
1876 let input = input.trim();
1877
1878 if !input.is_empty() {
1879 config.token = input.to_string();
1880 println!("{}", "令牌已更新".green());
1881 }
1882 Ok(())
1883}
1884
1885fn edit_field_url(config: &mut Configuration) -> Result<()> {
1887 println!("\n编辑 URL:");
1888 println!("当前值: {}", config.url.cyan());
1889 print!("新值 (回车保持不变): ");
1890 io::stdout().flush()?;
1891
1892 let mut input = String::new();
1893 io::stdin().read_line(&mut input)?;
1894 let input = input.trim();
1895
1896 if !input.is_empty() {
1897 config.url = input.to_string();
1898 println!("URL 已更新为: {}", input.green());
1899 }
1900 Ok(())
1901}
1902
1903fn edit_field_model(config: &mut Configuration) -> Result<()> {
1905 println!("\n编辑模型:");
1906 println!(
1907 "当前值: {}",
1908 config.model.as_deref().unwrap_or("[未设置]").cyan()
1909 );
1910 print!("新值 (回车保持不变,输入空格清除): ");
1911 io::stdout().flush()?;
1912
1913 let mut input = String::new();
1914 io::stdin().read_line(&mut input)?;
1915 let input = input.trim();
1916
1917 if !input.is_empty() {
1918 if input == " " {
1919 config.model = None;
1920 println!("{}", "模型已清除".green());
1921 } else {
1922 config.model = Some(input.to_string());
1923 println!("模型已更新为: {}", input.green());
1924 }
1925 }
1926 Ok(())
1927}
1928
1929fn edit_field_small_fast_model(config: &mut Configuration) -> Result<()> {
1931 println!("\n编辑快速模型:");
1932 println!(
1933 "当前值: {}",
1934 config
1935 .small_fast_model
1936 .as_deref()
1937 .unwrap_or("[未设置]")
1938 .cyan()
1939 );
1940 print!("新值 (回车保持不变,输入空格清除): ");
1941 io::stdout().flush()?;
1942
1943 let mut input = String::new();
1944 io::stdin().read_line(&mut input)?;
1945 let input = input.trim();
1946
1947 if !input.is_empty() {
1948 if input == " " {
1949 config.small_fast_model = None;
1950 println!("{}", "快速模型已清除".green());
1951 } else {
1952 config.small_fast_model = Some(input.to_string());
1953 println!("快速模型已更新为: {}", input.green());
1954 }
1955 }
1956 Ok(())
1957}
1958
1959fn edit_field_max_thinking_tokens(config: &mut Configuration) -> Result<()> {
1961 println!("\n编辑最大思考令牌数:");
1962 println!(
1963 "当前值: {}",
1964 config
1965 .max_thinking_tokens
1966 .map(|t| t.to_string())
1967 .unwrap_or("[未设置]".to_string())
1968 .cyan()
1969 );
1970 print!("新值 (回车保持不变,输入 0 清除): ");
1971 io::stdout().flush()?;
1972
1973 let mut input = String::new();
1974 io::stdin().read_line(&mut input)?;
1975 let input = input.trim();
1976
1977 if !input.is_empty() {
1978 if input == "0" {
1979 config.max_thinking_tokens = None;
1980 println!("{}", "最大思考令牌数已清除".green());
1981 } else if let Ok(tokens) = input.parse::<u32>() {
1982 config.max_thinking_tokens = Some(tokens);
1983 println!("最大思考令牌数已更新为: {}", tokens.to_string().green());
1984 } else {
1985 println!("{}", "错误: 请输入有效的数字".red());
1986 }
1987 }
1988 Ok(())
1989}
1990
1991fn edit_field_api_timeout_ms(config: &mut Configuration) -> Result<()> {
1993 println!("\n编辑 API 超时时间 (毫秒):");
1994 println!(
1995 "当前值: {}",
1996 config
1997 .api_timeout_ms
1998 .map(|t| t.to_string())
1999 .unwrap_or("[未设置]".to_string())
2000 .cyan()
2001 );
2002 print!("新值 (回车保持不变,输入 0 清除): ");
2003 io::stdout().flush()?;
2004
2005 let mut input = String::new();
2006 io::stdin().read_line(&mut input)?;
2007 let input = input.trim();
2008
2009 if !input.is_empty() {
2010 if input == "0" {
2011 config.api_timeout_ms = None;
2012 println!("{}", "API 超时时间已清除".green());
2013 } else if let Ok(timeout) = input.parse::<u32>() {
2014 config.api_timeout_ms = Some(timeout);
2015 println!("API 超时时间已更新为: {}", timeout.to_string().green());
2016 } else {
2017 println!("{}", "错误: 请输入有效的数字".red());
2018 }
2019 }
2020 Ok(())
2021}
2022
2023fn edit_field_claude_code_disable_nonessential_traffic(config: &mut Configuration) -> Result<()> {
2025 println!("\n编辑禁用非必要流量标志:");
2026 println!(
2027 "当前值: {}",
2028 config
2029 .claude_code_disable_nonessential_traffic
2030 .map(|t| t.to_string())
2031 .unwrap_or("[未设置]".to_string())
2032 .cyan()
2033 );
2034 print!("新值 (回车保持不变,输入 0 清除): ");
2035 io::stdout().flush()?;
2036
2037 let mut input = String::new();
2038 io::stdin().read_line(&mut input)?;
2039 let input = input.trim();
2040
2041 if !input.is_empty() {
2042 if input == "0" {
2043 config.claude_code_disable_nonessential_traffic = None;
2044 println!("{}", "禁用非必要流量标志已清除".green());
2045 } else if let Ok(flag) = input.parse::<u32>() {
2046 config.claude_code_disable_nonessential_traffic = Some(flag);
2047 println!("禁用非必要流量标志已更新为: {}", flag.to_string().green());
2048 } else {
2049 println!("{}", "错误: 请输入有效的数字".red());
2050 }
2051 }
2052 Ok(())
2053}
2054
2055fn edit_field_anthropic_default_sonnet_model(config: &mut Configuration) -> Result<()> {
2057 println!("\n编辑默认 Sonnet 模型:");
2058 println!(
2059 "当前值: {}",
2060 config
2061 .anthropic_default_sonnet_model
2062 .as_deref()
2063 .unwrap_or("[未设置]")
2064 .cyan()
2065 );
2066 print!("新值 (回车保持不变,输入空格清除): ");
2067 io::stdout().flush()?;
2068
2069 let mut input = String::new();
2070 io::stdin().read_line(&mut input)?;
2071 let input = input.trim();
2072
2073 if !input.is_empty() {
2074 if input == " " {
2075 config.anthropic_default_sonnet_model = None;
2076 println!("{}", "默认 Sonnet 模型已清除".green());
2077 } else {
2078 config.anthropic_default_sonnet_model = Some(input.to_string());
2079 println!("默认 Sonnet 模型已更新为: {}", input.green());
2080 }
2081 }
2082 Ok(())
2083}
2084
2085fn edit_field_anthropic_default_opus_model(config: &mut Configuration) -> Result<()> {
2087 println!("\n编辑默认 Opus 模型:");
2088 println!(
2089 "当前值: {}",
2090 config
2091 .anthropic_default_opus_model
2092 .as_deref()
2093 .unwrap_or("[未设置]")
2094 .cyan()
2095 );
2096 print!("新值 (回车保持不变,输入空格清除): ");
2097 io::stdout().flush()?;
2098
2099 let mut input = String::new();
2100 io::stdin().read_line(&mut input)?;
2101 let input = input.trim();
2102
2103 if !input.is_empty() {
2104 if input == " " {
2105 config.anthropic_default_opus_model = None;
2106 println!("{}", "默认 Opus 模型已清除".green());
2107 } else {
2108 config.anthropic_default_opus_model = Some(input.to_string());
2109 println!("默认 Opus 模型已更新为: {}", input.green());
2110 }
2111 }
2112 Ok(())
2113}
2114
2115fn edit_field_anthropic_default_haiku_model(config: &mut Configuration) -> Result<()> {
2117 println!("\n编辑默认 Haiku 模型:");
2118 println!(
2119 "当前值: {}",
2120 config
2121 .anthropic_default_haiku_model
2122 .as_deref()
2123 .unwrap_or("[未设置]")
2124 .cyan()
2125 );
2126 print!("新值 (回车保持不变,输入空格清除): ");
2127 io::stdout().flush()?;
2128
2129 let mut input = String::new();
2130 io::stdin().read_line(&mut input)?;
2131 let input = input.trim();
2132
2133 if !input.is_empty() {
2134 if input == " " {
2135 config.anthropic_default_haiku_model = None;
2136 println!("{}", "默认 Haiku 模型已清除".green());
2137 } else {
2138 config.anthropic_default_haiku_model = Some(input.to_string());
2139 println!("默认 Haiku 模型已更新为: {}", input.green());
2140 }
2141 }
2142 Ok(())
2143}
2144
2145fn save_configuration_changes(original_alias: &str, new_config: &Configuration) -> Result<()> {
2147 let mut storage = ConfigStorage::load()?;
2149
2150 if original_alias != new_config.alias_name
2152 && storage.get_configuration(&new_config.alias_name).is_some()
2153 {
2154 println!("\n{}", "别名冲突!".red().bold());
2155 println!("配置 '{}' 已存在", new_config.alias_name.yellow());
2156 print!("是否覆盖现有配置? (y/N): ");
2157 io::stdout().flush()?;
2158
2159 let mut input = String::new();
2160 io::stdin().read_line(&mut input)?;
2161 let input = input.trim().to_lowercase();
2162
2163 if input != "y" && input != "yes" {
2164 println!("{}", "编辑已取消".yellow());
2165 return Ok(());
2166 }
2167 }
2168
2169 storage.update_configuration(original_alias, new_config.clone())?;
2171 storage.save()?;
2172
2173 println!("\n{}", "配置已成功保存!".green().bold());
2174
2175 Ok(())
2176}