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