use std::path::PathBuf; use codex_core::config::set_windows_wsl_setup_acknowledged; 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::style::Color; use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::widgets::Paragraph; use ratatui::widgets::WidgetRef; use ratatui::widgets::Wrap; use crate::onboarding::onboarding_screen::KeyboardHandler; use crate::onboarding::onboarding_screen::StepStateProvider; use super::onboarding_screen::StepState; pub(crate) const WSL_INSTRUCTIONS: &str = r"Install WSL2 by opening PowerShell as Administrator and running: wsl --install Instructions for how to install and run Codex in WSL: https://developers.openai.com/codex/windows"; pub(crate) struct WindowsSetupWidget { pub codex_home: PathBuf, pub selection: Option, pub highlighted: WindowsSetupSelection, pub error: Option, exit_requested: bool, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum WindowsSetupSelection { Continue, Install, } impl WindowsSetupWidget { pub fn new(codex_home: PathBuf) -> Self { Self { codex_home, selection: None, highlighted: WindowsSetupSelection::Install, error: None, exit_requested: false, } } fn handle_continue(&mut self) { self.highlighted = WindowsSetupSelection::Continue; match set_windows_wsl_setup_acknowledged(&self.codex_home, true) { Ok(()) => { self.selection = Some(WindowsSetupSelection::Continue); self.exit_requested = false; self.error = None; } Err(err) => { tracing::error!("Failed to persist Windows onboarding acknowledgement: {err:?}"); self.error = Some(format!("Failed to update config: {err}")); self.selection = None; } } } fn handle_install(&mut self) { self.highlighted = WindowsSetupSelection::Install; self.selection = Some(WindowsSetupSelection::Install); self.exit_requested = true; } pub fn exit_requested(&self) -> bool { self.exit_requested } } impl WidgetRef for &WindowsSetupWidget { fn render_ref(&self, area: Rect, buf: &mut Buffer) { let mut lines: Vec = vec![ Line::from(vec![ "> ".into(), "For best performance, run Codex in Windows Subsystem for Linux (WSL2)".bold(), ]), Line::from(""), ]; let create_option = |idx: usize, option: WindowsSetupSelection, text: &str| -> Line<'static> { if self.highlighted == option { Line::from(format!("> {}. {text}", idx + 1)).cyan() } else { Line::from(format!(" {}. {}", idx + 1, text)) } }; lines.push(create_option( 0, WindowsSetupSelection::Install, "Exit and install WSL2", )); lines.push(create_option( 1, WindowsSetupSelection::Continue, "Continue anyway", )); lines.push("".into()); if let Some(error) = &self.error { lines.push(Line::from(format!(" {error}")).fg(Color::Red)); lines.push("".into()); } lines.push(Line::from(vec![" Press Enter to continue".dim()])); Paragraph::new(lines) .wrap(Wrap { trim: false }) .render(area, buf); } } impl KeyboardHandler for WindowsSetupWidget { fn handle_key_event(&mut self, key_event: KeyEvent) { if key_event.kind == KeyEventKind::Release { return; } match key_event.code { KeyCode::Up | KeyCode::Char('k') => { self.highlighted = WindowsSetupSelection::Install; } KeyCode::Down | KeyCode::Char('j') => { self.highlighted = WindowsSetupSelection::Continue; } KeyCode::Char('1') => self.handle_install(), KeyCode::Char('2') => self.handle_continue(), KeyCode::Enter => match self.highlighted { WindowsSetupSelection::Install => self.handle_install(), WindowsSetupSelection::Continue => self.handle_continue(), }, _ => {} } } } impl StepStateProvider for WindowsSetupWidget { fn get_step_state(&self) -> StepState { match self.selection { Some(WindowsSetupSelection::Continue) => StepState::Hidden, Some(WindowsSetupSelection::Install) => StepState::Complete, None => StepState::InProgress, } } } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; #[test] fn windows_step_hidden_after_continue() { let temp_dir = TempDir::new().expect("temp dir"); let mut widget = WindowsSetupWidget::new(temp_dir.path().to_path_buf()); assert_eq!(widget.get_step_state(), StepState::InProgress); widget.handle_continue(); assert_eq!(widget.get_step_state(), StepState::Hidden); assert!(!widget.exit_requested()); } #[test] fn windows_step_complete_after_install_selection() { let temp_dir = TempDir::new().expect("temp dir"); let mut widget = WindowsSetupWidget::new(temp_dir.path().to_path_buf()); widget.handle_install(); assert_eq!(widget.get_step_state(), StepState::Complete); assert!(widget.exit_requested()); } }