2025-08-07 09:27:38 -07:00
|
|
|
use codex_core::util::is_inside_git_repo;
|
2025-08-20 13:47:24 -07:00
|
|
|
use crossterm::event::KeyCode;
|
2025-08-06 15:22:14 -07:00
|
|
|
use crossterm::event::KeyEvent;
|
2025-08-20 13:47:24 -07:00
|
|
|
use crossterm::event::KeyEventKind;
|
2025-08-06 15:22:14 -07:00
|
|
|
use ratatui::buffer::Buffer;
|
|
|
|
|
use ratatui::layout::Rect;
|
2025-08-14 17:11:26 -07:00
|
|
|
use ratatui::prelude::Widget;
|
|
|
|
|
use ratatui::widgets::Clear;
|
2025-08-06 15:22:14 -07:00
|
|
|
use ratatui::widgets::WidgetRef;
|
|
|
|
|
|
|
|
|
|
use codex_login::AuthMode;
|
|
|
|
|
|
2025-08-18 20:22:48 -07:00
|
|
|
use crate::LoginStatus;
|
2025-08-06 15:22:14 -07:00
|
|
|
use crate::onboarding::auth::AuthModeWidget;
|
|
|
|
|
use crate::onboarding::auth::SignInState;
|
2025-08-07 09:27:38 -07:00
|
|
|
use crate::onboarding::trust_directory::TrustDirectorySelection;
|
|
|
|
|
use crate::onboarding::trust_directory::TrustDirectoryWidget;
|
2025-08-06 15:22:14 -07:00
|
|
|
use crate::onboarding::welcome::WelcomeWidget;
|
2025-08-20 13:47:24 -07:00
|
|
|
use crate::tui::FrameRequester;
|
|
|
|
|
use crate::tui::Tui;
|
|
|
|
|
use crate::tui::TuiEvent;
|
|
|
|
|
use color_eyre::eyre::Result;
|
2025-08-06 15:22:14 -07:00
|
|
|
use std::path::PathBuf;
|
2025-08-07 09:27:38 -07:00
|
|
|
use std::sync::Arc;
|
2025-08-20 13:47:24 -07:00
|
|
|
use std::sync::RwLock;
|
2025-08-06 15:22:14 -07:00
|
|
|
|
2025-08-06 19:39:07 -07:00
|
|
|
#[allow(clippy::large_enum_variant)]
|
2025-08-06 15:22:14 -07:00
|
|
|
enum Step {
|
|
|
|
|
Welcome(WelcomeWidget),
|
|
|
|
|
Auth(AuthModeWidget),
|
2025-08-07 09:27:38 -07:00
|
|
|
TrustDirectory(TrustDirectoryWidget),
|
2025-08-06 15:22:14 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) trait KeyboardHandler {
|
2025-08-06 19:39:07 -07:00
|
|
|
fn handle_key_event(&mut self, key_event: KeyEvent);
|
2025-08-06 15:22:14 -07:00
|
|
|
}
|
|
|
|
|
|
2025-08-06 19:39:07 -07:00
|
|
|
pub(crate) enum StepState {
|
|
|
|
|
Hidden,
|
|
|
|
|
InProgress,
|
|
|
|
|
Complete,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) trait StepStateProvider {
|
|
|
|
|
fn get_step_state(&self) -> StepState;
|
2025-08-06 15:22:14 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) struct OnboardingScreen {
|
2025-08-20 13:47:24 -07:00
|
|
|
request_frame: FrameRequester,
|
2025-08-06 15:22:14 -07:00
|
|
|
steps: Vec<Step>,
|
2025-08-20 13:47:24 -07:00
|
|
|
is_done: bool,
|
2025-08-06 15:22:14 -07:00
|
|
|
}
|
|
|
|
|
|
2025-08-06 19:39:07 -07:00
|
|
|
pub(crate) struct OnboardingScreenArgs {
|
|
|
|
|
pub codex_home: PathBuf,
|
|
|
|
|
pub cwd: PathBuf,
|
2025-08-07 09:27:38 -07:00
|
|
|
pub show_trust_screen: bool,
|
2025-08-18 20:22:48 -07:00
|
|
|
pub show_login_screen: bool,
|
|
|
|
|
pub login_status: LoginStatus,
|
2025-08-20 13:47:24 -07:00
|
|
|
pub preferred_auth_method: AuthMode,
|
2025-08-06 19:39:07 -07:00
|
|
|
}
|
|
|
|
|
|
2025-08-06 15:22:14 -07:00
|
|
|
impl OnboardingScreen {
|
2025-08-20 13:47:24 -07:00
|
|
|
pub(crate) fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self {
|
2025-08-06 19:39:07 -07:00
|
|
|
let OnboardingScreenArgs {
|
|
|
|
|
codex_home,
|
|
|
|
|
cwd,
|
2025-08-07 09:27:38 -07:00
|
|
|
show_trust_screen,
|
2025-08-18 20:22:48 -07:00
|
|
|
show_login_screen,
|
|
|
|
|
login_status,
|
2025-08-20 13:47:24 -07:00
|
|
|
preferred_auth_method,
|
2025-08-06 19:39:07 -07:00
|
|
|
} = args;
|
|
|
|
|
let mut steps: Vec<Step> = vec![Step::Welcome(WelcomeWidget {
|
2025-08-18 20:22:48 -07:00
|
|
|
is_logged_in: !matches!(login_status, LoginStatus::NotAuthenticated),
|
2025-08-06 19:39:07 -07:00
|
|
|
})];
|
|
|
|
|
if show_login_screen {
|
|
|
|
|
steps.push(Step::Auth(AuthModeWidget {
|
2025-08-20 13:47:24 -07:00
|
|
|
request_frame: tui.frame_requester(),
|
2025-08-06 19:39:07 -07:00
|
|
|
highlighted_mode: AuthMode::ChatGPT,
|
2025-08-06 15:22:14 -07:00
|
|
|
error: None,
|
2025-08-20 13:47:24 -07:00
|
|
|
sign_in_state: Arc::new(RwLock::new(SignInState::PickMode)),
|
2025-08-07 09:27:38 -07:00
|
|
|
codex_home: codex_home.clone(),
|
2025-08-18 20:22:48 -07:00
|
|
|
login_status,
|
2025-08-20 13:47:24 -07:00
|
|
|
preferred_auth_method,
|
2025-08-06 19:39:07 -07:00
|
|
|
}))
|
|
|
|
|
}
|
2025-08-07 09:27:38 -07:00
|
|
|
let is_git_repo = is_inside_git_repo(&cwd);
|
|
|
|
|
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 {
|
2025-08-06 19:39:07 -07:00
|
|
|
cwd,
|
2025-08-07 09:27:38 -07:00
|
|
|
codex_home,
|
|
|
|
|
is_git_repo,
|
2025-08-06 19:39:07 -07:00
|
|
|
selection: None,
|
2025-08-07 09:27:38 -07:00
|
|
|
highlighted,
|
|
|
|
|
error: None,
|
2025-08-06 19:39:07 -07:00
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
// TODO: add git warning.
|
2025-08-20 13:47:24 -07:00
|
|
|
Self {
|
|
|
|
|
request_frame: tui.frame_requester(),
|
|
|
|
|
steps,
|
|
|
|
|
is_done: false,
|
2025-08-06 15:22:14 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-08-06 19:39:07 -07:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-20 13:47:24 -07:00
|
|
|
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<TrustDirectorySelection> {
|
2025-08-06 19:39:07 -07:00
|
|
|
self.steps
|
2025-08-20 13:47:24 -07:00
|
|
|
.iter()
|
|
|
|
|
.find_map(|step| {
|
|
|
|
|
if let Step::TrustDirectory(TrustDirectoryWidget { selection, .. }) = step {
|
|
|
|
|
Some(*selection)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.flatten()
|
2025-08-06 19:39:07 -07:00
|
|
|
}
|
2025-08-06 15:22:14 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl KeyboardHandler for OnboardingScreen {
|
2025-08-06 19:39:07 -07:00
|
|
|
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
2025-08-20 13:47:24 -07:00
|
|
|
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();
|
2025-08-06 15:22:14 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl WidgetRef for &OnboardingScreen {
|
|
|
|
|
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
2025-08-14 17:11:26 -07:00
|
|
|
Clear.render(area, buf);
|
2025-08-06 15:22:14 -07:00
|
|
|
// 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<u16> = 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;
|
2025-08-06 19:39:07 -07:00
|
|
|
let current_steps = self.current_steps();
|
|
|
|
|
|
|
|
|
|
while i < current_steps.len() && y < bottom {
|
|
|
|
|
let step = ¤t_steps[i];
|
2025-08-06 15:22:14 -07:00
|
|
|
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,
|
|
|
|
|
};
|
2025-08-14 17:11:26 -07:00
|
|
|
Clear.render(target, buf);
|
2025-08-06 15:22:14 -07:00
|
|
|
step.render_ref(target, buf);
|
|
|
|
|
y = y.saturating_add(h);
|
|
|
|
|
}
|
|
|
|
|
i += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl KeyboardHandler for Step {
|
2025-08-06 19:39:07 -07:00
|
|
|
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
2025-08-06 15:22:14 -07:00
|
|
|
match self {
|
2025-08-20 13:47:24 -07:00
|
|
|
Step::Welcome(_) => (),
|
2025-08-06 15:22:14 -07:00
|
|
|
Step::Auth(widget) => widget.handle_key_event(key_event),
|
2025-08-07 09:27:38 -07:00
|
|
|
Step::TrustDirectory(widget) => widget.handle_key_event(key_event),
|
2025-08-06 19:39:07 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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(),
|
2025-08-07 09:27:38 -07:00
|
|
|
Step::TrustDirectory(w) => w.get_step_state(),
|
2025-08-06 15:22:14 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
2025-08-07 09:27:38 -07:00
|
|
|
Step::TrustDirectory(widget) => {
|
2025-08-06 19:39:07 -07:00
|
|
|
widget.render_ref(area, buf);
|
|
|
|
|
}
|
2025-08-20 13:47:24 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) async fn run_onboarding_app(
|
|
|
|
|
args: OnboardingScreenArgs,
|
|
|
|
|
tui: &mut Tui,
|
|
|
|
|
) -> Result<Option<crate::onboarding::TrustDirectorySelection>> {
|
|
|
|
|
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());
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
2025-08-06 19:39:07 -07:00
|
|
|
}
|
2025-08-06 15:22:14 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-08-20 13:47:24 -07:00
|
|
|
Ok(onboarding_screen.directory_trust_decision())
|
2025-08-06 15:22:14 -07:00
|
|
|
}
|