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