use codex_core::AuthManager; use codex_core::config::Config; use codex_core::git_info::get_git_repo_root; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::prelude::Widget; use ratatui::widgets::Clear; use ratatui::widgets::WidgetRef; use codex_protocol::mcp_protocol::AuthMode; use crate::LoginStatus; use crate::onboarding::auth::AuthModeWidget; use crate::onboarding::auth::SignInState; use crate::onboarding::trust_directory::TrustDirectorySelection; use crate::onboarding::trust_directory::TrustDirectoryWidget; use crate::onboarding::welcome::WelcomeWidget; use crate::tui::FrameRequester; use crate::tui::Tui; use crate::tui::TuiEvent; use color_eyre::eyre::Result; use std::sync::Arc; use std::sync::RwLock; #[allow(clippy::large_enum_variant)] enum Step { Welcome(WelcomeWidget), Auth(AuthModeWidget), TrustDirectory(TrustDirectoryWidget), } pub(crate) trait KeyboardHandler { fn handle_key_event(&mut self, key_event: KeyEvent); } pub(crate) enum StepState { Hidden, InProgress, Complete, } pub(crate) trait StepStateProvider { fn get_step_state(&self) -> StepState; } pub(crate) struct OnboardingScreen { request_frame: FrameRequester, steps: Vec, is_done: bool, } pub(crate) struct OnboardingScreenArgs { pub show_trust_screen: bool, pub show_login_screen: bool, pub login_status: LoginStatus, pub auth_manager: Arc, pub config: Config, } impl OnboardingScreen { pub(crate) fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self { let OnboardingScreenArgs { show_trust_screen, show_login_screen, login_status, auth_manager, config, } = args; let preferred_auth_method = config.preferred_auth_method; let cwd = config.cwd.clone(); let codex_home = config.codex_home.clone(); let mut steps: Vec = vec![Step::Welcome(WelcomeWidget { is_logged_in: !matches!(login_status, LoginStatus::NotAuthenticated), })]; if show_login_screen { steps.push(Step::Auth(AuthModeWidget { request_frame: tui.frame_requester(), highlighted_mode: AuthMode::ChatGPT, error: None, sign_in_state: Arc::new(RwLock::new(SignInState::PickMode)), codex_home: codex_home.clone(), login_status, auth_manager, preferred_auth_method, })) } let is_git_repo = get_git_repo_root(&cwd).is_some(); let highlighted = if is_git_repo { TrustDirectorySelection::Trust } else { // Default to not trusting the directory if it's not a git repo. TrustDirectorySelection::DontTrust }; if show_trust_screen { steps.push(Step::TrustDirectory(TrustDirectoryWidget { cwd, codex_home, is_git_repo, selection: None, highlighted, error: None, })) } // TODO: add git warning. Self { request_frame: tui.frame_requester(), steps, is_done: false, } } fn current_steps_mut(&mut self) -> Vec<&mut Step> { let mut out: Vec<&mut Step> = Vec::new(); for step in self.steps.iter_mut() { match step.get_step_state() { StepState::Hidden => continue, StepState::Complete => out.push(step), StepState::InProgress => { out.push(step); break; } } } out } fn current_steps(&self) -> Vec<&Step> { let mut out: Vec<&Step> = Vec::new(); for step in self.steps.iter() { match step.get_step_state() { StepState::Hidden => continue, StepState::Complete => out.push(step), StepState::InProgress => { out.push(step); break; } } } out } pub(crate) fn is_done(&self) -> bool { self.is_done || !self .steps .iter() .any(|step| matches!(step.get_step_state(), StepState::InProgress)) } pub fn directory_trust_decision(&self) -> Option { self.steps .iter() .find_map(|step| { if let Step::TrustDirectory(TrustDirectoryWidget { selection, .. }) = step { Some(*selection) } else { None } }) .flatten() } } impl KeyboardHandler for OnboardingScreen { fn handle_key_event(&mut self, key_event: KeyEvent) { match key_event { KeyEvent { code: KeyCode::Char('d'), modifiers: crossterm::event::KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. } | KeyEvent { code: KeyCode::Char('c'), modifiers: crossterm::event::KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. } | KeyEvent { code: KeyCode::Char('q'), kind: KeyEventKind::Press, .. } => { self.is_done = true; } _ => { if let Some(active_step) = self.current_steps_mut().into_iter().last() { active_step.handle_key_event(key_event); } } }; self.request_frame.schedule_frame(); } } impl WidgetRef for &OnboardingScreen { fn render_ref(&self, area: Rect, buf: &mut Buffer) { Clear.render(area, buf); // Render steps top-to-bottom, measuring each step's height dynamically. let mut y = area.y; let bottom = area.y.saturating_add(area.height); let width = area.width; // Helper to scan a temporary buffer and return number of used rows. fn used_rows(tmp: &Buffer, width: u16, height: u16) -> u16 { if width == 0 || height == 0 { return 0; } let mut last_non_empty: Option = None; for yy in 0..height { let mut any = false; for xx in 0..width { let sym = tmp[(xx, yy)].symbol(); if !sym.trim().is_empty() { any = true; break; } } if any { last_non_empty = Some(yy); } } last_non_empty.map(|v| v + 2).unwrap_or(0) } let mut i = 0usize; let current_steps = self.current_steps(); while i < current_steps.len() && y < bottom { let step = ¤t_steps[i]; let max_h = bottom.saturating_sub(y); if max_h == 0 || width == 0 { break; } let scratch_area = Rect::new(0, 0, width, max_h); let mut scratch = Buffer::empty(scratch_area); step.render_ref(scratch_area, &mut scratch); let h = used_rows(&scratch, width, max_h).min(max_h); if h > 0 { let target = Rect { x: area.x, y, width, height: h, }; Clear.render(target, buf); step.render_ref(target, buf); y = y.saturating_add(h); } i += 1; } } } impl KeyboardHandler for Step { fn handle_key_event(&mut self, key_event: KeyEvent) { match self { Step::Welcome(_) => (), Step::Auth(widget) => widget.handle_key_event(key_event), Step::TrustDirectory(widget) => widget.handle_key_event(key_event), } } } impl StepStateProvider for Step { fn get_step_state(&self) -> StepState { match self { Step::Welcome(w) => w.get_step_state(), Step::Auth(w) => w.get_step_state(), Step::TrustDirectory(w) => w.get_step_state(), } } } impl WidgetRef for Step { fn render_ref(&self, area: Rect, buf: &mut Buffer) { match self { Step::Welcome(widget) => { widget.render_ref(area, buf); } Step::Auth(widget) => { widget.render_ref(area, buf); } Step::TrustDirectory(widget) => { widget.render_ref(area, buf); } } } } pub(crate) async fn run_onboarding_app( args: OnboardingScreenArgs, tui: &mut Tui, ) -> Result> { use tokio_stream::StreamExt; let mut onboarding_screen = OnboardingScreen::new(tui, args); tui.draw(u16::MAX, |frame| { frame.render_widget_ref(&onboarding_screen, frame.area()); })?; let tui_events = tui.event_stream(); tokio::pin!(tui_events); while !onboarding_screen.is_done() { if let Some(event) = tui_events.next().await { match event { TuiEvent::Key(key_event) => { onboarding_screen.handle_key_event(key_event); } TuiEvent::Draw => { let _ = tui.draw(u16::MAX, |frame| { frame.render_widget_ref(&onboarding_screen, frame.area()); }); } _ => {} } } } Ok(onboarding_screen.directory_trust_decision()) }