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::{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
18/// Calculate display width of a character
19/// Returns 2 for wide characters (CJK), 1 for others
20fn 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
36/// Truncate text to fit within available width, considering character display width
37fn 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
55/// Clean up terminal state by leaving alternate screen and disabling raw mode
56fn cleanup_terminal(stdout: &mut io::Stdout) {
57    let _ = execute!(stdout, terminal::LeaveAlternateScreen);
58    let _ = terminal::disable_raw_mode();
59}
60
61/// Border drawing utilities for terminal compatibility
62struct BorderDrawing {
63    /// Check if terminal supports Unicode box drawing characters
64    pub unicode_supported: bool,
65}
66
67impl BorderDrawing {
68    /// Create new border drawing utility
69    fn new() -> Self {
70        let unicode_supported = Self::detect_unicode_support();
71        Self { unicode_supported }
72    }
73
74    /// Detect if terminal supports Unicode characters
75    fn detect_unicode_support() -> bool {
76        // Check environment variables that indicate Unicode support
77        if let Ok(term) = std::env::var("TERM") {
78            // Modern terminals that support Unicode
79            if term.contains("xterm") || term.contains("screen") || term == "tmux-256color" {
80                return true;
81            }
82        }
83
84        // Check locale settings
85        if let Ok(lang) = std::env::var("LANG")
86            && (lang.contains("UTF-8") || lang.contains("utf8"))
87        {
88            return true;
89        }
90
91        // Conservative fallback - assume Unicode is supported for better UX
92        // If issues arise, ASCII fallback will be manually triggered
93        true
94    }
95
96    /// Draw top border with title
97    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                // Title too long, use simple border
104                format!("╔{}╗", "═".repeat(width.saturating_sub(2)))
105            } else {
106                let inner_width = width.saturating_sub(2); // Total width minus borders
107                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            // ASCII fallback
119            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    /// Draw middle border line with text
140    fn draw_middle_line(&self, text: &str, width: usize) -> String {
141        let text_len = text_display_width(text);
142        // Account for borders: "║ " (1+1) + " ║" (1+1) = 4 characters
143        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            // Truncate text to fit within available width, considering display width
153            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    /// Draw bottom border
167    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
176/// Handle interactive current command
177///
178/// Provides interactive menu for:
179/// 1. Execute claude --dangerously-skip-permissions
180/// 2. Switch configuration (lists available aliases)
181/// 3. Exit
182///
183/// # Errors
184/// Returns error if file operations fail or user input fails
185pub 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    // Try to enable interactive menu with keyboard navigation
194    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            // Full interactive mode with arrow keys for main menu
206            let result = handle_main_menu_interactive(&mut stdout, &storage);
207
208            // Always restore terminal
209            let _ = execute!(stdout, terminal::LeaveAlternateScreen);
210            let _ = terminal::disable_raw_mode();
211
212            return result;
213        } else {
214            // Fallback to simple mode
215            let _ = terminal::disable_raw_mode();
216        }
217    }
218
219    // Fallback to simple numbered menu
220    handle_main_menu_simple(&storage)
221}
222
223/// Handle main menu with keyboard navigation
224fn 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        // Clear screen and redraw
234        execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
235        execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
236
237        // Header - use BorderDrawing for compatibility
238        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        // Draw menu items
258        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        // Ensure output is flushed
267        stdout.flush()?;
268
269        // Handle input with error recovery
270        let event = match event::read() {
271            Ok(event) => event,
272            Err(e) => {
273                // Clean up terminal state on input error
274                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                        // Execute terminal cleanup here
296                        cleanup_terminal(stdout);
297
298                        return handle_main_menu_action(selected_index, storage);
299                    }
300                    KeyCode::Esc => {
301                        // Clean up terminal before exit
302                        cleanup_terminal(stdout);
303
304                        println!("\nExiting...");
305                        return Ok(());
306                    }
307                    _ => {}
308                }
309            }
310            Event::Key(_) => {} // Ignore key release events
311            _ => {}
312        }
313    }
314}
315
316/// Handle main menu simple fallback
317fn 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
345/// Handle main menu action based on selected index
346fn 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            // Use the interactive selection instead of simple menu
354            handle_interactive_selection(storage)?;
355        }
356        2 => {
357            println!("Exiting...");
358        }
359        _ => {
360            println!("Invalid selection");
361        }
362    }
363    Ok(())
364}
365
366/// Handle interactive configuration selection with real-time preview
367///
368/// # Arguments
369/// * `storage` - Reference to configuration storage
370///
371/// # Errors
372/// Returns error if terminal operations fail or user selection fails
373pub 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    // Try to enable raw mode, fallback to simple menu if it fails
385    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            // Full interactive mode with arrow keys
397            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            // Always restore terminal
407            let _ = execute!(stdout, terminal::LeaveAlternateScreen);
408            let _ = terminal::disable_raw_mode();
409
410            return result;
411        } else {
412            // Fallback to simple mode
413            let _ = terminal::disable_raw_mode();
414        }
415    }
416
417    // Fallback to simple numbered menu
418    handle_simple_interactive_menu(&configs.iter().collect::<Vec<_>>(), storage)
419}
420
421/// Handle full interactive menu with arrow key navigation and pagination
422fn 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    // Handle empty configuration list
430    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(); // Wait for user input
438        return Ok(());
439    }
440
441    const PAGE_SIZE: usize = 9; // Maximum 9 configs per page
442
443    // Calculate pagination info
444    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        // Calculate current page config range
453        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        // Clear screen and redraw
458        execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
459        execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
460
461        // Header with pagination info - use BorderDrawing for compatibility
462        let border = BorderDrawing::new();
463        // Width needs to accommodate: ║ (1) + space (1) + text (76) + space (1) + ║ (1) = 80
464        // Text width includes arrows (↑↓) and Chinese characters counted as 2 columns each
465        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        // Add official option (always visible, always red)
507        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        // Draw current page configs with proper numbering
522        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; // Numbers 1-9 for current page
525            let actual_index = actual_config_index + 1; // +1 because official is at index 0
526            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                // Show details with improved formatting and alignment
537                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        // Add exit option (always visible)
553        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        // Show pagination help if needed
573        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        // Ensure output is flushed
586        stdout.flush()?;
587
588        // Handle input with error recovery
589        let event = match event::read() {
590            Ok(event) => event,
591            Err(e) => {
592                // Clean up terminal state on input error
593                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                        // Reset selection to first item of new page
616                        let new_page_start_idx = current_page * PAGE_SIZE;
617                        *selected_index = new_page_start_idx + 1; // +1 because official is at index 0
618                    }
619                }
620                KeyCode::PageUp | KeyCode::Char('p') | KeyCode::Char('P') => {
621                    if total_pages > 1 && current_page > 0 {
622                        current_page -= 1;
623                        // Reset selection to first item of new page
624                        let new_page_start_idx = current_page * PAGE_SIZE;
625                        *selected_index = new_page_start_idx + 1; // +1 because official is at index 0
626                    }
627                }
628                KeyCode::Enter => {
629                    // Clean up terminal before processing selection
630                    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                    // Clean up terminal before exit
641                    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                    // Map digit to current page config
649                    if digit >= 1 && digit <= page_configs.len() {
650                        let actual_config_index = start_idx + (digit - 1);
651                        let selection_index = actual_config_index + 1; // +1 because official is at index 0
652
653                        // Clean up terminal before processing selection
654                        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                    // Invalid digit - ignore silently
664                }
665                KeyCode::Char('r') | KeyCode::Char('R') => {
666                    // Clean up terminal before processing selection
667                    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                    // Only allow editing if a config is selected (not official or exit)
673                    if *selected_index > 0 && *selected_index <= configs.len() {
674                        // Clean up terminal before entering edit mode
675                        cleanup_terminal(stdout);
676
677                        let config_index = *selected_index - 1; // -1 because official is at index 0
678
679                        // Try to edit the configuration
680                        let edit_result = handle_config_edit(&configs[config_index]);
681
682                        // Re-enter alternate screen and raw mode before continuing
683                        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                            // Check if edit was successful (saved) or if user cancelled
692                            match edit_result {
693                                Ok(_) => {
694                                    // Configuration was saved successfully, reload configs
695                                    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                                        // Keep selection within bounds
699                                        if *selected_index > configs.len() + 1 {
700                                            *selected_index = configs.len() + 1;
701                                        }
702                                    }
703                                    // Continue the loop to display updated configs
704                                    continue;
705                                }
706                                Err(e) => {
707                                    // Check if this is a "return to menu" error (user cancelled)
708                                    if e.downcast_ref::<EditModeError>()
709                                        == Some(&EditModeError::ReturnToMenu)
710                                    {
711                                        // User cancelled edit, just continue the loop
712                                        continue;
713                                    }
714                                    // For other errors, propagate them up
715                                    cleanup_terminal(stdout);
716                                    return Err(e);
717                                }
718                            }
719                        }
720                    }
721                    // Invalid selection - ignore silently
722                }
723                KeyCode::Char('q') | KeyCode::Char('Q') => {
724                    // Clean up terminal before processing selection
725                    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(_) => {} // Ignore key release events
737            _ => {}
738        }
739    }
740}
741
742/// Handle simple interactive menu (fallback)
743fn handle_simple_interactive_menu(
744    configs: &[&Configuration],
745    storage: &ConfigStorage,
746) -> Result<()> {
747    const PAGE_SIZE: usize = 9; // Same page size as full interactive menu
748
749    // If configs fit in one page, show the simple original menu
750    if configs.len() <= PAGE_SIZE {
751        return handle_simple_single_page_menu(configs, storage);
752    }
753
754    // Multi-page simple menu
755    let total_pages = configs.len().div_ceil(PAGE_SIZE);
756    let mut current_page = 0;
757
758    loop {
759        // Calculate current page config range
760        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        // Add official option (always available)
772        println!("{} {}", "[r]".red().bold(), "official".red());
773        println!("   Use official Claude API (no custom configuration)");
774        println!();
775
776        // Show current page configs with improved formatting
777        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            // Show config details with consistent formatting
787            let details = format_config_details(config, "   ", true);
788            for detail_line in details {
789                println!("{detail_line}");
790            }
791            println!();
792        }
793
794        // Exit option
795        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                // Official option
814                println!("Using official Claude configuration");
815
816                // Update settings.json to remove Anthropic configuration
817                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                // Edit functionality for simple menu
827                // In simple menu, we don't have a selected config, so we can't edit
828                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; // +1 because official is at index 0
849                    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
863/// Handle simple single page menu (original behavior for ≤9 configs)
864fn handle_simple_single_page_menu(
865    configs: &[&Configuration],
866    storage: &ConfigStorage,
867) -> Result<()> {
868    println!("\n{}", "Available Configurations:".blue().bold());
869
870    // Add official option (first)
871    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, // +2 because official is at position 1
879            config.alias_name.green()
880        );
881
882        // Show config details with consistent formatting
883        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            // Official option
901            println!("Using official Claude configuration");
902
903            // Update settings.json to remove Anthropic configuration
904            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) // -1 to account for official option at index 0
915        }
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
927/// Handle the actual selection and configuration switch
928fn 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        // Official option (reset to default)
936        println!("\nUsing official Claude configuration");
937
938        // Update settings.json to remove Anthropic configuration
939        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        // Switch to selected configuration
948        let config_index = selected_index - 1; // -1 because official is at index 0
949        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        // Show selected configuration details with consistent formatting
958        let details = format_config_details(&selected_config, "", false);
959        for detail_line in details {
960            println!("{detail_line}");
961        }
962
963        // Update settings.json with the configuration
964        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        // Exit
976        println!("\nExiting...");
977        Ok(())
978    }
979}
980
981/// Launch Claude CLI with environment variables and exec to replace current process
982pub 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    // Set environment variables for current process
989    for (key, value) in env_config.as_env_tuples() {
990        unsafe {
991            std::env::set_var(&key, &value);
992        }
993    }
994
995    // On Unix systems, use exec to replace current process
996    #[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        // exec never returns on success, so if we get here, it failed
1006        anyhow::bail!("Failed to exec claude: {}", error);
1007    }
1008
1009    // On non-Unix systems, fallback to spawn and wait
1010    #[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
1035/// Execute claude command with or without --dangerously-skip-permissions using exec
1036///
1037/// # Arguments
1038/// * `skip_permissions` - Whether to add --dangerously-skip-permissions flag
1039fn execute_claude_command(skip_permissions: bool) -> Result<()> {
1040    println!("Launching Claude CLI...");
1041
1042    // On Unix systems, use exec to replace current process
1043    #[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        // exec never returns on success, so if we get here, it failed
1053        anyhow::bail!("Failed to exec claude: {}", error);
1054    }
1055
1056    // On non-Unix systems, fallback to spawn and wait
1057    #[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
1084/// Read input from stdin with a prompt
1085///
1086/// # Arguments
1087/// * `prompt` - The prompt to display to the user
1088///
1089/// # Returns
1090/// The user's input as a String
1091pub 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
1101/// Read sensitive input (token) with a prompt (without echoing)
1102///
1103/// # Arguments
1104/// * `prompt` - The prompt to display to the user
1105///
1106/// # Returns
1107/// The user's input as a String
1108pub 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
1118/// Format configuration details with consistent indentation and alignment
1119///
1120/// This function provides unified formatting for configuration display across
1121/// all interactive menus, ensuring consistent visual presentation.
1122///
1123/// # Arguments
1124/// * `config` - The configuration to format
1125/// * `indent` - Base indentation string (e.g., "    " or "   ")
1126/// * `compact` - Whether to use compact formatting (single line where possible)
1127///
1128/// # Returns  
1129/// Vector of formatted lines for configuration display
1130fn format_config_details(config: &Configuration, indent: &str, _compact: bool) -> Vec<String> {
1131    let mut lines = Vec::new();
1132
1133    // Calculate optimal field width for alignment
1134    let terminal_width = get_terminal_width();
1135    let _available_width = terminal_width.saturating_sub(text_display_width(indent) + 8);
1136
1137    // Field labels with consistent width for alignment
1138    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    // Find the widest label for alignment
1150    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    // Format token with proper alignment
1168    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    // Format URL with proper alignment
1177    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    // Format model information if available
1186    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    // Format small fast model if available
1197    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    // Format max thinking tokens if available
1208    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    // Format API timeout if available
1224    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    // Format disable nonessential traffic flag if available
1240    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    // Format default Sonnet model if available
1256    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    // Format default Opus model if available
1272    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    // Format default Haiku model if available
1288    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        // Should create without panic - testing that BorderDrawing can be instantiated
1314    }
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        // All borders should have the same character length (approximately)
1368        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 pagination calculation logic
1378    #[test]
1379    fn test_pagination_calculation() {
1380        const PAGE_SIZE: usize = 9;
1381
1382        // Test single page scenarios
1383        assert_eq!(1_usize.div_ceil(PAGE_SIZE), 1); // 1 config -> 1 page
1384        assert_eq!(9_usize.div_ceil(PAGE_SIZE), 1); // 9 configs -> 1 page
1385
1386        // Test multi-page scenarios
1387        assert_eq!(10_usize.div_ceil(PAGE_SIZE), 2); // 10 configs -> 2 pages
1388        assert_eq!(18_usize.div_ceil(PAGE_SIZE), 2); // 18 configs -> 2 pages
1389        assert_eq!(19_usize.div_ceil(PAGE_SIZE), 3); // 19 configs -> 3 pages
1390        assert_eq!(27_usize.div_ceil(PAGE_SIZE), 3); // 27 configs -> 3 pages
1391        assert_eq!(28_usize.div_ceil(PAGE_SIZE), 4); // 28 configs -> 4 pages
1392    }
1393
1394    /// Test page range calculation
1395    #[test]
1396    fn test_page_range_calculation() {
1397        const PAGE_SIZE: usize = 9;
1398
1399        // Test first page
1400        let current_page = 0;
1401        let start_idx = current_page * PAGE_SIZE; // 0
1402        let end_idx = std::cmp::min(start_idx + PAGE_SIZE, 15); // min(9, 15) = 9
1403        assert_eq!(start_idx, 0);
1404        assert_eq!(end_idx, 9);
1405        assert_eq!(end_idx - start_idx, 9); // Full page
1406
1407        // Test second page
1408        let current_page = 1;
1409        let start_idx = current_page * PAGE_SIZE; // 9
1410        let end_idx = std::cmp::min(start_idx + PAGE_SIZE, 15); // min(18, 15) = 15
1411        assert_eq!(start_idx, 9);
1412        assert_eq!(end_idx, 15);
1413        assert_eq!(end_idx - start_idx, 6); // Partial page
1414
1415        // Test edge case: exactly PAGE_SIZE configs
1416        let current_page = 0;
1417        let start_idx = current_page * PAGE_SIZE; // 0
1418        let end_idx = std::cmp::min(start_idx + PAGE_SIZE, PAGE_SIZE); // min(9, 9) = 9
1419        assert_eq!(start_idx, 0);
1420        assert_eq!(end_idx, 9);
1421        assert_eq!(end_idx - start_idx, 9); // Full page
1422    }
1423
1424    /// Test digit key mapping to config indices
1425    #[test]
1426    fn test_digit_mapping_to_config_index() {
1427        const PAGE_SIZE: usize = 9;
1428
1429        // Test first page mapping (configs 0-8)
1430        let current_page = 0;
1431        let start_idx = current_page * PAGE_SIZE; // 0
1432
1433        // Digit 1 should map to config index 0
1434        let digit = 1;
1435        let actual_config_index = start_idx + (digit - 1); // 0 + (1-1) = 0
1436        assert_eq!(actual_config_index, 0);
1437
1438        // Digit 9 should map to config index 8
1439        let digit = 9;
1440        let actual_config_index = start_idx + (digit - 1); // 0 + (9-1) = 8
1441        assert_eq!(actual_config_index, 8);
1442
1443        // Test second page mapping (configs 9-17)
1444        let current_page = 1;
1445        let start_idx = current_page * PAGE_SIZE; // 9
1446
1447        // Digit 1 should map to config index 9
1448        let digit = 1;
1449        let actual_config_index = start_idx + (digit - 1); // 9 + (1-1) = 9
1450        assert_eq!(actual_config_index, 9);
1451
1452        // Digit 5 should map to config index 13
1453        let digit = 5;
1454        let actual_config_index = start_idx + (digit - 1); // 9 + (5-1) = 13
1455        assert_eq!(actual_config_index, 13);
1456    }
1457
1458    /// Test selection index conversion for handle_selection_action
1459    #[test]
1460    fn test_selection_index_conversion() {
1461        // Test mapping digit to selection index for handle_selection_action
1462        // Note: handle_selection_action expects indices where:
1463        // - 0 = official config
1464        // - 1 = first user config
1465        // - 2 = second user config, etc.
1466
1467        const PAGE_SIZE: usize = 9;
1468
1469        // First page, digit 1 -> config index 0 -> selection index 1
1470        let current_page = 0;
1471        let start_idx = current_page * PAGE_SIZE; // 0
1472        let digit = 1;
1473        let actual_config_index = start_idx + (digit - 1); // 0
1474        let selection_index = actual_config_index + 1; // +1 because official is at index 0
1475        assert_eq!(selection_index, 1);
1476
1477        // Second page, digit 1 -> config index 9 -> selection index 10
1478        let current_page = 1;
1479        let start_idx = current_page * PAGE_SIZE; // 9
1480        let digit = 1;
1481        let actual_config_index = start_idx + (digit - 1); // 9
1482        let selection_index = actual_config_index + 1; // +1 because official is at index 0
1483        assert_eq!(selection_index, 10);
1484    }
1485
1486    /// Test page navigation bounds checking
1487    #[test]
1488    fn test_page_navigation_bounds() {
1489        const PAGE_SIZE: usize = 9;
1490        let total_configs: usize = 25; // 3 pages total
1491        let total_pages = total_configs.div_ceil(PAGE_SIZE); // 3 pages
1492        assert_eq!(total_pages, 3);
1493
1494        // Test first page - can't go to previous
1495        let mut current_page = 0;
1496        if current_page > 0 {
1497            current_page -= 1;
1498        }
1499        assert_eq!(current_page, 0); // Should stay at 0
1500
1501        // Test last page - can't go to next
1502        let mut current_page = total_pages - 1; // 2 (last page)
1503        if current_page < total_pages - 1 {
1504            current_page += 1;
1505        }
1506        assert_eq!(current_page, 2); // Should stay at 2
1507
1508        // Test middle page navigation
1509        let mut current_page = 1;
1510
1511        // Can go to next page
1512        if current_page < total_pages - 1 {
1513            current_page += 1;
1514        }
1515        assert_eq!(current_page, 2);
1516
1517        // Can go to previous page
1518        if current_page > 0 {
1519            current_page = current_page.saturating_sub(1);
1520        }
1521        assert_eq!(current_page, 1);
1522    }
1523
1524    /// Test boundary conditions for digit key processing
1525    #[test]
1526    fn test_digit_key_boundary_conditions() {
1527        const PAGE_SIZE: usize = 9;
1528
1529        // Test digit 0 (should be ignored)
1530        let digit = 0;
1531        assert!(digit < 1, "Digit 0 should be less than 1 and ignored");
1532
1533        // Test digit beyond available configs (should be ignored)
1534        let configs_len = 5; // Only 5 configs available
1535        let page_configs_len = std::cmp::min(PAGE_SIZE, configs_len); // 5
1536        let digit = 9; // User presses 9
1537        assert!(
1538            digit > page_configs_len,
1539            "Digit 9 should be beyond available configs (5) and ignored"
1540        );
1541
1542        // Test valid digit range
1543        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 empty configuration list handling
1553    #[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        // Verify that empty check comes before pagination calculation
1562        let configs_len = empty_configs.len(); // 0
1563        assert_eq!(configs_len, 0, "Empty configs should have length 0");
1564
1565        // No pagination should be calculated for empty configs
1566        // (function should return early)
1567    }
1568
1569    /// Test page navigation boundary conditions
1570    #[test]
1571    fn test_page_navigation_boundaries() {
1572        const PAGE_SIZE: usize = 9;
1573        let total_configs: usize = 20; // 3 pages total
1574        let total_pages = total_configs.div_ceil(PAGE_SIZE); // 3 pages
1575
1576        // Test first page navigation (cannot go to previous page)
1577        let mut current_page = 0;
1578        let original_page = current_page;
1579
1580        // Simulate PageUp on first page (should not change)
1581        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        // Test last page navigation (cannot go to next page)
1590        let mut current_page = total_pages - 1; // Last page (2)
1591        let original_page = current_page;
1592
1593        // Simulate PageDown on last page (should not change)
1594        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        // Test valid navigation from middle page
1603        let mut current_page = 1; // Middle page
1604
1605        // Navigate to next page
1606        if current_page < total_pages - 1 {
1607            current_page += 1;
1608        }
1609        assert_eq!(current_page, 2, "Should navigate to next page");
1610
1611        // Navigate to previous page
1612        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 j key navigation (should move selection down like Down arrow)
1619    #[test]
1620    fn test_j_key_navigation() {
1621        let mut selected_index: usize = 0;
1622        let configs_len = 5; // 5 configs + 1 official + 1 exit = 7 total options
1623
1624        // Test j key moves selection down
1625        // j key should behave like Down arrow
1626        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        // Test j key at bottom boundary (should not go beyond configs_len + 1)
1632        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 k key navigation (should move selection up like Up arrow)
1644    #[test]
1645    fn test_k_key_navigation() {
1646        let mut selected_index: usize = 5;
1647
1648        // Test k key moves selection up
1649        // k key should behave like Up arrow
1650        selected_index = selected_index.saturating_sub(1);
1651        assert_eq!(selected_index, 4, "k key should move selection up by one");
1652
1653        // Test k key at top boundary (should not go below 0)
1654        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 j/k key boundary conditions match arrow key behavior
1664    #[test]
1665    fn test_jk_key_boundary_conditions() {
1666        const CONFIGS_LEN: usize = 5;
1667
1668        // Test j key at bottom boundary (same as Down arrow)
1669        let mut selected_index: usize = CONFIGS_LEN + 1; // At exit option
1670        let original_index = selected_index;
1671        if selected_index < CONFIGS_LEN + 1 {
1672            selected_index += 1; // This is what j key does
1673        }
1674        assert_eq!(
1675            selected_index, original_index,
1676            "j key should respect bottom boundary like Down arrow"
1677        );
1678
1679        // Test k key at top boundary (same as Up arrow)
1680        let mut selected_index: usize = 0; // At official option
1681        let original_index = selected_index;
1682        selected_index = selected_index.saturating_sub(1); // This is what k key does
1683        assert_eq!(
1684            selected_index, original_index,
1685            "k key should respect top boundary like Up arrow"
1686        );
1687    }
1688}
1689
1690/// Error type for handling edit mode navigation
1691#[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
1706/// Handle configuration editing with interactive field selection
1707fn 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    // Create a mutable copy for editing
1714    let mut editing_config = config.clone();
1715    let original_alias = config.alias_name.clone();
1716
1717    loop {
1718        // Display current field values
1719        display_edit_menu(&editing_config);
1720
1721        // Get user input for field selection
1722        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        // Note: Both lowercase and uppercase are accepted for commands
1731        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                // Save changes
1745                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
1758/// Display the edit menu with current field values
1759fn 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
1848/// Helper function to edit a string field
1849fn 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
1872/// Type alias for optional string field result
1873type OptionalStringResult = Result<Option<Option<String>>>;
1874
1875/// Helper function to edit an optional string field (can be cleared)
1876fn 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
1902/// Type alias for optional u32 field result
1903type OptionalU32Result = Result<Option<Option<u32>>>;
1904
1905/// Helper function to edit an optional u32 field (can be cleared)
1906fn 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
1938/// Edit alias field
1939fn 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
1958/// Edit token field
1959fn 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
1972/// Edit URL field
1973fn 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
1981/// Edit model field
1982fn 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
1989/// Edit small_fast_model field
1990fn 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
1999/// Edit max_thinking_tokens field
2000fn 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
2008/// Edit api_timeout_ms field
2009fn 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
2017/// Edit claude_code_disable_nonessential_traffic field
2018fn 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
2028/// Edit anthropic_default_sonnet_model field
2029fn 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
2039/// Edit anthropic_default_opus_model field
2040fn 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
2050/// Edit anthropic_default_haiku_model field
2051fn 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
2061/// Save configuration changes to disk and handle alias conflicts
2062fn save_configuration_changes(original_alias: &str, new_config: &Configuration) -> Result<()> {
2063    // Load current storage
2064    let mut storage = ConfigStorage::load()?;
2065
2066    // Check for alias conflicts if alias changed
2067    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    // Update configuration using the method from config_storage.rs
2086    storage.update_configuration(original_alias, new_config.clone())?;
2087    storage.save()?;
2088
2089    println!("\n{}", "配置已成功保存!".green().bold());
2090
2091    Ok(())
2092}