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