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
18fn char_display_width(c: char) -> usize {
21 match c as u32 {
22 0x00..=0x7F => 1,
23 0x80..=0x2FF => 1,
24 0x2190..=0x21FF => 2,
25 0x3000..=0x303F => 2,
26 0x3040..=0x309F => 2,
27 0x30A0..=0x30FF => 2,
28 0x4E00..=0x9FFF => 2,
29 0xAC00..=0xD7AF => 2,
30 0x3400..=0x4DBF => 2,
31 0xFF01..=0xFF60 => 2,
32 _ => 1,
33 }
34}
35
36fn truncate_text_to_width(text: &str, available_width: usize) -> (String, usize) {
38 let mut current_width = 0;
39 let truncated: String = text
40 .chars()
41 .take_while(|&c| {
42 let char_width = char_display_width(c);
43 if current_width + char_width <= available_width {
44 current_width += char_width;
45 true
46 } else {
47 false
48 }
49 })
50 .collect();
51 let truncated_width = text_display_width(&truncated);
52 (truncated, truncated_width)
53}
54
55fn cleanup_terminal(stdout: &mut io::Stdout) {
57 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
58 let _ = terminal::disable_raw_mode();
59}
60
61struct BorderDrawing {
63 pub unicode_supported: bool,
65}
66
67impl BorderDrawing {
68 fn new() -> Self {
70 let unicode_supported = Self::detect_unicode_support();
71 Self { unicode_supported }
72 }
73
74 fn detect_unicode_support() -> bool {
76 if let Ok(term) = std::env::var("TERM") {
78 if term.contains("xterm") || term.contains("screen") || term == "tmux-256color" {
80 return true;
81 }
82 }
83
84 if let Ok(lang) = std::env::var("LANG")
86 && (lang.contains("UTF-8") || lang.contains("utf8"))
87 {
88 return true;
89 }
90
91 true
94 }
95
96 fn draw_top_border(&self, title: &str, width: usize) -> String {
98 if self.unicode_supported {
99 let title_padded = format!(" {title} ");
100 let title_len = text_display_width(&title_padded);
101
102 if title_len >= width.saturating_sub(2) {
103 format!("╔{}╗", "═".repeat(width.saturating_sub(2)))
105 } else {
106 let inner_width = width.saturating_sub(2); let padding_total = inner_width.saturating_sub(title_len);
108 let padding_left = padding_total / 2;
109 let padding_right = padding_total - padding_left;
110 format!(
111 "╔{}{}{}╗",
112 "═".repeat(padding_left),
113 title_padded,
114 "═".repeat(padding_right)
115 )
116 }
117 } else {
118 let title_padded = format!(" {title} ");
120 let title_len = title_padded.len();
121
122 if title_len >= width.saturating_sub(2) {
123 format!("+{}+", "-".repeat(width.saturating_sub(2)))
124 } else {
125 let inner_width = width.saturating_sub(2);
126 let padding_total = inner_width.saturating_sub(title_len);
127 let padding_left = padding_total / 2;
128 let padding_right = padding_total - padding_left;
129 format!(
130 "+{}{}{}+",
131 "-".repeat(padding_left),
132 title_padded,
133 "-".repeat(padding_right)
134 )
135 }
136 }
137 }
138
139 fn draw_middle_line(&self, text: &str, width: usize) -> String {
141 let text_len = text_display_width(text);
142 let available_width = width.saturating_sub(4);
144
145 let (left_border, right_border) = if self.unicode_supported {
146 ("║", "║")
147 } else {
148 ("|", "|")
149 };
150
151 if text_len > available_width {
152 let (truncated, truncated_width) = truncate_text_to_width(text, available_width);
154 let padding_spaces = available_width.saturating_sub(truncated_width);
155 format!(
156 "{left_border} {}{} {right_border}",
157 truncated,
158 " ".repeat(padding_spaces)
159 )
160 } else {
161 let padded_text = pad_text_to_width(text, available_width, TextAlignment::Left, ' ');
162 format!("{left_border} {padded_text} {right_border}")
163 }
164 }
165
166 fn draw_bottom_border(&self, width: usize) -> String {
168 if self.unicode_supported {
169 format!("╚{}╝", "═".repeat(width - 2))
170 } else {
171 format!("+{}+", "-".repeat(width - 2))
172 }
173 }
174}
175
176pub fn handle_current_command() -> Result<()> {
186 let storage = ConfigStorage::load()?;
187
188 println!("\n{}", "Current Configuration:".green().bold());
189 println!("Environment variable mode: configurations are set per-command execution");
190 println!("Select a configuration from the menu below to launch Claude");
191 println!("Select 'cc' to launch Claude with default settings");
192
193 let raw_mode_enabled = terminal::enable_raw_mode().is_ok();
195
196 if raw_mode_enabled {
197 let mut stdout = io::stdout();
198 if execute!(
199 stdout,
200 terminal::EnterAlternateScreen,
201 terminal::Clear(terminal::ClearType::All)
202 )
203 .is_ok()
204 {
205 let result = handle_main_menu_interactive(&mut stdout, &storage);
207
208 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
210 let _ = terminal::disable_raw_mode();
211
212 return result;
213 } else {
214 let _ = terminal::disable_raw_mode();
216 }
217 }
218
219 handle_main_menu_simple(&storage)
221}
222
223fn handle_main_menu_interactive(stdout: &mut io::Stdout, storage: &ConfigStorage) -> Result<()> {
225 let menu_items = [
226 "Execute claude --dangerously-skip-permissions",
227 "Switch configuration",
228 "Exit",
229 ];
230 let mut selected_index = 0;
231
232 loop {
233 execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
235 execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
236
237 let border = BorderDrawing::new();
239 const MAIN_MENU_WIDTH: usize = 68;
240
241 println!(
242 "\r{}",
243 border.draw_top_border("Main Menu", MAIN_MENU_WIDTH).green()
244 );
245 println!(
246 "\r{}",
247 border
248 .draw_middle_line(
249 "↑↓/jk导航,1-9快选,E-编辑,R-官方,Q-退出,Enter确认,Esc取消",
250 MAIN_MENU_WIDTH
251 )
252 .green()
253 );
254 println!("\r{}", border.draw_bottom_border(MAIN_MENU_WIDTH).green());
255 println!();
256
257 for (index, item) in menu_items.iter().enumerate() {
259 if index == selected_index {
260 println!("\r> {} {}", "●".blue().bold(), item.blue().bold());
261 } else {
262 println!("\r {} {}", "○".dimmed(), item.dimmed());
263 }
264 }
265
266 stdout.flush()?;
268
269 let event = match event::read() {
271 Ok(event) => event,
272 Err(e) => {
273 cleanup_terminal(stdout);
275 return Err(e.into());
276 }
277 };
278
279 match event {
280 Event::Key(KeyEvent {
281 code,
282 kind: KeyEventKind::Press,
283 ..
284 }) => {
285 match code {
286 KeyCode::Up => {
287 selected_index = selected_index.saturating_sub(1);
288 }
289 KeyCode::Down => {
290 if selected_index < menu_items.len() - 1 {
291 selected_index += 1;
292 }
293 }
294 KeyCode::Enter => {
295 cleanup_terminal(stdout);
297
298 return handle_main_menu_action(selected_index, storage);
299 }
300 KeyCode::Esc => {
301 cleanup_terminal(stdout);
303
304 println!("\nExiting...");
305 return Ok(());
306 }
307 _ => {}
308 }
309 }
310 Event::Key(_) => {} _ => {}
312 }
313 }
314}
315
316fn handle_main_menu_simple(storage: &ConfigStorage) -> Result<()> {
318 loop {
319 println!("\n{}", "Available Actions:".blue().bold());
320 println!("1. Execute claude --dangerously-skip-permissions");
321 println!("2. Switch configuration");
322 println!("3. Exit");
323
324 print!("\nPlease select an option (1-3): ");
325 io::stdout().flush().context("Failed to flush stdout")?;
326
327 let mut input = String::new();
328 io::stdin()
329 .read_line(&mut input)
330 .context("Failed to read input")?;
331
332 let choice = input.trim();
333
334 match choice {
335 "1" => return handle_main_menu_action(0, storage),
336 "2" => return handle_main_menu_action(1, storage),
337 "3" => return handle_main_menu_action(2, storage),
338 _ => {
339 println!("Invalid option. Please select 1-3.");
340 }
341 }
342 }
343}
344
345fn handle_main_menu_action(selected_index: usize, storage: &ConfigStorage) -> Result<()> {
347 match selected_index {
348 0 => {
349 println!("\nExecuting: claude --dangerously-skip-permissions");
350 execute_claude_command(true)?;
351 }
352 1 => {
353 handle_interactive_selection(storage)?;
355 }
356 2 => {
357 println!("Exiting...");
358 }
359 _ => {
360 println!("Invalid selection");
361 }
362 }
363 Ok(())
364}
365
366pub fn handle_interactive_selection(storage: &ConfigStorage) -> Result<()> {
374 if storage.configurations.is_empty() {
375 println!("No configurations available. Use 'add' command to create configurations first.");
376 return Ok(());
377 }
378
379 let mut configs: Vec<Configuration> = storage.configurations.values().cloned().collect();
380 configs.sort_by(|a, b| a.alias_name.cmp(&b.alias_name));
381
382 let mut selected_index = 0;
383
384 let raw_mode_enabled = terminal::enable_raw_mode().is_ok();
386
387 if raw_mode_enabled {
388 let mut stdout = io::stdout();
389 if execute!(
390 stdout,
391 terminal::EnterAlternateScreen,
392 terminal::Clear(terminal::ClearType::All)
393 )
394 .is_ok()
395 {
396 let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
398 let result = handle_full_interactive_menu(
399 &mut stdout,
400 &mut configs,
401 &mut selected_index,
402 storage,
403 storage_mode,
404 );
405
406 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
408 let _ = terminal::disable_raw_mode();
409
410 return result;
411 } else {
412 let _ = terminal::disable_raw_mode();
414 }
415 }
416
417 handle_simple_interactive_menu(&configs.iter().collect::<Vec<_>>(), storage)
419}
420
421fn handle_full_interactive_menu(
423 stdout: &mut io::Stdout,
424 configs: &mut Vec<Configuration>,
425 selected_index: &mut usize,
426 storage: &ConfigStorage,
427 storage_mode: crate::config::types::StorageMode,
428) -> Result<()> {
429 if configs.is_empty() {
431 println!("\r{}", "No configurations available".yellow());
432 println!(
433 "\r{}",
434 "Use 'cc-switch add <alias> <token> <url>' to add configurations first.".dimmed()
435 );
436 println!("\r{}", "Press any key to continue...".dimmed());
437 let _ = event::read(); return Ok(());
439 }
440
441 const PAGE_SIZE: usize = 9; let total_pages = if configs.len() <= PAGE_SIZE {
445 1
446 } else {
447 configs.len().div_ceil(PAGE_SIZE)
448 };
449 let mut current_page = 0;
450
451 loop {
452 let start_idx = current_page * PAGE_SIZE;
454 let end_idx = std::cmp::min(start_idx + PAGE_SIZE, configs.len());
455 let page_configs = &configs[start_idx..end_idx];
456
457 execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
459 execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
460
461 let border = BorderDrawing::new();
463 const CONFIG_MENU_WIDTH: usize = 80;
466
467 println!(
468 "\r{}",
469 border
470 .draw_top_border("Select Configuration", CONFIG_MENU_WIDTH)
471 .green()
472 );
473 if total_pages > 1 {
474 println!(
475 "\r{}",
476 border
477 .draw_middle_line(
478 &format!("第 {} 页,共 {} 页", current_page + 1, total_pages),
479 CONFIG_MENU_WIDTH
480 )
481 .green()
482 );
483 println!(
484 "\r{}",
485 border
486 .draw_middle_line(
487 "↑↓/jk导航,1-9快选,E-编辑,N/P翻页,R-官方,Q-退出,Enter确认",
488 CONFIG_MENU_WIDTH
489 )
490 .green()
491 );
492 } else {
493 println!(
494 "\r{}",
495 border
496 .draw_middle_line(
497 "↑↓/jk导航,1-9快选,E-编辑,R-官方,Q-退出,Enter确认,Esc取消",
498 CONFIG_MENU_WIDTH
499 )
500 .green()
501 );
502 }
503 println!("\r{}", border.draw_bottom_border(CONFIG_MENU_WIDTH).green());
504 println!();
505
506 let official_index = 0;
508 if *selected_index == official_index {
509 println!(
510 "\r> {} {} {}",
511 "●".red().bold(),
512 "[R]".red().bold(),
513 "official".red().bold()
514 );
515 println!("\r Use official Claude API (no custom configuration)");
516 println!();
517 } else {
518 println!("\r {} {} {}", "○".red(), "[R]".red(), "official".red());
519 }
520
521 for (page_index, config) in page_configs.iter().enumerate() {
523 let actual_config_index = start_idx + page_index;
524 let display_number = page_index + 1; let actual_index = actual_config_index + 1; let number_label = format!("[{display_number}]");
527
528 if *selected_index == actual_index {
529 println!(
530 "\r> {} {} {}",
531 "●".blue().bold(),
532 number_label.blue().bold(),
533 config.alias_name.blue().bold()
534 );
535
536 let details = format_config_details(config, "\r ", false);
538 for detail_line in details {
539 println!("{detail_line}");
540 }
541 println!();
542 } else {
543 println!(
544 "\r {} {} {}",
545 "○".dimmed(),
546 number_label.dimmed(),
547 config.alias_name.dimmed()
548 );
549 }
550 }
551
552 let exit_index = configs.len() + 1;
554 if *selected_index == exit_index {
555 println!(
556 "\r> {} {} {}",
557 "●".yellow().bold(),
558 "[Q]".yellow().bold(),
559 "Exit".yellow().bold()
560 );
561 println!("\r Exit without making changes");
562 println!();
563 } else {
564 println!(
565 "\r {} {} {}",
566 "○".dimmed(),
567 "[Q]".dimmed(),
568 "Exit".dimmed()
569 );
570 }
571
572 if total_pages > 1 {
574 println!(
575 "\r{}",
576 format!(
577 "Page Navigation: [N]ext, [P]revious (第 {} 页,共 {} 页)",
578 current_page + 1,
579 total_pages
580 )
581 .dimmed()
582 );
583 }
584
585 stdout.flush()?;
587
588 let event = match event::read() {
590 Ok(event) => event,
591 Err(e) => {
592 cleanup_terminal(stdout);
594 return Err(e.into());
595 }
596 };
597
598 match event {
599 Event::Key(KeyEvent {
600 code,
601 kind: KeyEventKind::Press,
602 ..
603 }) => match code {
604 KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => {
605 *selected_index = selected_index.saturating_sub(1);
606 }
607 KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => {
608 if *selected_index < configs.len() + 1 {
609 *selected_index += 1;
610 }
611 }
612 KeyCode::PageDown | KeyCode::Char('n') | KeyCode::Char('N') => {
613 if total_pages > 1 && current_page < total_pages - 1 {
614 current_page += 1;
615 let new_page_start_idx = current_page * PAGE_SIZE;
617 *selected_index = new_page_start_idx + 1; }
619 }
620 KeyCode::PageUp | KeyCode::Char('p') | KeyCode::Char('P') => {
621 if total_pages > 1 && current_page > 0 {
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::Enter => {
629 cleanup_terminal(stdout);
631
632 return handle_selection_action(
633 &configs.iter().collect::<Vec<_>>(),
634 *selected_index,
635 storage,
636 storage_mode,
637 );
638 }
639 KeyCode::Esc => {
640 cleanup_terminal(stdout);
642
643 println!("\nSelection cancelled");
644 return Ok(());
645 }
646 KeyCode::Char(c) if c.is_ascii_digit() => {
647 let digit = c.to_digit(10).unwrap() as usize;
648 if digit >= 1 && digit <= page_configs.len() {
650 let actual_config_index = start_idx + (digit - 1);
651 let selection_index = actual_config_index + 1; cleanup_terminal(stdout);
655
656 return handle_selection_action(
657 &configs.iter().collect::<Vec<_>>(),
658 selection_index,
659 storage,
660 storage_mode,
661 );
662 }
663 }
665 KeyCode::Char('r') | KeyCode::Char('R') => {
666 cleanup_terminal(stdout);
668
669 return handle_selection_action(&configs.iter().collect::<Vec<_>>(), 0, storage, storage_mode);
670 }
671 KeyCode::Char('e') | KeyCode::Char('E') => {
672 if *selected_index > 0 && *selected_index <= configs.len() {
674 cleanup_terminal(stdout);
676
677 let config_index = *selected_index - 1; let edit_result = handle_config_edit(&configs[config_index]);
681
682 if execute!(
684 stdout,
685 terminal::EnterAlternateScreen,
686 terminal::Clear(terminal::ClearType::All)
687 )
688 .is_ok()
689 && terminal::enable_raw_mode().is_ok()
690 {
691 match edit_result {
693 Ok(_) => {
694 if let Ok(reloaded_storage) = ConfigStorage::load() {
696 *configs = reloaded_storage.configurations.values().cloned().collect();
697 configs.sort_by(|a, b| a.alias_name.cmp(&b.alias_name));
698 if *selected_index > configs.len() + 1 {
700 *selected_index = configs.len() + 1;
701 }
702 }
703 continue;
705 }
706 Err(e) => {
707 if e.downcast_ref::<EditModeError>()
709 == Some(&EditModeError::ReturnToMenu)
710 {
711 continue;
713 }
714 cleanup_terminal(stdout);
716 return Err(e);
717 }
718 }
719 }
720 }
721 }
723 KeyCode::Char('q') | KeyCode::Char('Q') => {
724 cleanup_terminal(stdout);
726
727 return handle_selection_action(
728 &configs.iter().collect::<Vec<_>>(),
729 configs.len() + 1,
730 storage,
731 storage_mode,
732 );
733 }
734 _ => {}
735 },
736 Event::Key(_) => {} _ => {}
738 }
739 }
740}
741
742fn handle_simple_interactive_menu(
744 configs: &[&Configuration],
745 storage: &ConfigStorage,
746) -> Result<()> {
747 const PAGE_SIZE: usize = 9; if configs.len() <= PAGE_SIZE {
751 return handle_simple_single_page_menu(configs, storage);
752 }
753
754 let total_pages = configs.len().div_ceil(PAGE_SIZE);
756 let mut current_page = 0;
757
758 loop {
759 let start_idx = current_page * PAGE_SIZE;
761 let end_idx = std::cmp::min(start_idx + PAGE_SIZE, configs.len());
762 let page_configs = &configs[start_idx..end_idx];
763
764 println!("\n{}", "Available Configurations:".blue().bold());
765 if total_pages > 1 {
766 println!("第 {} 页,共 {} 页", current_page + 1, total_pages);
767 println!("使用 'n' 下一页, 'p' 上一页, 'r' 官方配置, 'q' 退出");
768 }
769 println!();
770
771 println!("{} {}", "[r]".red().bold(), "official".red());
773 println!(" Use official Claude API (no custom configuration)");
774 println!();
775
776 for (page_index, config) in page_configs.iter().enumerate() {
778 let display_number = page_index + 1;
779
780 println!(
781 "{}. {}",
782 format!("[{display_number}]").green().bold(),
783 config.alias_name.green()
784 );
785
786 let details = format_config_details(config, " ", true);
788 for detail_line in details {
789 println!("{detail_line}");
790 }
791 println!();
792 }
793
794 println!("{} {}", "[q]".yellow().bold(), "Exit".yellow());
796
797 if total_pages > 1 {
798 println!(
799 "\n页面导航: [n]下页, [p]上页 | 配置选择: [1-{}] | [e]编辑 | [r]官方 | [q]退出",
800 page_configs.len()
801 );
802 }
803
804 print!("\n请输入选择: ");
805 io::stdout().flush()?;
806
807 let mut input = String::new();
808 io::stdin().read_line(&mut input)?;
809 let choice = input.trim().to_lowercase();
810
811 match choice.as_str() {
812 "r" => {
813 println!("Using official Claude configuration");
815
816 let mut settings = crate::config::types::ClaudeSettings::load(
818 storage.get_claude_settings_dir().map(|s| s.as_str()),
819 )?;
820 settings.remove_anthropic_env();
821 settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
822
823 return launch_claude_with_env(EnvironmentConfig::empty(), None);
824 }
825 "e" => {
826 println!("编辑功能在交互式菜单中可用");
829 }
830 "q" => {
831 println!("Exiting...");
832 return Ok(());
833 }
834 "n" if total_pages > 1 && current_page < total_pages - 1 => {
835 current_page += 1;
836 continue;
837 }
838 "p" if total_pages > 1 && current_page > 0 => {
839 current_page -= 1;
840 continue;
841 }
842 digit_str => {
843 if let Ok(digit) = digit_str.parse::<usize>()
844 && digit >= 1
845 && digit <= page_configs.len()
846 {
847 let actual_config_index = start_idx + (digit - 1);
848 let selection_index = actual_config_index + 1; let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
850 return handle_selection_action(
851 configs,
852 selection_index,
853 storage,
854 storage_mode,
855 );
856 }
857 println!("无效选择,请重新输入");
858 }
859 }
860 }
861}
862
863fn handle_simple_single_page_menu(
865 configs: &[&Configuration],
866 storage: &ConfigStorage,
867) -> Result<()> {
868 println!("\n{}", "Available Configurations:".blue().bold());
869
870 println!("1. {}", "official".red());
872 println!(" Use official Claude API (no custom configuration)");
873 println!();
874
875 for (index, config) in configs.iter().enumerate() {
876 println!(
877 "{}. {}",
878 index + 2, config.alias_name.green()
880 );
881
882 let details = format_config_details(config, " ", true);
884 for detail_line in details {
885 println!("{detail_line}");
886 }
887 println!();
888 }
889
890 println!("{}. {}", configs.len() + 2, "Exit".yellow());
891
892 print!("\nSelect configuration (1-{}): ", configs.len() + 2);
893 io::stdout().flush()?;
894
895 let mut input = String::new();
896 io::stdin().read_line(&mut input)?;
897
898 match input.trim().parse::<usize>() {
899 Ok(1) => {
900 println!("Using official Claude configuration");
902
903 let mut settings = crate::config::types::ClaudeSettings::load(
905 storage.get_claude_settings_dir().map(|s| s.as_str()),
906 )?;
907 settings.remove_anthropic_env();
908 settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
909
910 launch_claude_with_env(EnvironmentConfig::empty(), None)
911 }
912 Ok(num) if num >= 2 && num <= configs.len() + 1 => {
913 let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
914 handle_selection_action(configs, num - 1, storage, storage_mode) }
916 Ok(num) if num == configs.len() + 2 => {
917 println!("Exiting...");
918 Ok(())
919 }
920 _ => {
921 println!("Invalid selection");
922 Ok(())
923 }
924 }
925}
926
927fn handle_selection_action(
929 configs: &[&Configuration],
930 selected_index: usize,
931 storage: &ConfigStorage,
932 storage_mode: crate::config::types::StorageMode,
933) -> Result<()> {
934 if selected_index == 0 {
935 println!("\nUsing official Claude configuration");
937
938 let mut settings = crate::config::types::ClaudeSettings::load(
940 storage.get_claude_settings_dir().map(|s| s.as_str()),
941 )?;
942 settings.remove_anthropic_env();
943 settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
944
945 launch_claude_with_env(EnvironmentConfig::empty(), None)
946 } else if selected_index <= configs.len() {
947 let config_index = selected_index - 1; let selected_config = configs[config_index].clone();
950 let env_config = EnvironmentConfig::from_config(&selected_config);
951
952 println!(
953 "\nSwitched to configuration '{}'",
954 selected_config.alias_name.green().bold()
955 );
956
957 let details = format_config_details(&selected_config, "", false);
959 for detail_line in details {
960 println!("{detail_line}");
961 }
962
963 let mut settings = crate::config::types::ClaudeSettings::load(
965 storage.get_claude_settings_dir().map(|s| s.as_str()),
966 )?;
967 settings.switch_to_config_with_mode(
968 &selected_config,
969 storage_mode,
970 storage.get_claude_settings_dir().map(|s| s.as_str()),
971 )?;
972
973 launch_claude_with_env(env_config, None)
974 } else {
975 println!("\nExiting...");
977 Ok(())
978 }
979}
980
981pub fn launch_claude_with_env(env_config: EnvironmentConfig, prompt: Option<&str>) -> Result<()> {
983 println!("\nWaiting 0.5 seconds before launching Claude...");
984 thread::sleep(Duration::from_millis(500));
985
986 println!("Launching Claude CLI...");
987
988 for (key, value) in env_config.as_env_tuples() {
990 unsafe {
991 std::env::set_var(&key, &value);
992 }
993 }
994
995 #[cfg(unix)]
997 {
998 use std::os::unix::process::CommandExt;
999 let mut command = Command::new("claude");
1000 command.arg("--dangerously-skip-permissions");
1001 if let Some(p) = prompt {
1002 command.arg(p);
1003 }
1004 let error = command.exec();
1005 anyhow::bail!("Failed to exec claude: {}", error);
1007 }
1008
1009 #[cfg(not(unix))]
1011 {
1012 use std::process::Stdio;
1013 let mut command = Command::new("claude");
1014 command.arg("--dangerously-skip-permissions");
1015 if let Some(p) = prompt {
1016 command.arg(p);
1017 }
1018 command
1019 .stdin(Stdio::inherit())
1020 .stdout(Stdio::inherit())
1021 .stderr(Stdio::inherit());
1022
1023 let mut child = command.spawn().context(
1024 "Failed to launch Claude CLI. Make sure 'claude' command is available in PATH",
1025 )?;
1026
1027 let status = child.wait()?;
1028
1029 if !status.success() {
1030 anyhow::bail!("Claude CLI exited with error status: {}", status);
1031 }
1032 }
1033}
1034
1035fn execute_claude_command(skip_permissions: bool) -> Result<()> {
1040 println!("Launching Claude CLI...");
1041
1042 #[cfg(unix)]
1044 {
1045 use std::os::unix::process::CommandExt;
1046 let mut command = Command::new("claude");
1047 if skip_permissions {
1048 command.arg("--dangerously-skip-permissions");
1049 }
1050
1051 let error = command.exec();
1052 anyhow::bail!("Failed to exec claude: {}", error);
1054 }
1055
1056 #[cfg(not(unix))]
1058 {
1059 use std::process::Stdio;
1060 let mut command = Command::new("claude");
1061 if skip_permissions {
1062 command.arg("--dangerously-skip-permissions");
1063 }
1064
1065 command
1066 .stdin(Stdio::inherit())
1067 .stdout(Stdio::inherit())
1068 .stderr(Stdio::inherit());
1069
1070 let mut child = command.spawn().context(
1071 "Failed to launch Claude CLI. Make sure 'claude' command is available in PATH",
1072 )?;
1073
1074 let status = child
1075 .wait()
1076 .context("Failed to wait for Claude CLI process")?;
1077
1078 if !status.success() {
1079 anyhow::bail!("Claude CLI exited with error status: {}", status);
1080 }
1081 }
1082}
1083
1084pub fn read_input(prompt: &str) -> Result<String> {
1092 print!("{prompt}");
1093 io::stdout().flush().context("Failed to flush stdout")?;
1094 let mut input = String::new();
1095 io::stdin()
1096 .read_line(&mut input)
1097 .context("Failed to read input")?;
1098 Ok(input.trim().to_string())
1099}
1100
1101pub fn read_sensitive_input(prompt: &str) -> Result<String> {
1109 print!("{prompt}");
1110 io::stdout().flush().context("Failed to flush stdout")?;
1111 let mut input = String::new();
1112 io::stdin()
1113 .read_line(&mut input)
1114 .context("Failed to read input")?;
1115 Ok(input.trim().to_string())
1116}
1117
1118fn format_config_details(config: &Configuration, indent: &str, _compact: bool) -> Vec<String> {
1131 let mut lines = Vec::new();
1132
1133 let terminal_width = get_terminal_width();
1135 let _available_width = terminal_width.saturating_sub(text_display_width(indent) + 8);
1136
1137 let token_label = "Token:";
1139 let url_label = "URL:";
1140 let model_label = "Model:";
1141 let small_model_label = "Small Fast Model:";
1142 let max_thinking_tokens_label = "Max Thinking Tokens:";
1143 let api_timeout_ms_label = "API Timeout (ms):";
1144 let disable_nonessential_traffic_label = "Disable Nonessential Traffic:";
1145 let default_sonnet_model_label = "Default Sonnet Model:";
1146 let default_opus_model_label = "Default Opus Model:";
1147 let default_haiku_model_label = "Default Haiku Model:";
1148
1149 let max_label_width = [
1151 token_label,
1152 url_label,
1153 model_label,
1154 small_model_label,
1155 max_thinking_tokens_label,
1156 api_timeout_ms_label,
1157 disable_nonessential_traffic_label,
1158 default_sonnet_model_label,
1159 default_opus_model_label,
1160 default_haiku_model_label,
1161 ]
1162 .iter()
1163 .map(|label| text_display_width(label))
1164 .max()
1165 .unwrap_or(0);
1166
1167 let token_line = format!(
1169 "{}{} {}",
1170 indent,
1171 pad_text_to_width(token_label, max_label_width, TextAlignment::Left, ' '),
1172 format_token_for_display(&config.token).dimmed()
1173 );
1174 lines.push(token_line);
1175
1176 let url_line = format!(
1178 "{}{} {}",
1179 indent,
1180 pad_text_to_width(url_label, max_label_width, TextAlignment::Left, ' '),
1181 config.url.cyan()
1182 );
1183 lines.push(url_line);
1184
1185 if let Some(model) = &config.model {
1187 let model_line = format!(
1188 "{}{} {}",
1189 indent,
1190 pad_text_to_width(model_label, max_label_width, TextAlignment::Left, ' '),
1191 model.yellow()
1192 );
1193 lines.push(model_line);
1194 }
1195
1196 if let Some(small_fast_model) = &config.small_fast_model {
1198 let small_model_line = format!(
1199 "{}{} {}",
1200 indent,
1201 pad_text_to_width(small_model_label, max_label_width, TextAlignment::Left, ' '),
1202 small_fast_model.yellow()
1203 );
1204 lines.push(small_model_line);
1205 }
1206
1207 if let Some(max_thinking_tokens) = config.max_thinking_tokens {
1209 let tokens_line = format!(
1210 "{}{} {}",
1211 indent,
1212 pad_text_to_width(
1213 max_thinking_tokens_label,
1214 max_label_width,
1215 TextAlignment::Left,
1216 ' '
1217 ),
1218 format!("{}", max_thinking_tokens).yellow()
1219 );
1220 lines.push(tokens_line);
1221 }
1222
1223 if let Some(api_timeout_ms) = config.api_timeout_ms {
1225 let timeout_line = format!(
1226 "{}{} {}",
1227 indent,
1228 pad_text_to_width(
1229 api_timeout_ms_label,
1230 max_label_width,
1231 TextAlignment::Left,
1232 ' '
1233 ),
1234 format!("{}", api_timeout_ms).yellow()
1235 );
1236 lines.push(timeout_line);
1237 }
1238
1239 if let Some(disable_flag) = config.claude_code_disable_nonessential_traffic {
1241 let flag_line = format!(
1242 "{}{} {}",
1243 indent,
1244 pad_text_to_width(
1245 disable_nonessential_traffic_label,
1246 max_label_width,
1247 TextAlignment::Left,
1248 ' '
1249 ),
1250 format!("{}", disable_flag).yellow()
1251 );
1252 lines.push(flag_line);
1253 }
1254
1255 if let Some(sonnet_model) = &config.anthropic_default_sonnet_model {
1257 let sonnet_line = format!(
1258 "{}{} {}",
1259 indent,
1260 pad_text_to_width(
1261 default_sonnet_model_label,
1262 max_label_width,
1263 TextAlignment::Left,
1264 ' '
1265 ),
1266 sonnet_model.yellow()
1267 );
1268 lines.push(sonnet_line);
1269 }
1270
1271 if let Some(opus_model) = &config.anthropic_default_opus_model {
1273 let opus_line = format!(
1274 "{}{} {}",
1275 indent,
1276 pad_text_to_width(
1277 default_opus_model_label,
1278 max_label_width,
1279 TextAlignment::Left,
1280 ' '
1281 ),
1282 opus_model.yellow()
1283 );
1284 lines.push(opus_line);
1285 }
1286
1287 if let Some(haiku_model) = &config.anthropic_default_haiku_model {
1289 let haiku_line = format!(
1290 "{}{} {}",
1291 indent,
1292 pad_text_to_width(
1293 default_haiku_model_label,
1294 max_label_width,
1295 TextAlignment::Left,
1296 ' '
1297 ),
1298 haiku_model.yellow()
1299 );
1300 lines.push(haiku_line);
1301 }
1302
1303 lines
1304}
1305
1306#[cfg(test)]
1307mod border_drawing_tests {
1308 use super::*;
1309
1310 #[test]
1311 fn test_border_drawing_unicode_support() {
1312 let _border = BorderDrawing::new();
1313 }
1315
1316 #[test]
1317 fn test_border_drawing_top_border() {
1318 let border = BorderDrawing {
1319 unicode_supported: true,
1320 };
1321 let result = border.draw_top_border("Test", 20);
1322 assert!(!result.is_empty());
1323 assert!(result.contains("Test"));
1324 }
1325
1326 #[test]
1327 fn test_border_drawing_ascii_fallback() {
1328 let border = BorderDrawing {
1329 unicode_supported: false,
1330 };
1331 let result = border.draw_top_border("Test", 20);
1332 assert!(!result.is_empty());
1333 assert!(result.contains("Test"));
1334 assert!(result.contains("+"));
1335 assert!(result.contains("-"));
1336 }
1337
1338 #[test]
1339 fn test_border_drawing_middle_line() {
1340 let border = BorderDrawing {
1341 unicode_supported: true,
1342 };
1343 let result = border.draw_middle_line("Test message", 30);
1344 assert!(!result.is_empty());
1345 assert!(result.contains("Test message"));
1346 }
1347
1348 #[test]
1349 fn test_border_drawing_bottom_border() {
1350 let border = BorderDrawing {
1351 unicode_supported: true,
1352 };
1353 let result = border.draw_bottom_border(20);
1354 assert!(!result.is_empty());
1355 }
1356
1357 #[test]
1358 fn test_border_drawing_width_consistency() {
1359 let border = BorderDrawing {
1360 unicode_supported: true,
1361 };
1362 let width = 30;
1363 let top = border.draw_top_border("Title", width);
1364 let middle = border.draw_middle_line("Content", width);
1365 let bottom = border.draw_bottom_border(width);
1366
1367 assert!(top.chars().count() >= width - 2);
1369 assert!(middle.chars().count() >= width - 2);
1370 assert!(bottom.chars().count() >= width - 2);
1371 }
1372}
1373
1374#[cfg(test)]
1375mod pagination_tests {
1376
1377 #[test]
1379 fn test_pagination_calculation() {
1380 const PAGE_SIZE: usize = 9;
1381
1382 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); }
1393
1394 #[test]
1396 fn test_page_range_calculation() {
1397 const PAGE_SIZE: usize = 9;
1398
1399 let current_page = 0;
1401 let start_idx = current_page * PAGE_SIZE; let end_idx = std::cmp::min(start_idx + PAGE_SIZE, 15); assert_eq!(start_idx, 0);
1404 assert_eq!(end_idx, 9);
1405 assert_eq!(end_idx - start_idx, 9); let current_page = 1;
1409 let start_idx = current_page * PAGE_SIZE; let end_idx = std::cmp::min(start_idx + PAGE_SIZE, 15); assert_eq!(start_idx, 9);
1412 assert_eq!(end_idx, 15);
1413 assert_eq!(end_idx - start_idx, 6); let current_page = 0;
1417 let start_idx = current_page * PAGE_SIZE; let end_idx = std::cmp::min(start_idx + PAGE_SIZE, PAGE_SIZE); assert_eq!(start_idx, 0);
1420 assert_eq!(end_idx, 9);
1421 assert_eq!(end_idx - start_idx, 9); }
1423
1424 #[test]
1426 fn test_digit_mapping_to_config_index() {
1427 const PAGE_SIZE: usize = 9;
1428
1429 let current_page = 0;
1431 let start_idx = current_page * PAGE_SIZE; let digit = 1;
1435 let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 0);
1437
1438 let digit = 9;
1440 let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 8);
1442
1443 let current_page = 1;
1445 let start_idx = current_page * PAGE_SIZE; let digit = 1;
1449 let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 9);
1451
1452 let digit = 5;
1454 let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 13);
1456 }
1457
1458 #[test]
1460 fn test_selection_index_conversion() {
1461 const PAGE_SIZE: usize = 9;
1468
1469 let current_page = 0;
1471 let start_idx = current_page * PAGE_SIZE; let digit = 1;
1473 let actual_config_index = start_idx + (digit - 1); let selection_index = actual_config_index + 1; assert_eq!(selection_index, 1);
1476
1477 let current_page = 1;
1479 let start_idx = current_page * PAGE_SIZE; let digit = 1;
1481 let actual_config_index = start_idx + (digit - 1); let selection_index = actual_config_index + 1; assert_eq!(selection_index, 10);
1484 }
1485
1486 #[test]
1488 fn test_page_navigation_bounds() {
1489 const PAGE_SIZE: usize = 9;
1490 let total_configs: usize = 25; let total_pages = total_configs.div_ceil(PAGE_SIZE); assert_eq!(total_pages, 3);
1493
1494 let mut current_page = 0;
1496 if current_page > 0 {
1497 current_page -= 1;
1498 }
1499 assert_eq!(current_page, 0); let mut current_page = total_pages - 1; if current_page < total_pages - 1 {
1504 current_page += 1;
1505 }
1506 assert_eq!(current_page, 2); let mut current_page = 1;
1510
1511 if current_page < total_pages - 1 {
1513 current_page += 1;
1514 }
1515 assert_eq!(current_page, 2);
1516
1517 if current_page > 0 {
1519 current_page = current_page.saturating_sub(1);
1520 }
1521 assert_eq!(current_page, 1);
1522 }
1523
1524 #[test]
1526 fn test_digit_key_boundary_conditions() {
1527 const PAGE_SIZE: usize = 9;
1528
1529 let digit = 0;
1531 assert!(digit < 1, "Digit 0 should be less than 1 and ignored");
1532
1533 let configs_len = 5; let page_configs_len = std::cmp::min(PAGE_SIZE, configs_len); let digit = 9; assert!(
1538 digit > page_configs_len,
1539 "Digit 9 should be beyond available configs (5) and ignored"
1540 );
1541
1542 for digit in 1..=page_configs_len {
1544 assert!(
1545 digit >= 1 && digit <= page_configs_len,
1546 "Digit {} should be valid",
1547 digit
1548 );
1549 }
1550 }
1551
1552 #[test]
1554 fn test_empty_configs_handling() {
1555 let empty_configs: Vec<String> = Vec::new();
1556 assert!(
1557 empty_configs.is_empty(),
1558 "Empty config list should be properly detected"
1559 );
1560
1561 let configs_len = empty_configs.len(); assert_eq!(configs_len, 0, "Empty configs should have length 0");
1564
1565 }
1568
1569 #[test]
1571 fn test_page_navigation_boundaries() {
1572 const PAGE_SIZE: usize = 9;
1573 let total_configs: usize = 20; let total_pages = total_configs.div_ceil(PAGE_SIZE); let mut current_page = 0;
1578 let original_page = current_page;
1579
1580 if current_page > 0 {
1582 current_page -= 1;
1583 }
1584 assert_eq!(
1585 current_page, original_page,
1586 "First page should not navigate to previous"
1587 );
1588
1589 let mut current_page = total_pages - 1; let original_page = current_page;
1592
1593 if current_page < total_pages - 1 {
1595 current_page += 1;
1596 }
1597 assert_eq!(
1598 current_page, original_page,
1599 "Last page should not navigate to next"
1600 );
1601
1602 let mut current_page = 1; if current_page < total_pages - 1 {
1607 current_page += 1;
1608 }
1609 assert_eq!(current_page, 2, "Should navigate to next page");
1610
1611 if current_page > 0 {
1613 current_page = current_page.saturating_sub(1);
1614 }
1615 assert_eq!(current_page, 1, "Should navigate to previous page");
1616 }
1617
1618 #[test]
1620 fn test_j_key_navigation() {
1621 let mut selected_index: usize = 0;
1622 let configs_len = 5; if selected_index < configs_len + 1 {
1627 selected_index += 1;
1628 }
1629 assert_eq!(selected_index, 1, "j key should move selection down by one");
1630
1631 selected_index = configs_len + 1;
1633 let original_index = selected_index;
1634 if selected_index < configs_len + 1 {
1635 selected_index += 1;
1636 }
1637 assert_eq!(
1638 selected_index, original_index,
1639 "j key should not move beyond bottom boundary"
1640 );
1641 }
1642
1643 #[test]
1645 fn test_k_key_navigation() {
1646 let mut selected_index: usize = 5;
1647
1648 selected_index = selected_index.saturating_sub(1);
1651 assert_eq!(selected_index, 4, "k key should move selection up by one");
1652
1653 selected_index = 0;
1655 let original_index = selected_index;
1656 selected_index = selected_index.saturating_sub(1);
1657 assert_eq!(
1658 selected_index, original_index,
1659 "k key should not move beyond top boundary"
1660 );
1661 }
1662
1663 #[test]
1665 fn test_jk_key_boundary_conditions() {
1666 const CONFIGS_LEN: usize = 5;
1667
1668 let mut selected_index: usize = CONFIGS_LEN + 1; let original_index = selected_index;
1671 if selected_index < CONFIGS_LEN + 1 {
1672 selected_index += 1; }
1674 assert_eq!(
1675 selected_index, original_index,
1676 "j key should respect bottom boundary like Down arrow"
1677 );
1678
1679 let mut selected_index: usize = 0; let original_index = selected_index;
1682 selected_index = selected_index.saturating_sub(1); assert_eq!(
1684 selected_index, original_index,
1685 "k key should respect top boundary like Up arrow"
1686 );
1687 }
1688}
1689
1690#[derive(Debug, PartialEq)]
1692enum EditModeError {
1693 ReturnToMenu,
1694}
1695
1696impl std::fmt::Display for EditModeError {
1697 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1698 match self {
1699 EditModeError::ReturnToMenu => write!(f, "return_to_menu"),
1700 }
1701 }
1702}
1703
1704impl std::error::Error for EditModeError {}
1705
1706fn handle_config_edit(config: &Configuration) -> Result<()> {
1708 println!("\n{}", "配置编辑模式".green().bold());
1709 println!("{}", "===================".green());
1710 println!("正在编辑配置: {}", config.alias_name.cyan().bold());
1711 println!();
1712
1713 let mut editing_config = config.clone();
1715 let original_alias = config.alias_name.clone();
1716
1717 loop {
1718 display_edit_menu(&editing_config);
1720
1721 println!("\n{}", "提示: 可使用大小写字母".dimmed());
1723 print!("请选择要编辑的字段 (1-9, A-B), 或输入 S 保存, Q 返回上一级菜单: ");
1724 io::stdout().flush()?;
1725
1726 let mut input = String::new();
1727 io::stdin().read_line(&mut input)?;
1728 let input = input.trim();
1729
1730 match input {
1732 "1" => edit_field_alias(&mut editing_config)?,
1733 "2" => edit_field_token(&mut editing_config)?,
1734 "3" => edit_field_url(&mut editing_config)?,
1735 "4" => edit_field_model(&mut editing_config)?,
1736 "5" => edit_field_small_fast_model(&mut editing_config)?,
1737 "6" => edit_field_max_thinking_tokens(&mut editing_config)?,
1738 "7" => edit_field_api_timeout_ms(&mut editing_config)?,
1739 "8" => edit_field_claude_code_disable_nonessential_traffic(&mut editing_config)?,
1740 "9" => edit_field_anthropic_default_sonnet_model(&mut editing_config)?,
1741 "10" | "a" | "A" => edit_field_anthropic_default_opus_model(&mut editing_config)?,
1742 "11" | "b" | "B" => edit_field_anthropic_default_haiku_model(&mut editing_config)?,
1743 "s" | "S" => {
1744 return save_configuration_changes(&original_alias, &editing_config);
1746 }
1747 "q" | "Q" => {
1748 println!("\n{}", "返回上一级菜单".blue());
1749 return Err(EditModeError::ReturnToMenu.into());
1750 }
1751 _ => {
1752 println!("{}", "无效选择,请重试".red());
1753 }
1754 }
1755 }
1756}
1757
1758fn display_edit_menu(config: &Configuration) {
1760 println!("\n{}", "当前配置值:".blue().bold());
1761 println!("{}", "─────────────────────────".blue());
1762
1763 println!("1. 别名 (alias_name): {}", config.alias_name.green());
1764
1765 println!(
1766 "2. 令牌 (ANTHROPIC_AUTH_TOKEN): {}",
1767 format_token_for_display(&config.token).green()
1768 );
1769
1770 println!("3. URL (ANTHROPIC_BASE_URL): {}", config.url.green());
1771
1772 println!(
1773 "4. 模型 (ANTHROPIC_MODEL): {}",
1774 config.model.as_deref().unwrap_or("[未设置]").green()
1775 );
1776
1777 println!(
1778 "5. 快速模型 (ANTHROPIC_SMALL_FAST_MODEL): {}",
1779 config
1780 .small_fast_model
1781 .as_deref()
1782 .unwrap_or("[未设置]")
1783 .green()
1784 );
1785
1786 println!(
1787 "6. 最大思考令牌数 (ANTHROPIC_MAX_THINKING_TOKENS): {}",
1788 config
1789 .max_thinking_tokens
1790 .map(|t| t.to_string())
1791 .unwrap_or("[未设置]".to_string())
1792 .green()
1793 );
1794
1795 println!(
1796 "7. API超时时间 (API_TIMEOUT_MS): {}",
1797 config
1798 .api_timeout_ms
1799 .map(|t| t.to_string())
1800 .unwrap_or("[未设置]".to_string())
1801 .green()
1802 );
1803
1804 println!(
1805 "8. 禁用非必要流量 (CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC): {}",
1806 config
1807 .claude_code_disable_nonessential_traffic
1808 .map(|t| t.to_string())
1809 .unwrap_or("[未设置]".to_string())
1810 .green()
1811 );
1812
1813 println!(
1814 "9. 默认 Sonnet 模型 (ANTHROPIC_DEFAULT_SONNET_MODEL): {}",
1815 config
1816 .anthropic_default_sonnet_model
1817 .as_deref()
1818 .unwrap_or("[未设置]")
1819 .green()
1820 );
1821
1822 println!(
1823 "A. 默认 Opus 模型 (ANTHROPIC_DEFAULT_OPUS_MODEL): {}",
1824 config
1825 .anthropic_default_opus_model
1826 .as_deref()
1827 .unwrap_or("[未设置]")
1828 .green()
1829 );
1830
1831 println!(
1832 "B. 默认 Haiku 模型 (ANTHROPIC_DEFAULT_HAIKU_MODEL): {}",
1833 config
1834 .anthropic_default_haiku_model
1835 .as_deref()
1836 .unwrap_or("[未设置]")
1837 .green()
1838 );
1839
1840 println!("{}", "─────────────────────────".blue());
1841 println!(
1842 "S. {} | Q. {}",
1843 "保存更改".green().bold(),
1844 "返回上一级菜单".blue()
1845 );
1846}
1847
1848fn edit_string_field(
1850 field_name: &str,
1851 current_value: &str,
1852 validator: impl Fn(&str) -> Result<()>,
1853) -> Result<Option<String>> {
1854 println!("\n编辑{field_name}:");
1855 println!("当前值: {}", current_value.cyan());
1856 print!("新值 (回车保持不变): ");
1857 io::stdout().flush()?;
1858
1859 let mut input = String::new();
1860 io::stdin().read_line(&mut input)?;
1861 let input = input.trim();
1862
1863 if !input.is_empty() {
1864 validator(input)?;
1865 println!("{field_name}已更新为: {}", input.green());
1866 Ok(Some(input.to_string()))
1867 } else {
1868 Ok(None)
1869 }
1870}
1871
1872type OptionalStringResult = Result<Option<Option<String>>>;
1874
1875fn edit_optional_string_field(
1877 field_name: &str,
1878 current_value: Option<&str>,
1879) -> OptionalStringResult {
1880 println!("\n编辑{field_name}:");
1881 println!("当前值: {}", current_value.unwrap_or("[未设置]").cyan());
1882 print!("新值 (回车保持不变,输入空格清除): ");
1883 io::stdout().flush()?;
1884
1885 let mut input = String::new();
1886 io::stdin().read_line(&mut input)?;
1887 let input = input.trim();
1888
1889 if !input.is_empty() {
1890 if input == " " {
1891 println!("{}", format!("{field_name}已清除").green());
1892 Ok(Some(None))
1893 } else {
1894 println!("{field_name}已更新为: {}", input.green());
1895 Ok(Some(Some(input.to_string())))
1896 }
1897 } else {
1898 Ok(None)
1899 }
1900}
1901
1902type OptionalU32Result = Result<Option<Option<u32>>>;
1904
1905fn edit_optional_u32_field(field_name: &str, current_value: Option<u32>) -> OptionalU32Result {
1907 println!("\n编辑{field_name}:");
1908 println!(
1909 "当前值: {}",
1910 current_value
1911 .map(|t| t.to_string())
1912 .unwrap_or("[未设置]".to_string())
1913 .cyan()
1914 );
1915 print!("新值 (回车保持不变,输入 0 清除): ");
1916 io::stdout().flush()?;
1917
1918 let mut input = String::new();
1919 io::stdin().read_line(&mut input)?;
1920 let input = input.trim();
1921
1922 if !input.is_empty() {
1923 if input == "0" {
1924 println!("{}", format!("{field_name}已清除").green());
1925 Ok(Some(None))
1926 } else if let Ok(value) = input.parse::<u32>() {
1927 println!("{field_name}已更新为: {}", value.to_string().green());
1928 Ok(Some(Some(value)))
1929 } else {
1930 println!("{}", "错误: 请输入有效的数字".red());
1931 Ok(None)
1932 }
1933 } else {
1934 Ok(None)
1935 }
1936}
1937
1938fn edit_field_alias(config: &mut Configuration) -> Result<()> {
1940 let validator = |input: &str| -> Result<()> {
1941 if input.contains(char::is_whitespace) {
1942 anyhow::bail!("错误: 别名不能包含空白字符");
1943 }
1944 if input == "cc" {
1945 anyhow::bail!("错误: 'cc' 是保留名称");
1946 }
1947 Ok(())
1948 };
1949
1950 match edit_string_field("别名", &config.alias_name, validator) {
1951 Ok(Some(new_value)) => config.alias_name = new_value,
1952 Ok(None) => {}
1953 Err(e) => println!("{}", e.to_string().red()),
1954 }
1955 Ok(())
1956}
1957
1958fn edit_field_token(config: &mut Configuration) -> Result<()> {
1960 let no_validator = |_: &str| -> Result<()> { Ok(()) };
1961 if let Some(new_value) = edit_string_field(
1962 "令牌",
1963 &format_token_for_display(&config.token),
1964 no_validator,
1965 )? {
1966 config.token = new_value;
1967 println!("{}", "令牌已更新".green());
1968 }
1969 Ok(())
1970}
1971
1972fn edit_field_url(config: &mut Configuration) -> Result<()> {
1974 let no_validator = |_: &str| -> Result<()> { Ok(()) };
1975 if let Some(new_value) = edit_string_field("URL", &config.url, no_validator)? {
1976 config.url = new_value;
1977 }
1978 Ok(())
1979}
1980
1981fn edit_field_model(config: &mut Configuration) -> Result<()> {
1983 if let Some(result) = edit_optional_string_field("模型", config.model.as_deref())? {
1984 config.model = result;
1985 }
1986 Ok(())
1987}
1988
1989fn edit_field_small_fast_model(config: &mut Configuration) -> Result<()> {
1991 if let Some(result) =
1992 edit_optional_string_field("快速模型", config.small_fast_model.as_deref())?
1993 {
1994 config.small_fast_model = result;
1995 }
1996 Ok(())
1997}
1998
1999fn edit_field_max_thinking_tokens(config: &mut Configuration) -> Result<()> {
2001 if let Some(result) = edit_optional_u32_field("最大思考令牌数", config.max_thinking_tokens)?
2002 {
2003 config.max_thinking_tokens = result;
2004 }
2005 Ok(())
2006}
2007
2008fn edit_field_api_timeout_ms(config: &mut Configuration) -> Result<()> {
2010 if let Some(result) = edit_optional_u32_field("API超时时间 (毫秒)", config.api_timeout_ms)?
2011 {
2012 config.api_timeout_ms = result;
2013 }
2014 Ok(())
2015}
2016
2017fn edit_field_claude_code_disable_nonessential_traffic(config: &mut Configuration) -> Result<()> {
2019 if let Some(result) = edit_optional_u32_field(
2020 "禁用非必要流量标志",
2021 config.claude_code_disable_nonessential_traffic,
2022 )? {
2023 config.claude_code_disable_nonessential_traffic = result;
2024 }
2025 Ok(())
2026}
2027
2028fn edit_field_anthropic_default_sonnet_model(config: &mut Configuration) -> Result<()> {
2030 if let Some(result) = edit_optional_string_field(
2031 "默认 Sonnet 模型",
2032 config.anthropic_default_sonnet_model.as_deref(),
2033 )? {
2034 config.anthropic_default_sonnet_model = result;
2035 }
2036 Ok(())
2037}
2038
2039fn edit_field_anthropic_default_opus_model(config: &mut Configuration) -> Result<()> {
2041 if let Some(result) = edit_optional_string_field(
2042 "默认 Opus 模型",
2043 config.anthropic_default_opus_model.as_deref(),
2044 )? {
2045 config.anthropic_default_opus_model = result;
2046 }
2047 Ok(())
2048}
2049
2050fn edit_field_anthropic_default_haiku_model(config: &mut Configuration) -> Result<()> {
2052 if let Some(result) = edit_optional_string_field(
2053 "默认 Haiku 模型",
2054 config.anthropic_default_haiku_model.as_deref(),
2055 )? {
2056 config.anthropic_default_haiku_model = result;
2057 }
2058 Ok(())
2059}
2060
2061fn save_configuration_changes(original_alias: &str, new_config: &Configuration) -> Result<()> {
2063 let mut storage = ConfigStorage::load()?;
2065
2066 if original_alias != new_config.alias_name
2068 && storage.get_configuration(&new_config.alias_name).is_some()
2069 {
2070 println!("\n{}", "别名冲突!".red().bold());
2071 println!("配置 '{}' 已存在", new_config.alias_name.yellow());
2072 print!("是否覆盖现有配置? (y/N): ");
2073 io::stdout().flush()?;
2074
2075 let mut input = String::new();
2076 io::stdin().read_line(&mut input)?;
2077 let input = input.trim().to_lowercase();
2078
2079 if input != "y" && input != "yes" {
2080 println!("{}", "编辑已取消".yellow());
2081 return Ok(());
2082 }
2083 }
2084
2085 storage.update_configuration(original_alias, new_config.clone())?;
2087 storage.save()?;
2088
2089 println!("\n{}", "配置已成功保存!".green().bold());
2090
2091 Ok(())
2092}