use crate::cli::display_utils::{
TextAlignment, format_token_for_display, get_terminal_width, pad_text_to_width,
text_display_width,
};
use crate::config::EnvironmentConfig;
use crate::config::types::{ConfigStorage, Configuration};
use anyhow::{Context, Result};
use colored::*;
use crossterm::{
event::{self, Event, KeyCode, KeyEvent, KeyEventKind},
execute, terminal,
};
use std::io::{self, Write};
use std::process::Command;
fn char_display_width(c: char) -> usize {
match c as u32 {
0x00..=0x7F => 1,
0x80..=0x2FF => 1,
0x2190..=0x21FF => 2,
0x3000..=0x303F => 2,
0x3040..=0x309F => 2,
0x30A0..=0x30FF => 2,
0x4E00..=0x9FFF => 2,
0xAC00..=0xD7AF => 2,
0x3400..=0x4DBF => 2,
0xFF01..=0xFF60 => 2,
_ => 1,
}
}
fn truncate_text_to_width(text: &str, available_width: usize) -> (String, usize) {
let mut current_width = 0;
let truncated: String = text
.chars()
.take_while(|&c| {
let char_width = char_display_width(c);
if current_width + char_width <= available_width {
current_width += char_width;
true
} else {
false
}
})
.collect();
let truncated_width = text_display_width(&truncated);
(truncated, truncated_width)
}
fn cleanup_terminal(stdout: &mut io::Stdout) {
let _ = execute!(stdout, terminal::LeaveAlternateScreen);
let _ = terminal::disable_raw_mode();
}
struct BorderDrawing {
pub unicode_supported: bool,
}
impl BorderDrawing {
fn new() -> Self {
let unicode_supported = Self::detect_unicode_support();
Self { unicode_supported }
}
fn detect_unicode_support() -> bool {
if let Ok(term) = std::env::var("TERM") {
if term.contains("xterm") || term.contains("screen") || term == "tmux-256color" {
return true;
}
}
if let Ok(lang) = std::env::var("LANG")
&& (lang.contains("UTF-8") || lang.contains("utf8"))
{
return true;
}
true
}
fn draw_top_border(&self, title: &str, width: usize) -> String {
if self.unicode_supported {
let title_padded = format!(" {title} ");
let title_len = text_display_width(&title_padded);
if title_len >= width.saturating_sub(2) {
format!("╔{}╗", "═".repeat(width.saturating_sub(2)))
} else {
let inner_width = width.saturating_sub(2); let padding_total = inner_width.saturating_sub(title_len);
let padding_left = padding_total / 2;
let padding_right = padding_total - padding_left;
format!(
"╔{}{}{}╗",
"═".repeat(padding_left),
title_padded,
"═".repeat(padding_right)
)
}
} else {
let title_padded = format!(" {title} ");
let title_len = title_padded.len();
if title_len >= width.saturating_sub(2) {
format!("+{}+", "-".repeat(width.saturating_sub(2)))
} else {
let inner_width = width.saturating_sub(2);
let padding_total = inner_width.saturating_sub(title_len);
let padding_left = padding_total / 2;
let padding_right = padding_total - padding_left;
format!(
"+{}{}{}+",
"-".repeat(padding_left),
title_padded,
"-".repeat(padding_right)
)
}
}
}
fn draw_middle_line(&self, text: &str, width: usize) -> String {
let text_len = text_display_width(text);
let available_width = width.saturating_sub(4);
let (left_border, right_border) = if self.unicode_supported {
("║", "║")
} else {
("|", "|")
};
if text_len > available_width {
let (truncated, truncated_width) = truncate_text_to_width(text, available_width);
let padding_spaces = available_width.saturating_sub(truncated_width);
format!(
"{left_border} {}{} {right_border}",
truncated,
" ".repeat(padding_spaces)
)
} else {
let padded_text = pad_text_to_width(text, available_width, TextAlignment::Left, ' ');
format!("{left_border} {padded_text} {right_border}")
}
}
fn draw_bottom_border(&self, width: usize) -> String {
if self.unicode_supported {
format!("╚{}╝", "═".repeat(width - 2))
} else {
format!("+{}+", "-".repeat(width - 2))
}
}
}
pub fn handle_current_command() -> Result<()> {
let storage = ConfigStorage::load()?;
println!("\n{}", "Current Configuration:".green().bold());
println!("Environment variable mode: configurations are set per-command execution");
println!("Select a configuration from the menu below to launch Claude");
println!("Select 'cc' to launch Claude with default settings");
let raw_mode_enabled = terminal::enable_raw_mode().is_ok();
if raw_mode_enabled {
let mut stdout = io::stdout();
if execute!(
stdout,
terminal::EnterAlternateScreen,
terminal::Clear(terminal::ClearType::All)
)
.is_ok()
{
let result = handle_main_menu_interactive(&mut stdout, &storage);
let _ = execute!(stdout, terminal::LeaveAlternateScreen);
let _ = terminal::disable_raw_mode();
return result;
} else {
let _ = terminal::disable_raw_mode();
}
}
handle_main_menu_simple(&storage)
}
fn handle_main_menu_interactive(stdout: &mut io::Stdout, storage: &ConfigStorage) -> Result<()> {
let menu_items = [
"Execute claude --dangerously-skip-permissions",
"Switch configuration",
"Exit",
];
let mut selected_index = 0;
loop {
execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
let border = BorderDrawing::new();
const MAIN_MENU_WIDTH: usize = 68;
println!(
"\r{}",
border.draw_top_border("Main Menu", MAIN_MENU_WIDTH).green()
);
println!(
"\r{}",
border
.draw_middle_line(
"↑↓/jk导航,1-9快选,E-编辑,R-官方,Q-退出,Enter确认,Esc取消",
MAIN_MENU_WIDTH
)
.green()
);
println!("\r{}", border.draw_bottom_border(MAIN_MENU_WIDTH).green());
println!();
for (index, item) in menu_items.iter().enumerate() {
if index == selected_index {
println!("\r> {} {}", "●".blue().bold(), item.blue().bold());
} else {
println!("\r {} {}", "○".dimmed(), item.dimmed());
}
}
stdout.flush()?;
let event = match event::read() {
Ok(event) => event,
Err(e) => {
cleanup_terminal(stdout);
return Err(e.into());
}
};
match event {
Event::Key(KeyEvent {
code,
kind: KeyEventKind::Press,
..
}) => {
match code {
KeyCode::Up => {
selected_index = selected_index.saturating_sub(1);
}
KeyCode::Down => {
if selected_index < menu_items.len() - 1 {
selected_index += 1;
}
}
KeyCode::Enter => {
cleanup_terminal(stdout);
return handle_main_menu_action(selected_index, storage);
}
KeyCode::Esc => {
cleanup_terminal(stdout);
println!("\nExiting...");
return Ok(());
}
_ => {}
}
}
Event::Key(_) => {} _ => {}
}
}
}
fn handle_main_menu_simple(storage: &ConfigStorage) -> Result<()> {
loop {
println!("\n{}", "Available Actions:".blue().bold());
println!("1. Execute claude --dangerously-skip-permissions");
println!("2. Switch configuration");
println!("3. Exit");
print!("\nPlease select an option (1-3): ");
io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.context("Failed to read input")?;
let choice = input.trim();
match choice {
"1" => return handle_main_menu_action(0, storage),
"2" => return handle_main_menu_action(1, storage),
"3" => return handle_main_menu_action(2, storage),
_ => {
println!("Invalid option. Please select 1-3.");
}
}
}
}
fn handle_main_menu_action(selected_index: usize, storage: &ConfigStorage) -> Result<()> {
match selected_index {
0 => {
println!("\nExecuting: claude --dangerously-skip-permissions");
execute_claude_command(true)?;
}
1 => {
handle_interactive_selection(storage)?;
}
2 => {
println!("Exiting...");
}
_ => {
println!("Invalid selection");
}
}
Ok(())
}
pub fn handle_interactive_selection(storage: &ConfigStorage) -> Result<()> {
if storage.configurations.is_empty() {
println!("No configurations available. Use 'add' command to create configurations first.");
return Ok(());
}
let mut configs: Vec<Configuration> = storage.configurations.values().cloned().collect();
configs.sort_by(|a, b| a.alias_name.cmp(&b.alias_name));
let mut selected_index = 0;
let raw_mode_enabled = terminal::enable_raw_mode().is_ok();
if raw_mode_enabled {
let mut stdout = io::stdout();
if execute!(
stdout,
terminal::EnterAlternateScreen,
terminal::Clear(terminal::ClearType::All)
)
.is_ok()
{
let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
let result = handle_full_interactive_menu(
&mut stdout,
&mut configs,
&mut selected_index,
storage,
storage_mode,
);
let _ = execute!(stdout, terminal::LeaveAlternateScreen);
let _ = terminal::disable_raw_mode();
return result;
} else {
let _ = terminal::disable_raw_mode();
}
}
handle_simple_interactive_menu(&configs.iter().collect::<Vec<_>>(), storage)
}
fn handle_full_interactive_menu(
stdout: &mut io::Stdout,
configs: &mut Vec<Configuration>,
selected_index: &mut usize,
storage: &ConfigStorage,
storage_mode: crate::config::types::StorageMode,
) -> Result<()> {
if configs.is_empty() {
println!("\r{}", "No configurations available".yellow());
println!(
"\r{}",
"Use 'cc-switch add <alias> <token> <url>' to add configurations first.".dimmed()
);
println!("\r{}", "Press any key to continue...".dimmed());
let _ = event::read(); return Ok(());
}
const PAGE_SIZE: usize = 9;
let total_pages = if configs.len() <= PAGE_SIZE {
1
} else {
configs.len().div_ceil(PAGE_SIZE)
};
let mut current_page = 0;
loop {
let start_idx = current_page * PAGE_SIZE;
let end_idx = std::cmp::min(start_idx + PAGE_SIZE, configs.len());
let page_configs = &configs[start_idx..end_idx];
execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
let border = BorderDrawing::new();
const CONFIG_MENU_WIDTH: usize = 80;
println!(
"\r{}",
border
.draw_top_border("Select Configuration", CONFIG_MENU_WIDTH)
.green()
);
if total_pages > 1 {
println!(
"\r{}",
border
.draw_middle_line(
&format!("第 {} 页,共 {} 页", current_page + 1, total_pages),
CONFIG_MENU_WIDTH
)
.green()
);
println!(
"\r{}",
border
.draw_middle_line(
"↑↓/jk导航,1-9快选,E-编辑,N/P翻页,R-官方,Q-退出,Enter确认",
CONFIG_MENU_WIDTH
)
.green()
);
} else {
println!(
"\r{}",
border
.draw_middle_line(
"↑↓/jk导航,1-9快选,E-编辑,R-官方,Q-退出,Enter确认,Esc取消",
CONFIG_MENU_WIDTH
)
.green()
);
}
println!("\r{}", border.draw_bottom_border(CONFIG_MENU_WIDTH).green());
println!();
let official_index = 0;
if *selected_index == official_index {
println!(
"\r> {} {} {}",
"●".red().bold(),
"[R]".red().bold(),
"official".red().bold()
);
println!("\r Use official Claude API (no custom configuration)");
println!();
} else {
println!("\r {} {} {}", "○".red(), "[R]".red(), "official".red());
}
for (page_index, config) in page_configs.iter().enumerate() {
let actual_config_index = start_idx + page_index;
let display_number = page_index + 1; let actual_index = actual_config_index + 1; let number_label = format!("[{display_number}]");
if *selected_index == actual_index {
println!(
"\r> {} {} {}",
"●".blue().bold(),
number_label.blue().bold(),
config.alias_name.blue().bold()
);
let details = format_config_details(config, "\r ", false);
for detail_line in details {
println!("{detail_line}");
}
println!();
} else {
println!(
"\r {} {} {}",
"○".dimmed(),
number_label.dimmed(),
config.alias_name.dimmed()
);
}
}
let exit_index = configs.len() + 1;
if *selected_index == exit_index {
println!(
"\r> {} {} {}",
"●".yellow().bold(),
"[Q]".yellow().bold(),
"Exit".yellow().bold()
);
println!("\r Exit without making changes");
println!();
} else {
println!(
"\r {} {} {}",
"○".dimmed(),
"[Q]".dimmed(),
"Exit".dimmed()
);
}
if total_pages > 1 {
println!(
"\r{}",
format!(
"Page Navigation: [N]ext, [P]revious (第 {} 页,共 {} 页)",
current_page + 1,
total_pages
)
.dimmed()
);
}
stdout.flush()?;
let event = match event::read() {
Ok(event) => event,
Err(e) => {
cleanup_terminal(stdout);
return Err(e.into());
}
};
match event {
Event::Key(KeyEvent {
code,
kind: KeyEventKind::Press,
..
}) => match code {
KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => {
*selected_index = selected_index.saturating_sub(1);
}
KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => {
if *selected_index < configs.len() + 1 {
*selected_index += 1;
}
}
KeyCode::PageDown | KeyCode::Char('n') | KeyCode::Char('N') => {
if total_pages > 1 && current_page < total_pages - 1 {
current_page += 1;
let new_page_start_idx = current_page * PAGE_SIZE;
*selected_index = new_page_start_idx + 1; }
}
KeyCode::PageUp | KeyCode::Char('p') | KeyCode::Char('P') => {
if total_pages > 1 && current_page > 0 {
current_page -= 1;
let new_page_start_idx = current_page * PAGE_SIZE;
*selected_index = new_page_start_idx + 1; }
}
KeyCode::Enter => {
cleanup_terminal(stdout);
return handle_selection_action(
&configs.iter().collect::<Vec<_>>(),
*selected_index,
storage,
storage_mode,
);
}
KeyCode::Esc => {
cleanup_terminal(stdout);
println!("\nSelection cancelled");
return Ok(());
}
KeyCode::Char(c) if c.is_ascii_digit() => {
let digit = c.to_digit(10).unwrap() as usize;
if digit >= 1 && digit <= page_configs.len() {
let actual_config_index = start_idx + (digit - 1);
let selection_index = actual_config_index + 1;
cleanup_terminal(stdout);
return handle_selection_action(
&configs.iter().collect::<Vec<_>>(),
selection_index,
storage,
storage_mode,
);
}
}
KeyCode::Char('r') | KeyCode::Char('R') => {
cleanup_terminal(stdout);
return handle_selection_action(
&configs.iter().collect::<Vec<_>>(),
0,
storage,
storage_mode,
);
}
KeyCode::Char('e') | KeyCode::Char('E') => {
if *selected_index > 0 && *selected_index <= configs.len() {
cleanup_terminal(stdout);
let config_index = *selected_index - 1;
let edit_result = handle_config_edit(&configs[config_index]);
if execute!(
stdout,
terminal::EnterAlternateScreen,
terminal::Clear(terminal::ClearType::All)
)
.is_ok()
&& terminal::enable_raw_mode().is_ok()
{
match edit_result {
Ok(_) => {
if let Ok(reloaded_storage) = ConfigStorage::load() {
*configs = reloaded_storage
.configurations
.values()
.cloned()
.collect();
configs.sort_by(|a, b| a.alias_name.cmp(&b.alias_name));
if *selected_index > configs.len() + 1 {
*selected_index = configs.len() + 1;
}
}
continue;
}
Err(e) => {
if e.downcast_ref::<EditModeError>()
== Some(&EditModeError::ReturnToMenu)
{
continue;
}
cleanup_terminal(stdout);
return Err(e);
}
}
}
}
}
KeyCode::Char('q') | KeyCode::Char('Q') => {
cleanup_terminal(stdout);
return handle_selection_action(
&configs.iter().collect::<Vec<_>>(),
configs.len() + 1,
storage,
storage_mode,
);
}
_ => {}
},
Event::Key(_) => {} _ => {}
}
}
}
fn handle_simple_interactive_menu(
configs: &[&Configuration],
storage: &ConfigStorage,
) -> Result<()> {
const PAGE_SIZE: usize = 9;
if configs.len() <= PAGE_SIZE {
return handle_simple_single_page_menu(configs, storage);
}
let total_pages = configs.len().div_ceil(PAGE_SIZE);
let mut current_page = 0;
loop {
let start_idx = current_page * PAGE_SIZE;
let end_idx = std::cmp::min(start_idx + PAGE_SIZE, configs.len());
let page_configs = &configs[start_idx..end_idx];
println!("\n{}", "Available Configurations:".blue().bold());
if total_pages > 1 {
println!("第 {} 页,共 {} 页", current_page + 1, total_pages);
println!("使用 'n' 下一页, 'p' 上一页, 'r' 官方配置, 'q' 退出");
}
println!();
println!("{} {}", "[r]".red().bold(), "official".red());
println!(" Use official Claude API (no custom configuration)");
println!();
for (page_index, config) in page_configs.iter().enumerate() {
let display_number = page_index + 1;
println!(
"{}. {}",
format!("[{display_number}]").green().bold(),
config.alias_name.green()
);
let details = format_config_details(config, " ", true);
for detail_line in details {
println!("{detail_line}");
}
println!();
}
println!("{} {}", "[q]".yellow().bold(), "Exit".yellow());
if total_pages > 1 {
println!(
"\n页面导航: [n]下页, [p]上页 | 配置选择: [1-{}] | [e]编辑 | [r]官方 | [q]退出",
page_configs.len()
);
}
print!("\n请输入选择: ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let choice = input.trim().to_lowercase();
match choice.as_str() {
"r" => {
println!("Using official Claude configuration");
let mut settings = crate::config::types::ClaudeSettings::load(
storage.get_claude_settings_dir().map(|s| s.as_str()),
)?;
settings.remove_anthropic_env();
settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
return launch_claude_with_env(EnvironmentConfig::empty(), None, None, false);
}
"e" => {
println!("编辑功能在交互式菜单中可用");
}
"q" => {
println!("Exiting...");
return Ok(());
}
"n" if total_pages > 1 && current_page < total_pages - 1 => {
current_page += 1;
continue;
}
"p" if total_pages > 1 && current_page > 0 => {
current_page -= 1;
continue;
}
digit_str => {
if let Ok(digit) = digit_str.parse::<usize>()
&& digit >= 1
&& digit <= page_configs.len()
{
let actual_config_index = start_idx + (digit - 1);
let selection_index = actual_config_index + 1; let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
return handle_selection_action(
configs,
selection_index,
storage,
storage_mode,
);
}
println!("无效选择,请重新输入");
}
}
}
}
fn handle_simple_single_page_menu(
configs: &[&Configuration],
storage: &ConfigStorage,
) -> Result<()> {
println!("\n{}", "Available Configurations:".blue().bold());
println!("1. {}", "official".red());
println!(" Use official Claude API (no custom configuration)");
println!();
for (index, config) in configs.iter().enumerate() {
println!(
"{}. {}",
index + 2, config.alias_name.green()
);
let details = format_config_details(config, " ", true);
for detail_line in details {
println!("{detail_line}");
}
println!();
}
println!("{}. {}", configs.len() + 2, "Exit".yellow());
print!("\nSelect configuration (1-{}): ", configs.len() + 2);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
match input.trim().parse::<usize>() {
Ok(1) => {
println!("Using official Claude configuration");
let mut settings = crate::config::types::ClaudeSettings::load(
storage.get_claude_settings_dir().map(|s| s.as_str()),
)?;
settings.remove_anthropic_env();
settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
launch_claude_with_env(EnvironmentConfig::empty(), None, None, false)
}
Ok(num) if num >= 2 && num <= configs.len() + 1 => {
let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
handle_selection_action(configs, num - 1, storage, storage_mode) }
Ok(num) if num == configs.len() + 2 => {
println!("Exiting...");
Ok(())
}
_ => {
println!("Invalid selection");
Ok(())
}
}
}
fn handle_selection_action(
configs: &[&Configuration],
selected_index: usize,
storage: &ConfigStorage,
storage_mode: crate::config::types::StorageMode,
) -> Result<()> {
if selected_index == 0 {
println!("\nUsing official Claude configuration");
let mut settings = crate::config::types::ClaudeSettings::load(
storage.get_claude_settings_dir().map(|s| s.as_str()),
)?;
settings.remove_anthropic_env();
settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
launch_claude_with_env(EnvironmentConfig::empty(), None, None, false)
} else if selected_index <= configs.len() {
let config_index = selected_index - 1; let selected_config = configs[config_index].clone();
let env_config = EnvironmentConfig::from_config(&selected_config);
println!(
"\nSwitched to configuration '{}'",
selected_config.alias_name.green().bold()
);
let details = format_config_details(&selected_config, "", false);
for detail_line in details {
println!("{detail_line}");
}
let mut settings = crate::config::types::ClaudeSettings::load(
storage.get_claude_settings_dir().map(|s| s.as_str()),
)?;
settings.switch_to_config_with_mode(
&selected_config,
storage_mode,
storage.get_claude_settings_dir().map(|s| s.as_str()),
)?;
launch_claude_with_env(env_config, None, None, false)
} else {
println!("\nExiting...");
Ok(())
}
}
pub fn launch_claude_with_env(
env_config: EnvironmentConfig,
prompt: Option<&str>,
resume: Option<&str>,
continue_session: bool,
) -> Result<()> {
println!("\nLaunching Claude CLI...");
for (key, value) in env_config.as_env_tuples() {
unsafe {
std::env::set_var(&key, &value);
}
}
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
let mut command = Command::new("claude");
command.arg("--dangerously-skip-permissions");
if let Some(session_id) = resume {
command.args(["--resume", session_id]);
}
if continue_session {
command.arg("--continue");
}
if let Some(p) = prompt {
command.arg(p);
}
let error = command.exec();
anyhow::bail!("Failed to exec claude: {}", error);
}
#[cfg(not(unix))]
{
use std::process::Stdio;
let mut command = Command::new("claude");
command.arg("--dangerously-skip-permissions");
if let Some(session_id) = resume {
command.args(["--resume", session_id]);
}
if continue_session {
command.arg("--continue");
}
if let Some(p) = prompt {
command.arg(p);
}
command
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
let mut child = command.spawn().context(
"Failed to launch Claude CLI. Make sure 'claude' command is available in PATH",
)?;
let status = child.wait()?;
if !status.success() {
anyhow::bail!("Claude CLI exited with error status: {}", status);
}
}
}
fn execute_claude_command(skip_permissions: bool) -> Result<()> {
println!("Launching Claude CLI...");
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
let mut command = Command::new("claude");
if skip_permissions {
command.arg("--dangerously-skip-permissions");
}
let error = command.exec();
anyhow::bail!("Failed to exec claude: {}", error);
}
#[cfg(not(unix))]
{
use std::process::Stdio;
let mut command = Command::new("claude");
if skip_permissions {
command.arg("--dangerously-skip-permissions");
}
command
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
let mut child = command.spawn().context(
"Failed to launch Claude CLI. Make sure 'claude' command is available in PATH",
)?;
let status = child
.wait()
.context("Failed to wait for Claude CLI process")?;
if !status.success() {
anyhow::bail!("Claude CLI exited with error status: {}", status);
}
}
}
pub fn read_input(prompt: &str) -> Result<String> {
print!("{prompt}");
io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
pub fn read_sensitive_input(prompt: &str) -> Result<String> {
print!("{prompt}");
io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
fn format_config_details(config: &Configuration, indent: &str, _compact: bool) -> Vec<String> {
let mut lines = Vec::new();
let terminal_width = get_terminal_width();
let _available_width = terminal_width.saturating_sub(text_display_width(indent) + 8);
let token_label = "Token:";
let url_label = "URL:";
let model_label = "Model:";
let small_model_label = "Small Fast Model:";
let max_thinking_tokens_label = "Max Thinking Tokens:";
let api_timeout_ms_label = "API Timeout (ms):";
let disable_nonessential_traffic_label = "Disable Nonessential Traffic:";
let default_sonnet_model_label = "Default Sonnet Model:";
let default_opus_model_label = "Default Opus Model:";
let default_haiku_model_label = "Default Haiku Model:";
let max_label_width = [
token_label,
url_label,
model_label,
small_model_label,
max_thinking_tokens_label,
api_timeout_ms_label,
disable_nonessential_traffic_label,
default_sonnet_model_label,
default_opus_model_label,
default_haiku_model_label,
]
.iter()
.map(|label| text_display_width(label))
.max()
.unwrap_or(0);
let token_line = format!(
"{}{} {}",
indent,
pad_text_to_width(token_label, max_label_width, TextAlignment::Left, ' '),
format_token_for_display(&config.token).dimmed()
);
lines.push(token_line);
let url_line = format!(
"{}{} {}",
indent,
pad_text_to_width(url_label, max_label_width, TextAlignment::Left, ' '),
config.url.cyan()
);
lines.push(url_line);
if let Some(model) = &config.model {
let model_line = format!(
"{}{} {}",
indent,
pad_text_to_width(model_label, max_label_width, TextAlignment::Left, ' '),
model.yellow()
);
lines.push(model_line);
}
if let Some(small_fast_model) = &config.small_fast_model {
let small_model_line = format!(
"{}{} {}",
indent,
pad_text_to_width(small_model_label, max_label_width, TextAlignment::Left, ' '),
small_fast_model.yellow()
);
lines.push(small_model_line);
}
if let Some(max_thinking_tokens) = config.max_thinking_tokens {
let tokens_line = format!(
"{}{} {}",
indent,
pad_text_to_width(
max_thinking_tokens_label,
max_label_width,
TextAlignment::Left,
' '
),
format!("{}", max_thinking_tokens).yellow()
);
lines.push(tokens_line);
}
if let Some(api_timeout_ms) = config.api_timeout_ms {
let timeout_line = format!(
"{}{} {}",
indent,
pad_text_to_width(
api_timeout_ms_label,
max_label_width,
TextAlignment::Left,
' '
),
format!("{}", api_timeout_ms).yellow()
);
lines.push(timeout_line);
}
if let Some(disable_flag) = config.claude_code_disable_nonessential_traffic {
let flag_line = format!(
"{}{} {}",
indent,
pad_text_to_width(
disable_nonessential_traffic_label,
max_label_width,
TextAlignment::Left,
' '
),
format!("{}", disable_flag).yellow()
);
lines.push(flag_line);
}
if let Some(sonnet_model) = &config.anthropic_default_sonnet_model {
let sonnet_line = format!(
"{}{} {}",
indent,
pad_text_to_width(
default_sonnet_model_label,
max_label_width,
TextAlignment::Left,
' '
),
sonnet_model.yellow()
);
lines.push(sonnet_line);
}
if let Some(opus_model) = &config.anthropic_default_opus_model {
let opus_line = format!(
"{}{} {}",
indent,
pad_text_to_width(
default_opus_model_label,
max_label_width,
TextAlignment::Left,
' '
),
opus_model.yellow()
);
lines.push(opus_line);
}
if let Some(haiku_model) = &config.anthropic_default_haiku_model {
let haiku_line = format!(
"{}{} {}",
indent,
pad_text_to_width(
default_haiku_model_label,
max_label_width,
TextAlignment::Left,
' '
),
haiku_model.yellow()
);
lines.push(haiku_line);
}
lines
}
#[cfg(test)]
mod border_drawing_tests {
use super::*;
#[test]
fn test_border_drawing_unicode_support() {
let _border = BorderDrawing::new();
}
#[test]
fn test_border_drawing_top_border() {
let border = BorderDrawing {
unicode_supported: true,
};
let result = border.draw_top_border("Test", 20);
assert!(!result.is_empty());
assert!(result.contains("Test"));
}
#[test]
fn test_border_drawing_ascii_fallback() {
let border = BorderDrawing {
unicode_supported: false,
};
let result = border.draw_top_border("Test", 20);
assert!(!result.is_empty());
assert!(result.contains("Test"));
assert!(result.contains("+"));
assert!(result.contains("-"));
}
#[test]
fn test_border_drawing_middle_line() {
let border = BorderDrawing {
unicode_supported: true,
};
let result = border.draw_middle_line("Test message", 30);
assert!(!result.is_empty());
assert!(result.contains("Test message"));
}
#[test]
fn test_border_drawing_bottom_border() {
let border = BorderDrawing {
unicode_supported: true,
};
let result = border.draw_bottom_border(20);
assert!(!result.is_empty());
}
#[test]
fn test_border_drawing_width_consistency() {
let border = BorderDrawing {
unicode_supported: true,
};
let width = 30;
let top = border.draw_top_border("Title", width);
let middle = border.draw_middle_line("Content", width);
let bottom = border.draw_bottom_border(width);
assert!(top.chars().count() >= width - 2);
assert!(middle.chars().count() >= width - 2);
assert!(bottom.chars().count() >= width - 2);
}
}
#[cfg(test)]
mod pagination_tests {
#[test]
fn test_pagination_calculation() {
const PAGE_SIZE: usize = 9;
assert_eq!(1_usize.div_ceil(PAGE_SIZE), 1); assert_eq!(9_usize.div_ceil(PAGE_SIZE), 1);
assert_eq!(10_usize.div_ceil(PAGE_SIZE), 2); assert_eq!(18_usize.div_ceil(PAGE_SIZE), 2); assert_eq!(19_usize.div_ceil(PAGE_SIZE), 3); assert_eq!(27_usize.div_ceil(PAGE_SIZE), 3); assert_eq!(28_usize.div_ceil(PAGE_SIZE), 4); }
#[test]
fn test_page_range_calculation() {
const PAGE_SIZE: usize = 9;
let current_page = 0;
let start_idx = current_page * PAGE_SIZE; let end_idx = std::cmp::min(start_idx + PAGE_SIZE, 15); assert_eq!(start_idx, 0);
assert_eq!(end_idx, 9);
assert_eq!(end_idx - start_idx, 9);
let current_page = 1;
let start_idx = current_page * PAGE_SIZE; let end_idx = std::cmp::min(start_idx + PAGE_SIZE, 15); assert_eq!(start_idx, 9);
assert_eq!(end_idx, 15);
assert_eq!(end_idx - start_idx, 6);
let current_page = 0;
let start_idx = current_page * PAGE_SIZE; let end_idx = std::cmp::min(start_idx + PAGE_SIZE, PAGE_SIZE); assert_eq!(start_idx, 0);
assert_eq!(end_idx, 9);
assert_eq!(end_idx - start_idx, 9); }
#[test]
fn test_digit_mapping_to_config_index() {
const PAGE_SIZE: usize = 9;
let current_page = 0;
let start_idx = current_page * PAGE_SIZE;
let digit = 1;
let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 0);
let digit = 9;
let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 8);
let current_page = 1;
let start_idx = current_page * PAGE_SIZE;
let digit = 1;
let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 9);
let digit = 5;
let actual_config_index = start_idx + (digit - 1); assert_eq!(actual_config_index, 13);
}
#[test]
fn test_selection_index_conversion() {
const PAGE_SIZE: usize = 9;
let current_page = 0;
let start_idx = current_page * PAGE_SIZE; let digit = 1;
let actual_config_index = start_idx + (digit - 1); let selection_index = actual_config_index + 1; assert_eq!(selection_index, 1);
let current_page = 1;
let start_idx = current_page * PAGE_SIZE; let digit = 1;
let actual_config_index = start_idx + (digit - 1); let selection_index = actual_config_index + 1; assert_eq!(selection_index, 10);
}
#[test]
fn test_page_navigation_bounds() {
const PAGE_SIZE: usize = 9;
let total_configs: usize = 25; let total_pages = total_configs.div_ceil(PAGE_SIZE); assert_eq!(total_pages, 3);
let mut current_page = 0;
if current_page > 0 {
current_page -= 1;
}
assert_eq!(current_page, 0);
let mut current_page = total_pages - 1; if current_page < total_pages - 1 {
current_page += 1;
}
assert_eq!(current_page, 2);
let mut current_page = 1;
if current_page < total_pages - 1 {
current_page += 1;
}
assert_eq!(current_page, 2);
if current_page > 0 {
current_page = current_page.saturating_sub(1);
}
assert_eq!(current_page, 1);
}
#[test]
fn test_digit_key_boundary_conditions() {
const PAGE_SIZE: usize = 9;
let digit = 0;
assert!(digit < 1, "Digit 0 should be less than 1 and ignored");
let configs_len = 5; let page_configs_len = std::cmp::min(PAGE_SIZE, configs_len); let digit = 9; assert!(
digit > page_configs_len,
"Digit 9 should be beyond available configs (5) and ignored"
);
for digit in 1..=page_configs_len {
assert!(
digit >= 1 && digit <= page_configs_len,
"Digit {} should be valid",
digit
);
}
}
#[test]
fn test_empty_configs_handling() {
let empty_configs: Vec<String> = Vec::new();
assert!(
empty_configs.is_empty(),
"Empty config list should be properly detected"
);
let configs_len = empty_configs.len(); assert_eq!(configs_len, 0, "Empty configs should have length 0");
}
#[test]
fn test_page_navigation_boundaries() {
const PAGE_SIZE: usize = 9;
let total_configs: usize = 20; let total_pages = total_configs.div_ceil(PAGE_SIZE);
let mut current_page = 0;
let original_page = current_page;
if current_page > 0 {
current_page -= 1;
}
assert_eq!(
current_page, original_page,
"First page should not navigate to previous"
);
let mut current_page = total_pages - 1; let original_page = current_page;
if current_page < total_pages - 1 {
current_page += 1;
}
assert_eq!(
current_page, original_page,
"Last page should not navigate to next"
);
let mut current_page = 1;
if current_page < total_pages - 1 {
current_page += 1;
}
assert_eq!(current_page, 2, "Should navigate to next page");
if current_page > 0 {
current_page = current_page.saturating_sub(1);
}
assert_eq!(current_page, 1, "Should navigate to previous page");
}
#[test]
fn test_j_key_navigation() {
let mut selected_index: usize = 0;
let configs_len = 5;
if selected_index < configs_len + 1 {
selected_index += 1;
}
assert_eq!(selected_index, 1, "j key should move selection down by one");
selected_index = configs_len + 1;
let original_index = selected_index;
if selected_index < configs_len + 1 {
selected_index += 1;
}
assert_eq!(
selected_index, original_index,
"j key should not move beyond bottom boundary"
);
}
#[test]
fn test_k_key_navigation() {
let mut selected_index: usize = 5;
selected_index = selected_index.saturating_sub(1);
assert_eq!(selected_index, 4, "k key should move selection up by one");
selected_index = 0;
let original_index = selected_index;
selected_index = selected_index.saturating_sub(1);
assert_eq!(
selected_index, original_index,
"k key should not move beyond top boundary"
);
}
#[test]
fn test_jk_key_boundary_conditions() {
const CONFIGS_LEN: usize = 5;
let mut selected_index: usize = CONFIGS_LEN + 1; let original_index = selected_index;
if selected_index < CONFIGS_LEN + 1 {
selected_index += 1; }
assert_eq!(
selected_index, original_index,
"j key should respect bottom boundary like Down arrow"
);
let mut selected_index: usize = 0; let original_index = selected_index;
selected_index = selected_index.saturating_sub(1); assert_eq!(
selected_index, original_index,
"k key should respect top boundary like Up arrow"
);
}
}
#[derive(Debug, PartialEq)]
enum EditModeError {
ReturnToMenu,
}
impl std::fmt::Display for EditModeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EditModeError::ReturnToMenu => write!(f, "return_to_menu"),
}
}
}
impl std::error::Error for EditModeError {}
fn handle_config_edit(config: &Configuration) -> Result<()> {
println!("\n{}", "配置编辑模式".green().bold());
println!("{}", "===================".green());
println!("正在编辑配置: {}", config.alias_name.cyan().bold());
println!();
let mut editing_config = config.clone();
let original_alias = config.alias_name.clone();
loop {
display_edit_menu(&editing_config);
println!("\n{}", "提示: 可使用大小写字母".dimmed());
print!("请选择要编辑的字段 (1-9, A-B), 或输入 S 保存, Q 返回上一级菜单: ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
match input {
"1" => edit_field_alias(&mut editing_config)?,
"2" => edit_field_token(&mut editing_config)?,
"3" => edit_field_url(&mut editing_config)?,
"4" => edit_field_model(&mut editing_config)?,
"5" => edit_field_small_fast_model(&mut editing_config)?,
"6" => edit_field_max_thinking_tokens(&mut editing_config)?,
"7" => edit_field_api_timeout_ms(&mut editing_config)?,
"8" => edit_field_claude_code_disable_nonessential_traffic(&mut editing_config)?,
"9" => edit_field_anthropic_default_sonnet_model(&mut editing_config)?,
"10" | "a" | "A" => edit_field_anthropic_default_opus_model(&mut editing_config)?,
"11" | "b" | "B" => edit_field_anthropic_default_haiku_model(&mut editing_config)?,
"s" | "S" => {
return save_configuration_changes(&original_alias, &editing_config);
}
"q" | "Q" => {
println!("\n{}", "返回上一级菜单".blue());
return Err(EditModeError::ReturnToMenu.into());
}
_ => {
println!("{}", "无效选择,请重试".red());
}
}
}
}
fn display_edit_menu(config: &Configuration) {
println!("\n{}", "当前配置值:".blue().bold());
println!("{}", "─────────────────────────".blue());
println!("1. 别名 (alias_name): {}", config.alias_name.green());
println!(
"2. 令牌 (ANTHROPIC_AUTH_TOKEN): {}",
format_token_for_display(&config.token).green()
);
println!("3. URL (ANTHROPIC_BASE_URL): {}", config.url.green());
println!(
"4. 模型 (ANTHROPIC_MODEL): {}",
config.model.as_deref().unwrap_or("[未设置]").green()
);
println!(
"5. 快速模型 (ANTHROPIC_SMALL_FAST_MODEL): {}",
config
.small_fast_model
.as_deref()
.unwrap_or("[未设置]")
.green()
);
println!(
"6. 最大思考令牌数 (ANTHROPIC_MAX_THINKING_TOKENS): {}",
config
.max_thinking_tokens
.map(|t| t.to_string())
.unwrap_or("[未设置]".to_string())
.green()
);
println!(
"7. API超时时间 (API_TIMEOUT_MS): {}",
config
.api_timeout_ms
.map(|t| t.to_string())
.unwrap_or("[未设置]".to_string())
.green()
);
println!(
"8. 禁用非必要流量 (CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC): {}",
config
.claude_code_disable_nonessential_traffic
.map(|t| t.to_string())
.unwrap_or("[未设置]".to_string())
.green()
);
println!(
"9. 默认 Sonnet 模型 (ANTHROPIC_DEFAULT_SONNET_MODEL): {}",
config
.anthropic_default_sonnet_model
.as_deref()
.unwrap_or("[未设置]")
.green()
);
println!(
"A. 默认 Opus 模型 (ANTHROPIC_DEFAULT_OPUS_MODEL): {}",
config
.anthropic_default_opus_model
.as_deref()
.unwrap_or("[未设置]")
.green()
);
println!(
"B. 默认 Haiku 模型 (ANTHROPIC_DEFAULT_HAIKU_MODEL): {}",
config
.anthropic_default_haiku_model
.as_deref()
.unwrap_or("[未设置]")
.green()
);
println!("{}", "─────────────────────────".blue());
println!(
"S. {} | Q. {}",
"保存更改".green().bold(),
"返回上一级菜单".blue()
);
}
fn edit_string_field(
field_name: &str,
current_value: &str,
validator: impl Fn(&str) -> Result<()>,
) -> Result<Option<String>> {
println!("\n编辑{field_name}:");
println!("当前值: {}", current_value.cyan());
print!("新值 (回车保持不变): ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
if !input.is_empty() {
validator(input)?;
println!("{field_name}已更新为: {}", input.green());
Ok(Some(input.to_string()))
} else {
Ok(None)
}
}
type OptionalStringResult = Result<Option<Option<String>>>;
fn edit_optional_string_field(
field_name: &str,
current_value: Option<&str>,
) -> OptionalStringResult {
println!("\n编辑{field_name}:");
println!("当前值: {}", current_value.unwrap_or("[未设置]").cyan());
print!("新值 (回车保持不变,输入空格清除): ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
if !input.is_empty() {
if input == " " {
println!("{}", format!("{field_name}已清除").green());
Ok(Some(None))
} else {
println!("{field_name}已更新为: {}", input.green());
Ok(Some(Some(input.to_string())))
}
} else {
Ok(None)
}
}
type OptionalU32Result = Result<Option<Option<u32>>>;
fn edit_optional_u32_field(field_name: &str, current_value: Option<u32>) -> OptionalU32Result {
println!("\n编辑{field_name}:");
println!(
"当前值: {}",
current_value
.map(|t| t.to_string())
.unwrap_or("[未设置]".to_string())
.cyan()
);
print!("新值 (回车保持不变,输入 0 清除): ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
if !input.is_empty() {
if input == "0" {
println!("{}", format!("{field_name}已清除").green());
Ok(Some(None))
} else if let Ok(value) = input.parse::<u32>() {
println!("{field_name}已更新为: {}", value.to_string().green());
Ok(Some(Some(value)))
} else {
println!("{}", "错误: 请输入有效的数字".red());
Ok(None)
}
} else {
Ok(None)
}
}
fn edit_field_alias(config: &mut Configuration) -> Result<()> {
let validator = |input: &str| -> Result<()> {
if input.contains(char::is_whitespace) {
anyhow::bail!("错误: 别名不能包含空白字符");
}
if input == "cc" {
anyhow::bail!("错误: 'cc' 是保留名称");
}
if input == "official" {
anyhow::bail!("错误: 'official' 是保留名称");
}
Ok(())
};
match edit_string_field("别名", &config.alias_name, validator) {
Ok(Some(new_value)) => config.alias_name = new_value,
Ok(None) => {}
Err(e) => println!("{}", e.to_string().red()),
}
Ok(())
}
fn edit_field_token(config: &mut Configuration) -> Result<()> {
let no_validator = |_: &str| -> Result<()> { Ok(()) };
if let Some(new_value) = edit_string_field(
"令牌",
&format_token_for_display(&config.token),
no_validator,
)? {
config.token = new_value;
println!("{}", "令牌已更新".green());
}
Ok(())
}
fn edit_field_url(config: &mut Configuration) -> Result<()> {
let no_validator = |_: &str| -> Result<()> { Ok(()) };
if let Some(new_value) = edit_string_field("URL", &config.url, no_validator)? {
config.url = new_value;
}
Ok(())
}
fn edit_field_model(config: &mut Configuration) -> Result<()> {
if let Some(result) = edit_optional_string_field("模型", config.model.as_deref())? {
config.model = result;
}
Ok(())
}
fn edit_field_small_fast_model(config: &mut Configuration) -> Result<()> {
if let Some(result) =
edit_optional_string_field("快速模型", config.small_fast_model.as_deref())?
{
config.small_fast_model = result;
}
Ok(())
}
fn edit_field_max_thinking_tokens(config: &mut Configuration) -> Result<()> {
if let Some(result) = edit_optional_u32_field("最大思考令牌数", config.max_thinking_tokens)?
{
config.max_thinking_tokens = result;
}
Ok(())
}
fn edit_field_api_timeout_ms(config: &mut Configuration) -> Result<()> {
if let Some(result) = edit_optional_u32_field("API超时时间 (毫秒)", config.api_timeout_ms)?
{
config.api_timeout_ms = result;
}
Ok(())
}
fn edit_field_claude_code_disable_nonessential_traffic(config: &mut Configuration) -> Result<()> {
if let Some(result) = edit_optional_u32_field(
"禁用非必要流量标志",
config.claude_code_disable_nonessential_traffic,
)? {
config.claude_code_disable_nonessential_traffic = result;
}
Ok(())
}
fn edit_field_anthropic_default_sonnet_model(config: &mut Configuration) -> Result<()> {
if let Some(result) = edit_optional_string_field(
"默认 Sonnet 模型",
config.anthropic_default_sonnet_model.as_deref(),
)? {
config.anthropic_default_sonnet_model = result;
}
Ok(())
}
fn edit_field_anthropic_default_opus_model(config: &mut Configuration) -> Result<()> {
if let Some(result) = edit_optional_string_field(
"默认 Opus 模型",
config.anthropic_default_opus_model.as_deref(),
)? {
config.anthropic_default_opus_model = result;
}
Ok(())
}
fn edit_field_anthropic_default_haiku_model(config: &mut Configuration) -> Result<()> {
if let Some(result) = edit_optional_string_field(
"默认 Haiku 模型",
config.anthropic_default_haiku_model.as_deref(),
)? {
config.anthropic_default_haiku_model = result;
}
Ok(())
}
fn save_configuration_changes(original_alias: &str, new_config: &Configuration) -> Result<()> {
let mut storage = ConfigStorage::load()?;
if original_alias != new_config.alias_name
&& storage.get_configuration(&new_config.alias_name).is_some()
{
println!("\n{}", "别名冲突!".red().bold());
println!("配置 '{}' 已存在", new_config.alias_name.yellow());
print!("是否覆盖现有配置? (y/N): ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim().to_lowercase();
if input != "y" && input != "yes" {
println!("{}", "编辑已取消".yellow());
return Ok(());
}
}
storage.update_configuration(original_alias, new_config.clone())?;
storage.save()?;
println!("\n{}", "配置已成功保存!".green().bold());
Ok(())
}