1use crate::cli::display_utils::{
2 TextAlignment, get_terminal_width, pad_text_to_width, text_display_width,
3};
4use crate::codex::{CodexConfiguration, write_auth_json};
5use crate::config::types::ConfigStorage;
6use crate::interactive::interactive::{
7 BorderDrawing, EditModeError, cleanup_terminal, edit_optional_string_field, edit_string_field,
8};
9use anyhow::Result;
10use colored::*;
11use crossterm::{
12 event::{self, Event, KeyCode, KeyEvent, KeyEventKind},
13 execute, terminal,
14};
15use std::io::{self, Write};
16use std::process::Command;
17
18pub fn handle_codex_interactive_selection(storage: &ConfigStorage) -> Result<()> {
29 let configs_map = match &storage.codex_configurations {
30 Some(configs) if !configs.is_empty() => configs,
31 _ => {
32 println!(
33 "No Codex configurations available. Use 'cc-switch codex add' to create configurations first."
34 );
35 return Ok(());
36 }
37 };
38
39 let mut configs: Vec<CodexConfiguration> = configs_map.values().cloned().collect();
40 configs.sort_by(|a, b| a.alias_name.cmp(&b.alias_name));
41
42 let mut selected_index: usize = 0;
43
44 let raw_mode_enabled = terminal::enable_raw_mode().is_ok();
46
47 if raw_mode_enabled {
48 let mut stdout = io::stdout();
49 if execute!(
50 stdout,
51 terminal::EnterAlternateScreen,
52 terminal::Clear(terminal::ClearType::All)
53 )
54 .is_ok()
55 {
56 let result =
57 handle_codex_full_interactive_menu(&mut stdout, &mut configs, &mut selected_index);
58
59 let _ = execute!(stdout, terminal::LeaveAlternateScreen);
61 let _ = terminal::disable_raw_mode();
62
63 return result;
64 } else {
65 let _ = terminal::disable_raw_mode();
66 }
67 }
68
69 handle_codex_simple_interactive_menu(&configs)
71}
72
73fn format_codex_config_details(config: &CodexConfiguration, indent: &str) -> Vec<String> {
82 let mut lines = Vec::new();
83
84 let terminal_width = get_terminal_width();
85 let _available_width = terminal_width.saturating_sub(text_display_width(indent) + 8);
86
87 let auth_mode_label = "Auth Mode:";
89 let account_id_label = "Account ID:";
90 let api_key_label = "API Key:";
91 let last_refresh_label = "Last Refresh:";
92
93 let max_label_width = [
94 auth_mode_label,
95 account_id_label,
96 api_key_label,
97 last_refresh_label,
98 ]
99 .iter()
100 .map(|label| text_display_width(label))
101 .max()
102 .unwrap_or(0);
103
104 let mode_value = if config.auth_mode == "apikey" {
106 "apikey".cyan()
107 } else {
108 "chatgpt".cyan()
109 };
110 lines.push(format!(
111 "{}{} {}",
112 indent,
113 pad_text_to_width(auth_mode_label, max_label_width, TextAlignment::Left, ' '),
114 mode_value
115 ));
116
117 if let Some(ref account_id) = config.account_id {
119 lines.push(format!(
120 "{}{} {}",
121 indent,
122 pad_text_to_width(account_id_label, max_label_width, TextAlignment::Left, ' '),
123 account_id.yellow()
124 ));
125 }
126
127 if let Some(ref key) = config.openai_api_key {
129 let prefix = if key.len() > 8 {
130 format!("{}...", &key[..8])
131 } else {
132 key.clone()
133 };
134 lines.push(format!(
135 "{}{} {}",
136 indent,
137 pad_text_to_width(api_key_label, max_label_width, TextAlignment::Left, ' '),
138 prefix.dimmed()
139 ));
140 }
141
142 if let Some(ref last_refresh) = config.last_refresh {
144 lines.push(format!(
145 "{}{} {}",
146 indent,
147 pad_text_to_width(
148 last_refresh_label,
149 max_label_width,
150 TextAlignment::Left,
151 ' '
152 ),
153 last_refresh.dimmed()
154 ));
155 }
156
157 lines
158}
159
160#[allow(clippy::ptr_arg)]
162fn handle_codex_full_interactive_menu(
163 stdout: &mut io::Stdout,
164 configs: &mut Vec<CodexConfiguration>,
165 selected_index: &mut usize,
166) -> Result<()> {
167 if configs.is_empty() {
168 println!("\r{}", "No Codex configurations available".yellow());
169 println!(
170 "\r{}",
171 "Use 'cc-switch codex add' to add configurations first.".dimmed()
172 );
173 println!("\r{}", "Press any key to continue...".dimmed());
174 let _ = event::read();
175 return Ok(());
176 }
177
178 const PAGE_SIZE: usize = 9;
179
180 let total_pages = if configs.len() <= PAGE_SIZE {
181 1
182 } else {
183 configs.len().div_ceil(PAGE_SIZE)
184 };
185 let mut current_page = 0;
186
187 loop {
188 let start_idx = current_page * PAGE_SIZE;
189 let end_idx = std::cmp::min(start_idx + PAGE_SIZE, configs.len());
190 let page_configs = &configs[start_idx..end_idx];
191
192 execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
194 execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
195
196 let border = BorderDrawing::new();
197 const CONFIG_MENU_WIDTH: usize = 80;
198
199 println!(
200 "\r{}",
201 border
202 .draw_top_border("Select Codex Configuration", CONFIG_MENU_WIDTH)
203 .green()
204 );
205 if total_pages > 1 {
206 println!(
207 "\r{}",
208 border
209 .draw_middle_line(
210 &format!("第 {} 页,共 {} 页", current_page + 1, total_pages),
211 CONFIG_MENU_WIDTH
212 )
213 .green()
214 );
215 println!(
216 "\r{}",
217 border
218 .draw_middle_line(
219 "↑↓/jk导航,1-9快选,N/P翻页,E编辑,Q-退出,Enter确认",
220 CONFIG_MENU_WIDTH
221 )
222 .green()
223 );
224 } else {
225 println!(
226 "\r{}",
227 border
228 .draw_middle_line(
229 "↑↓/jk导航,1-9快选,E编辑,Q-退出,Enter确认,Esc取消",
230 CONFIG_MENU_WIDTH
231 )
232 .green()
233 );
234 }
235 println!("\r{}", border.draw_bottom_border(CONFIG_MENU_WIDTH).green());
236 println!();
237
238 for (page_index, config) in page_configs.iter().enumerate() {
243 let actual_config_index = start_idx + page_index;
244 let display_number = page_index + 1; let number_label = format!("[{display_number}]");
246
247 if *selected_index == actual_config_index {
248 println!(
249 "\r> {} {} {}",
250 "●".blue().bold(),
251 number_label.blue().bold(),
252 config.alias_name.blue().bold()
253 );
254
255 let details = format_codex_config_details(config, "\r ");
256 for detail_line in details {
257 println!("{detail_line}");
258 }
259 println!();
260 } else {
261 println!(
262 "\r {} {} {}",
263 "○".dimmed(),
264 number_label.dimmed(),
265 config.alias_name.dimmed()
266 );
267 }
268 }
269
270 let exit_index = configs.len();
272 if *selected_index == exit_index {
273 println!(
274 "\r> {} {} {}",
275 "●".yellow().bold(),
276 "[Q]".yellow().bold(),
277 "Exit".yellow().bold()
278 );
279 println!("\r Exit without making changes");
280 println!();
281 } else {
282 println!(
283 "\r {} {} {}",
284 "○".dimmed(),
285 "[Q]".dimmed(),
286 "Exit".dimmed()
287 );
288 }
289
290 if total_pages > 1 {
292 println!(
293 "\r{}",
294 format!(
295 "Page Navigation: [N]ext, [P]revious (第 {} 页,共 {} 页)",
296 current_page + 1,
297 total_pages
298 )
299 .dimmed()
300 );
301 }
302
303 stdout.flush()?;
304
305 let event = match event::read() {
307 Ok(event) => event,
308 Err(e) => {
309 cleanup_terminal(stdout);
310 return Err(e.into());
311 }
312 };
313
314 match event {
315 Event::Key(KeyEvent {
316 code,
317 kind: KeyEventKind::Press,
318 ..
319 }) => match code {
320 KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => {
321 *selected_index = selected_index.saturating_sub(1);
322 }
323 KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J')
324 if *selected_index < configs.len() =>
325 {
326 *selected_index += 1;
327 }
328 KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => {}
329 KeyCode::PageDown | KeyCode::Char('n') | KeyCode::Char('N')
330 if total_pages > 1 && current_page < total_pages - 1 =>
331 {
332 current_page += 1;
333 let new_page_start_idx = current_page * PAGE_SIZE;
334 *selected_index = new_page_start_idx;
335 }
336 KeyCode::PageDown | KeyCode::Char('n') | KeyCode::Char('N') => {}
337 KeyCode::PageUp | KeyCode::Char('p') | KeyCode::Char('P')
338 if total_pages > 1 && current_page > 0 =>
339 {
340 current_page -= 1;
341 let new_page_start_idx = current_page * PAGE_SIZE;
342 *selected_index = new_page_start_idx;
343 }
344 KeyCode::PageUp | KeyCode::Char('p') | KeyCode::Char('P') => {}
345 KeyCode::Enter => {
346 cleanup_terminal(stdout);
347 return handle_codex_selection_action(configs, *selected_index);
348 }
349 KeyCode::Esc => {
350 cleanup_terminal(stdout);
351 println!("\nSelection cancelled");
352 return Ok(());
353 }
354 KeyCode::Char(c) if c.is_ascii_digit() => {
355 let digit = c.to_digit(10).unwrap() as usize;
356 if digit >= 1 && digit <= page_configs.len() {
357 let actual_config_index = start_idx + (digit - 1);
358 cleanup_terminal(stdout);
359 return handle_codex_selection_action(configs, actual_config_index);
360 }
361 }
362 KeyCode::Char('e') | KeyCode::Char('E') if *selected_index < configs.len() => {
363 cleanup_terminal(stdout);
364 let edit_result = handle_codex_config_edit(&configs[*selected_index]);
365 if execute!(
366 stdout,
367 terminal::EnterAlternateScreen,
368 terminal::Clear(terminal::ClearType::All)
369 )
370 .is_ok()
371 && terminal::enable_raw_mode().is_ok()
372 {
373 match edit_result {
374 Ok(_) => {
375 if let Ok(reloaded_storage) = ConfigStorage::load() {
376 if let Some(ref map) = reloaded_storage.codex_configurations {
377 *configs = map.values().cloned().collect();
378 configs.sort_by(|a, b| a.alias_name.cmp(&b.alias_name));
379 }
380 if *selected_index > configs.len() {
381 *selected_index = configs.len().saturating_sub(1);
382 }
383 }
384 continue;
385 }
386 Err(e) => {
387 if e.downcast_ref::<EditModeError>()
388 == Some(&EditModeError::ReturnToMenu)
389 {
390 continue;
391 }
392 cleanup_terminal(stdout);
393 return Err(e);
394 }
395 }
396 }
397 }
398 KeyCode::Char('e') | KeyCode::Char('E') => {}
399 KeyCode::Char('q') | KeyCode::Char('Q') => {
400 cleanup_terminal(stdout);
401 return handle_codex_selection_action(configs, configs.len());
402 }
403 _ => {}
404 },
405 Event::Key(_) => {}
406 _ => {}
407 }
408 }
409}
410
411fn handle_codex_simple_interactive_menu(configs: &[CodexConfiguration]) -> Result<()> {
413 const PAGE_SIZE: usize = 9;
414
415 if configs.len() <= PAGE_SIZE {
416 return handle_codex_simple_single_page_menu(configs);
417 }
418
419 let total_pages = configs.len().div_ceil(PAGE_SIZE);
421 let mut current_page = 0;
422
423 loop {
424 let start_idx = current_page * PAGE_SIZE;
425 let end_idx = std::cmp::min(start_idx + PAGE_SIZE, configs.len());
426 let page_configs = &configs[start_idx..end_idx];
427
428 println!("\n{}", "Available Codex Configurations:".blue().bold());
429 println!("第 {} 页,共 {} 页", current_page + 1, total_pages);
430 println!("使用 'n' 下一页, 'p' 上一页, 'q' 退出");
431 println!();
432
433 for (page_index, config) in page_configs.iter().enumerate() {
434 let display_number = page_index + 1;
435 println!(
436 "{}. {}",
437 format!("[{display_number}]").green().bold(),
438 config.alias_name.green()
439 );
440
441 let details = format_codex_config_details(config, " ");
442 for detail_line in details {
443 println!("{detail_line}");
444 }
445 println!();
446 }
447
448 println!("{} {}", "[q]".yellow().bold(), "Exit".yellow());
449
450 println!(
451 "\n页面导航: [n]下页, [p]上页 | 配置选择: [1-{}] | [q]退出",
452 page_configs.len()
453 );
454
455 print!("\n请输入选择: ");
456 io::stdout().flush()?;
457
458 let mut input = String::new();
459 io::stdin().read_line(&mut input)?;
460 let choice = input.trim().to_lowercase();
461
462 match choice.as_str() {
463 "q" => {
464 println!("Exiting...");
465 return Ok(());
466 }
467 "n" if total_pages > 1 && current_page < total_pages - 1 => {
468 current_page += 1;
469 continue;
470 }
471 "p" if total_pages > 1 && current_page > 0 => {
472 current_page -= 1;
473 continue;
474 }
475 digit_str => {
476 if let Ok(digit) = digit_str.parse::<usize>()
477 && digit >= 1
478 && digit <= page_configs.len()
479 {
480 let actual_config_index = start_idx + (digit - 1);
481 return handle_codex_selection_action(configs, actual_config_index);
482 }
483 println!("无效选择,请重新输入");
484 }
485 }
486 }
487}
488
489fn handle_codex_simple_single_page_menu(configs: &[CodexConfiguration]) -> Result<()> {
491 println!("\n{}", "Available Codex Configurations:".blue().bold());
492
493 for (index, config) in configs.iter().enumerate() {
494 println!("{}. {}", index + 1, config.alias_name.green());
495
496 let details = format_codex_config_details(config, " ");
497 for detail_line in details {
498 println!("{detail_line}");
499 }
500 println!();
501 }
502
503 println!("{}. {}", configs.len() + 1, "Exit".yellow());
504
505 print!("\nSelect configuration (1-{}): ", configs.len() + 1);
506 io::stdout().flush()?;
507
508 let mut input = String::new();
509 io::stdin().read_line(&mut input)?;
510
511 match input.trim().parse::<usize>() {
512 Ok(num) if num >= 1 && num <= configs.len() => {
513 handle_codex_selection_action(configs, num - 1)
514 }
515 Ok(num) if num == configs.len() + 1 => {
516 println!("Exiting...");
517 Ok(())
518 }
519 _ => {
520 println!("Invalid selection");
521 Ok(())
522 }
523 }
524}
525
526fn handle_codex_selection_action(
532 configs: &[CodexConfiguration],
533 selected_index: usize,
534) -> Result<()> {
535 if selected_index < configs.len() {
536 let selected_config = &configs[selected_index];
537
538 println!(
539 "\nSwitching to Codex configuration '{}'",
540 selected_config.alias_name.green().bold()
541 );
542
543 let details = format_codex_config_details(selected_config, "");
544 for detail_line in details {
545 println!("{detail_line}");
546 }
547
548 write_auth_json(selected_config)?;
550
551 launch_codex_from_interactive()
553 } else {
554 println!("\nExiting...");
555 Ok(())
556 }
557}
558
559fn launch_codex_from_interactive() -> Result<()> {
561 println!("\nLaunching Codex CLI...");
562
563 #[cfg(unix)]
564 {
565 use std::os::unix::process::CommandExt;
566 let mut command = Command::new("codex");
567 let error = command.exec();
568 anyhow::bail!("Failed to exec codex: {}", error);
569 }
570
571 #[cfg(not(unix))]
572 {
573 use anyhow::Context;
574 use std::process::Stdio;
575 let mut command = Command::new("codex");
576 command
577 .stdin(Stdio::inherit())
578 .stdout(Stdio::inherit())
579 .stderr(Stdio::inherit());
580
581 let mut child = command.spawn().context(
582 "Failed to launch Codex CLI. Make sure 'codex' command is available in PATH",
583 )?;
584
585 let status = child.wait()?;
586
587 if !status.success() {
588 anyhow::bail!("Codex CLI exited with error status: {}", status);
589 }
590 Ok(())
591 }
592}
593
594fn format_key_for_display(key: &str) -> String {
596 if key.len() > 8 {
597 format!("{}...", &key[..8])
598 } else {
599 key.to_string()
600 }
601}
602
603fn handle_codex_config_edit(config: &CodexConfiguration) -> Result<()> {
605 println!("\n{}", "Codex 配置编辑模式".green().bold());
606 println!("{}", "===================".green());
607 println!("正在编辑配置: {}", config.alias_name.cyan().bold());
608 println!();
609
610 let mut editing_config = config.clone();
611 let original_alias = config.alias_name.clone();
612
613 loop {
614 display_codex_edit_menu(&editing_config);
615
616 println!("\n{}", "提示: 可使用大小写字母".dimmed());
617 print!("请选择要编辑的字段 (1-8), 或输入 S 保存, Q 返回上一级菜单: ");
618 io::stdout().flush()?;
619
620 let mut input = String::new();
621 io::stdin().read_line(&mut input)?;
622 let input = input.trim();
623
624 match input {
625 "1" => edit_codex_field_alias(&mut editing_config)?,
626 "2" => edit_codex_field_auth_mode(&mut editing_config)?,
627 "3" => edit_codex_field_openai_api_key(&mut editing_config)?,
628 "4" => edit_codex_field_id_token(&mut editing_config)?,
629 "5" => edit_codex_field_access_token(&mut editing_config)?,
630 "6" => edit_codex_field_refresh_token(&mut editing_config)?,
631 "7" => edit_codex_field_account_id(&mut editing_config)?,
632 "8" => edit_codex_field_last_refresh(&mut editing_config)?,
633 "s" | "S" => {
634 return save_codex_configuration_changes(&original_alias, &editing_config);
635 }
636 "q" | "Q" => {
637 println!("\n{}", "返回上一级菜单".blue());
638 return Err(EditModeError::ReturnToMenu.into());
639 }
640 _ => {
641 println!("{}", "无效选择,请重试".red());
642 }
643 }
644 }
645}
646
647fn display_codex_edit_menu(config: &CodexConfiguration) {
649 println!("\n{}", "当前配置值:".blue().bold());
650 println!("{}", "─────────────────────────".blue());
651
652 println!("1. 别名 (alias_name): {}", config.alias_name.green());
653
654 println!(
655 "2. 认证模式 (auth_mode): {}",
656 if config.auth_mode == "apikey" {
657 "apikey".green()
658 } else {
659 "chatgpt".green()
660 }
661 );
662
663 println!(
664 "3. API密钥 (OPENAI_API_KEY): {}",
665 config
666 .openai_api_key
667 .as_deref()
668 .map(format_key_for_display)
669 .unwrap_or("[未设置]".to_string())
670 .green()
671 );
672
673 println!(
674 "4. ID令牌 (id_token): {}",
675 config
676 .id_token
677 .as_deref()
678 .map(format_key_for_display)
679 .unwrap_or("[未设置]".to_string())
680 .green()
681 );
682
683 println!(
684 "5. 访问令牌 (access_token): {}",
685 config
686 .access_token
687 .as_deref()
688 .map(format_key_for_display)
689 .unwrap_or("[未设置]".to_string())
690 .green()
691 );
692
693 println!(
694 "6. 刷新令牌 (refresh_token): {}",
695 config
696 .refresh_token
697 .as_deref()
698 .map(format_key_for_display)
699 .unwrap_or("[未设置]".to_string())
700 .green()
701 );
702
703 println!(
704 "7. 账户ID (account_id): {}",
705 config.account_id.as_deref().unwrap_or("[未设置]").green()
706 );
707
708 println!(
709 "8. 上次刷新 (last_refresh): {}",
710 config.last_refresh.as_deref().unwrap_or("[未设置]").green()
711 );
712
713 println!("{}", "─────────────────────────".blue());
714 println!(
715 "S. {} | Q. {}",
716 "保存更改".green().bold(),
717 "返回上一级菜单".blue()
718 );
719}
720
721fn edit_codex_field_alias(config: &mut CodexConfiguration) -> Result<()> {
723 let validator = |input: &str| -> Result<()> {
724 if input.contains(char::is_whitespace) {
725 anyhow::bail!("错误: 别名不能包含空白字符");
726 }
727 Ok(())
728 };
729
730 match edit_string_field("别名", &config.alias_name, validator) {
731 Ok(Some(new_value)) => config.alias_name = new_value,
732 Ok(None) => {}
733 Err(e) => println!("{}", e.to_string().red()),
734 }
735 Ok(())
736}
737
738fn edit_codex_field_auth_mode(config: &mut CodexConfiguration) -> Result<()> {
740 println!("\n编辑认证模式:");
741 println!("当前值: {}", config.auth_mode.cyan());
742 print!("新值 (chatgpt/apikey, 回车保持不变): ");
743 io::stdout().flush()?;
744
745 let mut input = String::new();
746 io::stdin().read_line(&mut input)?;
747 let input = input.trim().to_lowercase();
748
749 if !input.is_empty() {
750 if input == "chatgpt" || input == "apikey" {
751 config.auth_mode = input;
752 println!("认证模式已更新为: {}", config.auth_mode.green());
753 } else {
754 println!(
755 "{}",
756 "错误: 无效认证模式,请使用 'chatgpt' 或 'apikey'".red()
757 );
758 }
759 }
760 Ok(())
761}
762
763fn edit_codex_field_openai_api_key(config: &mut CodexConfiguration) -> Result<()> {
765 if let Some(result) = edit_optional_string_field("API密钥", config.openai_api_key.as_deref())?
766 {
767 config.openai_api_key = result;
768 }
769 Ok(())
770}
771
772fn edit_codex_field_id_token(config: &mut CodexConfiguration) -> Result<()> {
774 if let Some(result) = edit_optional_string_field("ID令牌", config.id_token.as_deref())? {
775 config.id_token = result;
776 }
777 Ok(())
778}
779
780fn edit_codex_field_access_token(config: &mut CodexConfiguration) -> Result<()> {
782 if let Some(result) = edit_optional_string_field("访问令牌", config.access_token.as_deref())?
783 {
784 config.access_token = result;
785 }
786 Ok(())
787}
788
789fn edit_codex_field_refresh_token(config: &mut CodexConfiguration) -> Result<()> {
791 if let Some(result) = edit_optional_string_field("刷新令牌", config.refresh_token.as_deref())?
792 {
793 config.refresh_token = result;
794 }
795 Ok(())
796}
797
798fn edit_codex_field_account_id(config: &mut CodexConfiguration) -> Result<()> {
800 if let Some(result) = edit_optional_string_field("账户ID", config.account_id.as_deref())? {
801 config.account_id = result;
802 }
803 Ok(())
804}
805
806fn edit_codex_field_last_refresh(config: &mut CodexConfiguration) -> Result<()> {
808 if let Some(result) =
809 edit_optional_string_field("上次刷新时间", config.last_refresh.as_deref())?
810 {
811 config.last_refresh = result;
812 }
813 Ok(())
814}
815
816fn save_codex_configuration_changes(
818 original_alias: &str,
819 new_config: &CodexConfiguration,
820) -> Result<()> {
821 let mut storage = ConfigStorage::load()?;
822
823 if original_alias != new_config.alias_name
824 && storage
825 .get_codex_configuration(&new_config.alias_name)
826 .is_some()
827 {
828 println!("\n{}", "别名冲突!".red().bold());
829 println!("配置 '{}' 已存在", new_config.alias_name.yellow());
830 print!("是否覆盖现有配置? (y/N): ");
831 io::stdout().flush()?;
832
833 let mut input = String::new();
834 io::stdin().read_line(&mut input)?;
835 let input = input.trim().to_lowercase();
836
837 if input != "y" && input != "yes" {
838 println!("{}", "编辑已取消".yellow());
839 return Ok(());
840 }
841 }
842
843 storage.update_codex_configuration(original_alias, new_config.clone())?;
844 storage.save()?;
845
846 println!("\n{}", "Codex 配置已成功保存!".green().bold());
847
848 Ok(())
849}
850
851#[cfg(test)]
852mod codex_interactive_tests {
853 use super::*;
854
855 #[test]
856 fn test_format_codex_config_details_apikey() {
857 let config = CodexConfiguration {
858 alias_name: "test".to_string(),
859 auth_mode: "apikey".to_string(),
860 openai_api_key: Some("sk-abc123longkey".to_string()),
861 id_token: None,
862 access_token: None,
863 refresh_token: None,
864 account_id: None,
865 last_refresh: None,
866 };
867
868 let lines = format_codex_config_details(&config, " ");
869 assert!(!lines.is_empty());
871 assert!(lines.iter().any(|l| l.contains("apikey")));
872 assert!(lines.iter().any(|l| l.contains("sk-abc12...")));
873 }
874
875 #[test]
876 fn test_format_codex_config_details_chatgpt() {
877 let config = CodexConfiguration {
878 alias_name: "test".to_string(),
879 auth_mode: "chatgpt".to_string(),
880 openai_api_key: None,
881 id_token: Some("id-xyz".to_string()),
882 access_token: Some("at-xyz".to_string()),
883 refresh_token: Some("rt-xyz".to_string()),
884 account_id: Some("acc-123".to_string()),
885 last_refresh: Some("2026-05-16T00:00:00Z".to_string()),
886 };
887
888 let lines = format_codex_config_details(&config, " ");
889 assert!(lines.iter().any(|l| l.contains("chatgpt")));
890 assert!(lines.iter().any(|l| l.contains("acc-123")));
891 assert!(lines.iter().any(|l| l.contains("2026-05-16")));
892 }
893
894 #[test]
895 fn test_pagination_calculation_codex() {
896 const PAGE_SIZE: usize = 9;
897 assert_eq!(1_usize.div_ceil(PAGE_SIZE), 1);
898 assert_eq!(9_usize.div_ceil(PAGE_SIZE), 1);
899 assert_eq!(10_usize.div_ceil(PAGE_SIZE), 2);
900 assert_eq!(18_usize.div_ceil(PAGE_SIZE), 2);
901 assert_eq!(19_usize.div_ceil(PAGE_SIZE), 3);
902 }
903
904 #[test]
905 fn test_exit_index_no_official_option() {
906 let configs_len: usize = 5;
908 let exit_index = configs_len;
909 assert_eq!(exit_index, 5);
910 }
911
912 #[test]
913 fn test_digit_mapping_without_official_offset() {
914 const PAGE_SIZE: usize = 9;
915 let current_page = 0;
916 let start_idx = current_page * PAGE_SIZE;
917
918 let digit = 1;
920 let actual_config_index = start_idx + (digit - 1);
921 assert_eq!(actual_config_index, 0);
922
923 let digit = 5;
925 let actual_config_index = start_idx + (digit - 1);
926 assert_eq!(actual_config_index, 4);
927 }
928}