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/// Border drawing utilities for terminal compatibility
19struct BorderDrawing {
20    /// Check if terminal supports Unicode box drawing characters
21    pub unicode_supported: bool,
22}
23
24impl BorderDrawing {
25    /// Create new border drawing utility
26    fn new() -> Self {
27        let unicode_supported = Self::detect_unicode_support();
28        Self { unicode_supported }
29    }
30
31    /// Detect if terminal supports Unicode characters
32    fn detect_unicode_support() -> bool {
33        // Check environment variables that indicate Unicode support
34        if let Ok(term) = std::env::var("TERM") {
35            // Modern terminals that support Unicode
36            if term.contains("xterm") || term.contains("screen") || term == "tmux-256color" {
37                return true;
38            }
39        }
40
41        // Check locale settings
42        if let Ok(lang) = std::env::var("LANG")
43            && (lang.contains("UTF-8") || lang.contains("utf8"))
44        {
45            return true;
46        }
47
48        // Conservative fallback - assume Unicode is supported for better UX
49        // If issues arise, ASCII fallback will be manually triggered
50        true
51    }
52
53    /// Draw top border with title
54    fn draw_top_border(&self, title: &str, width: usize) -> String {
55        if self.unicode_supported {
56            let title_padded = format!(" {title} ");
57            let title_len = text_display_width(&title_padded);
58
59            if title_len >= width.saturating_sub(2) {
60                // Title too long, use simple border
61                format!("╔{}╗", "═".repeat(width.saturating_sub(2)))
62            } else {
63                let inner_width = width.saturating_sub(2); // Total width minus borders
64                let padding_total = inner_width.saturating_sub(title_len);
65                let padding_left = padding_total / 2;
66                let padding_right = padding_total - padding_left;
67                format!(
68                    "╔{}{}{}╗",
69                    "═".repeat(padding_left),
70                    title_padded,
71                    "═".repeat(padding_right)
72                )
73            }
74        } else {
75            // ASCII fallback
76            let title_padded = format!(" {title} ");
77            let title_len = title_padded.len();
78
79            if title_len >= width.saturating_sub(2) {
80                format!("+{}+", "-".repeat(width.saturating_sub(2)))
81            } else {
82                let inner_width = width.saturating_sub(2);
83                let padding_total = inner_width.saturating_sub(title_len);
84                let padding_left = padding_total / 2;
85                let padding_right = padding_total - padding_left;
86                format!(
87                    "+{}{}{}+",
88                    "-".repeat(padding_left),
89                    title_padded,
90                    "-".repeat(padding_right)
91                )
92            }
93        }
94    }
95
96    /// Draw middle border line with text
97    fn draw_middle_line(&self, text: &str, width: usize) -> String {
98        if self.unicode_supported {
99            let text_len = text_display_width(text);
100            // Account for borders: "║ " (1+1) + " ║" (1+1) = 4 characters
101            // But we need to account for actual display width of the text
102            let available_width = width.saturating_sub(4);
103            if text_len > available_width {
104                // Truncate text to fit within available width, considering display width
105                let mut current_width = 0;
106                let truncated: String = text
107                    .chars()
108                    .take_while(|&c| {
109                        let char_width = match c as u32 {
110                            0x00..=0x7F => 1,
111                            0x80..=0x2FF => 1,
112                            0x2190..=0x21FF => 2,
113                            0x3000..=0x303F => 2,
114                            0x3040..=0x309F => 2,
115                            0x30A0..=0x30FF => 2,
116                            0x4E00..=0x9FFF => 2,
117                            0xAC00..=0xD7AF => 2,
118                            0x3400..=0x4DBF => 2,
119                            0xFF01..=0xFF60 => 2,
120                            _ => 1,
121                        };
122                        if current_width + char_width <= available_width {
123                            current_width += char_width;
124                            true
125                        } else {
126                            false
127                        }
128                    })
129                    .collect();
130                // Calculate actual display width of truncated text
131                let truncated_width = text_display_width(&truncated);
132                let padding_spaces = available_width.saturating_sub(truncated_width);
133                format!("║ {}{} ║", truncated, " ".repeat(padding_spaces))
134            } else {
135                let padded_text =
136                    pad_text_to_width(text, available_width, TextAlignment::Left, ' ');
137                format!("║ {padded_text} ║")
138            }
139        } else {
140            // ASCII fallback
141            let text_len = text_display_width(text);
142            let available_width = width.saturating_sub(4);
143            if text_len > available_width {
144                // Truncate text to fit within available width
145                let mut current_width = 0;
146                let truncated: String = text
147                    .chars()
148                    .take_while(|&c| {
149                        let char_width = if (c as u32) <= 0x7F { 1 } else { 2 };
150                        if current_width + char_width <= available_width {
151                            current_width += char_width;
152                            true
153                        } else {
154                            false
155                        }
156                    })
157                    .collect();
158                // Calculate actual display width of truncated text
159                let truncated_width = text_display_width(&truncated);
160                let padding_spaces = available_width.saturating_sub(truncated_width);
161                format!("| {}{} |", truncated, " ".repeat(padding_spaces))
162            } else {
163                let padded_text =
164                    pad_text_to_width(text, available_width, TextAlignment::Left, ' ');
165                format!("| {padded_text} |")
166            }
167        }
168    }
169
170    /// Draw bottom border
171    fn draw_bottom_border(&self, width: usize) -> String {
172        if self.unicode_supported {
173            format!("╚{}╝", "═".repeat(width - 2))
174        } else {
175            format!("+{}+", "-".repeat(width - 2))
176        }
177    }
178}
179
180/// Handle interactive current command
181///
182/// Provides interactive menu for:
183/// 1. Execute claude --dangerously-skip-permissions
184/// 2. Switch configuration (lists available aliases)
185/// 3. Exit
186///
187/// # Errors
188/// Returns error if file operations fail or user input fails
189pub fn handle_current_command() -> Result<()> {
190    let storage = ConfigStorage::load()?;
191
192    println!("\n{}", "Current Configuration:".green().bold());
193    println!("Environment variable mode: configurations are set per-command execution");
194    println!("Use 'cc-switch use <alias>' to launch Claude with specific configuration");
195    println!("Use 'cc-switch use cc' to launch Claude with default settings");
196
197    // Try to enable interactive menu with keyboard navigation
198    let raw_mode_enabled = terminal::enable_raw_mode().is_ok();
199
200    if raw_mode_enabled {
201        let mut stdout = io::stdout();
202        if execute!(
203            stdout,
204            terminal::EnterAlternateScreen,
205            terminal::Clear(terminal::ClearType::All)
206        )
207        .is_ok()
208        {
209            // Full interactive mode with arrow keys for main menu
210            let result = handle_main_menu_interactive(&mut stdout, &storage);
211
212            // Always restore terminal
213            let _ = execute!(stdout, terminal::LeaveAlternateScreen);
214            let _ = terminal::disable_raw_mode();
215
216            return result;
217        } else {
218            // Fallback to simple mode
219            let _ = terminal::disable_raw_mode();
220        }
221    }
222
223    // Fallback to simple numbered menu
224    handle_main_menu_simple(&storage)
225}
226
227/// Handle main menu with keyboard navigation
228fn handle_main_menu_interactive(stdout: &mut io::Stdout, storage: &ConfigStorage) -> Result<()> {
229    let menu_items = [
230        "Execute claude --dangerously-skip-permissions",
231        "Switch configuration",
232        "Exit",
233    ];
234    let mut selected_index = 0;
235
236    loop {
237        // Clear screen and redraw
238        execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
239        execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
240
241        // Header - use BorderDrawing for compatibility
242        let border = BorderDrawing::new();
243        const MAIN_MENU_WIDTH: usize = 68;
244
245        println!(
246            "\r{}",
247            border.draw_top_border("Main Menu", MAIN_MENU_WIDTH).green()
248        );
249        println!(
250            "\r{}",
251            border
252                .draw_middle_line(
253                    "↑↓/jk导航,1-9快选,E-编辑,R-官方,Q-退出,Enter确认,Esc取消",
254                    MAIN_MENU_WIDTH
255                )
256                .green()
257        );
258        println!("\r{}", border.draw_bottom_border(MAIN_MENU_WIDTH).green());
259        println!();
260
261        // Draw menu items
262        for (index, item) in menu_items.iter().enumerate() {
263            if index == selected_index {
264                println!("\r> {} {}", "●".blue().bold(), item.blue().bold());
265            } else {
266                println!("\r  {} {}", "○".dimmed(), item.dimmed());
267            }
268        }
269
270        // Ensure output is flushed
271        stdout.flush()?;
272
273        // Handle input with error recovery
274        let event = match event::read() {
275            Ok(event) => event,
276            Err(e) => {
277                // Clean up terminal state on input error
278                let _ = execute!(stdout, terminal::LeaveAlternateScreen);
279                let _ = terminal::disable_raw_mode();
280                return Err(e.into());
281            }
282        };
283
284        match event {
285            Event::Key(KeyEvent {
286                code,
287                kind: KeyEventKind::Press,
288                ..
289            }) => {
290                match code {
291                    KeyCode::Up => {
292                        selected_index = selected_index.saturating_sub(1);
293                    }
294                    KeyCode::Down => {
295                        if selected_index < menu_items.len() - 1 {
296                            selected_index += 1;
297                        }
298                    }
299                    KeyCode::Enter => {
300                        // Execute terminal cleanup here
301                        let _ = execute!(stdout, terminal::LeaveAlternateScreen);
302                        let _ = terminal::disable_raw_mode();
303
304                        return handle_main_menu_action(selected_index, storage);
305                    }
306                    KeyCode::Esc => {
307                        // Clean up terminal before exit
308                        let _ = execute!(stdout, terminal::LeaveAlternateScreen);
309                        let _ = terminal::disable_raw_mode();
310
311                        println!("\nExiting...");
312                        return Ok(());
313                    }
314                    _ => {}
315                }
316            }
317            Event::Key(_) => {} // Ignore key release events
318            _ => {}
319        }
320    }
321}
322
323/// Handle main menu simple fallback
324fn handle_main_menu_simple(storage: &ConfigStorage) -> Result<()> {
325    loop {
326        println!("\n{}", "Available Actions:".blue().bold());
327        println!("1. Execute claude --dangerously-skip-permissions");
328        println!("2. Switch configuration");
329        println!("3. Exit");
330
331        print!("\nPlease select an option (1-3): ");
332        io::stdout().flush().context("Failed to flush stdout")?;
333
334        let mut input = String::new();
335        io::stdin()
336            .read_line(&mut input)
337            .context("Failed to read input")?;
338
339        let choice = input.trim();
340
341        match choice {
342            "1" => return handle_main_menu_action(0, storage),
343            "2" => return handle_main_menu_action(1, storage),
344            "3" => return handle_main_menu_action(2, storage),
345            _ => {
346                println!("Invalid option. Please select 1-3.");
347            }
348        }
349    }
350}
351
352/// Handle main menu action based on selected index
353fn handle_main_menu_action(selected_index: usize, storage: &ConfigStorage) -> Result<()> {
354    match selected_index {
355        0 => {
356            println!("\nExecuting: claude --dangerously-skip-permissions");
357            execute_claude_command(true)?;
358        }
359        1 => {
360            // Use the interactive selection instead of simple menu
361            handle_interactive_selection(storage)?;
362        }
363        2 => {
364            println!("Exiting...");
365        }
366        _ => {
367            println!("Invalid selection");
368        }
369    }
370    Ok(())
371}
372
373/// Handle interactive configuration selection with real-time preview
374///
375/// # Arguments
376/// * `storage` - Reference to configuration storage
377///
378/// # Errors
379/// Returns error if terminal operations fail or user selection fails
380pub fn handle_interactive_selection(storage: &ConfigStorage) -> Result<()> {
381    if storage.configurations.is_empty() {
382        println!("No configurations available. Use 'add' command to create configurations first.");
383        return Ok(());
384    }
385
386    let mut configs: Vec<&Configuration> = storage.configurations.values().collect();
387    configs.sort_by(|a, b| a.alias_name.cmp(&b.alias_name));
388
389    let mut selected_index = 0;
390
391    // Try to enable raw mode, fallback to simple menu if it fails
392    let raw_mode_enabled = terminal::enable_raw_mode().is_ok();
393
394    if raw_mode_enabled {
395        let mut stdout = io::stdout();
396        if execute!(
397            stdout,
398            terminal::EnterAlternateScreen,
399            terminal::Clear(terminal::ClearType::All)
400        )
401        .is_ok()
402        {
403            // Full interactive mode with arrow keys
404            let result = handle_full_interactive_menu(&mut stdout, &configs, &mut selected_index);
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, 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: &[&Configuration],
425    selected_index: &mut usize,
426) -> Result<()> {
427    // Handle empty configuration list
428    if configs.is_empty() {
429        println!("\r{}", "No configurations available".yellow());
430        println!(
431            "\r{}",
432            "Use 'cc-switch add <alias> <token> <url>' to add configurations first.".dimmed()
433        );
434        println!("\r{}", "Press any key to continue...".dimmed());
435        let _ = event::read(); // Wait for user input
436        return Ok(());
437    }
438
439    const PAGE_SIZE: usize = 9; // Maximum 9 configs per page
440
441    // Calculate pagination info
442    let total_pages = if configs.len() <= PAGE_SIZE {
443        1
444    } else {
445        configs.len().div_ceil(PAGE_SIZE)
446    };
447    let mut current_page = 0;
448
449    loop {
450        // Calculate current page config range
451        let start_idx = current_page * PAGE_SIZE;
452        let end_idx = std::cmp::min(start_idx + PAGE_SIZE, configs.len());
453        let page_configs = &configs[start_idx..end_idx];
454
455        // Clear screen and redraw
456        execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
457        execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
458
459        // Header with pagination info - use BorderDrawing for compatibility
460        let border = BorderDrawing::new();
461        // Width needs to accommodate: ║ (1) + space (1) + text (76) + space (1) + ║ (1) = 80
462        // Text width includes arrows (↑↓) and Chinese characters counted as 2 columns each
463        const CONFIG_MENU_WIDTH: usize = 80;
464
465        println!(
466            "\r{}",
467            border
468                .draw_top_border("Select Configuration", CONFIG_MENU_WIDTH)
469                .green()
470        );
471        if total_pages > 1 {
472            println!(
473                "\r{}",
474                border
475                    .draw_middle_line(
476                        &format!("第 {} 页,共 {} 页", current_page + 1, total_pages),
477                        CONFIG_MENU_WIDTH
478                    )
479                    .green()
480            );
481            println!(
482                "\r{}",
483                border
484                    .draw_middle_line(
485                        "↑↓/jk导航,1-9快选,E-编辑,N/P翻页,R-官方,Q-退出,Enter确认",
486                        CONFIG_MENU_WIDTH
487                    )
488                    .green()
489            );
490        } else {
491            println!(
492                "\r{}",
493                border
494                    .draw_middle_line(
495                        "↑↓/jk导航,1-9快选,E-编辑,R-官方,Q-退出,Enter确认,Esc取消",
496                        CONFIG_MENU_WIDTH
497                    )
498                    .green()
499            );
500        }
501        println!("\r{}", border.draw_bottom_border(CONFIG_MENU_WIDTH).green());
502        println!();
503
504        // Add official option (always visible, always red)
505        let official_index = 0;
506        if *selected_index == official_index {
507            println!(
508                "\r> {} {} {}",
509                "●".red().bold(),
510                "[R]".red().bold(),
511                "official".red().bold()
512            );
513            println!("\r    Use official Claude API (no custom configuration)");
514            println!();
515        } else {
516            println!("\r  {} {} {}", "○".red(), "[R]".red(), "official".red());
517        }
518
519        // Draw current page configs with proper numbering
520        for (page_index, config) in page_configs.iter().enumerate() {
521            let actual_config_index = start_idx + page_index;
522            let display_number = page_index + 1; // Numbers 1-9 for current page
523            let actual_index = actual_config_index + 1; // +1 because official is at index 0
524            let number_label = format!("[{display_number}]");
525
526            if *selected_index == actual_index {
527                println!(
528                    "\r> {} {} {}",
529                    "●".blue().bold(),
530                    number_label.blue().bold(),
531                    config.alias_name.blue().bold()
532                );
533
534                // Show details with improved formatting and alignment
535                let details = format_config_details(config, "\r    ", false);
536                for detail_line in details {
537                    println!("{detail_line}");
538                }
539                println!();
540            } else {
541                println!(
542                    "\r  {} {} {}",
543                    "○".dimmed(),
544                    number_label.dimmed(),
545                    config.alias_name.dimmed()
546                );
547            }
548        }
549
550        // Add exit option (always visible)
551        let exit_index = configs.len() + 1;
552        if *selected_index == exit_index {
553            println!(
554                "\r> {} {} {}",
555                "●".yellow().bold(),
556                "[Q]".yellow().bold(),
557                "Exit".yellow().bold()
558            );
559            println!("\r    Exit without making changes");
560            println!();
561        } else {
562            println!(
563                "\r  {} {} {}",
564                "○".dimmed(),
565                "[Q]".dimmed(),
566                "Exit".dimmed()
567            );
568        }
569
570        // Show pagination help if needed
571        if total_pages > 1 {
572            println!(
573                "\r{}",
574                format!(
575                    "Page Navigation: [N]ext, [P]revious (第 {} 页,共 {} 页)",
576                    current_page + 1,
577                    total_pages
578                )
579                .dimmed()
580            );
581        }
582
583        // Ensure output is flushed
584        stdout.flush()?;
585
586        // Handle input with error recovery
587        let event = match event::read() {
588            Ok(event) => event,
589            Err(e) => {
590                // Clean up terminal state on input error
591                let _ = execute!(stdout, terminal::LeaveAlternateScreen);
592                let _ = terminal::disable_raw_mode();
593                return Err(e.into());
594            }
595        };
596
597        match event {
598            Event::Key(KeyEvent {
599                code,
600                kind: KeyEventKind::Press,
601                ..
602            }) => match code {
603                KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => {
604                    *selected_index = selected_index.saturating_sub(1);
605                }
606                KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => {
607                    if *selected_index < configs.len() + 1 {
608                        *selected_index += 1;
609                    }
610                }
611                KeyCode::PageDown | KeyCode::Char('n') | KeyCode::Char('N') => {
612                    if total_pages > 1 && current_page < total_pages - 1 {
613                        current_page += 1;
614                        // Reset selection to first item of new page
615                        let new_page_start_idx = current_page * PAGE_SIZE;
616                        *selected_index = new_page_start_idx + 1; // +1 because official is at index 0
617                    }
618                }
619                KeyCode::PageUp | KeyCode::Char('p') | KeyCode::Char('P') => {
620                    if total_pages > 1 && current_page > 0 {
621                        current_page -= 1;
622                        // Reset selection to first item of new page
623                        let new_page_start_idx = current_page * PAGE_SIZE;
624                        *selected_index = new_page_start_idx + 1; // +1 because official is at index 0
625                    }
626                }
627                KeyCode::Enter => {
628                    // Clean up terminal before processing selection
629                    let _ = execute!(stdout, terminal::LeaveAlternateScreen);
630                    let _ = terminal::disable_raw_mode();
631
632                    return handle_selection_action(configs, *selected_index);
633                }
634                KeyCode::Esc => {
635                    // Clean up terminal before exit
636                    let _ = execute!(stdout, terminal::LeaveAlternateScreen);
637                    let _ = terminal::disable_raw_mode();
638
639                    println!("\nSelection cancelled");
640                    return Ok(());
641                }
642                KeyCode::Char(c) if c.is_ascii_digit() => {
643                    let digit = c.to_digit(10).unwrap() as usize;
644                    // Map digit to current page config
645                    if digit >= 1 && digit <= page_configs.len() {
646                        let actual_config_index = start_idx + (digit - 1);
647                        let selection_index = actual_config_index + 1; // +1 because official is at index 0
648
649                        // Clean up terminal before processing selection
650                        let _ = execute!(stdout, terminal::LeaveAlternateScreen);
651                        let _ = terminal::disable_raw_mode();
652
653                        return handle_selection_action(configs, selection_index);
654                    }
655                    // Invalid digit - ignore silently
656                }
657                KeyCode::Char('r') | KeyCode::Char('R') => {
658                    // Clean up terminal before processing selection
659                    let _ = execute!(stdout, terminal::LeaveAlternateScreen);
660                    let _ = terminal::disable_raw_mode();
661
662                    return handle_selection_action(configs, 0);
663                }
664                KeyCode::Char('e') | KeyCode::Char('E') => {
665                    // Only allow editing if a config is selected (not official or exit)
666                    if *selected_index > 0 && *selected_index <= configs.len() {
667                        // Clean up terminal before entering edit mode
668                        let _ = execute!(stdout, terminal::LeaveAlternateScreen);
669                        let _ = terminal::disable_raw_mode();
670
671                        let config_index = *selected_index - 1; // -1 because official is at index 0
672
673                        // Check if we should return to menu (user pressed 'q' in edit mode)
674                        if let Err(e) = handle_config_edit(configs[config_index]) {
675                            // Check if this is a "return to menu" error
676                            if e.downcast_ref::<EditModeError>()
677                                == Some(&EditModeError::ReturnToMenu)
678                            {
679                                // Re-enter alternate screen and raw mode, then continue the loop
680                                if execute!(
681                                    stdout,
682                                    terminal::EnterAlternateScreen,
683                                    terminal::Clear(terminal::ClearType::All)
684                                )
685                                .is_ok()
686                                    && terminal::enable_raw_mode().is_ok()
687                                {
688                                    // Continue the menu loop
689                                    continue;
690                                }
691                            }
692                            // For other errors, propagate them up
693                            return Err(e);
694                        }
695                    }
696                    // Invalid selection - ignore silently
697                }
698                KeyCode::Char('q') | KeyCode::Char('Q') => {
699                    // Clean up terminal before processing selection
700                    let _ = execute!(stdout, terminal::LeaveAlternateScreen);
701                    let _ = terminal::disable_raw_mode();
702
703                    return handle_selection_action(configs, configs.len() + 1);
704                }
705                _ => {}
706            },
707            Event::Key(_) => {} // Ignore key release events
708            _ => {}
709        }
710    }
711}
712
713/// Handle simple interactive menu (fallback)
714fn handle_simple_interactive_menu(
715    configs: &[&Configuration],
716    _storage: &ConfigStorage,
717) -> Result<()> {
718    const PAGE_SIZE: usize = 9; // Same page size as full interactive menu
719
720    // If configs fit in one page, show the simple original menu
721    if configs.len() <= PAGE_SIZE {
722        return handle_simple_single_page_menu(configs);
723    }
724
725    // Multi-page simple menu
726    let total_pages = configs.len().div_ceil(PAGE_SIZE);
727    let mut current_page = 0;
728
729    loop {
730        // Calculate current page config range
731        let start_idx = current_page * PAGE_SIZE;
732        let end_idx = std::cmp::min(start_idx + PAGE_SIZE, configs.len());
733        let page_configs = &configs[start_idx..end_idx];
734
735        println!("\n{}", "Available Configurations:".blue().bold());
736        if total_pages > 1 {
737            println!("第 {} 页,共 {} 页", current_page + 1, total_pages);
738            println!("使用 'n' 下一页, 'p' 上一页, 'r' 官方配置, 'q' 退出");
739        }
740        println!();
741
742        // Add official option (always available)
743        println!("{} {}", "[r]".red().bold(), "official".red());
744        println!("   Use official Claude API (no custom configuration)");
745        println!();
746
747        // Show current page configs with improved formatting
748        for (page_index, config) in page_configs.iter().enumerate() {
749            let display_number = page_index + 1;
750
751            println!(
752                "{}. {}",
753                format!("[{display_number}]").green().bold(),
754                config.alias_name.green()
755            );
756
757            // Show config details with consistent formatting
758            let details = format_config_details(config, "   ", true);
759            for detail_line in details {
760                println!("{detail_line}");
761            }
762            println!();
763        }
764
765        // Exit option
766        println!("{} {}", "[q]".yellow().bold(), "Exit".yellow());
767
768        if total_pages > 1 {
769            println!(
770                "\n页面导航: [n]下页, [p]上页 | 配置选择: [1-{}] | [e]编辑 | [r]官方 | [q]退出",
771                page_configs.len()
772            );
773        }
774
775        print!("\n请输入选择: ");
776        io::stdout().flush()?;
777
778        let mut input = String::new();
779        io::stdin().read_line(&mut input)?;
780        let choice = input.trim().to_lowercase();
781
782        match choice.as_str() {
783            "r" => {
784                // Official option
785                println!("Using official Claude configuration");
786                return launch_claude_with_env(EnvironmentConfig::empty());
787            }
788            "e" => {
789                // Edit functionality for simple menu
790                // In simple menu, we don't have a selected config, so we can't edit
791                println!("编辑功能在交互式菜单中可用");
792            }
793            "q" => {
794                println!("Exiting...");
795                return Ok(());
796            }
797            "n" if total_pages > 1 && current_page < total_pages - 1 => {
798                current_page += 1;
799                continue;
800            }
801            "p" if total_pages > 1 && current_page > 0 => {
802                current_page -= 1;
803                continue;
804            }
805            digit_str => {
806                if let Ok(digit) = digit_str.parse::<usize>()
807                    && digit >= 1
808                    && digit <= page_configs.len()
809                {
810                    let actual_config_index = start_idx + (digit - 1);
811                    let selection_index = actual_config_index + 1; // +1 because official is at index 0
812                    return handle_selection_action(configs, selection_index);
813                }
814                println!("无效选择,请重新输入");
815            }
816        }
817    }
818}
819
820/// Handle simple single page menu (original behavior for ≤9 configs)
821fn handle_simple_single_page_menu(configs: &[&Configuration]) -> Result<()> {
822    println!("\n{}", "Available Configurations:".blue().bold());
823
824    // Add official option (first)
825    println!("1. {}", "official".red());
826    println!("   Use official Claude API (no custom configuration)");
827    println!();
828
829    for (index, config) in configs.iter().enumerate() {
830        println!(
831            "{}. {}",
832            index + 2, // +2 because official is at position 1
833            config.alias_name.green()
834        );
835
836        // Show config details with consistent formatting
837        let details = format_config_details(config, "   ", true);
838        for detail_line in details {
839            println!("{detail_line}");
840        }
841        println!();
842    }
843
844    println!("{}. {}", configs.len() + 2, "Exit".yellow());
845
846    print!("\nSelect configuration (1-{}): ", configs.len() + 2);
847    io::stdout().flush()?;
848
849    let mut input = String::new();
850    io::stdin().read_line(&mut input)?;
851
852    match input.trim().parse::<usize>() {
853        Ok(1) => {
854            // Official option
855            println!("Using official Claude configuration");
856            launch_claude_with_env(EnvironmentConfig::empty())
857        }
858        Ok(num) if num >= 2 && num <= configs.len() + 1 => {
859            handle_selection_action(configs, num - 2) // -2 because official is at position 1
860        }
861        Ok(num) if num == configs.len() + 2 => {
862            println!("Exiting...");
863            Ok(())
864        }
865        _ => {
866            println!("Invalid selection");
867            Ok(())
868        }
869    }
870}
871
872/// Handle the actual selection and configuration switch
873fn handle_selection_action(configs: &[&Configuration], selected_index: usize) -> Result<()> {
874    if selected_index == 0 {
875        // Official option (reset to default)
876        println!("\nUsing official Claude configuration");
877        launch_claude_with_env(EnvironmentConfig::empty())
878    } else if selected_index <= configs.len() {
879        // Switch to selected configuration
880        let config_index = selected_index - 1; // -1 because official is at index 0
881        let selected_config = configs[config_index].clone();
882        let env_config = EnvironmentConfig::from_config(&selected_config);
883
884        println!(
885            "\nSwitched to configuration '{}'",
886            selected_config.alias_name.green().bold()
887        );
888
889        // Show selected configuration details with consistent formatting
890        let details = format_config_details(&selected_config, "", false);
891        for detail_line in details {
892            println!("{detail_line}");
893        }
894
895        launch_claude_with_env(env_config)
896    } else {
897        // Exit
898        println!("\nExiting...");
899        Ok(())
900    }
901}
902
903/// Launch Claude CLI with environment variables and exec to replace current process
904fn launch_claude_with_env(env_config: EnvironmentConfig) -> Result<()> {
905    println!("\nWaiting 0.5 seconds before launching Claude...");
906    thread::sleep(Duration::from_millis(500));
907
908    println!("Launching Claude CLI...");
909
910    // Set environment variables for current process
911    for (key, value) in env_config.as_env_tuples() {
912        unsafe {
913            std::env::set_var(&key, &value);
914        }
915    }
916
917    // On Unix systems, use exec to replace current process
918    #[cfg(unix)]
919    {
920        use std::os::unix::process::CommandExt;
921        let error = Command::new("claude")
922            .arg("--dangerously-skip-permissions")
923            .exec();
924        // exec never returns on success, so if we get here, it failed
925        anyhow::bail!("Failed to exec claude: {}", error);
926    }
927
928    // On non-Unix systems, fallback to spawn and wait
929    #[cfg(not(unix))]
930    {
931        use std::process::Stdio;
932        let mut child = Command::new("claude")
933            .arg("--dangerously-skip-permissions")
934            .stdin(Stdio::inherit())
935            .stdout(Stdio::inherit())
936            .stderr(Stdio::inherit())
937            .spawn()
938            .context(
939                "Failed to launch Claude CLI. Make sure 'claude' command is available in PATH",
940            )?;
941
942        let status = child.wait()?;
943
944        if !status.success() {
945            anyhow::bail!("Claude CLI exited with error status: {}", status);
946        }
947    }
948}
949
950/// Execute claude command with or without --dangerously-skip-permissions using exec
951///
952/// # Arguments
953/// * `skip_permissions` - Whether to add --dangerously-skip-permissions flag
954fn execute_claude_command(skip_permissions: bool) -> Result<()> {
955    println!("Launching Claude CLI...");
956
957    // On Unix systems, use exec to replace current process
958    #[cfg(unix)]
959    {
960        use std::os::unix::process::CommandExt;
961        let mut command = Command::new("claude");
962        if skip_permissions {
963            command.arg("--dangerously-skip-permissions");
964        }
965
966        let error = command.exec();
967        // exec never returns on success, so if we get here, it failed
968        anyhow::bail!("Failed to exec claude: {}", error);
969    }
970
971    // On non-Unix systems, fallback to spawn and wait
972    #[cfg(not(unix))]
973    {
974        use std::process::Stdio;
975        let mut command = Command::new("claude");
976        if skip_permissions {
977            command.arg("--dangerously-skip-permissions");
978        }
979
980        command
981            .stdin(Stdio::inherit())
982            .stdout(Stdio::inherit())
983            .stderr(Stdio::inherit());
984
985        let mut child = command.spawn().context(
986            "Failed to launch Claude CLI. Make sure 'claude' command is available in PATH",
987        )?;
988
989        let status = child
990            .wait()
991            .context("Failed to wait for Claude CLI process")?;
992
993        if !status.success() {
994            anyhow::bail!("Claude CLI exited with error status: {}", status);
995        }
996    }
997}
998
999/// Read input from stdin with a prompt
1000///
1001/// # Arguments
1002/// * `prompt` - The prompt to display to the user
1003///
1004/// # Returns
1005/// The user's input as a String
1006pub fn read_input(prompt: &str) -> Result<String> {
1007    print!("{prompt}");
1008    io::stdout().flush().context("Failed to flush stdout")?;
1009    let mut input = String::new();
1010    io::stdin()
1011        .read_line(&mut input)
1012        .context("Failed to read input")?;
1013    Ok(input.trim().to_string())
1014}
1015
1016/// Read sensitive input (token) with a prompt (without echoing)
1017///
1018/// # Arguments
1019/// * `prompt` - The prompt to display to the user
1020///
1021/// # Returns
1022/// The user's input as a String
1023pub fn read_sensitive_input(prompt: &str) -> Result<String> {
1024    print!("{prompt}");
1025    io::stdout().flush().context("Failed to flush stdout")?;
1026    let mut input = String::new();
1027    io::stdin()
1028        .read_line(&mut input)
1029        .context("Failed to read input")?;
1030    Ok(input.trim().to_string())
1031}
1032
1033/// Format configuration details with consistent indentation and alignment
1034///
1035/// This function provides unified formatting for configuration display across
1036/// all interactive menus, ensuring consistent visual presentation.
1037///
1038/// # Arguments
1039/// * `config` - The configuration to format
1040/// * `indent` - Base indentation string (e.g., "    " or "   ")
1041/// * `compact` - Whether to use compact formatting (single line where possible)
1042///
1043/// # Returns  
1044/// Vector of formatted lines for configuration display
1045fn format_config_details(config: &Configuration, indent: &str, _compact: bool) -> Vec<String> {
1046    let mut lines = Vec::new();
1047
1048    // Calculate optimal field width for alignment
1049    let terminal_width = get_terminal_width();
1050    let _available_width = terminal_width.saturating_sub(text_display_width(indent) + 8);
1051
1052    // Field labels with consistent width for alignment
1053    let token_label = "Token:";
1054    let url_label = "URL:";
1055    let model_label = "Model:";
1056    let small_model_label = "Small Fast Model:";
1057    let max_thinking_tokens_label = "Max Thinking Tokens:";
1058    let api_timeout_ms_label = "API Timeout (ms):";
1059    let disable_nonessential_traffic_label = "Disable Nonessential Traffic:";
1060    let default_sonnet_model_label = "Default Sonnet Model:";
1061    let default_opus_model_label = "Default Opus Model:";
1062    let default_haiku_model_label = "Default Haiku Model:";
1063
1064    // Find the widest label for alignment
1065    let max_label_width = [
1066        token_label,
1067        url_label,
1068        model_label,
1069        small_model_label,
1070        max_thinking_tokens_label,
1071        api_timeout_ms_label,
1072        disable_nonessential_traffic_label,
1073        default_sonnet_model_label,
1074        default_opus_model_label,
1075        default_haiku_model_label,
1076    ]
1077    .iter()
1078    .map(|label| text_display_width(label))
1079    .max()
1080    .unwrap_or(0);
1081
1082    // Format token with proper alignment
1083    let token_line = format!(
1084        "{}{} {}",
1085        indent,
1086        pad_text_to_width(token_label, max_label_width, TextAlignment::Left, ' '),
1087        format_token_for_display(&config.token).dimmed()
1088    );
1089    lines.push(token_line);
1090
1091    // Format URL with proper alignment
1092    let url_line = format!(
1093        "{}{} {}",
1094        indent,
1095        pad_text_to_width(url_label, max_label_width, TextAlignment::Left, ' '),
1096        config.url.cyan()
1097    );
1098    lines.push(url_line);
1099
1100    // Format model information if available
1101    if let Some(model) = &config.model {
1102        let model_line = format!(
1103            "{}{} {}",
1104            indent,
1105            pad_text_to_width(model_label, max_label_width, TextAlignment::Left, ' '),
1106            model.yellow()
1107        );
1108        lines.push(model_line);
1109    }
1110
1111    // Format small fast model if available
1112    if let Some(small_fast_model) = &config.small_fast_model {
1113        let small_model_line = format!(
1114            "{}{} {}",
1115            indent,
1116            pad_text_to_width(small_model_label, max_label_width, TextAlignment::Left, ' '),
1117            small_fast_model.yellow()
1118        );
1119        lines.push(small_model_line);
1120    }
1121
1122    // Format max thinking tokens if available
1123    if let Some(max_thinking_tokens) = config.max_thinking_tokens {
1124        let tokens_line = format!(
1125            "{}{} {}",
1126            indent,
1127            pad_text_to_width(
1128                max_thinking_tokens_label,
1129                max_label_width,
1130                TextAlignment::Left,
1131                ' '
1132            ),
1133            format!("{}", max_thinking_tokens).yellow()
1134        );
1135        lines.push(tokens_line);
1136    }
1137
1138    // Format API timeout if available
1139    if let Some(api_timeout_ms) = config.api_timeout_ms {
1140        let timeout_line = format!(
1141            "{}{} {}",
1142            indent,
1143            pad_text_to_width(
1144                api_timeout_ms_label,
1145                max_label_width,
1146                TextAlignment::Left,
1147                ' '
1148            ),
1149            format!("{}", api_timeout_ms).yellow()
1150        );
1151        lines.push(timeout_line);
1152    }
1153
1154    // Format disable nonessential traffic flag if available
1155    if let Some(disable_flag) = config.claude_code_disable_nonessential_traffic {
1156        let flag_line = format!(
1157            "{}{} {}",
1158            indent,
1159            pad_text_to_width(
1160                disable_nonessential_traffic_label,
1161                max_label_width,
1162                TextAlignment::Left,
1163                ' '
1164            ),
1165            format!("{}", disable_flag).yellow()
1166        );
1167        lines.push(flag_line);
1168    }
1169
1170    // Format default Sonnet model if available
1171    if let Some(sonnet_model) = &config.anthropic_default_sonnet_model {
1172        let sonnet_line = format!(
1173            "{}{} {}",
1174            indent,
1175            pad_text_to_width(
1176                default_sonnet_model_label,
1177                max_label_width,
1178                TextAlignment::Left,
1179                ' '
1180            ),
1181            sonnet_model.yellow()
1182        );
1183        lines.push(sonnet_line);
1184    }
1185
1186    // Format default Opus model if available
1187    if let Some(opus_model) = &config.anthropic_default_opus_model {
1188        let opus_line = format!(
1189            "{}{} {}",
1190            indent,
1191            pad_text_to_width(
1192                default_opus_model_label,
1193                max_label_width,
1194                TextAlignment::Left,
1195                ' '
1196            ),
1197            opus_model.yellow()
1198        );
1199        lines.push(opus_line);
1200    }
1201
1202    // Format default Haiku model if available
1203    if let Some(haiku_model) = &config.anthropic_default_haiku_model {
1204        let haiku_line = format!(
1205            "{}{} {}",
1206            indent,
1207            pad_text_to_width(
1208                default_haiku_model_label,
1209                max_label_width,
1210                TextAlignment::Left,
1211                ' '
1212            ),
1213            haiku_model.yellow()
1214        );
1215        lines.push(haiku_line);
1216    }
1217
1218    lines
1219}
1220
1221#[cfg(test)]
1222mod border_drawing_tests {
1223    use super::*;
1224
1225    #[test]
1226    fn test_border_drawing_unicode_support() {
1227        let _border = BorderDrawing::new();
1228        // Should create without panic - testing that BorderDrawing can be instantiated
1229    }
1230
1231    #[test]
1232    fn test_border_drawing_top_border() {
1233        let border = BorderDrawing {
1234            unicode_supported: true,
1235        };
1236        let result = border.draw_top_border("Test", 20);
1237        assert!(!result.is_empty());
1238        assert!(result.contains("Test"));
1239    }
1240
1241    #[test]
1242    fn test_border_drawing_ascii_fallback() {
1243        let border = BorderDrawing {
1244            unicode_supported: false,
1245        };
1246        let result = border.draw_top_border("Test", 20);
1247        assert!(!result.is_empty());
1248        assert!(result.contains("Test"));
1249        assert!(result.contains("+"));
1250        assert!(result.contains("-"));
1251    }
1252
1253    #[test]
1254    fn test_border_drawing_middle_line() {
1255        let border = BorderDrawing {
1256            unicode_supported: true,
1257        };
1258        let result = border.draw_middle_line("Test message", 30);
1259        assert!(!result.is_empty());
1260        assert!(result.contains("Test message"));
1261    }
1262
1263    #[test]
1264    fn test_border_drawing_bottom_border() {
1265        let border = BorderDrawing {
1266            unicode_supported: true,
1267        };
1268        let result = border.draw_bottom_border(20);
1269        assert!(!result.is_empty());
1270    }
1271
1272    #[test]
1273    fn test_border_drawing_width_consistency() {
1274        let border = BorderDrawing {
1275            unicode_supported: true,
1276        };
1277        let width = 30;
1278        let top = border.draw_top_border("Title", width);
1279        let middle = border.draw_middle_line("Content", width);
1280        let bottom = border.draw_bottom_border(width);
1281
1282        // All borders should have the same character length (approximately)
1283        assert!(top.chars().count() >= width - 2);
1284        assert!(middle.chars().count() >= width - 2);
1285        assert!(bottom.chars().count() >= width - 2);
1286    }
1287}
1288
1289#[cfg(test)]
1290mod pagination_tests {
1291
1292    /// Test pagination calculation logic
1293    #[test]
1294    fn test_pagination_calculation() {
1295        const PAGE_SIZE: usize = 9;
1296
1297        // Test single page scenarios
1298        assert_eq!(1_usize.div_ceil(PAGE_SIZE), 1); // 1 config -> 1 page
1299        assert_eq!(9_usize.div_ceil(PAGE_SIZE), 1); // 9 configs -> 1 page
1300
1301        // Test multi-page scenarios
1302        assert_eq!(10_usize.div_ceil(PAGE_SIZE), 2); // 10 configs -> 2 pages
1303        assert_eq!(18_usize.div_ceil(PAGE_SIZE), 2); // 18 configs -> 2 pages
1304        assert_eq!(19_usize.div_ceil(PAGE_SIZE), 3); // 19 configs -> 3 pages
1305        assert_eq!(27_usize.div_ceil(PAGE_SIZE), 3); // 27 configs -> 3 pages
1306        assert_eq!(28_usize.div_ceil(PAGE_SIZE), 4); // 28 configs -> 4 pages
1307    }
1308
1309    /// Test page range calculation
1310    #[test]
1311    fn test_page_range_calculation() {
1312        const PAGE_SIZE: usize = 9;
1313
1314        // Test first page
1315        let current_page = 0;
1316        let start_idx = current_page * PAGE_SIZE; // 0
1317        let end_idx = std::cmp::min(start_idx + PAGE_SIZE, 15); // min(9, 15) = 9
1318        assert_eq!(start_idx, 0);
1319        assert_eq!(end_idx, 9);
1320        assert_eq!(end_idx - start_idx, 9); // Full page
1321
1322        // Test second page
1323        let current_page = 1;
1324        let start_idx = current_page * PAGE_SIZE; // 9
1325        let end_idx = std::cmp::min(start_idx + PAGE_SIZE, 15); // min(18, 15) = 15
1326        assert_eq!(start_idx, 9);
1327        assert_eq!(end_idx, 15);
1328        assert_eq!(end_idx - start_idx, 6); // Partial page
1329
1330        // Test edge case: exactly PAGE_SIZE configs
1331        let current_page = 0;
1332        let start_idx = current_page * PAGE_SIZE; // 0
1333        let end_idx = std::cmp::min(start_idx + PAGE_SIZE, PAGE_SIZE); // min(9, 9) = 9
1334        assert_eq!(start_idx, 0);
1335        assert_eq!(end_idx, 9);
1336        assert_eq!(end_idx - start_idx, 9); // Full page
1337    }
1338
1339    /// Test digit key mapping to config indices
1340    #[test]
1341    fn test_digit_mapping_to_config_index() {
1342        const PAGE_SIZE: usize = 9;
1343
1344        // Test first page mapping (configs 0-8)
1345        let current_page = 0;
1346        let start_idx = current_page * PAGE_SIZE; // 0
1347
1348        // Digit 1 should map to config index 0
1349        let digit = 1;
1350        let actual_config_index = start_idx + (digit - 1); // 0 + (1-1) = 0
1351        assert_eq!(actual_config_index, 0);
1352
1353        // Digit 9 should map to config index 8
1354        let digit = 9;
1355        let actual_config_index = start_idx + (digit - 1); // 0 + (9-1) = 8
1356        assert_eq!(actual_config_index, 8);
1357
1358        // Test second page mapping (configs 9-17)
1359        let current_page = 1;
1360        let start_idx = current_page * PAGE_SIZE; // 9
1361
1362        // Digit 1 should map to config index 9
1363        let digit = 1;
1364        let actual_config_index = start_idx + (digit - 1); // 9 + (1-1) = 9
1365        assert_eq!(actual_config_index, 9);
1366
1367        // Digit 5 should map to config index 13
1368        let digit = 5;
1369        let actual_config_index = start_idx + (digit - 1); // 9 + (5-1) = 13
1370        assert_eq!(actual_config_index, 13);
1371    }
1372
1373    /// Test selection index conversion for handle_selection_action
1374    #[test]
1375    fn test_selection_index_conversion() {
1376        // Test mapping digit to selection index for handle_selection_action
1377        // Note: handle_selection_action expects indices where:
1378        // - 0 = official config
1379        // - 1 = first user config
1380        // - 2 = second user config, etc.
1381
1382        const PAGE_SIZE: usize = 9;
1383
1384        // First page, digit 1 -> config index 0 -> selection index 1
1385        let current_page = 0;
1386        let start_idx = current_page * PAGE_SIZE; // 0
1387        let digit = 1;
1388        let actual_config_index = start_idx + (digit - 1); // 0
1389        let selection_index = actual_config_index + 1; // +1 because official is at index 0
1390        assert_eq!(selection_index, 1);
1391
1392        // Second page, digit 1 -> config index 9 -> selection index 10
1393        let current_page = 1;
1394        let start_idx = current_page * PAGE_SIZE; // 9
1395        let digit = 1;
1396        let actual_config_index = start_idx + (digit - 1); // 9
1397        let selection_index = actual_config_index + 1; // +1 because official is at index 0
1398        assert_eq!(selection_index, 10);
1399    }
1400
1401    /// Test page navigation bounds checking
1402    #[test]
1403    fn test_page_navigation_bounds() {
1404        const PAGE_SIZE: usize = 9;
1405        let total_configs: usize = 25; // 3 pages total
1406        let total_pages = total_configs.div_ceil(PAGE_SIZE); // 3 pages
1407        assert_eq!(total_pages, 3);
1408
1409        // Test first page - can't go to previous
1410        let mut current_page = 0;
1411        if current_page > 0 {
1412            current_page -= 1;
1413        }
1414        assert_eq!(current_page, 0); // Should stay at 0
1415
1416        // Test last page - can't go to next
1417        let mut current_page = total_pages - 1; // 2 (last page)
1418        if current_page < total_pages - 1 {
1419            current_page += 1;
1420        }
1421        assert_eq!(current_page, 2); // Should stay at 2
1422
1423        // Test middle page navigation
1424        let mut current_page = 1;
1425
1426        // Can go to next page
1427        if current_page < total_pages - 1 {
1428            current_page += 1;
1429        }
1430        assert_eq!(current_page, 2);
1431
1432        // Can go to previous page
1433        if current_page > 0 {
1434            current_page = current_page.saturating_sub(1);
1435        }
1436        assert_eq!(current_page, 1);
1437    }
1438
1439    /// Test boundary conditions for digit key processing
1440    #[test]
1441    fn test_digit_key_boundary_conditions() {
1442        const PAGE_SIZE: usize = 9;
1443
1444        // Test digit 0 (should be ignored)
1445        let digit = 0;
1446        assert!(digit < 1, "Digit 0 should be less than 1 and ignored");
1447
1448        // Test digit beyond available configs (should be ignored)
1449        let configs_len = 5; // Only 5 configs available
1450        let page_configs_len = std::cmp::min(PAGE_SIZE, configs_len); // 5
1451        let digit = 9; // User presses 9
1452        assert!(
1453            digit > page_configs_len,
1454            "Digit 9 should be beyond available configs (5) and ignored"
1455        );
1456
1457        // Test valid digit range
1458        for digit in 1..=page_configs_len {
1459            assert!(
1460                digit >= 1 && digit <= page_configs_len,
1461                "Digit {} should be valid",
1462                digit
1463            );
1464        }
1465    }
1466
1467    /// Test empty configuration list handling
1468    #[test]
1469    fn test_empty_configs_handling() {
1470        let empty_configs: Vec<String> = Vec::new();
1471        assert!(
1472            empty_configs.is_empty(),
1473            "Empty config list should be properly detected"
1474        );
1475
1476        // Verify that empty check comes before pagination calculation
1477        let configs_len = empty_configs.len(); // 0
1478        assert_eq!(configs_len, 0, "Empty configs should have length 0");
1479
1480        // No pagination should be calculated for empty configs
1481        // (function should return early)
1482    }
1483
1484    /// Test page navigation boundary conditions
1485    #[test]
1486    fn test_page_navigation_boundaries() {
1487        const PAGE_SIZE: usize = 9;
1488        let total_configs: usize = 20; // 3 pages total
1489        let total_pages = total_configs.div_ceil(PAGE_SIZE); // 3 pages
1490
1491        // Test first page navigation (cannot go to previous page)
1492        let mut current_page = 0;
1493        let original_page = current_page;
1494
1495        // Simulate PageUp on first page (should not change)
1496        if current_page > 0 {
1497            current_page -= 1;
1498        }
1499        assert_eq!(
1500            current_page, original_page,
1501            "First page should not navigate to previous"
1502        );
1503
1504        // Test last page navigation (cannot go to next page)
1505        let mut current_page = total_pages - 1; // Last page (2)
1506        let original_page = current_page;
1507
1508        // Simulate PageDown on last page (should not change)
1509        if current_page < total_pages - 1 {
1510            current_page += 1;
1511        }
1512        assert_eq!(
1513            current_page, original_page,
1514            "Last page should not navigate to next"
1515        );
1516
1517        // Test valid navigation from middle page
1518        let mut current_page = 1; // Middle page
1519
1520        // Navigate to next page
1521        if current_page < total_pages - 1 {
1522            current_page += 1;
1523        }
1524        assert_eq!(current_page, 2, "Should navigate to next page");
1525
1526        // Navigate to previous page
1527        if current_page > 0 {
1528            current_page = current_page.saturating_sub(1);
1529        }
1530        assert_eq!(current_page, 1, "Should navigate to previous page");
1531    }
1532
1533    /// Test j key navigation (should move selection down like Down arrow)
1534    #[test]
1535    fn test_j_key_navigation() {
1536        let mut selected_index: usize = 0;
1537        let configs_len = 5; // 5 configs + 1 official + 1 exit = 7 total options
1538
1539        // Test j key moves selection down
1540        // j key should behave like Down arrow
1541        if selected_index < configs_len + 1 {
1542            selected_index += 1;
1543        }
1544        assert_eq!(selected_index, 1, "j key should move selection down by one");
1545
1546        // Test j key at bottom boundary (should not go beyond configs_len + 1)
1547        selected_index = configs_len + 1;
1548        let original_index = selected_index;
1549        if selected_index < configs_len + 1 {
1550            selected_index += 1;
1551        }
1552        assert_eq!(
1553            selected_index, original_index,
1554            "j key should not move beyond bottom boundary"
1555        );
1556    }
1557
1558    /// Test k key navigation (should move selection up like Up arrow)
1559    #[test]
1560    fn test_k_key_navigation() {
1561        let mut selected_index: usize = 5;
1562
1563        // Test k key moves selection up
1564        // k key should behave like Up arrow
1565        selected_index = selected_index.saturating_sub(1);
1566        assert_eq!(selected_index, 4, "k key should move selection up by one");
1567
1568        // Test k key at top boundary (should not go below 0)
1569        selected_index = 0;
1570        let original_index = selected_index;
1571        selected_index = selected_index.saturating_sub(1);
1572        assert_eq!(
1573            selected_index, original_index,
1574            "k key should not move beyond top boundary"
1575        );
1576    }
1577
1578    /// Test j/k key boundary conditions match arrow key behavior
1579    #[test]
1580    fn test_jk_key_boundary_conditions() {
1581        const CONFIGS_LEN: usize = 5;
1582
1583        // Test j key at bottom boundary (same as Down arrow)
1584        let mut selected_index: usize = CONFIGS_LEN + 1; // At exit option
1585        let original_index = selected_index;
1586        if selected_index < CONFIGS_LEN + 1 {
1587            selected_index += 1; // This is what j key does
1588        }
1589        assert_eq!(
1590            selected_index, original_index,
1591            "j key should respect bottom boundary like Down arrow"
1592        );
1593
1594        // Test k key at top boundary (same as Up arrow)
1595        let mut selected_index: usize = 0; // At official option
1596        let original_index = selected_index;
1597        selected_index = selected_index.saturating_sub(1); // This is what k key does
1598        assert_eq!(
1599            selected_index, original_index,
1600            "k key should respect top boundary like Up arrow"
1601        );
1602    }
1603}
1604
1605/// Error type for handling edit mode navigation
1606#[derive(Debug, PartialEq)]
1607enum EditModeError {
1608    ReturnToMenu,
1609}
1610
1611impl std::fmt::Display for EditModeError {
1612    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1613        match self {
1614            EditModeError::ReturnToMenu => write!(f, "return_to_menu"),
1615        }
1616    }
1617}
1618
1619impl std::error::Error for EditModeError {}
1620
1621/// Handle configuration editing with interactive field selection
1622fn handle_config_edit(config: &Configuration) -> Result<()> {
1623    println!("\n{}", "配置编辑模式".green().bold());
1624    println!("{}", "===================".green());
1625    println!("正在编辑配置: {}", config.alias_name.cyan().bold());
1626    println!();
1627
1628    // Create a mutable copy for editing
1629    let mut editing_config = config.clone();
1630    let original_alias = config.alias_name.clone();
1631
1632    loop {
1633        // Display current field values
1634        display_edit_menu(&editing_config);
1635
1636        // Get user input for field selection
1637        println!("\n{}", "提示: 可使用大小写字母".dimmed());
1638        print!("请选择要编辑的字段 (1-9, A-B), 或输入 S 保存, Q 返回上一级菜单: ");
1639        io::stdout().flush()?;
1640
1641        let mut input = String::new();
1642        io::stdin().read_line(&mut input)?;
1643        let input = input.trim();
1644
1645        // Note: Both lowercase and uppercase are accepted for commands
1646        match input {
1647            "1" => edit_field_alias(&mut editing_config)?,
1648            "2" => edit_field_token(&mut editing_config)?,
1649            "3" => edit_field_url(&mut editing_config)?,
1650            "4" => edit_field_model(&mut editing_config)?,
1651            "5" => edit_field_small_fast_model(&mut editing_config)?,
1652            "6" => edit_field_max_thinking_tokens(&mut editing_config)?,
1653            "7" => edit_field_api_timeout_ms(&mut editing_config)?,
1654            "8" => edit_field_claude_code_disable_nonessential_traffic(&mut editing_config)?,
1655            "9" => edit_field_anthropic_default_sonnet_model(&mut editing_config)?,
1656            "10" | "a" | "A" => edit_field_anthropic_default_opus_model(&mut editing_config)?,
1657            "11" | "b" | "B" => edit_field_anthropic_default_haiku_model(&mut editing_config)?,
1658            "s" | "S" => {
1659                // Save changes
1660                return save_configuration_changes(&original_alias, &editing_config);
1661            }
1662            "q" | "Q" => {
1663                println!("\n{}", "返回上一级菜单".blue());
1664                return Err(EditModeError::ReturnToMenu.into());
1665            }
1666            _ => {
1667                println!("{}", "无效选择,请重试".red());
1668            }
1669        }
1670    }
1671}
1672
1673/// Display the edit menu with current field values
1674fn display_edit_menu(config: &Configuration) {
1675    println!("\n{}", "当前配置值:".blue().bold());
1676    println!("{}", "─────────────────────────".blue());
1677
1678    println!("1. 别名 (alias_name): {}", config.alias_name.green());
1679
1680    println!(
1681        "2. 令牌 (ANTHROPIC_AUTH_TOKEN): {}",
1682        format_token_for_display(&config.token).green()
1683    );
1684
1685    println!("3. URL (ANTHROPIC_BASE_URL): {}", config.url.green());
1686
1687    println!(
1688        "4. 模型 (ANTHROPIC_MODEL): {}",
1689        config.model.as_deref().unwrap_or("[未设置]").green()
1690    );
1691
1692    println!(
1693        "5. 快速模型 (ANTHROPIC_SMALL_FAST_MODEL): {}",
1694        config
1695            .small_fast_model
1696            .as_deref()
1697            .unwrap_or("[未设置]")
1698            .green()
1699    );
1700
1701    println!(
1702        "6. 最大思考令牌数 (ANTHROPIC_MAX_THINKING_TOKENS): {}",
1703        config
1704            .max_thinking_tokens
1705            .map(|t| t.to_string())
1706            .unwrap_or("[未设置]".to_string())
1707            .green()
1708    );
1709
1710    println!(
1711        "7. API超时时间 (API_TIMEOUT_MS): {}",
1712        config
1713            .api_timeout_ms
1714            .map(|t| t.to_string())
1715            .unwrap_or("[未设置]".to_string())
1716            .green()
1717    );
1718
1719    println!(
1720        "8. 禁用非必要流量 (CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC): {}",
1721        config
1722            .claude_code_disable_nonessential_traffic
1723            .map(|t| t.to_string())
1724            .unwrap_or("[未设置]".to_string())
1725            .green()
1726    );
1727
1728    println!(
1729        "9. 默认 Sonnet 模型 (ANTHROPIC_DEFAULT_SONNET_MODEL): {}",
1730        config
1731            .anthropic_default_sonnet_model
1732            .as_deref()
1733            .unwrap_or("[未设置]")
1734            .green()
1735    );
1736
1737    println!(
1738        "A. 默认 Opus 模型 (ANTHROPIC_DEFAULT_OPUS_MODEL): {}",
1739        config
1740            .anthropic_default_opus_model
1741            .as_deref()
1742            .unwrap_or("[未设置]")
1743            .green()
1744    );
1745
1746    println!(
1747        "B. 默认 Haiku 模型 (ANTHROPIC_DEFAULT_HAIKU_MODEL): {}",
1748        config
1749            .anthropic_default_haiku_model
1750            .as_deref()
1751            .unwrap_or("[未设置]")
1752            .green()
1753    );
1754
1755    println!("{}", "─────────────────────────".blue());
1756    println!(
1757        "S. {} | Q. {}",
1758        "保存更改".green().bold(),
1759        "返回上一级菜单".blue()
1760    );
1761}
1762
1763/// Edit alias field
1764fn edit_field_alias(config: &mut Configuration) -> Result<()> {
1765    println!("\n编辑别名:");
1766    println!("当前值: {}", config.alias_name.cyan());
1767    print!("新值 (回车保持不变): ");
1768    io::stdout().flush()?;
1769
1770    let mut input = String::new();
1771    io::stdin().read_line(&mut input)?;
1772    let input = input.trim();
1773
1774    if !input.is_empty() {
1775        // Validate alias (reuse existing validation logic)
1776        if input.contains(char::is_whitespace) {
1777            println!("{}", "错误: 别名不能包含空白字符".red());
1778            return Ok(());
1779        }
1780        if input == "cc" {
1781            println!("{}", "错误: 'cc' 是保留名称".red());
1782            return Ok(());
1783        }
1784
1785        config.alias_name = input.to_string();
1786        println!("别名已更新为: {}", input.green());
1787    }
1788    Ok(())
1789}
1790
1791/// Edit token field
1792fn edit_field_token(config: &mut Configuration) -> Result<()> {
1793    println!("\n编辑令牌:");
1794    println!("当前值: {}", format_token_for_display(&config.token).cyan());
1795    print!("新值 (回车保持不变): ");
1796    io::stdout().flush()?;
1797
1798    let mut input = String::new();
1799    io::stdin().read_line(&mut input)?;
1800    let input = input.trim();
1801
1802    if !input.is_empty() {
1803        config.token = input.to_string();
1804        println!("{}", "令牌已更新".green());
1805    }
1806    Ok(())
1807}
1808
1809/// Edit URL field
1810fn edit_field_url(config: &mut Configuration) -> Result<()> {
1811    println!("\n编辑 URL:");
1812    println!("当前值: {}", config.url.cyan());
1813    print!("新值 (回车保持不变): ");
1814    io::stdout().flush()?;
1815
1816    let mut input = String::new();
1817    io::stdin().read_line(&mut input)?;
1818    let input = input.trim();
1819
1820    if !input.is_empty() {
1821        config.url = input.to_string();
1822        println!("URL 已更新为: {}", input.green());
1823    }
1824    Ok(())
1825}
1826
1827/// Edit model field
1828fn edit_field_model(config: &mut Configuration) -> Result<()> {
1829    println!("\n编辑模型:");
1830    println!(
1831        "当前值: {}",
1832        config.model.as_deref().unwrap_or("[未设置]").cyan()
1833    );
1834    print!("新值 (回车保持不变,输入空格清除): ");
1835    io::stdout().flush()?;
1836
1837    let mut input = String::new();
1838    io::stdin().read_line(&mut input)?;
1839    let input = input.trim();
1840
1841    if !input.is_empty() {
1842        if input == " " {
1843            config.model = None;
1844            println!("{}", "模型已清除".green());
1845        } else {
1846            config.model = Some(input.to_string());
1847            println!("模型已更新为: {}", input.green());
1848        }
1849    }
1850    Ok(())
1851}
1852
1853/// Edit small_fast_model field
1854fn edit_field_small_fast_model(config: &mut Configuration) -> Result<()> {
1855    println!("\n编辑快速模型:");
1856    println!(
1857        "当前值: {}",
1858        config
1859            .small_fast_model
1860            .as_deref()
1861            .unwrap_or("[未设置]")
1862            .cyan()
1863    );
1864    print!("新值 (回车保持不变,输入空格清除): ");
1865    io::stdout().flush()?;
1866
1867    let mut input = String::new();
1868    io::stdin().read_line(&mut input)?;
1869    let input = input.trim();
1870
1871    if !input.is_empty() {
1872        if input == " " {
1873            config.small_fast_model = None;
1874            println!("{}", "快速模型已清除".green());
1875        } else {
1876            config.small_fast_model = Some(input.to_string());
1877            println!("快速模型已更新为: {}", input.green());
1878        }
1879    }
1880    Ok(())
1881}
1882
1883/// Edit max_thinking_tokens field
1884fn edit_field_max_thinking_tokens(config: &mut Configuration) -> Result<()> {
1885    println!("\n编辑最大思考令牌数:");
1886    println!(
1887        "当前值: {}",
1888        config
1889            .max_thinking_tokens
1890            .map(|t| t.to_string())
1891            .unwrap_or("[未设置]".to_string())
1892            .cyan()
1893    );
1894    print!("新值 (回车保持不变,输入 0 清除): ");
1895    io::stdout().flush()?;
1896
1897    let mut input = String::new();
1898    io::stdin().read_line(&mut input)?;
1899    let input = input.trim();
1900
1901    if !input.is_empty() {
1902        if input == "0" {
1903            config.max_thinking_tokens = None;
1904            println!("{}", "最大思考令牌数已清除".green());
1905        } else if let Ok(tokens) = input.parse::<u32>() {
1906            config.max_thinking_tokens = Some(tokens);
1907            println!("最大思考令牌数已更新为: {}", tokens.to_string().green());
1908        } else {
1909            println!("{}", "错误: 请输入有效的数字".red());
1910        }
1911    }
1912    Ok(())
1913}
1914
1915/// Edit api_timeout_ms field
1916fn edit_field_api_timeout_ms(config: &mut Configuration) -> Result<()> {
1917    println!("\n编辑 API 超时时间 (毫秒):");
1918    println!(
1919        "当前值: {}",
1920        config
1921            .api_timeout_ms
1922            .map(|t| t.to_string())
1923            .unwrap_or("[未设置]".to_string())
1924            .cyan()
1925    );
1926    print!("新值 (回车保持不变,输入 0 清除): ");
1927    io::stdout().flush()?;
1928
1929    let mut input = String::new();
1930    io::stdin().read_line(&mut input)?;
1931    let input = input.trim();
1932
1933    if !input.is_empty() {
1934        if input == "0" {
1935            config.api_timeout_ms = None;
1936            println!("{}", "API 超时时间已清除".green());
1937        } else if let Ok(timeout) = input.parse::<u32>() {
1938            config.api_timeout_ms = Some(timeout);
1939            println!("API 超时时间已更新为: {}", timeout.to_string().green());
1940        } else {
1941            println!("{}", "错误: 请输入有效的数字".red());
1942        }
1943    }
1944    Ok(())
1945}
1946
1947/// Edit claude_code_disable_nonessential_traffic field
1948fn edit_field_claude_code_disable_nonessential_traffic(config: &mut Configuration) -> Result<()> {
1949    println!("\n编辑禁用非必要流量标志:");
1950    println!(
1951        "当前值: {}",
1952        config
1953            .claude_code_disable_nonessential_traffic
1954            .map(|t| t.to_string())
1955            .unwrap_or("[未设置]".to_string())
1956            .cyan()
1957    );
1958    print!("新值 (回车保持不变,输入 0 清除): ");
1959    io::stdout().flush()?;
1960
1961    let mut input = String::new();
1962    io::stdin().read_line(&mut input)?;
1963    let input = input.trim();
1964
1965    if !input.is_empty() {
1966        if input == "0" {
1967            config.claude_code_disable_nonessential_traffic = None;
1968            println!("{}", "禁用非必要流量标志已清除".green());
1969        } else if let Ok(flag) = input.parse::<u32>() {
1970            config.claude_code_disable_nonessential_traffic = Some(flag);
1971            println!("禁用非必要流量标志已更新为: {}", flag.to_string().green());
1972        } else {
1973            println!("{}", "错误: 请输入有效的数字".red());
1974        }
1975    }
1976    Ok(())
1977}
1978
1979/// Edit anthropic_default_sonnet_model field
1980fn edit_field_anthropic_default_sonnet_model(config: &mut Configuration) -> Result<()> {
1981    println!("\n编辑默认 Sonnet 模型:");
1982    println!(
1983        "当前值: {}",
1984        config
1985            .anthropic_default_sonnet_model
1986            .as_deref()
1987            .unwrap_or("[未设置]")
1988            .cyan()
1989    );
1990    print!("新值 (回车保持不变,输入空格清除): ");
1991    io::stdout().flush()?;
1992
1993    let mut input = String::new();
1994    io::stdin().read_line(&mut input)?;
1995    let input = input.trim();
1996
1997    if !input.is_empty() {
1998        if input == " " {
1999            config.anthropic_default_sonnet_model = None;
2000            println!("{}", "默认 Sonnet 模型已清除".green());
2001        } else {
2002            config.anthropic_default_sonnet_model = Some(input.to_string());
2003            println!("默认 Sonnet 模型已更新为: {}", input.green());
2004        }
2005    }
2006    Ok(())
2007}
2008
2009/// Edit anthropic_default_opus_model field
2010fn edit_field_anthropic_default_opus_model(config: &mut Configuration) -> Result<()> {
2011    println!("\n编辑默认 Opus 模型:");
2012    println!(
2013        "当前值: {}",
2014        config
2015            .anthropic_default_opus_model
2016            .as_deref()
2017            .unwrap_or("[未设置]")
2018            .cyan()
2019    );
2020    print!("新值 (回车保持不变,输入空格清除): ");
2021    io::stdout().flush()?;
2022
2023    let mut input = String::new();
2024    io::stdin().read_line(&mut input)?;
2025    let input = input.trim();
2026
2027    if !input.is_empty() {
2028        if input == " " {
2029            config.anthropic_default_opus_model = None;
2030            println!("{}", "默认 Opus 模型已清除".green());
2031        } else {
2032            config.anthropic_default_opus_model = Some(input.to_string());
2033            println!("默认 Opus 模型已更新为: {}", input.green());
2034        }
2035    }
2036    Ok(())
2037}
2038
2039/// Edit anthropic_default_haiku_model field
2040fn edit_field_anthropic_default_haiku_model(config: &mut Configuration) -> Result<()> {
2041    println!("\n编辑默认 Haiku 模型:");
2042    println!(
2043        "当前值: {}",
2044        config
2045            .anthropic_default_haiku_model
2046            .as_deref()
2047            .unwrap_or("[未设置]")
2048            .cyan()
2049    );
2050    print!("新值 (回车保持不变,输入空格清除): ");
2051    io::stdout().flush()?;
2052
2053    let mut input = String::new();
2054    io::stdin().read_line(&mut input)?;
2055    let input = input.trim();
2056
2057    if !input.is_empty() {
2058        if input == " " {
2059            config.anthropic_default_haiku_model = None;
2060            println!("{}", "默认 Haiku 模型已清除".green());
2061        } else {
2062            config.anthropic_default_haiku_model = Some(input.to_string());
2063            println!("默认 Haiku 模型已更新为: {}", input.green());
2064        }
2065    }
2066    Ok(())
2067}
2068
2069/// Save configuration changes to disk and handle alias conflicts
2070fn save_configuration_changes(original_alias: &str, new_config: &Configuration) -> Result<()> {
2071    // Load current storage
2072    let mut storage = ConfigStorage::load()?;
2073
2074    // Check for alias conflicts if alias changed
2075    if original_alias != new_config.alias_name
2076        && storage.get_configuration(&new_config.alias_name).is_some()
2077    {
2078        println!("\n{}", "别名冲突!".red().bold());
2079        println!("配置 '{}' 已存在", new_config.alias_name.yellow());
2080        print!("是否覆盖现有配置? (y/N): ");
2081        io::stdout().flush()?;
2082
2083        let mut input = String::new();
2084        io::stdin().read_line(&mut input)?;
2085        let input = input.trim().to_lowercase();
2086
2087        if input != "y" && input != "yes" {
2088            println!("{}", "编辑已取消".yellow());
2089            return Ok(());
2090        }
2091    }
2092
2093    // Update configuration using the method from config_storage.rs
2094    storage.update_configuration(original_alias, new_config.clone())?;
2095    storage.save()?;
2096
2097    println!("\n{}", "配置已成功保存!".green().bold());
2098
2099    Ok(())
2100}