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::{ClaudeSettings, ConfigStorage, Configuration};
7use crate::platform::resolve_npm_cli;
8use anyhow::{Context, Result};
9use colored::*;
10use crossterm::{
11 event::{self, Event, KeyCode, KeyEvent, KeyEventKind},
12 execute, terminal,
13};
14use std::io::{self, Write};
15use std::process::Command;
16
17pub(crate) fn char_display_width(c: char) -> usize {
20 match c as u32 {
21 0x00..=0x7F => 1,
22 0x80..=0x2FF => 1,
23 0x2190..=0x21FF => 2,
24 0x3000..=0x303F => 2,
25 0x3040..=0x309F => 2,
26 0x30A0..=0x30FF => 2,
27 0x4E00..=0x9FFF => 2,
28 0xAC00..=0xD7AF => 2,
29 0x3400..=0x4DBF => 2,
30 0xFF01..=0xFF60 => 2,
31 _ => 1,
32 }
33}
34
35pub(crate) fn truncate_text_to_width(text: &str, available_width: usize) -> (String, usize) {
37 let mut current_width = 0;
38 let truncated: String = text
39 .chars()
40 .take_while(|&c| {
41 let char_width = char_display_width(c);
42 if current_width + char_width <= available_width {
43 current_width += char_width;
44 true
45 } else {
46 false
47 }
48 })
49 .collect();
50 let truncated_width = text_display_width(&truncated);
51 (truncated, truncated_width)
52}
53
54pub(crate) fn cleanup_terminal(stdout: &mut io::Stdout) {
56 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
57 let _ = terminal::disable_raw_mode();
58}
59
60pub(crate) struct BorderDrawing {
62 pub unicode_supported: bool,
64}
65
66impl BorderDrawing {
67 pub(crate) fn new() -> Self {
69 let unicode_supported = Self::detect_unicode_support();
70 Self { unicode_supported }
71 }
72
73 fn detect_unicode_support() -> bool {
75 crate::platform::unicode_support_enabled()
76 }
77
78 pub(crate) fn draw_top_border(&self, title: &str, width: usize) -> String {
80 if self.unicode_supported {
81 let title_padded = format!(" {title} ");
82 let title_len = text_display_width(&title_padded);
83
84 if title_len >= width.saturating_sub(2) {
85 format!("╔{}╗", "═".repeat(width.saturating_sub(2)))
87 } else {
88 let inner_width = width.saturating_sub(2); let padding_total = inner_width.saturating_sub(title_len);
90 let padding_left = padding_total / 2;
91 let padding_right = padding_total - padding_left;
92 format!(
93 "╔{}{}{}╗",
94 "═".repeat(padding_left),
95 title_padded,
96 "═".repeat(padding_right)
97 )
98 }
99 } else {
100 let title_padded = format!(" {title} ");
102 let title_len = title_padded.len();
103
104 if title_len >= width.saturating_sub(2) {
105 format!("+{}+", "-".repeat(width.saturating_sub(2)))
106 } else {
107 let inner_width = width.saturating_sub(2);
108 let padding_total = inner_width.saturating_sub(title_len);
109 let padding_left = padding_total / 2;
110 let padding_right = padding_total - padding_left;
111 format!(
112 "+{}{}{}+",
113 "-".repeat(padding_left),
114 title_padded,
115 "-".repeat(padding_right)
116 )
117 }
118 }
119 }
120
121 pub(crate) fn draw_middle_line(&self, text: &str, width: usize) -> String {
123 let text_len = text_display_width(text);
124 let available_width = width.saturating_sub(4);
126
127 let (left_border, right_border) = if self.unicode_supported {
128 ("║", "║")
129 } else {
130 ("|", "|")
131 };
132
133 if text_len > available_width {
134 let (truncated, truncated_width) = truncate_text_to_width(text, available_width);
136 let padding_spaces = available_width.saturating_sub(truncated_width);
137 format!(
138 "{left_border} {}{} {right_border}",
139 truncated,
140 " ".repeat(padding_spaces)
141 )
142 } else {
143 let padded_text = pad_text_to_width(text, available_width, TextAlignment::Left, ' ');
144 format!("{left_border} {padded_text} {right_border}")
145 }
146 }
147
148 pub(crate) fn draw_bottom_border(&self, width: usize) -> String {
150 if self.unicode_supported {
151 format!("╚{}╝", "═".repeat(width - 2))
152 } else {
153 format!("+{}+", "-".repeat(width - 2))
154 }
155 }
156}
157
158pub fn handle_current_command() -> Result<()> {
168 let storage = ConfigStorage::load()?;
169
170 println!("\n{}", "Current Configuration:".green().bold());
171 println!("Environment variable mode: configurations are set per-command execution");
172 println!("Select a configuration from the menu below to launch Claude");
173 println!("Select 'cc' to launch Claude with default settings");
174
175 let raw_mode_enabled = terminal::enable_raw_mode().is_ok();
177
178 if raw_mode_enabled {
179 let mut stdout = io::stdout();
180 if execute!(
181 stdout,
182 terminal::EnterAlternateScreen,
183 terminal::Clear(terminal::ClearType::All)
184 )
185 .is_ok()
186 {
187 let result = handle_main_menu_interactive(&mut stdout, &storage);
189
190 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
192 let _ = terminal::disable_raw_mode();
193
194 return result;
195 } else {
196 let _ = terminal::disable_raw_mode();
198 }
199 }
200
201 handle_main_menu_simple(&storage)
203}
204
205fn handle_main_menu_interactive(stdout: &mut io::Stdout, storage: &ConfigStorage) -> Result<()> {
207 let menu_items = [
208 "Execute claude --dangerously-skip-permissions",
209 "Switch configuration",
210 "Exit",
211 ];
212 let mut selected_index = 0;
213
214 loop {
215 execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
217 execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
218
219 let border = BorderDrawing::new();
221 const MAIN_MENU_WIDTH: usize = 68;
222
223 println!(
224 "\r{}",
225 border.draw_top_border("Main Menu", MAIN_MENU_WIDTH).green()
226 );
227 println!(
228 "\r{}",
229 border
230 .draw_middle_line(
231 "↑↓/jk导航,1-9快选,E-编辑,R-官方,Q-退出,Enter确认,Esc取消",
232 MAIN_MENU_WIDTH
233 )
234 .green()
235 );
236 println!("\r{}", border.draw_bottom_border(MAIN_MENU_WIDTH).green());
237 println!();
238
239 for (index, item) in menu_items.iter().enumerate() {
241 if index == selected_index {
242 println!("\r> {} {}", "●".blue().bold(), item.blue().bold());
243 } else {
244 println!("\r {} {}", "○".dimmed(), item.dimmed());
245 }
246 }
247
248 stdout.flush()?;
250
251 let event = match event::read() {
253 Ok(event) => event,
254 Err(e) => {
255 cleanup_terminal(stdout);
257 return Err(e.into());
258 }
259 };
260
261 match event {
262 Event::Key(KeyEvent {
263 code,
264 kind: KeyEventKind::Press,
265 ..
266 }) => {
267 match code {
268 KeyCode::Up => {
269 selected_index = selected_index.saturating_sub(1);
270 }
271 KeyCode::Down if selected_index < menu_items.len() - 1 => {
272 selected_index += 1;
273 }
274 KeyCode::Down => {}
275 KeyCode::Enter => {
276 cleanup_terminal(stdout);
278
279 return handle_main_menu_action(selected_index, storage);
280 }
281 KeyCode::Esc => {
282 cleanup_terminal(stdout);
284
285 println!("\nExiting...");
286 return Ok(());
287 }
288 _ => {}
289 }
290 }
291 Event::Key(_) => {} _ => {}
293 }
294 }
295}
296
297fn handle_main_menu_simple(storage: &ConfigStorage) -> Result<()> {
299 loop {
300 println!("\n{}", "Available Actions:".blue().bold());
301 println!("1. Execute claude --dangerously-skip-permissions");
302 println!("2. Switch configuration");
303 println!("3. Exit");
304
305 print!("\nPlease select an option (1-3): ");
306 io::stdout().flush().context("Failed to flush stdout")?;
307
308 let mut input = String::new();
309 io::stdin()
310 .read_line(&mut input)
311 .context("Failed to read input")?;
312
313 let choice = input.trim();
314
315 match choice {
316 "1" => return handle_main_menu_action(0, storage),
317 "2" => return handle_main_menu_action(1, storage),
318 "3" => return handle_main_menu_action(2, storage),
319 _ => {
320 println!("Invalid option. Please select 1-3.");
321 }
322 }
323 }
324}
325
326fn handle_main_menu_action(selected_index: usize, storage: &ConfigStorage) -> Result<()> {
328 match selected_index {
329 0 => {
330 println!("\nExecuting: claude --dangerously-skip-permissions");
331 execute_claude_command(true)?;
332 }
333 1 => {
334 handle_interactive_selection(storage)?;
336 }
337 2 => {
338 println!("Exiting...");
339 }
340 _ => {
341 println!("Invalid selection");
342 }
343 }
344 Ok(())
345}
346
347pub fn handle_interactive_selection(storage: &ConfigStorage) -> Result<()> {
355 if storage.configurations.is_empty() {
356 println!("No configurations available. Use 'add' command to create configurations first.");
357 return Ok(());
358 }
359
360 let mut configs: Vec<Configuration> = storage.configurations.values().cloned().collect();
361 configs.sort_by(|a, b| a.alias_name.cmp(&b.alias_name));
362
363 let mut selected_index = 0;
364
365 let raw_mode_enabled = terminal::enable_raw_mode().is_ok();
367
368 if raw_mode_enabled {
369 let mut stdout = io::stdout();
370 if execute!(
371 stdout,
372 terminal::EnterAlternateScreen,
373 terminal::Clear(terminal::ClearType::All)
374 )
375 .is_ok()
376 {
377 let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
379 let result = handle_full_interactive_menu(
380 &mut stdout,
381 &mut configs,
382 &mut selected_index,
383 storage,
384 storage_mode,
385 );
386
387 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
389 let _ = terminal::disable_raw_mode();
390
391 return result;
392 } else {
393 let _ = terminal::disable_raw_mode();
395 }
396 }
397
398 handle_simple_interactive_menu(&configs.iter().collect::<Vec<_>>(), storage)
400}
401
402fn handle_full_interactive_menu(
404 stdout: &mut io::Stdout,
405 configs: &mut Vec<Configuration>,
406 selected_index: &mut usize,
407 storage: &ConfigStorage,
408 storage_mode: crate::config::types::StorageMode,
409) -> Result<()> {
410 if configs.is_empty() {
412 println!("\r{}", "No configurations available".yellow());
413 println!(
414 "\r{}",
415 "Use 'cc-switch add <alias> <token> <url>' to add configurations first.".dimmed()
416 );
417 println!("\r{}", "Press any key to continue...".dimmed());
418 let _ = event::read(); return Ok(());
420 }
421
422 const PAGE_SIZE: usize = 9; let total_pages = if configs.len() <= PAGE_SIZE {
426 1
427 } else {
428 configs.len().div_ceil(PAGE_SIZE)
429 };
430 let mut current_page = 0;
431
432 loop {
433 let start_idx = current_page * PAGE_SIZE;
435 let end_idx = std::cmp::min(start_idx + PAGE_SIZE, configs.len());
436 let page_configs = &configs[start_idx..end_idx];
437
438 execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
440 execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
441
442 let border = BorderDrawing::new();
444 const CONFIG_MENU_WIDTH: usize = 80;
447
448 println!(
449 "\r{}",
450 border
451 .draw_top_border("Select Configuration", CONFIG_MENU_WIDTH)
452 .green()
453 );
454 if total_pages > 1 {
455 println!(
456 "\r{}",
457 border
458 .draw_middle_line(
459 &format!("第 {} 页,共 {} 页", current_page + 1, total_pages),
460 CONFIG_MENU_WIDTH
461 )
462 .green()
463 );
464 println!(
465 "\r{}",
466 border
467 .draw_middle_line(
468 "↑↓/jk导航,1-9快选,E-编辑,N/P翻页,R-官方,Q-退出,Enter确认",
469 CONFIG_MENU_WIDTH
470 )
471 .green()
472 );
473 } else {
474 println!(
475 "\r{}",
476 border
477 .draw_middle_line(
478 "↑↓/jk导航,1-9快选,E-编辑,R-官方,Q-退出,Enter确认,Esc取消",
479 CONFIG_MENU_WIDTH
480 )
481 .green()
482 );
483 }
484 println!("\r{}", border.draw_bottom_border(CONFIG_MENU_WIDTH).green());
485 println!();
486
487 let official_index = 0;
489 if *selected_index == official_index {
490 println!(
491 "\r> {} {} {}",
492 "●".red().bold(),
493 "[R]".red().bold(),
494 "official".red().bold()
495 );
496 println!("\r Use official Claude API (no custom configuration)");
497 println!();
498 } else {
499 println!("\r {} {} {}", "○".red(), "[R]".red(), "official".red());
500 }
501
502 for (page_index, config) in page_configs.iter().enumerate() {
504 let actual_config_index = start_idx + page_index;
505 let display_number = page_index + 1; let actual_index = actual_config_index + 1; let number_label = format!("[{display_number}]");
508
509 if *selected_index == actual_index {
510 println!(
511 "\r> {} {} {}",
512 "●".blue().bold(),
513 number_label.blue().bold(),
514 config.alias_name.blue().bold()
515 );
516
517 let details = format_config_details(config, "\r ", false);
519 for detail_line in details {
520 println!("{detail_line}");
521 }
522 println!();
523 } else {
524 println!(
525 "\r {} {} {}",
526 "○".dimmed(),
527 number_label.dimmed(),
528 config.alias_name.dimmed()
529 );
530 }
531 }
532
533 let exit_index = configs.len() + 1;
535 if *selected_index == exit_index {
536 println!(
537 "\r> {} {} {}",
538 "●".yellow().bold(),
539 "[Q]".yellow().bold(),
540 "Exit".yellow().bold()
541 );
542 println!("\r Exit without making changes");
543 println!();
544 } else {
545 println!(
546 "\r {} {} {}",
547 "○".dimmed(),
548 "[Q]".dimmed(),
549 "Exit".dimmed()
550 );
551 }
552
553 if total_pages > 1 {
555 println!(
556 "\r{}",
557 format!(
558 "Page Navigation: [N]ext, [P]revious (第 {} 页,共 {} 页)",
559 current_page + 1,
560 total_pages
561 )
562 .dimmed()
563 );
564 }
565
566 stdout.flush()?;
568
569 let event = match event::read() {
571 Ok(event) => event,
572 Err(e) => {
573 cleanup_terminal(stdout);
575 return Err(e.into());
576 }
577 };
578
579 match event {
580 Event::Key(KeyEvent {
581 code,
582 kind: KeyEventKind::Press,
583 ..
584 }) => match code {
585 KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => {
586 *selected_index = selected_index.saturating_sub(1);
587 }
588 KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J')
589 if *selected_index < configs.len() + 1 =>
590 {
591 *selected_index += 1;
592 }
593 KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => {}
594 KeyCode::PageDown | KeyCode::Char('n') | KeyCode::Char('N')
595 if total_pages > 1 && current_page < total_pages - 1 =>
596 {
597 current_page += 1;
598 let new_page_start_idx = current_page * PAGE_SIZE;
599 *selected_index = new_page_start_idx + 1;
600 }
601 KeyCode::PageDown | KeyCode::Char('n') | KeyCode::Char('N') => {}
602 KeyCode::PageUp | KeyCode::Char('p') | KeyCode::Char('P')
603 if total_pages > 1 && current_page > 0 =>
604 {
605 current_page -= 1;
606 let new_page_start_idx = current_page * PAGE_SIZE;
607 *selected_index = new_page_start_idx + 1;
608 }
609 KeyCode::PageUp | KeyCode::Char('p') | KeyCode::Char('P') => {}
610 KeyCode::Enter => {
611 cleanup_terminal(stdout);
613
614 return handle_selection_action(
615 &configs.iter().collect::<Vec<_>>(),
616 *selected_index,
617 storage,
618 storage_mode,
619 );
620 }
621 KeyCode::Esc => {
622 cleanup_terminal(stdout);
624
625 println!("\nSelection cancelled");
626 return Ok(());
627 }
628 KeyCode::Char(c) if c.is_ascii_digit() => {
629 let digit = c.to_digit(10).unwrap() as usize;
630 if digit >= 1 && digit <= page_configs.len() {
632 let actual_config_index = start_idx + (digit - 1);
633 let selection_index = actual_config_index + 1; cleanup_terminal(stdout);
637
638 return handle_selection_action(
639 &configs.iter().collect::<Vec<_>>(),
640 selection_index,
641 storage,
642 storage_mode,
643 );
644 }
645 }
647 KeyCode::Char('r') | KeyCode::Char('R') => {
648 cleanup_terminal(stdout);
650
651 return handle_selection_action(
652 &configs.iter().collect::<Vec<_>>(),
653 0,
654 storage,
655 storage_mode,
656 );
657 }
658 KeyCode::Char('e') | KeyCode::Char('E')
659 if *selected_index > 0 && *selected_index <= configs.len() =>
660 {
661 cleanup_terminal(stdout);
662 let config_index = *selected_index - 1;
663 let edit_result = handle_config_edit(&configs[config_index]);
664 if execute!(
665 stdout,
666 terminal::EnterAlternateScreen,
667 terminal::Clear(terminal::ClearType::All)
668 )
669 .is_ok()
670 && terminal::enable_raw_mode().is_ok()
671 {
672 match edit_result {
673 Ok(_) => {
674 if let Ok(reloaded_storage) = ConfigStorage::load() {
675 *configs =
676 reloaded_storage.configurations.values().cloned().collect();
677 configs.sort_by(|a, b| a.alias_name.cmp(&b.alias_name));
678 if *selected_index > configs.len() + 1 {
679 *selected_index = configs.len() + 1;
680 }
681 }
682 continue;
683 }
684 Err(e) => {
685 if e.downcast_ref::<EditModeError>()
686 == Some(&EditModeError::ReturnToMenu)
687 {
688 continue;
689 }
690 cleanup_terminal(stdout);
691 return Err(e);
692 }
693 }
694 }
695 }
696 KeyCode::Char('e') | KeyCode::Char('E') => {}
697 KeyCode::Char('q') | KeyCode::Char('Q') => {
698 cleanup_terminal(stdout);
700
701 return handle_selection_action(
702 &configs.iter().collect::<Vec<_>>(),
703 configs.len() + 1,
704 storage,
705 storage_mode,
706 );
707 }
708 _ => {}
709 },
710 Event::Key(_) => {} _ => {}
712 }
713 }
714}
715
716fn handle_simple_interactive_menu(
718 configs: &[&Configuration],
719 storage: &ConfigStorage,
720) -> Result<()> {
721 const PAGE_SIZE: usize = 9; if configs.len() <= PAGE_SIZE {
725 return handle_simple_single_page_menu(configs, storage);
726 }
727
728 let total_pages = configs.len().div_ceil(PAGE_SIZE);
730 let mut current_page = 0;
731
732 loop {
733 let start_idx = current_page * PAGE_SIZE;
735 let end_idx = std::cmp::min(start_idx + PAGE_SIZE, configs.len());
736 let page_configs = &configs[start_idx..end_idx];
737
738 println!("\n{}", "Available Configurations:".blue().bold());
739 if total_pages > 1 {
740 println!("第 {} 页,共 {} 页", current_page + 1, total_pages);
741 println!("使用 'n' 下一页, 'p' 上一页, 'r' 官方配置, 'q' 退出");
742 }
743 println!();
744
745 println!("{} {}", "[r]".red().bold(), "official".red());
747 println!(" Use official Claude API (no custom configuration)");
748 println!();
749
750 for (page_index, config) in page_configs.iter().enumerate() {
752 let display_number = page_index + 1;
753
754 println!(
755 "{}. {}",
756 format!("[{display_number}]").green().bold(),
757 config.alias_name.green()
758 );
759
760 let details = format_config_details(config, " ", true);
762 for detail_line in details {
763 println!("{detail_line}");
764 }
765 println!();
766 }
767
768 println!("{} {}", "[q]".yellow().bold(), "Exit".yellow());
770
771 if total_pages > 1 {
772 println!(
773 "\n页面导航: [n]下页, [p]上页 | 配置选择: [1-{}] | [e]编辑 | [r]官方 | [q]退出",
774 page_configs.len()
775 );
776 }
777
778 print!("\n请输入选择: ");
779 io::stdout().flush()?;
780
781 let mut input = String::new();
782 io::stdin().read_line(&mut input)?;
783 let choice = input.trim().to_lowercase();
784
785 match choice.as_str() {
786 "r" => {
787 println!("Using official Claude configuration");
789
790 let mut settings = crate::config::types::ClaudeSettings::load(
792 storage.get_claude_settings_dir().map(|s| s.as_str()),
793 )?;
794 settings.remove_anthropic_env();
795 settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
796
797 return launch_claude_with_env(EnvironmentConfig::empty(), None, None, false);
798 }
799 "e" => {
800 println!("编辑功能在交互式菜单中可用");
803 }
804 "q" => {
805 println!("Exiting...");
806 return Ok(());
807 }
808 "n" if total_pages > 1 && current_page < total_pages - 1 => {
809 current_page += 1;
810 continue;
811 }
812 "p" if total_pages > 1 && current_page > 0 => {
813 current_page -= 1;
814 continue;
815 }
816 digit_str => {
817 if let Ok(digit) = digit_str.parse::<usize>()
818 && digit >= 1
819 && digit <= page_configs.len()
820 {
821 let actual_config_index = start_idx + (digit - 1);
822 let selection_index = actual_config_index + 1; let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
824 return handle_selection_action(
825 configs,
826 selection_index,
827 storage,
828 storage_mode,
829 );
830 }
831 println!("无效选择,请重新输入");
832 }
833 }
834 }
835}
836
837fn handle_simple_single_page_menu(
839 configs: &[&Configuration],
840 storage: &ConfigStorage,
841) -> Result<()> {
842 println!("\n{}", "Available Configurations:".blue().bold());
843
844 println!("1. {}", "official".red());
846 println!(" Use official Claude API (no custom configuration)");
847 println!();
848
849 for (index, config) in configs.iter().enumerate() {
850 println!(
851 "{}. {}",
852 index + 2, config.alias_name.green()
854 );
855
856 let details = format_config_details(config, " ", true);
858 for detail_line in details {
859 println!("{detail_line}");
860 }
861 println!();
862 }
863
864 println!("{}. {}", configs.len() + 2, "Exit".yellow());
865
866 print!("\nSelect configuration (1-{}): ", configs.len() + 2);
867 io::stdout().flush()?;
868
869 let mut input = String::new();
870 io::stdin().read_line(&mut input)?;
871
872 match input.trim().parse::<usize>() {
873 Ok(1) => {
874 println!("Using official Claude configuration");
876
877 let mut settings = crate::config::types::ClaudeSettings::load(
879 storage.get_claude_settings_dir().map(|s| s.as_str()),
880 )?;
881 settings.remove_anthropic_env();
882 settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
883
884 launch_claude_with_env(EnvironmentConfig::empty(), None, None, false)
885 }
886 Ok(num) if num >= 2 && num <= configs.len() + 1 => {
887 let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
888 handle_selection_action(configs, num - 1, storage, storage_mode) }
890 Ok(num) if num == configs.len() + 2 => {
891 println!("Exiting...");
892 Ok(())
893 }
894 _ => {
895 println!("Invalid selection");
896 Ok(())
897 }
898 }
899}
900
901fn handle_selection_action(
903 configs: &[&Configuration],
904 selected_index: usize,
905 storage: &ConfigStorage,
906 storage_mode: crate::config::types::StorageMode,
907) -> Result<()> {
908 if selected_index == 0 {
909 println!("\nUsing official Claude configuration");
911
912 let mut settings = crate::config::types::ClaudeSettings::load(
914 storage.get_claude_settings_dir().map(|s| s.as_str()),
915 )?;
916 settings.remove_anthropic_env();
917 settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
918
919 launch_claude_with_env(
920 EnvironmentConfig::empty().with_alias("official"),
921 None,
922 None,
923 false,
924 )
925 } else if selected_index <= configs.len() {
926 let config_index = selected_index - 1; let mut selected_config = configs[config_index].clone();
929
930 let original_url = selected_config.url.clone();
932 match crate::daemon::try_resolve_proxy(&selected_config.url) {
933 crate::daemon::ProxyResolution::Proxied { proxy_url } => {
934 selected_config.url = proxy_url;
935 }
936 crate::daemon::ProxyResolution::Direct => {
937 if !original_url.is_empty() {
938 eprintln!(
939 "\u{2139} cc daemon is not running \u{2014} traffic for '{}' will NOT be captured.",
940 selected_config.alias_name
941 );
942 eprintln!(" Run `cc-switch daemon start` and re-run to enable capture.");
943 }
944 }
945 }
946
947 let env_config = EnvironmentConfig::from_config(&selected_config)
948 .with_alias(&selected_config.alias_name);
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 if selected_config.url != original_url {
961 println!(" (proxied from: {})", original_url);
962 }
963
964 let mut settings = crate::config::types::ClaudeSettings::load(
966 storage.get_claude_settings_dir().map(|s| s.as_str()),
967 )?;
968 settings.switch_to_config_with_mode(
969 &selected_config,
970 storage_mode,
971 storage.get_claude_settings_dir().map(|s| s.as_str()),
972 )?;
973
974 launch_claude_with_env(env_config, None, None, false)
975 } else {
976 println!("\nExiting...");
978 Ok(())
979 }
980}
981
982pub fn launch_claude_with_env(
984 env_config: EnvironmentConfig,
985 prompt: Option<&str>,
986 resume: Option<&str>,
987 continue_session: bool,
988) -> Result<()> {
989 println!("\nLaunching Claude CLI...");
990
991 let _ = ClaudeSettings::cleanup_orphan_alias_files();
993
994 if let Some(alias) = env_config.env_vars.get("CC_SWITCH_CURRENT_ALIAS") {
998 ClaudeSettings::write_current_alias_for_pid(alias)?;
999 }
1000
1001 #[cfg(unix)]
1003 {
1004 use std::os::unix::process::CommandExt;
1005 let mut command = Command::new(resolve_npm_cli("claude"));
1006 command.envs(env_config.as_env_tuples());
1008 command.arg("--dangerously-skip-permissions");
1009 if let Some(session_id) = resume {
1010 command.args(["--resume", session_id]);
1011 }
1012 if continue_session {
1013 command.arg("--continue");
1014 }
1015 if let Some(p) = prompt {
1016 command.arg(p);
1017 }
1018 let error = command.exec();
1019 let _ = ClaudeSettings::clear_current_alias_for_pid();
1022 anyhow::bail!("Failed to exec claude: {}", error);
1023 }
1024
1025 #[cfg(not(unix))]
1027 {
1028 use std::process::Stdio;
1029 let mut command = Command::new(resolve_npm_cli("claude"));
1030 command.envs(env_config.as_env_tuples());
1032 command.arg("--dangerously-skip-permissions");
1033 if let Some(session_id) = resume {
1034 command.args(["--resume", session_id]);
1035 }
1036 if continue_session {
1037 command.arg("--continue");
1038 }
1039 if let Some(p) = prompt {
1040 command.arg(p);
1041 }
1042 command
1043 .stdin(Stdio::inherit())
1044 .stdout(Stdio::inherit())
1045 .stderr(Stdio::inherit());
1046
1047 let mut child = command.spawn().context(
1048 "Failed to launch Claude CLI. Make sure 'claude' command is available in PATH",
1049 )?;
1050
1051 let status = child.wait()?;
1052
1053 let _ = ClaudeSettings::clear_current_alias_for_pid();
1055
1056 if !status.success() {
1057 anyhow::bail!("Claude CLI exited with error status: {}", status);
1058 }
1059 Ok(())
1060 }
1061}
1062
1063fn execute_claude_command(skip_permissions: bool) -> Result<()> {
1068 println!("Launching Claude CLI...");
1069
1070 #[cfg(unix)]
1072 {
1073 use std::os::unix::process::CommandExt;
1074 let mut command = Command::new(resolve_npm_cli("claude"));
1075 if skip_permissions {
1076 command.arg("--dangerously-skip-permissions");
1077 }
1078
1079 let error = command.exec();
1080 anyhow::bail!("Failed to exec claude: {}", error);
1082 }
1083
1084 #[cfg(not(unix))]
1086 {
1087 use std::process::Stdio;
1088 let mut command = Command::new(resolve_npm_cli("claude"));
1089 if skip_permissions {
1090 command.arg("--dangerously-skip-permissions");
1091 }
1092
1093 command
1094 .stdin(Stdio::inherit())
1095 .stdout(Stdio::inherit())
1096 .stderr(Stdio::inherit());
1097
1098 let mut child = command.spawn().context(
1099 "Failed to launch Claude CLI. Make sure 'claude' command is available in PATH",
1100 )?;
1101
1102 let status = child
1103 .wait()
1104 .context("Failed to wait for Claude CLI process")?;
1105
1106 if !status.success() {
1107 anyhow::bail!("Claude CLI exited with error status: {}", status);
1108 }
1109 Ok(())
1110 }
1111}
1112
1113pub fn read_input(prompt: &str) -> Result<String> {
1121 print!("{prompt}");
1122 io::stdout().flush().context("Failed to flush stdout")?;
1123 let mut input = String::new();
1124 io::stdin()
1125 .read_line(&mut input)
1126 .context("Failed to read input")?;
1127 Ok(input.trim().to_string())
1128}
1129
1130pub fn read_sensitive_input(prompt: &str) -> Result<String> {
1138 print!("{prompt}");
1139 io::stdout().flush().context("Failed to flush stdout")?;
1140 let mut input = String::new();
1141 io::stdin()
1142 .read_line(&mut input)
1143 .context("Failed to read input")?;
1144 Ok(input.trim().to_string())
1145}
1146
1147fn format_config_details(config: &Configuration, indent: &str, _compact: bool) -> Vec<String> {
1160 let mut lines = Vec::new();
1161
1162 let terminal_width = get_terminal_width();
1164 let _available_width = terminal_width.saturating_sub(text_display_width(indent) + 8);
1165
1166 let token_label = "Token:";
1168 let url_label = "URL:";
1169 let model_label = "Model:";
1170 let small_model_label = "Small Fast Model:";
1171 let max_thinking_tokens_label = "Max Thinking Tokens:";
1172 let api_timeout_ms_label = "API Timeout (ms):";
1173 let disable_nonessential_traffic_label = "Disable Nonessential Traffic:";
1174 let default_sonnet_model_label = "Default Sonnet Model:";
1175 let default_opus_model_label = "Default Opus Model:";
1176 let default_haiku_model_label = "Default Haiku Model:";
1177 let subagent_model_label = "Subagent Model:";
1178 let disable_nonstreaming_fallback_label = "Disable Nonstreaming Fallback:";
1179 let effort_level_label = "Effort Level:";
1180 let disable_prompt_caching_label = "Disable Prompt Caching:";
1181 let disable_experimental_betas_label = "Disable Experimental Betas:";
1182 let disable_autoupdater_label = "Disable Auto-Updater:";
1183
1184 let max_label_width = [
1186 token_label,
1187 url_label,
1188 model_label,
1189 small_model_label,
1190 max_thinking_tokens_label,
1191 api_timeout_ms_label,
1192 disable_nonessential_traffic_label,
1193 default_sonnet_model_label,
1194 default_opus_model_label,
1195 default_haiku_model_label,
1196 subagent_model_label,
1197 disable_nonstreaming_fallback_label,
1198 effort_level_label,
1199 disable_prompt_caching_label,
1200 disable_experimental_betas_label,
1201 disable_autoupdater_label,
1202 ]
1203 .iter()
1204 .map(|label| text_display_width(label))
1205 .max()
1206 .unwrap_or(0);
1207
1208 let token_line = format!(
1210 "{}{} {}",
1211 indent,
1212 pad_text_to_width(token_label, max_label_width, TextAlignment::Left, ' '),
1213 format_token_for_display(&config.token).dimmed()
1214 );
1215 lines.push(token_line);
1216
1217 let url_line = format!(
1219 "{}{} {}",
1220 indent,
1221 pad_text_to_width(url_label, max_label_width, TextAlignment::Left, ' '),
1222 config.url.cyan()
1223 );
1224 lines.push(url_line);
1225
1226 if let Some(model) = &config.model {
1228 let model_line = format!(
1229 "{}{} {}",
1230 indent,
1231 pad_text_to_width(model_label, max_label_width, TextAlignment::Left, ' '),
1232 model.yellow()
1233 );
1234 lines.push(model_line);
1235 }
1236
1237 if let Some(small_fast_model) = &config.small_fast_model {
1239 let small_model_line = format!(
1240 "{}{} {}",
1241 indent,
1242 pad_text_to_width(small_model_label, max_label_width, TextAlignment::Left, ' '),
1243 small_fast_model.yellow()
1244 );
1245 lines.push(small_model_line);
1246 }
1247
1248 if let Some(max_thinking_tokens) = config.max_thinking_tokens {
1250 let tokens_line = format!(
1251 "{}{} {}",
1252 indent,
1253 pad_text_to_width(
1254 max_thinking_tokens_label,
1255 max_label_width,
1256 TextAlignment::Left,
1257 ' '
1258 ),
1259 format!("{}", max_thinking_tokens).yellow()
1260 );
1261 lines.push(tokens_line);
1262 }
1263
1264 if let Some(api_timeout_ms) = config.api_timeout_ms {
1266 let timeout_line = format!(
1267 "{}{} {}",
1268 indent,
1269 pad_text_to_width(
1270 api_timeout_ms_label,
1271 max_label_width,
1272 TextAlignment::Left,
1273 ' '
1274 ),
1275 format!("{}", api_timeout_ms).yellow()
1276 );
1277 lines.push(timeout_line);
1278 }
1279
1280 if let Some(disable_flag) = config.claude_code_disable_nonessential_traffic {
1282 let flag_line = format!(
1283 "{}{} {}",
1284 indent,
1285 pad_text_to_width(
1286 disable_nonessential_traffic_label,
1287 max_label_width,
1288 TextAlignment::Left,
1289 ' '
1290 ),
1291 format!("{}", disable_flag).yellow()
1292 );
1293 lines.push(flag_line);
1294 }
1295
1296 if let Some(sonnet_model) = &config.anthropic_default_sonnet_model {
1298 let sonnet_line = format!(
1299 "{}{} {}",
1300 indent,
1301 pad_text_to_width(
1302 default_sonnet_model_label,
1303 max_label_width,
1304 TextAlignment::Left,
1305 ' '
1306 ),
1307 sonnet_model.yellow()
1308 );
1309 lines.push(sonnet_line);
1310 }
1311
1312 if let Some(opus_model) = &config.anthropic_default_opus_model {
1314 let opus_line = format!(
1315 "{}{} {}",
1316 indent,
1317 pad_text_to_width(
1318 default_opus_model_label,
1319 max_label_width,
1320 TextAlignment::Left,
1321 ' '
1322 ),
1323 opus_model.yellow()
1324 );
1325 lines.push(opus_line);
1326 }
1327
1328 if let Some(haiku_model) = &config.anthropic_default_haiku_model {
1330 let haiku_line = format!(
1331 "{}{} {}",
1332 indent,
1333 pad_text_to_width(
1334 default_haiku_model_label,
1335 max_label_width,
1336 TextAlignment::Left,
1337 ' '
1338 ),
1339 haiku_model.yellow()
1340 );
1341 lines.push(haiku_line);
1342 }
1343
1344 if let Some(subagent_model) = &config.claude_code_subagent_model {
1346 let subagent_line = format!(
1347 "{}{} {}",
1348 indent,
1349 pad_text_to_width(
1350 subagent_model_label,
1351 max_label_width,
1352 TextAlignment::Left,
1353 ' '
1354 ),
1355 subagent_model.yellow()
1356 );
1357 lines.push(subagent_line);
1358 }
1359
1360 if let Some(disable_flag) = config.claude_code_disable_nonstreaming_fallback {
1362 let flag_line = format!(
1363 "{}{} {}",
1364 indent,
1365 pad_text_to_width(
1366 disable_nonstreaming_fallback_label,
1367 max_label_width,
1368 TextAlignment::Left,
1369 ' '
1370 ),
1371 format!("{}", disable_flag).yellow()
1372 );
1373 lines.push(flag_line);
1374 }
1375
1376 if let Some(effort_level) = &config.claude_code_effort_level {
1378 let effort_line = format!(
1379 "{}{} {}",
1380 indent,
1381 pad_text_to_width(
1382 effort_level_label,
1383 max_label_width,
1384 TextAlignment::Left,
1385 ' '
1386 ),
1387 effort_level.yellow()
1388 );
1389 lines.push(effort_line);
1390 }
1391
1392 if let Some(disable_flag) = config.disable_prompt_caching {
1394 let flag_line = format!(
1395 "{}{} {}",
1396 indent,
1397 pad_text_to_width(
1398 disable_prompt_caching_label,
1399 max_label_width,
1400 TextAlignment::Left,
1401 ' '
1402 ),
1403 format!("{}", disable_flag).yellow()
1404 );
1405 lines.push(flag_line);
1406 }
1407
1408 if let Some(disable_flag) = config.claude_code_disable_experimental_betas {
1410 let flag_line = format!(
1411 "{}{} {}",
1412 indent,
1413 pad_text_to_width(
1414 disable_experimental_betas_label,
1415 max_label_width,
1416 TextAlignment::Left,
1417 ' '
1418 ),
1419 format!("{}", disable_flag).yellow()
1420 );
1421 lines.push(flag_line);
1422 }
1423
1424 if let Some(disable_flag) = config.disable_autoupdater {
1426 let flag_line = format!(
1427 "{}{} {}",
1428 indent,
1429 pad_text_to_width(
1430 disable_autoupdater_label,
1431 max_label_width,
1432 TextAlignment::Left,
1433 ' '
1434 ),
1435 format!("{}", disable_flag).yellow()
1436 );
1437 lines.push(flag_line);
1438 }
1439
1440 lines
1441}
1442
1443#[cfg(test)]
1444mod border_drawing_tests {
1445 use super::*;
1446
1447 #[test]
1448 fn test_border_drawing_unicode_support() {
1449 let _border = BorderDrawing::new();
1450 }
1452
1453 #[test]
1454 fn test_border_drawing_top_border() {
1455 let border = BorderDrawing {
1456 unicode_supported: true,
1457 };
1458 let result = border.draw_top_border("Test", 20);
1459 assert!(!result.is_empty());
1460 assert!(result.contains("Test"));
1461 }
1462
1463 #[test]
1464 fn test_border_drawing_ascii_fallback() {
1465 let border = BorderDrawing {
1466 unicode_supported: false,
1467 };
1468 let result = border.draw_top_border("Test", 20);
1469 assert!(!result.is_empty());
1470 assert!(result.contains("Test"));
1471 assert!(result.contains("+"));
1472 assert!(result.contains("-"));
1473 }
1474
1475 #[test]
1476 fn test_border_drawing_middle_line() {
1477 let border = BorderDrawing {
1478 unicode_supported: true,
1479 };
1480 let result = border.draw_middle_line("Test message", 30);
1481 assert!(!result.is_empty());
1482 assert!(result.contains("Test message"));
1483 }
1484
1485 #[test]
1486 fn test_border_drawing_bottom_border() {
1487 let border = BorderDrawing {
1488 unicode_supported: true,
1489 };
1490 let result = border.draw_bottom_border(20);
1491 assert!(!result.is_empty());
1492 }
1493
1494 #[test]
1495 fn test_border_drawing_width_consistency() {
1496 let border = BorderDrawing {
1497 unicode_supported: true,
1498 };
1499 let width = 30;
1500 let top = border.draw_top_border("Title", width);
1501 let middle = border.draw_middle_line("Content", width);
1502 let bottom = border.draw_bottom_border(width);
1503
1504 assert!(top.chars().count() >= width - 2);
1506 assert!(middle.chars().count() >= width - 2);
1507 assert!(bottom.chars().count() >= width - 2);
1508 }
1509}
1510
1511#[cfg(test)]
1512mod pagination_tests {
1513
1514 #[test]
1516 fn test_pagination_calculation() {
1517 const PAGE_SIZE: usize = 9;
1518
1519 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); }
1530
1531 #[test]
1533 fn test_page_range_calculation() {
1534 const PAGE_SIZE: usize = 9;
1535
1536 let current_page = 0;
1538 let start_idx = current_page * PAGE_SIZE; let end_idx = std::cmp::min(start_idx + PAGE_SIZE, 15); assert_eq!(start_idx, 0);
1541 assert_eq!(end_idx, 9);
1542 assert_eq!(end_idx - start_idx, 9); let current_page = 1;
1546 let start_idx = current_page * PAGE_SIZE; let end_idx = std::cmp::min(start_idx + PAGE_SIZE, 15); assert_eq!(start_idx, 9);
1549 assert_eq!(end_idx, 15);
1550 assert_eq!(end_idx - start_idx, 6); let current_page = 0;
1554 let start_idx = current_page * PAGE_SIZE; let end_idx = std::cmp::min(start_idx + PAGE_SIZE, PAGE_SIZE); assert_eq!(start_idx, 0);
1557 assert_eq!(end_idx, 9);
1558 assert_eq!(end_idx - start_idx, 9); }
1560
1561 #[test]
1563 fn test_digit_mapping_to_config_index() {
1564 const PAGE_SIZE: usize = 9;
1565
1566 let current_page = 0;
1568 let start_idx = current_page * PAGE_SIZE; let digit = 1;
1572 let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 0);
1574
1575 let digit = 9;
1577 let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 8);
1579
1580 let current_page = 1;
1582 let start_idx = current_page * PAGE_SIZE; let digit = 1;
1586 let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 9);
1588
1589 let digit = 5;
1591 let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 13);
1593 }
1594
1595 #[test]
1597 fn test_selection_index_conversion() {
1598 const PAGE_SIZE: usize = 9;
1605
1606 let current_page = 0;
1608 let start_idx = current_page * PAGE_SIZE; let digit = 1;
1610 let actual_config_index = start_idx + (digit - 1); let selection_index = actual_config_index + 1; assert_eq!(selection_index, 1);
1613
1614 let current_page = 1;
1616 let start_idx = current_page * PAGE_SIZE; let digit = 1;
1618 let actual_config_index = start_idx + (digit - 1); let selection_index = actual_config_index + 1; assert_eq!(selection_index, 10);
1621 }
1622
1623 #[test]
1625 fn test_page_navigation_bounds() {
1626 const PAGE_SIZE: usize = 9;
1627 let total_configs: usize = 25; let total_pages = total_configs.div_ceil(PAGE_SIZE); assert_eq!(total_pages, 3);
1630
1631 let mut current_page = 0;
1633 if current_page > 0 {
1634 current_page -= 1;
1635 }
1636 assert_eq!(current_page, 0); let mut current_page = total_pages - 1; if current_page < total_pages - 1 {
1641 current_page += 1;
1642 }
1643 assert_eq!(current_page, 2); let mut current_page = 1;
1647
1648 if current_page < total_pages - 1 {
1650 current_page += 1;
1651 }
1652 assert_eq!(current_page, 2);
1653
1654 if current_page > 0 {
1656 current_page = current_page.saturating_sub(1);
1657 }
1658 assert_eq!(current_page, 1);
1659 }
1660
1661 #[test]
1663 fn test_digit_key_boundary_conditions() {
1664 const PAGE_SIZE: usize = 9;
1665
1666 let digit = 0;
1668 assert!(digit < 1, "Digit 0 should be less than 1 and ignored");
1669
1670 let configs_len = 5; let page_configs_len = std::cmp::min(PAGE_SIZE, configs_len); let digit = 9; assert!(
1675 digit > page_configs_len,
1676 "Digit 9 should be beyond available configs (5) and ignored"
1677 );
1678
1679 for digit in 1..=page_configs_len {
1681 assert!(
1682 digit >= 1 && digit <= page_configs_len,
1683 "Digit {} should be valid",
1684 digit
1685 );
1686 }
1687 }
1688
1689 #[test]
1691 fn test_empty_configs_handling() {
1692 let empty_configs: Vec<String> = Vec::new();
1693 assert!(
1694 empty_configs.is_empty(),
1695 "Empty config list should be properly detected"
1696 );
1697
1698 let configs_len = empty_configs.len(); assert_eq!(configs_len, 0, "Empty configs should have length 0");
1701
1702 }
1705
1706 #[test]
1708 fn test_page_navigation_boundaries() {
1709 const PAGE_SIZE: usize = 9;
1710 let total_configs: usize = 20; let total_pages = total_configs.div_ceil(PAGE_SIZE); let mut current_page = 0;
1715 let original_page = current_page;
1716
1717 if current_page > 0 {
1719 current_page -= 1;
1720 }
1721 assert_eq!(
1722 current_page, original_page,
1723 "First page should not navigate to previous"
1724 );
1725
1726 let mut current_page = total_pages - 1; let original_page = current_page;
1729
1730 if current_page < total_pages - 1 {
1732 current_page += 1;
1733 }
1734 assert_eq!(
1735 current_page, original_page,
1736 "Last page should not navigate to next"
1737 );
1738
1739 let mut current_page = 1; if current_page < total_pages - 1 {
1744 current_page += 1;
1745 }
1746 assert_eq!(current_page, 2, "Should navigate to next page");
1747
1748 if current_page > 0 {
1750 current_page = current_page.saturating_sub(1);
1751 }
1752 assert_eq!(current_page, 1, "Should navigate to previous page");
1753 }
1754
1755 #[test]
1757 fn test_j_key_navigation() {
1758 let mut selected_index: usize = 0;
1759 let configs_len = 5; if selected_index < configs_len + 1 {
1764 selected_index += 1;
1765 }
1766 assert_eq!(selected_index, 1, "j key should move selection down by one");
1767
1768 selected_index = configs_len + 1;
1770 let original_index = selected_index;
1771 if selected_index < configs_len + 1 {
1772 selected_index += 1;
1773 }
1774 assert_eq!(
1775 selected_index, original_index,
1776 "j key should not move beyond bottom boundary"
1777 );
1778 }
1779
1780 #[test]
1782 fn test_k_key_navigation() {
1783 let mut selected_index: usize = 5;
1784
1785 selected_index = selected_index.saturating_sub(1);
1788 assert_eq!(selected_index, 4, "k key should move selection up by one");
1789
1790 selected_index = 0;
1792 let original_index = selected_index;
1793 selected_index = selected_index.saturating_sub(1);
1794 assert_eq!(
1795 selected_index, original_index,
1796 "k key should not move beyond top boundary"
1797 );
1798 }
1799
1800 #[test]
1802 fn test_jk_key_boundary_conditions() {
1803 const CONFIGS_LEN: usize = 5;
1804
1805 let mut selected_index: usize = CONFIGS_LEN + 1; let original_index = selected_index;
1808 if selected_index < CONFIGS_LEN + 1 {
1809 selected_index += 1; }
1811 assert_eq!(
1812 selected_index, original_index,
1813 "j key should respect bottom boundary like Down arrow"
1814 );
1815
1816 let mut selected_index: usize = 0; let original_index = selected_index;
1819 selected_index = selected_index.saturating_sub(1); assert_eq!(
1821 selected_index, original_index,
1822 "k key should respect top boundary like Up arrow"
1823 );
1824 }
1825}
1826
1827#[derive(Debug, PartialEq)]
1829pub(crate) enum EditModeError {
1830 ReturnToMenu,
1831}
1832
1833impl std::fmt::Display for EditModeError {
1834 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1835 match self {
1836 EditModeError::ReturnToMenu => write!(f, "return_to_menu"),
1837 }
1838 }
1839}
1840
1841impl std::error::Error for EditModeError {}
1842
1843fn handle_config_edit(config: &Configuration) -> Result<()> {
1845 println!("\n{}", "配置编辑模式".green().bold());
1846 println!("{}", "===================".green());
1847 println!("正在编辑配置: {}", config.alias_name.cyan().bold());
1848 println!();
1849
1850 let mut editing_config = config.clone();
1852 let original_alias = config.alias_name.clone();
1853
1854 loop {
1855 display_edit_menu(&editing_config);
1857
1858 println!("\n{}", "提示: 可使用大小写字母".dimmed());
1860 print!("请选择要编辑的字段 (1-9, A-H), 或输入 S 保存, Q 返回上一级菜单: ");
1861 io::stdout().flush()?;
1862
1863 let mut input = String::new();
1864 io::stdin().read_line(&mut input)?;
1865 let input = input.trim();
1866
1867 match input {
1869 "1" => edit_field_alias(&mut editing_config)?,
1870 "2" => edit_field_token(&mut editing_config)?,
1871 "3" => edit_field_url(&mut editing_config)?,
1872 "4" => edit_field_model(&mut editing_config)?,
1873 "5" => edit_field_small_fast_model(&mut editing_config)?,
1874 "6" => edit_field_max_thinking_tokens(&mut editing_config)?,
1875 "7" => edit_field_api_timeout_ms(&mut editing_config)?,
1876 "8" => edit_field_claude_code_disable_nonessential_traffic(&mut editing_config)?,
1877 "9" => edit_field_anthropic_default_sonnet_model(&mut editing_config)?,
1878 "10" | "a" | "A" => edit_field_anthropic_default_opus_model(&mut editing_config)?,
1879 "11" | "b" | "B" => edit_field_anthropic_default_haiku_model(&mut editing_config)?,
1880 "12" | "c" | "C" => edit_field_claude_code_subagent_model(&mut editing_config)?,
1881 "13" | "d" | "D" => {
1882 edit_field_claude_code_disable_nonstreaming_fallback(&mut editing_config)?
1883 }
1884 "14" | "e" | "E" => edit_field_claude_code_effort_level(&mut editing_config)?,
1885 "15" | "f" | "F" => edit_field_disable_prompt_caching(&mut editing_config)?,
1886 "16" | "g" | "G" => {
1887 edit_field_claude_code_disable_experimental_betas(&mut editing_config)?
1888 }
1889 "17" | "h" | "H" => edit_field_disable_autoupdater(&mut editing_config)?,
1890 "s" | "S" => {
1891 return save_configuration_changes(&original_alias, &editing_config);
1893 }
1894 "q" | "Q" => {
1895 println!("\n{}", "返回上一级菜单".blue());
1896 return Err(EditModeError::ReturnToMenu.into());
1897 }
1898 _ => {
1899 println!("{}", "无效选择,请重试".red());
1900 }
1901 }
1902 }
1903}
1904
1905fn display_edit_menu(config: &Configuration) {
1907 println!("\n{}", "当前配置值:".blue().bold());
1908 println!("{}", "─────────────────────────".blue());
1909
1910 println!("1. 别名 (alias_name): {}", config.alias_name.green());
1911
1912 println!(
1913 "2. 令牌 (ANTHROPIC_AUTH_TOKEN): {}",
1914 format_token_for_display(&config.token).green()
1915 );
1916
1917 println!("3. URL (ANTHROPIC_BASE_URL): {}", config.url.green());
1918
1919 println!(
1920 "4. 模型 (ANTHROPIC_MODEL): {}",
1921 config.model.as_deref().unwrap_or("[未设置]").green()
1922 );
1923
1924 println!(
1925 "5. 快速模型 (ANTHROPIC_SMALL_FAST_MODEL): {}",
1926 config
1927 .small_fast_model
1928 .as_deref()
1929 .unwrap_or("[未设置]")
1930 .green()
1931 );
1932
1933 println!(
1934 "6. 最大思考令牌数 (ANTHROPIC_MAX_THINKING_TOKENS): {}",
1935 config
1936 .max_thinking_tokens
1937 .map(|t| t.to_string())
1938 .unwrap_or("[未设置]".to_string())
1939 .green()
1940 );
1941
1942 println!(
1943 "7. API超时时间 (API_TIMEOUT_MS): {}",
1944 config
1945 .api_timeout_ms
1946 .map(|t| t.to_string())
1947 .unwrap_or("[未设置]".to_string())
1948 .green()
1949 );
1950
1951 println!(
1952 "8. 禁用非必要流量 (CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC): {}",
1953 config
1954 .claude_code_disable_nonessential_traffic
1955 .map(|t| t.to_string())
1956 .unwrap_or("[未设置]".to_string())
1957 .green()
1958 );
1959
1960 println!(
1961 "9. 默认 Sonnet 模型 (ANTHROPIC_DEFAULT_SONNET_MODEL): {}",
1962 config
1963 .anthropic_default_sonnet_model
1964 .as_deref()
1965 .unwrap_or("[未设置]")
1966 .green()
1967 );
1968
1969 println!(
1970 "A. 默认 Opus 模型 (ANTHROPIC_DEFAULT_OPUS_MODEL): {}",
1971 config
1972 .anthropic_default_opus_model
1973 .as_deref()
1974 .unwrap_or("[未设置]")
1975 .green()
1976 );
1977
1978 println!(
1979 "B. 默认 Haiku 模型 (ANTHROPIC_DEFAULT_HAIKU_MODEL): {}",
1980 config
1981 .anthropic_default_haiku_model
1982 .as_deref()
1983 .unwrap_or("[未设置]")
1984 .green()
1985 );
1986
1987 println!(
1988 "C. 子代理模型 (CLAUDE_CODE_SUBAGENT_MODEL): {}",
1989 config
1990 .claude_code_subagent_model
1991 .as_deref()
1992 .unwrap_or("[未设置]")
1993 .green()
1994 );
1995
1996 println!(
1997 "D. 禁用非流式回退 (CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK): {}",
1998 config
1999 .claude_code_disable_nonstreaming_fallback
2000 .map(|t| t.to_string())
2001 .unwrap_or("[未设置]".to_string())
2002 .green()
2003 );
2004
2005 println!(
2006 "E. 努力级别 (CLAUDE_CODE_EFFORT_LEVEL): {}",
2007 config
2008 .claude_code_effort_level
2009 .as_deref()
2010 .unwrap_or("[未设置]")
2011 .green()
2012 );
2013
2014 println!(
2015 "F. 禁用提示缓存 (DISABLE_PROMPT_CACHING): {}",
2016 config
2017 .disable_prompt_caching
2018 .map(|t| t.to_string())
2019 .unwrap_or("[未设置]".to_string())
2020 .green()
2021 );
2022
2023 println!(
2024 "G. 禁用实验性功能 (CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS): {}",
2025 config
2026 .claude_code_disable_experimental_betas
2027 .map(|t| t.to_string())
2028 .unwrap_or("[未设置]".to_string())
2029 .green()
2030 );
2031
2032 println!(
2033 "H. 禁用自动更新 (DISABLE_AUTOUPDATER): {}",
2034 config
2035 .disable_autoupdater
2036 .map(|t| t.to_string())
2037 .unwrap_or("[未设置]".to_string())
2038 .green()
2039 );
2040
2041 println!("{}", "─────────────────────────".blue());
2042 println!(
2043 "S. {} | Q. {}",
2044 "保存更改".green().bold(),
2045 "返回上一级菜单".blue()
2046 );
2047}
2048
2049pub(crate) fn edit_string_field(
2051 field_name: &str,
2052 current_value: &str,
2053 validator: impl Fn(&str) -> Result<()>,
2054) -> Result<Option<String>> {
2055 println!("\n编辑{field_name}:");
2056 println!("当前值: {}", current_value.cyan());
2057 print!("新值 (回车保持不变): ");
2058 io::stdout().flush()?;
2059
2060 let mut input = String::new();
2061 io::stdin().read_line(&mut input)?;
2062 let input = input.trim();
2063
2064 if !input.is_empty() {
2065 validator(input)?;
2066 println!("{field_name}已更新为: {}", input.green());
2067 Ok(Some(input.to_string()))
2068 } else {
2069 Ok(None)
2070 }
2071}
2072
2073pub(crate) type OptionalStringResult = Result<Option<Option<String>>>;
2075
2076pub(crate) fn edit_optional_string_field(
2078 field_name: &str,
2079 current_value: Option<&str>,
2080) -> OptionalStringResult {
2081 println!("\n编辑{field_name}:");
2082 println!("当前值: {}", current_value.unwrap_or("[未设置]").cyan());
2083 print!("新值 (回车保持不变,输入空格清除): ");
2084 io::stdout().flush()?;
2085
2086 let mut input = String::new();
2087 io::stdin().read_line(&mut input)?;
2088 let input = input.trim();
2089
2090 if !input.is_empty() {
2091 if input == " " {
2092 println!("{}", format!("{field_name}已清除").green());
2093 Ok(Some(None))
2094 } else {
2095 println!("{field_name}已更新为: {}", input.green());
2096 Ok(Some(Some(input.to_string())))
2097 }
2098 } else {
2099 Ok(None)
2100 }
2101}
2102
2103type OptionalU32Result = Result<Option<Option<u32>>>;
2105
2106fn edit_optional_u32_field(field_name: &str, current_value: Option<u32>) -> OptionalU32Result {
2108 println!("\n编辑{field_name}:");
2109 println!(
2110 "当前值: {}",
2111 current_value
2112 .map(|t| t.to_string())
2113 .unwrap_or("[未设置]".to_string())
2114 .cyan()
2115 );
2116 print!("新值 (回车保持不变,输入 0 清除): ");
2117 io::stdout().flush()?;
2118
2119 let mut input = String::new();
2120 io::stdin().read_line(&mut input)?;
2121 let input = input.trim();
2122
2123 if !input.is_empty() {
2124 if input == "0" {
2125 println!("{}", format!("{field_name}已清除").green());
2126 Ok(Some(None))
2127 } else if let Ok(value) = input.parse::<u32>() {
2128 println!("{field_name}已更新为: {}", value.to_string().green());
2129 Ok(Some(Some(value)))
2130 } else {
2131 println!("{}", "错误: 请输入有效的数字".red());
2132 Ok(None)
2133 }
2134 } else {
2135 Ok(None)
2136 }
2137}
2138
2139fn edit_field_alias(config: &mut Configuration) -> Result<()> {
2141 let validator = |input: &str| -> Result<()> {
2142 if input.contains(char::is_whitespace) {
2143 anyhow::bail!("错误: 别名不能包含空白字符");
2144 }
2145 if input == "cc" {
2146 anyhow::bail!("错误: 'cc' 是保留名称");
2147 }
2148 if input == "official" {
2149 anyhow::bail!("错误: 'official' 是保留名称");
2150 }
2151 Ok(())
2152 };
2153
2154 match edit_string_field("别名", &config.alias_name, validator) {
2155 Ok(Some(new_value)) => config.alias_name = new_value,
2156 Ok(None) => {}
2157 Err(e) => println!("{}", e.to_string().red()),
2158 }
2159 Ok(())
2160}
2161
2162fn edit_field_token(config: &mut Configuration) -> Result<()> {
2164 let no_validator = |_: &str| -> Result<()> { Ok(()) };
2165 if let Some(new_value) = edit_string_field(
2166 "令牌",
2167 &format_token_for_display(&config.token),
2168 no_validator,
2169 )? {
2170 config.token = new_value;
2171 println!("{}", "令牌已更新".green());
2172 }
2173 Ok(())
2174}
2175
2176fn edit_field_url(config: &mut Configuration) -> Result<()> {
2178 let no_validator = |_: &str| -> Result<()> { Ok(()) };
2179 if let Some(new_value) = edit_string_field("URL", &config.url, no_validator)? {
2180 config.url = new_value;
2181 }
2182 Ok(())
2183}
2184
2185fn edit_field_model(config: &mut Configuration) -> Result<()> {
2187 if let Some(result) = edit_optional_string_field("模型", config.model.as_deref())? {
2188 config.model = result;
2189 }
2190 Ok(())
2191}
2192
2193fn edit_field_small_fast_model(config: &mut Configuration) -> Result<()> {
2195 if let Some(result) =
2196 edit_optional_string_field("快速模型", config.small_fast_model.as_deref())?
2197 {
2198 config.small_fast_model = result;
2199 }
2200 Ok(())
2201}
2202
2203fn edit_field_max_thinking_tokens(config: &mut Configuration) -> Result<()> {
2205 if let Some(result) = edit_optional_u32_field("最大思考令牌数", config.max_thinking_tokens)?
2206 {
2207 config.max_thinking_tokens = result;
2208 }
2209 Ok(())
2210}
2211
2212fn edit_field_api_timeout_ms(config: &mut Configuration) -> Result<()> {
2214 if let Some(result) = edit_optional_u32_field("API超时时间 (毫秒)", config.api_timeout_ms)?
2215 {
2216 config.api_timeout_ms = result;
2217 }
2218 Ok(())
2219}
2220
2221fn edit_field_claude_code_disable_nonessential_traffic(config: &mut Configuration) -> Result<()> {
2223 if let Some(result) = edit_optional_u32_field(
2224 "禁用非必要流量标志",
2225 config.claude_code_disable_nonessential_traffic,
2226 )? {
2227 config.claude_code_disable_nonessential_traffic = result;
2228 }
2229 Ok(())
2230}
2231
2232fn edit_field_anthropic_default_sonnet_model(config: &mut Configuration) -> Result<()> {
2234 if let Some(result) = edit_optional_string_field(
2235 "默认 Sonnet 模型",
2236 config.anthropic_default_sonnet_model.as_deref(),
2237 )? {
2238 config.anthropic_default_sonnet_model = result;
2239 }
2240 Ok(())
2241}
2242
2243fn edit_field_anthropic_default_opus_model(config: &mut Configuration) -> Result<()> {
2245 if let Some(result) = edit_optional_string_field(
2246 "默认 Opus 模型",
2247 config.anthropic_default_opus_model.as_deref(),
2248 )? {
2249 config.anthropic_default_opus_model = result;
2250 }
2251 Ok(())
2252}
2253
2254fn edit_field_anthropic_default_haiku_model(config: &mut Configuration) -> Result<()> {
2256 if let Some(result) = edit_optional_string_field(
2257 "默认 Haiku 模型",
2258 config.anthropic_default_haiku_model.as_deref(),
2259 )? {
2260 config.anthropic_default_haiku_model = result;
2261 }
2262 Ok(())
2263}
2264
2265fn edit_field_claude_code_subagent_model(config: &mut Configuration) -> Result<()> {
2267 if let Some(result) =
2268 edit_optional_string_field("子代理模型", config.claude_code_subagent_model.as_deref())?
2269 {
2270 config.claude_code_subagent_model = result;
2271 }
2272 Ok(())
2273}
2274
2275fn edit_field_claude_code_disable_nonstreaming_fallback(config: &mut Configuration) -> Result<()> {
2277 if let Some(result) = edit_optional_u32_field(
2278 "禁用非流式回退标志",
2279 config.claude_code_disable_nonstreaming_fallback,
2280 )? {
2281 config.claude_code_disable_nonstreaming_fallback = result;
2282 }
2283 Ok(())
2284}
2285
2286fn edit_field_claude_code_effort_level(config: &mut Configuration) -> Result<()> {
2288 if let Some(result) =
2289 edit_optional_string_field("努力级别", config.claude_code_effort_level.as_deref())?
2290 {
2291 config.claude_code_effort_level = result;
2292 }
2293 Ok(())
2294}
2295
2296fn edit_field_disable_prompt_caching(config: &mut Configuration) -> Result<()> {
2298 if let Some(result) =
2299 edit_optional_u32_field("禁用提示缓存标志", config.disable_prompt_caching)?
2300 {
2301 config.disable_prompt_caching = result;
2302 }
2303 Ok(())
2304}
2305
2306fn edit_field_claude_code_disable_experimental_betas(config: &mut Configuration) -> Result<()> {
2308 if let Some(result) = edit_optional_u32_field(
2309 "禁用实验性功能标志",
2310 config.claude_code_disable_experimental_betas,
2311 )? {
2312 config.claude_code_disable_experimental_betas = result;
2313 }
2314 Ok(())
2315}
2316
2317fn edit_field_disable_autoupdater(config: &mut Configuration) -> Result<()> {
2319 if let Some(result) = edit_optional_u32_field("禁用自动更新标志", config.disable_autoupdater)?
2320 {
2321 config.disable_autoupdater = result;
2322 }
2323 Ok(())
2324}
2325
2326fn save_configuration_changes(original_alias: &str, new_config: &Configuration) -> Result<()> {
2328 let mut storage = ConfigStorage::load()?;
2330
2331 if original_alias != new_config.alias_name
2333 && storage.get_configuration(&new_config.alias_name).is_some()
2334 {
2335 println!("\n{}", "别名冲突!".red().bold());
2336 println!("配置 '{}' 已存在", new_config.alias_name.yellow());
2337 print!("是否覆盖现有配置? (y/N): ");
2338 io::stdout().flush()?;
2339
2340 let mut input = String::new();
2341 io::stdin().read_line(&mut input)?;
2342 let input = input.trim().to_lowercase();
2343
2344 if input != "y" && input != "yes" {
2345 println!("{}", "编辑已取消".yellow());
2346 return Ok(());
2347 }
2348 }
2349
2350 storage.update_configuration(original_alias, new_config.clone())?;
2352 storage.save()?;
2353
2354 println!("\n{}", "配置已成功保存!".green().bold());
2355
2356 Ok(())
2357}