Skip to main content

cc_switch/interactive/
interactive.rs

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
17/// Calculate display width of a character
18/// Returns 2 for wide characters (CJK), 1 for others
19pub(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
35/// Truncate text to fit within available width, considering character display width
36pub(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
54/// Clean up terminal state by leaving alternate screen and disabling raw mode
55pub(crate) fn cleanup_terminal(stdout: &mut io::Stdout) {
56    let _ = execute!(stdout, terminal::LeaveAlternateScreen);
57    let _ = terminal::disable_raw_mode();
58}
59
60/// Border drawing utilities for terminal compatibility
61pub(crate) struct BorderDrawing {
62    /// Check if terminal supports Unicode box drawing characters
63    pub unicode_supported: bool,
64}
65
66impl BorderDrawing {
67    /// Create new border drawing utility
68    pub(crate) fn new() -> Self {
69        let unicode_supported = Self::detect_unicode_support();
70        Self { unicode_supported }
71    }
72
73    /// Detect if terminal supports Unicode characters
74    fn detect_unicode_support() -> bool {
75        crate::platform::unicode_support_enabled()
76    }
77
78    /// Draw top border with title
79    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                // Title too long, use simple border
86                format!("╔{}╗", "═".repeat(width.saturating_sub(2)))
87            } else {
88                let inner_width = width.saturating_sub(2); // Total width minus borders
89                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            // ASCII fallback
101            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    /// Draw middle border line with text
122    pub(crate) fn draw_middle_line(&self, text: &str, width: usize) -> String {
123        let text_len = text_display_width(text);
124        // Account for borders: "║ " (1+1) + " ║" (1+1) = 4 characters
125        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            // Truncate text to fit within available width, considering display width
135            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    /// Draw bottom border
149    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
158/// Handle interactive current command
159///
160/// Provides interactive menu for:
161/// 1. Execute claude --dangerously-skip-permissions
162/// 2. Switch configuration (lists available aliases)
163/// 3. Exit
164///
165/// # Errors
166/// Returns error if file operations fail or user input fails
167pub 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    // Try to enable interactive menu with keyboard navigation
176    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            // Full interactive mode with arrow keys for main menu
188            let result = handle_main_menu_interactive(&mut stdout, &storage);
189
190            // Always restore terminal
191            let _ = execute!(stdout, terminal::LeaveAlternateScreen);
192            let _ = terminal::disable_raw_mode();
193
194            return result;
195        } else {
196            // Fallback to simple mode
197            let _ = terminal::disable_raw_mode();
198        }
199    }
200
201    // Fallback to simple numbered menu
202    handle_main_menu_simple(&storage)
203}
204
205/// Handle main menu with keyboard navigation
206fn 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        // Clear screen and redraw
216        execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
217        execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
218
219        // Header - use BorderDrawing for compatibility
220        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        // Draw menu items
240        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        // Ensure output is flushed
249        stdout.flush()?;
250
251        // Handle input with error recovery
252        let event = match event::read() {
253            Ok(event) => event,
254            Err(e) => {
255                // Clean up terminal state on input error
256                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                        // Execute terminal cleanup here
277                        cleanup_terminal(stdout);
278
279                        return handle_main_menu_action(selected_index, storage);
280                    }
281                    KeyCode::Esc => {
282                        // Clean up terminal before exit
283                        cleanup_terminal(stdout);
284
285                        println!("\nExiting...");
286                        return Ok(());
287                    }
288                    _ => {}
289                }
290            }
291            Event::Key(_) => {} // Ignore key release events
292            _ => {}
293        }
294    }
295}
296
297/// Handle main menu simple fallback
298fn 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
326/// Handle main menu action based on selected index
327fn 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            // Use the interactive selection instead of simple menu
335            handle_interactive_selection(storage)?;
336        }
337        2 => {
338            println!("Exiting...");
339        }
340        _ => {
341            println!("Invalid selection");
342        }
343    }
344    Ok(())
345}
346
347/// Handle interactive configuration selection with real-time preview
348///
349/// # Arguments
350/// * `storage` - Reference to configuration storage
351///
352/// # Errors
353/// Returns error if terminal operations fail or user selection fails
354pub 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    // Try to enable raw mode, fallback to simple menu if it fails
366    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            // Full interactive mode with arrow keys
378            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            // Always restore terminal
388            let _ = execute!(stdout, terminal::LeaveAlternateScreen);
389            let _ = terminal::disable_raw_mode();
390
391            return result;
392        } else {
393            // Fallback to simple mode
394            let _ = terminal::disable_raw_mode();
395        }
396    }
397
398    // Fallback to simple numbered menu
399    handle_simple_interactive_menu(&configs.iter().collect::<Vec<_>>(), storage)
400}
401
402/// Handle full interactive menu with arrow key navigation and pagination
403fn 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    // Handle empty configuration list
411    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(); // Wait for user input
419        return Ok(());
420    }
421
422    const PAGE_SIZE: usize = 9; // Maximum 9 configs per page
423
424    // Calculate pagination info
425    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        // Calculate current page config range
434        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        // Clear screen and redraw
439        execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
440        execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
441
442        // Header with pagination info - use BorderDrawing for compatibility
443        let border = BorderDrawing::new();
444        // Width needs to accommodate: ║ (1) + space (1) + text (76) + space (1) + ║ (1) = 80
445        // Text width includes arrows (↑↓) and Chinese characters counted as 2 columns each
446        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        // Add official option (always visible, always red)
488        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        // Draw current page configs with proper numbering
503        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; // Numbers 1-9 for current page
506            let actual_index = actual_config_index + 1; // +1 because official is at index 0
507            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                // Show details with improved formatting and alignment
518                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        // Add exit option (always visible)
534        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        // Show pagination help if needed
554        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        // Ensure output is flushed
567        stdout.flush()?;
568
569        // Handle input with error recovery
570        let event = match event::read() {
571            Ok(event) => event,
572            Err(e) => {
573                // Clean up terminal state on input error
574                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                    // Clean up terminal before processing selection
612                    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                    // Clean up terminal before exit
623                    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                    // Map digit to current page config
631                    if digit >= 1 && digit <= page_configs.len() {
632                        let actual_config_index = start_idx + (digit - 1);
633                        let selection_index = actual_config_index + 1; // +1 because official is at index 0
634
635                        // Clean up terminal before processing selection
636                        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                    // Invalid digit - ignore silently
646                }
647                KeyCode::Char('r') | KeyCode::Char('R') => {
648                    // Clean up terminal before processing selection
649                    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                    // Clean up terminal before processing selection
699                    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(_) => {} // Ignore key release events
711            _ => {}
712        }
713    }
714}
715
716/// Handle simple interactive menu (fallback)
717fn handle_simple_interactive_menu(
718    configs: &[&Configuration],
719    storage: &ConfigStorage,
720) -> Result<()> {
721    const PAGE_SIZE: usize = 9; // Same page size as full interactive menu
722
723    // If configs fit in one page, show the simple original menu
724    if configs.len() <= PAGE_SIZE {
725        return handle_simple_single_page_menu(configs, storage);
726    }
727
728    // Multi-page simple menu
729    let total_pages = configs.len().div_ceil(PAGE_SIZE);
730    let mut current_page = 0;
731
732    loop {
733        // Calculate current page config range
734        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        // Add official option (always available)
746        println!("{} {}", "[r]".red().bold(), "official".red());
747        println!("   Use official Claude API (no custom configuration)");
748        println!();
749
750        // Show current page configs with improved formatting
751        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            // Show config details with consistent formatting
761            let details = format_config_details(config, "   ", true);
762            for detail_line in details {
763                println!("{detail_line}");
764            }
765            println!();
766        }
767
768        // Exit option
769        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                // Official option
788                println!("Using official Claude configuration");
789
790                // Update settings.json to remove Anthropic configuration
791                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                // Edit functionality for simple menu
801                // In simple menu, we don't have a selected config, so we can't edit
802                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; // +1 because official is at index 0
823                    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
837/// Handle simple single page menu (original behavior for ≤9 configs)
838fn handle_simple_single_page_menu(
839    configs: &[&Configuration],
840    storage: &ConfigStorage,
841) -> Result<()> {
842    println!("\n{}", "Available Configurations:".blue().bold());
843
844    // Add official option (first)
845    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, // +2 because official is at position 1
853            config.alias_name.green()
854        );
855
856        // Show config details with consistent formatting
857        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            // Official option
875            println!("Using official Claude configuration");
876
877            // Update settings.json to remove Anthropic configuration
878            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) // -1 to account for official option at index 0
889        }
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
901/// Handle the actual selection and configuration switch
902fn 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        // Official option (reset to default)
910        println!("\nUsing official Claude configuration");
911
912        // Update settings.json to remove Anthropic configuration
913        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        // Switch to selected configuration
927        let config_index = selected_index - 1; // -1 because official is at index 0
928        let mut selected_config = configs[config_index].clone();
929
930        // Consult daemon state: substitute proxy URL if daemon is alive.
931        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        // Show selected configuration details with consistent formatting
956        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        // Update settings.json with the configuration
965        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        // Exit
977        println!("\nExiting...");
978        Ok(())
979    }
980}
981
982/// Launch Claude CLI with environment variables and exec to replace current process
983pub 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    // Clean up orphaned alias files from terminated sessions
992    let _ = ClaudeSettings::cleanup_orphan_alias_files();
993
994    // Write per-PID alias file for statusLine isolation
995    // On Unix with exec, this file won't be cleaned up (orphan), but that's acceptable
996    // On non-Unix, we clean it after the process exits
997    if let Some(alias) = env_config.env_vars.get("CC_SWITCH_CURRENT_ALIAS") {
998        ClaudeSettings::write_current_alias_for_pid(alias)?;
999    }
1000
1001    // On Unix systems, use exec to replace current process
1002    #[cfg(unix)]
1003    {
1004        use std::os::unix::process::CommandExt;
1005        let mut command = Command::new(resolve_npm_cli("claude"));
1006        // Explicitly pass environment variables to ensure they're inherited
1007        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        // exec never returns on success, so if we get here, it failed
1020        // Clean up per-PID file on exec failure
1021        let _ = ClaudeSettings::clear_current_alias_for_pid();
1022        anyhow::bail!("Failed to exec claude: {}", error);
1023    }
1024
1025    // On non-Unix systems, fallback to spawn and wait
1026    #[cfg(not(unix))]
1027    {
1028        use std::process::Stdio;
1029        let mut command = Command::new(resolve_npm_cli("claude"));
1030        // Explicitly pass environment variables to ensure they're inherited
1031        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        // Clean up per-PID file after Claude exits
1054        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
1063/// Execute claude command with or without --dangerously-skip-permissions using exec
1064///
1065/// # Arguments
1066/// * `skip_permissions` - Whether to add --dangerously-skip-permissions flag
1067fn execute_claude_command(skip_permissions: bool) -> Result<()> {
1068    println!("Launching Claude CLI...");
1069
1070    // On Unix systems, use exec to replace current process
1071    #[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        // exec never returns on success, so if we get here, it failed
1081        anyhow::bail!("Failed to exec claude: {}", error);
1082    }
1083
1084    // On non-Unix systems, fallback to spawn and wait
1085    #[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
1113/// Read input from stdin with a prompt
1114///
1115/// # Arguments
1116/// * `prompt` - The prompt to display to the user
1117///
1118/// # Returns
1119/// The user's input as a String
1120pub 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
1130/// Read sensitive input (token) with a prompt (without echoing)
1131///
1132/// # Arguments
1133/// * `prompt` - The prompt to display to the user
1134///
1135/// # Returns
1136/// The user's input as a String
1137pub 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
1147/// Format configuration details with consistent indentation and alignment
1148///
1149/// This function provides unified formatting for configuration display across
1150/// all interactive menus, ensuring consistent visual presentation.
1151///
1152/// # Arguments
1153/// * `config` - The configuration to format
1154/// * `indent` - Base indentation string (e.g., "    " or "   ")
1155/// * `compact` - Whether to use compact formatting (single line where possible)
1156///
1157/// # Returns  
1158/// Vector of formatted lines for configuration display
1159fn format_config_details(config: &Configuration, indent: &str, _compact: bool) -> Vec<String> {
1160    let mut lines = Vec::new();
1161
1162    // Calculate optimal field width for alignment
1163    let terminal_width = get_terminal_width();
1164    let _available_width = terminal_width.saturating_sub(text_display_width(indent) + 8);
1165
1166    // Field labels with consistent width for alignment
1167    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    // Find the widest label for alignment
1185    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    // Format token with proper alignment
1209    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    // Format URL with proper alignment
1218    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    // Format model information if available
1227    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    // Format small fast model if available
1238    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    // Format max thinking tokens if available
1249    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    // Format API timeout if available
1265    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    // Format disable nonessential traffic flag if available
1281    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    // Format default Sonnet model if available
1297    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    // Format default Opus model if available
1313    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    // Format default Haiku model if available
1329    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    // Format subagent model if available
1345    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    // Format disable non-streaming fallback if available
1361    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    // Format effort level if available
1377    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    // Format disable prompt caching if available
1393    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    // Format disable experimental betas if available
1409    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    // Format disable auto-updater if available
1425    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        // Should create without panic - testing that BorderDrawing can be instantiated
1451    }
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        // All borders should have the same character length (approximately)
1505        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 pagination calculation logic
1515    #[test]
1516    fn test_pagination_calculation() {
1517        const PAGE_SIZE: usize = 9;
1518
1519        // Test single page scenarios
1520        assert_eq!(1_usize.div_ceil(PAGE_SIZE), 1); // 1 config -> 1 page
1521        assert_eq!(9_usize.div_ceil(PAGE_SIZE), 1); // 9 configs -> 1 page
1522
1523        // Test multi-page scenarios
1524        assert_eq!(10_usize.div_ceil(PAGE_SIZE), 2); // 10 configs -> 2 pages
1525        assert_eq!(18_usize.div_ceil(PAGE_SIZE), 2); // 18 configs -> 2 pages
1526        assert_eq!(19_usize.div_ceil(PAGE_SIZE), 3); // 19 configs -> 3 pages
1527        assert_eq!(27_usize.div_ceil(PAGE_SIZE), 3); // 27 configs -> 3 pages
1528        assert_eq!(28_usize.div_ceil(PAGE_SIZE), 4); // 28 configs -> 4 pages
1529    }
1530
1531    /// Test page range calculation
1532    #[test]
1533    fn test_page_range_calculation() {
1534        const PAGE_SIZE: usize = 9;
1535
1536        // Test first page
1537        let current_page = 0;
1538        let start_idx = current_page * PAGE_SIZE; // 0
1539        let end_idx = std::cmp::min(start_idx + PAGE_SIZE, 15); // min(9, 15) = 9
1540        assert_eq!(start_idx, 0);
1541        assert_eq!(end_idx, 9);
1542        assert_eq!(end_idx - start_idx, 9); // Full page
1543
1544        // Test second page
1545        let current_page = 1;
1546        let start_idx = current_page * PAGE_SIZE; // 9
1547        let end_idx = std::cmp::min(start_idx + PAGE_SIZE, 15); // min(18, 15) = 15
1548        assert_eq!(start_idx, 9);
1549        assert_eq!(end_idx, 15);
1550        assert_eq!(end_idx - start_idx, 6); // Partial page
1551
1552        // Test edge case: exactly PAGE_SIZE configs
1553        let current_page = 0;
1554        let start_idx = current_page * PAGE_SIZE; // 0
1555        let end_idx = std::cmp::min(start_idx + PAGE_SIZE, PAGE_SIZE); // min(9, 9) = 9
1556        assert_eq!(start_idx, 0);
1557        assert_eq!(end_idx, 9);
1558        assert_eq!(end_idx - start_idx, 9); // Full page
1559    }
1560
1561    /// Test digit key mapping to config indices
1562    #[test]
1563    fn test_digit_mapping_to_config_index() {
1564        const PAGE_SIZE: usize = 9;
1565
1566        // Test first page mapping (configs 0-8)
1567        let current_page = 0;
1568        let start_idx = current_page * PAGE_SIZE; // 0
1569
1570        // Digit 1 should map to config index 0
1571        let digit = 1;
1572        let actual_config_index = start_idx + (digit - 1); // 0 + (1-1) = 0
1573        assert_eq!(actual_config_index, 0);
1574
1575        // Digit 9 should map to config index 8
1576        let digit = 9;
1577        let actual_config_index = start_idx + (digit - 1); // 0 + (9-1) = 8
1578        assert_eq!(actual_config_index, 8);
1579
1580        // Test second page mapping (configs 9-17)
1581        let current_page = 1;
1582        let start_idx = current_page * PAGE_SIZE; // 9
1583
1584        // Digit 1 should map to config index 9
1585        let digit = 1;
1586        let actual_config_index = start_idx + (digit - 1); // 9 + (1-1) = 9
1587        assert_eq!(actual_config_index, 9);
1588
1589        // Digit 5 should map to config index 13
1590        let digit = 5;
1591        let actual_config_index = start_idx + (digit - 1); // 9 + (5-1) = 13
1592        assert_eq!(actual_config_index, 13);
1593    }
1594
1595    /// Test selection index conversion for handle_selection_action
1596    #[test]
1597    fn test_selection_index_conversion() {
1598        // Test mapping digit to selection index for handle_selection_action
1599        // Note: handle_selection_action expects indices where:
1600        // - 0 = official config
1601        // - 1 = first user config
1602        // - 2 = second user config, etc.
1603
1604        const PAGE_SIZE: usize = 9;
1605
1606        // First page, digit 1 -> config index 0 -> selection index 1
1607        let current_page = 0;
1608        let start_idx = current_page * PAGE_SIZE; // 0
1609        let digit = 1;
1610        let actual_config_index = start_idx + (digit - 1); // 0
1611        let selection_index = actual_config_index + 1; // +1 because official is at index 0
1612        assert_eq!(selection_index, 1);
1613
1614        // Second page, digit 1 -> config index 9 -> selection index 10
1615        let current_page = 1;
1616        let start_idx = current_page * PAGE_SIZE; // 9
1617        let digit = 1;
1618        let actual_config_index = start_idx + (digit - 1); // 9
1619        let selection_index = actual_config_index + 1; // +1 because official is at index 0
1620        assert_eq!(selection_index, 10);
1621    }
1622
1623    /// Test page navigation bounds checking
1624    #[test]
1625    fn test_page_navigation_bounds() {
1626        const PAGE_SIZE: usize = 9;
1627        let total_configs: usize = 25; // 3 pages total
1628        let total_pages = total_configs.div_ceil(PAGE_SIZE); // 3 pages
1629        assert_eq!(total_pages, 3);
1630
1631        // Test first page - can't go to previous
1632        let mut current_page = 0;
1633        if current_page > 0 {
1634            current_page -= 1;
1635        }
1636        assert_eq!(current_page, 0); // Should stay at 0
1637
1638        // Test last page - can't go to next
1639        let mut current_page = total_pages - 1; // 2 (last page)
1640        if current_page < total_pages - 1 {
1641            current_page += 1;
1642        }
1643        assert_eq!(current_page, 2); // Should stay at 2
1644
1645        // Test middle page navigation
1646        let mut current_page = 1;
1647
1648        // Can go to next page
1649        if current_page < total_pages - 1 {
1650            current_page += 1;
1651        }
1652        assert_eq!(current_page, 2);
1653
1654        // Can go to previous page
1655        if current_page > 0 {
1656            current_page = current_page.saturating_sub(1);
1657        }
1658        assert_eq!(current_page, 1);
1659    }
1660
1661    /// Test boundary conditions for digit key processing
1662    #[test]
1663    fn test_digit_key_boundary_conditions() {
1664        const PAGE_SIZE: usize = 9;
1665
1666        // Test digit 0 (should be ignored)
1667        let digit = 0;
1668        assert!(digit < 1, "Digit 0 should be less than 1 and ignored");
1669
1670        // Test digit beyond available configs (should be ignored)
1671        let configs_len = 5; // Only 5 configs available
1672        let page_configs_len = std::cmp::min(PAGE_SIZE, configs_len); // 5
1673        let digit = 9; // User presses 9
1674        assert!(
1675            digit > page_configs_len,
1676            "Digit 9 should be beyond available configs (5) and ignored"
1677        );
1678
1679        // Test valid digit range
1680        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 empty configuration list handling
1690    #[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        // Verify that empty check comes before pagination calculation
1699        let configs_len = empty_configs.len(); // 0
1700        assert_eq!(configs_len, 0, "Empty configs should have length 0");
1701
1702        // No pagination should be calculated for empty configs
1703        // (function should return early)
1704    }
1705
1706    /// Test page navigation boundary conditions
1707    #[test]
1708    fn test_page_navigation_boundaries() {
1709        const PAGE_SIZE: usize = 9;
1710        let total_configs: usize = 20; // 3 pages total
1711        let total_pages = total_configs.div_ceil(PAGE_SIZE); // 3 pages
1712
1713        // Test first page navigation (cannot go to previous page)
1714        let mut current_page = 0;
1715        let original_page = current_page;
1716
1717        // Simulate PageUp on first page (should not change)
1718        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        // Test last page navigation (cannot go to next page)
1727        let mut current_page = total_pages - 1; // Last page (2)
1728        let original_page = current_page;
1729
1730        // Simulate PageDown on last page (should not change)
1731        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        // Test valid navigation from middle page
1740        let mut current_page = 1; // Middle page
1741
1742        // Navigate to next page
1743        if current_page < total_pages - 1 {
1744            current_page += 1;
1745        }
1746        assert_eq!(current_page, 2, "Should navigate to next page");
1747
1748        // Navigate to previous page
1749        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 j key navigation (should move selection down like Down arrow)
1756    #[test]
1757    fn test_j_key_navigation() {
1758        let mut selected_index: usize = 0;
1759        let configs_len = 5; // 5 configs + 1 official + 1 exit = 7 total options
1760
1761        // Test j key moves selection down
1762        // j key should behave like Down arrow
1763        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        // Test j key at bottom boundary (should not go beyond configs_len + 1)
1769        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 k key navigation (should move selection up like Up arrow)
1781    #[test]
1782    fn test_k_key_navigation() {
1783        let mut selected_index: usize = 5;
1784
1785        // Test k key moves selection up
1786        // k key should behave like Up arrow
1787        selected_index = selected_index.saturating_sub(1);
1788        assert_eq!(selected_index, 4, "k key should move selection up by one");
1789
1790        // Test k key at top boundary (should not go below 0)
1791        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 j/k key boundary conditions match arrow key behavior
1801    #[test]
1802    fn test_jk_key_boundary_conditions() {
1803        const CONFIGS_LEN: usize = 5;
1804
1805        // Test j key at bottom boundary (same as Down arrow)
1806        let mut selected_index: usize = CONFIGS_LEN + 1; // At exit option
1807        let original_index = selected_index;
1808        if selected_index < CONFIGS_LEN + 1 {
1809            selected_index += 1; // This is what j key does
1810        }
1811        assert_eq!(
1812            selected_index, original_index,
1813            "j key should respect bottom boundary like Down arrow"
1814        );
1815
1816        // Test k key at top boundary (same as Up arrow)
1817        let mut selected_index: usize = 0; // At official option
1818        let original_index = selected_index;
1819        selected_index = selected_index.saturating_sub(1); // This is what k key does
1820        assert_eq!(
1821            selected_index, original_index,
1822            "k key should respect top boundary like Up arrow"
1823        );
1824    }
1825}
1826
1827/// Error type for handling edit mode navigation
1828#[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
1843/// Handle configuration editing with interactive field selection
1844fn 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    // Create a mutable copy for editing
1851    let mut editing_config = config.clone();
1852    let original_alias = config.alias_name.clone();
1853
1854    loop {
1855        // Display current field values
1856        display_edit_menu(&editing_config);
1857
1858        // Get user input for field selection
1859        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        // Note: Both lowercase and uppercase are accepted for commands
1868        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                // Save changes
1892                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
1905/// Display the edit menu with current field values
1906fn 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
2049/// Helper function to edit a string field
2050pub(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
2073/// Type alias for optional string field result
2074pub(crate) type OptionalStringResult = Result<Option<Option<String>>>;
2075
2076/// Helper function to edit an optional string field (can be cleared)
2077pub(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
2103/// Type alias for optional u32 field result
2104type OptionalU32Result = Result<Option<Option<u32>>>;
2105
2106/// Helper function to edit an optional u32 field (can be cleared)
2107fn 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
2139/// Edit alias field
2140fn 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
2162/// Edit token field
2163fn 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
2176/// Edit URL field
2177fn 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
2185/// Edit model field
2186fn 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
2193/// Edit small_fast_model field
2194fn 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
2203/// Edit max_thinking_tokens field
2204fn 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
2212/// Edit api_timeout_ms field
2213fn 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
2221/// Edit claude_code_disable_nonessential_traffic field
2222fn 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
2232/// Edit anthropic_default_sonnet_model field
2233fn 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
2243/// Edit anthropic_default_opus_model field
2244fn 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
2254/// Edit anthropic_default_haiku_model field
2255fn 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
2265/// Edit claude_code_subagent_model field
2266fn 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
2275/// Edit claude_code_disable_nonstreaming_fallback field
2276fn 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
2286/// Edit claude_code_effort_level field
2287fn 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
2296/// Edit disable_prompt_caching field
2297fn 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
2306/// Edit claude_code_disable_experimental_betas field
2307fn 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
2317/// Edit disable_autoupdater field
2318fn 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
2326/// Save configuration changes to disk and handle alias conflicts
2327fn save_configuration_changes(original_alias: &str, new_config: &Configuration) -> Result<()> {
2328    // Load current storage
2329    let mut storage = ConfigStorage::load()?;
2330
2331    // Check for alias conflicts if alias changed
2332    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    // Update configuration using the method from config_storage.rs
2351    storage.update_configuration(original_alias, new_config.clone())?;
2352    storage.save()?;
2353
2354    println!("\n{}", "配置已成功保存!".green().bold());
2355
2356    Ok(())
2357}