add an onboarding informing Windows of better support in WSL (#4697)
This commit is contained in:
@@ -209,6 +209,9 @@ pub struct Config {
|
|||||||
/// The active profile name used to derive this `Config` (if any).
|
/// The active profile name used to derive this `Config` (if any).
|
||||||
pub active_profile: Option<String>,
|
pub active_profile: Option<String>,
|
||||||
|
|
||||||
|
/// Tracks whether the Windows onboarding screen has been acknowledged.
|
||||||
|
pub windows_wsl_setup_acknowledged: bool,
|
||||||
|
|
||||||
/// When true, disables burst-paste detection for typed input entirely.
|
/// When true, disables burst-paste detection for typed input entirely.
|
||||||
/// All characters are inserted as they are received, and no buffering
|
/// All characters are inserted as they are received, and no buffering
|
||||||
/// or placeholder replacement will occur for fast keypress bursts.
|
/// or placeholder replacement will occur for fast keypress bursts.
|
||||||
@@ -471,6 +474,29 @@ pub fn set_project_trusted(codex_home: &Path, project_path: &Path) -> anyhow::Re
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Persist the acknowledgement flag for the Windows onboarding screen.
|
||||||
|
pub fn set_windows_wsl_setup_acknowledged(
|
||||||
|
codex_home: &Path,
|
||||||
|
acknowledged: bool,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let config_path = codex_home.join(CONFIG_TOML_FILE);
|
||||||
|
let mut doc = match std::fs::read_to_string(config_path.clone()) {
|
||||||
|
Ok(s) => s.parse::<DocumentMut>()?,
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
doc["windows_wsl_setup_acknowledged"] = toml_edit::value(acknowledged);
|
||||||
|
|
||||||
|
std::fs::create_dir_all(codex_home)?;
|
||||||
|
|
||||||
|
let tmp_file = NamedTempFile::new_in(codex_home)?;
|
||||||
|
std::fs::write(tmp_file.path(), doc.to_string())?;
|
||||||
|
tmp_file.persist(config_path)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn ensure_profile_table<'a>(
|
fn ensure_profile_table<'a>(
|
||||||
doc: &'a mut DocumentMut,
|
doc: &'a mut DocumentMut,
|
||||||
profile_name: &str,
|
profile_name: &str,
|
||||||
@@ -737,6 +763,9 @@ pub struct ConfigToml {
|
|||||||
|
|
||||||
/// OTEL configuration.
|
/// OTEL configuration.
|
||||||
pub otel: Option<crate::config_types::OtelConfigToml>,
|
pub otel: Option<crate::config_types::OtelConfigToml>,
|
||||||
|
|
||||||
|
/// Tracks whether the Windows onboarding screen has been acknowledged.
|
||||||
|
pub windows_wsl_setup_acknowledged: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ConfigToml> for UserSavedConfig {
|
impl From<ConfigToml> for UserSavedConfig {
|
||||||
@@ -1093,6 +1122,7 @@ impl Config {
|
|||||||
use_experimental_use_rmcp_client: cfg.experimental_use_rmcp_client.unwrap_or(false),
|
use_experimental_use_rmcp_client: cfg.experimental_use_rmcp_client.unwrap_or(false),
|
||||||
include_view_image_tool,
|
include_view_image_tool,
|
||||||
active_profile: active_profile_name,
|
active_profile: active_profile_name,
|
||||||
|
windows_wsl_setup_acknowledged: cfg.windows_wsl_setup_acknowledged.unwrap_or(false),
|
||||||
disable_paste_burst: cfg.disable_paste_burst.unwrap_or(false),
|
disable_paste_burst: cfg.disable_paste_burst.unwrap_or(false),
|
||||||
tui_notifications: cfg
|
tui_notifications: cfg
|
||||||
.tui
|
.tui
|
||||||
@@ -1885,6 +1915,7 @@ model_verbosity = "high"
|
|||||||
use_experimental_use_rmcp_client: false,
|
use_experimental_use_rmcp_client: false,
|
||||||
include_view_image_tool: true,
|
include_view_image_tool: true,
|
||||||
active_profile: Some("o3".to_string()),
|
active_profile: Some("o3".to_string()),
|
||||||
|
windows_wsl_setup_acknowledged: false,
|
||||||
disable_paste_burst: false,
|
disable_paste_burst: false,
|
||||||
tui_notifications: Default::default(),
|
tui_notifications: Default::default(),
|
||||||
otel: OtelConfig::default(),
|
otel: OtelConfig::default(),
|
||||||
@@ -1946,6 +1977,7 @@ model_verbosity = "high"
|
|||||||
use_experimental_use_rmcp_client: false,
|
use_experimental_use_rmcp_client: false,
|
||||||
include_view_image_tool: true,
|
include_view_image_tool: true,
|
||||||
active_profile: Some("gpt3".to_string()),
|
active_profile: Some("gpt3".to_string()),
|
||||||
|
windows_wsl_setup_acknowledged: false,
|
||||||
disable_paste_burst: false,
|
disable_paste_burst: false,
|
||||||
tui_notifications: Default::default(),
|
tui_notifications: Default::default(),
|
||||||
otel: OtelConfig::default(),
|
otel: OtelConfig::default(),
|
||||||
@@ -2022,6 +2054,7 @@ model_verbosity = "high"
|
|||||||
use_experimental_use_rmcp_client: false,
|
use_experimental_use_rmcp_client: false,
|
||||||
include_view_image_tool: true,
|
include_view_image_tool: true,
|
||||||
active_profile: Some("zdr".to_string()),
|
active_profile: Some("zdr".to_string()),
|
||||||
|
windows_wsl_setup_acknowledged: false,
|
||||||
disable_paste_burst: false,
|
disable_paste_burst: false,
|
||||||
tui_notifications: Default::default(),
|
tui_notifications: Default::default(),
|
||||||
otel: OtelConfig::default(),
|
otel: OtelConfig::default(),
|
||||||
@@ -2084,6 +2117,7 @@ model_verbosity = "high"
|
|||||||
use_experimental_use_rmcp_client: false,
|
use_experimental_use_rmcp_client: false,
|
||||||
include_view_image_tool: true,
|
include_view_image_tool: true,
|
||||||
active_profile: Some("gpt5".to_string()),
|
active_profile: Some("gpt5".to_string()),
|
||||||
|
windows_wsl_setup_acknowledged: false,
|
||||||
disable_paste_burst: false,
|
disable_paste_burst: false,
|
||||||
tui_notifications: Default::default(),
|
tui_notifications: Default::default(),
|
||||||
otel: OtelConfig::default(),
|
otel: OtelConfig::default(),
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn drops_from_last_user_only() {
|
fn drops_from_last_user_only() {
|
||||||
let items = vec![
|
let items = [
|
||||||
user_msg("u1"),
|
user_msg("u1"),
|
||||||
assistant_msg("a1"),
|
assistant_msg("a1"),
|
||||||
assistant_msg("a2"),
|
assistant_msg("a2"),
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ pub mod test_backend;
|
|||||||
mod updates;
|
mod updates;
|
||||||
|
|
||||||
use crate::onboarding::TrustDirectorySelection;
|
use crate::onboarding::TrustDirectorySelection;
|
||||||
|
use crate::onboarding::WSL_INSTRUCTIONS;
|
||||||
use crate::onboarding::onboarding_screen::OnboardingScreenArgs;
|
use crate::onboarding::onboarding_screen::OnboardingScreenArgs;
|
||||||
use crate::onboarding::onboarding_screen::run_onboarding_app;
|
use crate::onboarding::onboarding_screen::run_onboarding_app;
|
||||||
use crate::tui::Tui;
|
use crate::tui::Tui;
|
||||||
@@ -87,6 +88,7 @@ pub use cli::Cli;
|
|||||||
pub use markdown_render::render_markdown_text;
|
pub use markdown_render::render_markdown_text;
|
||||||
pub use public_widgets::composer_input::ComposerAction;
|
pub use public_widgets::composer_input::ComposerAction;
|
||||||
pub use public_widgets::composer_input::ComposerInput;
|
pub use public_widgets::composer_input::ComposerInput;
|
||||||
|
use std::io::Write as _;
|
||||||
|
|
||||||
// (tests access modules directly within the crate)
|
// (tests access modules directly within the crate)
|
||||||
|
|
||||||
@@ -364,11 +366,18 @@ async fn run_ratatui_app(
|
|||||||
|
|
||||||
let auth_manager = AuthManager::shared(config.codex_home.clone(), false);
|
let auth_manager = AuthManager::shared(config.codex_home.clone(), false);
|
||||||
let login_status = get_login_status(&config);
|
let login_status = get_login_status(&config);
|
||||||
let should_show_onboarding =
|
let should_show_windows_wsl_screen =
|
||||||
should_show_onboarding(login_status, &config, should_show_trust_screen);
|
cfg!(target_os = "windows") && !config.windows_wsl_setup_acknowledged;
|
||||||
|
let should_show_onboarding = should_show_onboarding(
|
||||||
|
login_status,
|
||||||
|
&config,
|
||||||
|
should_show_trust_screen,
|
||||||
|
should_show_windows_wsl_screen,
|
||||||
|
);
|
||||||
if should_show_onboarding {
|
if should_show_onboarding {
|
||||||
let directory_trust_decision = run_onboarding_app(
|
let onboarding_result = run_onboarding_app(
|
||||||
OnboardingScreenArgs {
|
OnboardingScreenArgs {
|
||||||
|
show_windows_wsl_screen: should_show_windows_wsl_screen,
|
||||||
show_login_screen: should_show_login_screen(login_status, &config),
|
show_login_screen: should_show_login_screen(login_status, &config),
|
||||||
show_trust_screen: should_show_trust_screen,
|
show_trust_screen: should_show_trust_screen,
|
||||||
login_status,
|
login_status,
|
||||||
@@ -378,7 +387,22 @@ async fn run_ratatui_app(
|
|||||||
&mut tui,
|
&mut tui,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
if let Some(TrustDirectorySelection::Trust) = directory_trust_decision {
|
if onboarding_result.windows_install_selected {
|
||||||
|
restore();
|
||||||
|
session_log::log_session_end();
|
||||||
|
let _ = tui.terminal.clear();
|
||||||
|
if let Err(err) = writeln!(std::io::stdout(), "{WSL_INSTRUCTIONS}") {
|
||||||
|
tracing::error!("Failed to write WSL instructions: {err}");
|
||||||
|
}
|
||||||
|
return Ok(AppExitInfo {
|
||||||
|
token_usage: codex_core::protocol::TokenUsage::default(),
|
||||||
|
conversation_id: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if should_show_windows_wsl_screen {
|
||||||
|
config.windows_wsl_setup_acknowledged = true;
|
||||||
|
}
|
||||||
|
if let Some(TrustDirectorySelection::Trust) = onboarding_result.directory_trust_decision {
|
||||||
config.approval_policy = AskForApproval::OnRequest;
|
config.approval_policy = AskForApproval::OnRequest;
|
||||||
config.sandbox_policy = SandboxPolicy::new_workspace_write_policy();
|
config.sandbox_policy = SandboxPolicy::new_workspace_write_policy();
|
||||||
}
|
}
|
||||||
@@ -521,7 +545,12 @@ fn should_show_onboarding(
|
|||||||
login_status: LoginStatus,
|
login_status: LoginStatus,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
show_trust_screen: bool,
|
show_trust_screen: bool,
|
||||||
|
show_windows_wsl_screen: bool,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
if show_windows_wsl_screen {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if show_trust_screen {
|
if show_trust_screen {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,3 +3,6 @@ pub mod onboarding_screen;
|
|||||||
mod trust_directory;
|
mod trust_directory;
|
||||||
pub use trust_directory::TrustDirectorySelection;
|
pub use trust_directory::TrustDirectorySelection;
|
||||||
mod welcome;
|
mod welcome;
|
||||||
|
mod windows;
|
||||||
|
|
||||||
|
pub(crate) use windows::WSL_INSTRUCTIONS;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use crate::onboarding::auth::SignInState;
|
|||||||
use crate::onboarding::trust_directory::TrustDirectorySelection;
|
use crate::onboarding::trust_directory::TrustDirectorySelection;
|
||||||
use crate::onboarding::trust_directory::TrustDirectoryWidget;
|
use crate::onboarding::trust_directory::TrustDirectoryWidget;
|
||||||
use crate::onboarding::welcome::WelcomeWidget;
|
use crate::onboarding::welcome::WelcomeWidget;
|
||||||
|
use crate::onboarding::windows::WindowsSetupWidget;
|
||||||
use crate::tui::FrameRequester;
|
use crate::tui::FrameRequester;
|
||||||
use crate::tui::Tui;
|
use crate::tui::Tui;
|
||||||
use crate::tui::TuiEvent;
|
use crate::tui::TuiEvent;
|
||||||
@@ -28,6 +29,7 @@ use std::sync::RwLock;
|
|||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
enum Step {
|
enum Step {
|
||||||
|
Windows(WindowsSetupWidget),
|
||||||
Welcome(WelcomeWidget),
|
Welcome(WelcomeWidget),
|
||||||
Auth(AuthModeWidget),
|
Auth(AuthModeWidget),
|
||||||
TrustDirectory(TrustDirectoryWidget),
|
TrustDirectory(TrustDirectoryWidget),
|
||||||
@@ -38,6 +40,7 @@ pub(crate) trait KeyboardHandler {
|
|||||||
fn handle_paste(&mut self, _pasted: String) {}
|
fn handle_paste(&mut self, _pasted: String) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub(crate) enum StepState {
|
pub(crate) enum StepState {
|
||||||
Hidden,
|
Hidden,
|
||||||
InProgress,
|
InProgress,
|
||||||
@@ -52,9 +55,11 @@ pub(crate) struct OnboardingScreen {
|
|||||||
request_frame: FrameRequester,
|
request_frame: FrameRequester,
|
||||||
steps: Vec<Step>,
|
steps: Vec<Step>,
|
||||||
is_done: bool,
|
is_done: bool,
|
||||||
|
windows_install_selected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct OnboardingScreenArgs {
|
pub(crate) struct OnboardingScreenArgs {
|
||||||
|
pub show_windows_wsl_screen: bool,
|
||||||
pub show_trust_screen: bool,
|
pub show_trust_screen: bool,
|
||||||
pub show_login_screen: bool,
|
pub show_login_screen: bool,
|
||||||
pub login_status: LoginStatus,
|
pub login_status: LoginStatus,
|
||||||
@@ -62,9 +67,15 @@ pub(crate) struct OnboardingScreenArgs {
|
|||||||
pub config: Config,
|
pub config: Config,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct OnboardingResult {
|
||||||
|
pub directory_trust_decision: Option<TrustDirectorySelection>,
|
||||||
|
pub windows_install_selected: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl OnboardingScreen {
|
impl OnboardingScreen {
|
||||||
pub(crate) fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self {
|
pub(crate) fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self {
|
||||||
let OnboardingScreenArgs {
|
let OnboardingScreenArgs {
|
||||||
|
show_windows_wsl_screen,
|
||||||
show_trust_screen,
|
show_trust_screen,
|
||||||
show_login_screen,
|
show_login_screen,
|
||||||
login_status,
|
login_status,
|
||||||
@@ -73,10 +84,14 @@ impl OnboardingScreen {
|
|||||||
} = args;
|
} = args;
|
||||||
let cwd = config.cwd.clone();
|
let cwd = config.cwd.clone();
|
||||||
let codex_home = config.codex_home;
|
let codex_home = config.codex_home;
|
||||||
let mut steps: Vec<Step> = vec![Step::Welcome(WelcomeWidget::new(
|
let mut steps: Vec<Step> = Vec::new();
|
||||||
|
if show_windows_wsl_screen {
|
||||||
|
steps.push(Step::Windows(WindowsSetupWidget::new(codex_home.clone())));
|
||||||
|
}
|
||||||
|
steps.push(Step::Welcome(WelcomeWidget::new(
|
||||||
!matches!(login_status, LoginStatus::NotAuthenticated),
|
!matches!(login_status, LoginStatus::NotAuthenticated),
|
||||||
tui.frame_requester(),
|
tui.frame_requester(),
|
||||||
))];
|
)));
|
||||||
if show_login_screen {
|
if show_login_screen {
|
||||||
steps.push(Step::Auth(AuthModeWidget {
|
steps.push(Step::Auth(AuthModeWidget {
|
||||||
request_frame: tui.frame_requester(),
|
request_frame: tui.frame_requester(),
|
||||||
@@ -110,6 +125,7 @@ impl OnboardingScreen {
|
|||||||
request_frame: tui.frame_requester(),
|
request_frame: tui.frame_requester(),
|
||||||
steps,
|
steps,
|
||||||
is_done: false,
|
is_done: false,
|
||||||
|
windows_install_selected: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,6 +179,10 @@ impl OnboardingScreen {
|
|||||||
})
|
})
|
||||||
.flatten()
|
.flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn windows_install_selected(&self) -> bool {
|
||||||
|
self.windows_install_selected
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeyboardHandler for OnboardingScreen {
|
impl KeyboardHandler for OnboardingScreen {
|
||||||
@@ -200,6 +220,14 @@ impl KeyboardHandler for OnboardingScreen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if self
|
||||||
|
.steps
|
||||||
|
.iter()
|
||||||
|
.any(|step| matches!(step, Step::Windows(widget) if widget.exit_requested()))
|
||||||
|
{
|
||||||
|
self.windows_install_selected = true;
|
||||||
|
self.is_done = true;
|
||||||
|
}
|
||||||
self.request_frame.schedule_frame();
|
self.request_frame.schedule_frame();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,6 +309,7 @@ impl WidgetRef for &OnboardingScreen {
|
|||||||
impl KeyboardHandler for Step {
|
impl KeyboardHandler for Step {
|
||||||
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||||
match self {
|
match self {
|
||||||
|
Step::Windows(widget) => widget.handle_key_event(key_event),
|
||||||
Step::Welcome(widget) => widget.handle_key_event(key_event),
|
Step::Welcome(widget) => widget.handle_key_event(key_event),
|
||||||
Step::Auth(widget) => widget.handle_key_event(key_event),
|
Step::Auth(widget) => widget.handle_key_event(key_event),
|
||||||
Step::TrustDirectory(widget) => widget.handle_key_event(key_event),
|
Step::TrustDirectory(widget) => widget.handle_key_event(key_event),
|
||||||
@@ -289,6 +318,7 @@ impl KeyboardHandler for Step {
|
|||||||
|
|
||||||
fn handle_paste(&mut self, pasted: String) {
|
fn handle_paste(&mut self, pasted: String) {
|
||||||
match self {
|
match self {
|
||||||
|
Step::Windows(_) => {}
|
||||||
Step::Welcome(_) => {}
|
Step::Welcome(_) => {}
|
||||||
Step::Auth(widget) => widget.handle_paste(pasted),
|
Step::Auth(widget) => widget.handle_paste(pasted),
|
||||||
Step::TrustDirectory(widget) => widget.handle_paste(pasted),
|
Step::TrustDirectory(widget) => widget.handle_paste(pasted),
|
||||||
@@ -299,6 +329,7 @@ impl KeyboardHandler for Step {
|
|||||||
impl StepStateProvider for Step {
|
impl StepStateProvider for Step {
|
||||||
fn get_step_state(&self) -> StepState {
|
fn get_step_state(&self) -> StepState {
|
||||||
match self {
|
match self {
|
||||||
|
Step::Windows(w) => w.get_step_state(),
|
||||||
Step::Welcome(w) => w.get_step_state(),
|
Step::Welcome(w) => w.get_step_state(),
|
||||||
Step::Auth(w) => w.get_step_state(),
|
Step::Auth(w) => w.get_step_state(),
|
||||||
Step::TrustDirectory(w) => w.get_step_state(),
|
Step::TrustDirectory(w) => w.get_step_state(),
|
||||||
@@ -309,6 +340,9 @@ impl StepStateProvider for Step {
|
|||||||
impl WidgetRef for Step {
|
impl WidgetRef for Step {
|
||||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||||
match self {
|
match self {
|
||||||
|
Step::Windows(widget) => {
|
||||||
|
widget.render_ref(area, buf);
|
||||||
|
}
|
||||||
Step::Welcome(widget) => {
|
Step::Welcome(widget) => {
|
||||||
widget.render_ref(area, buf);
|
widget.render_ref(area, buf);
|
||||||
}
|
}
|
||||||
@@ -325,7 +359,7 @@ impl WidgetRef for Step {
|
|||||||
pub(crate) async fn run_onboarding_app(
|
pub(crate) async fn run_onboarding_app(
|
||||||
args: OnboardingScreenArgs,
|
args: OnboardingScreenArgs,
|
||||||
tui: &mut Tui,
|
tui: &mut Tui,
|
||||||
) -> Result<Option<crate::onboarding::TrustDirectorySelection>> {
|
) -> Result<OnboardingResult> {
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
|
|
||||||
let mut onboarding_screen = OnboardingScreen::new(tui, args);
|
let mut onboarding_screen = OnboardingScreen::new(tui, args);
|
||||||
@@ -386,5 +420,8 @@ pub(crate) async fn run_onboarding_app(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(onboarding_screen.directory_trust_decision())
|
Ok(OnboardingResult {
|
||||||
|
directory_trust_decision: onboarding_screen.directory_trust_decision(),
|
||||||
|
windows_install_selected: onboarding_screen.windows_install_selected(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use codex_core::config::set_project_trusted;
|
|||||||
use codex_core::git_info::resolve_root_git_project_for_trust;
|
use codex_core::git_info::resolve_root_git_project_for_trust;
|
||||||
use crossterm::event::KeyCode;
|
use crossterm::event::KeyCode;
|
||||||
use crossterm::event::KeyEvent;
|
use crossterm::event::KeyEvent;
|
||||||
|
use crossterm::event::KeyEventKind;
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::prelude::Widget;
|
use ratatui::prelude::Widget;
|
||||||
@@ -109,6 +110,10 @@ impl WidgetRef for &TrustDirectoryWidget {
|
|||||||
|
|
||||||
impl KeyboardHandler for TrustDirectoryWidget {
|
impl KeyboardHandler for TrustDirectoryWidget {
|
||||||
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||||
|
if key_event.kind == KeyEventKind::Release {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
match key_event.code {
|
match key_event.code {
|
||||||
KeyCode::Up | KeyCode::Char('k') => {
|
KeyCode::Up | KeyCode::Char('k') => {
|
||||||
self.highlighted = TrustDirectorySelection::Trust;
|
self.highlighted = TrustDirectorySelection::Trust;
|
||||||
@@ -153,3 +158,37 @@ impl TrustDirectoryWidget {
|
|||||||
self.selection = Some(TrustDirectorySelection::DontTrust);
|
self.selection = Some(TrustDirectorySelection::DontTrust);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crossterm::event::KeyCode;
|
||||||
|
use crossterm::event::KeyEvent;
|
||||||
|
use crossterm::event::KeyEventKind;
|
||||||
|
use crossterm::event::KeyModifiers;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_event_does_not_change_selection() {
|
||||||
|
let mut widget = TrustDirectoryWidget {
|
||||||
|
codex_home: PathBuf::from("."),
|
||||||
|
cwd: PathBuf::from("."),
|
||||||
|
is_git_repo: false,
|
||||||
|
selection: None,
|
||||||
|
highlighted: TrustDirectorySelection::DontTrust,
|
||||||
|
error: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let release = KeyEvent {
|
||||||
|
kind: KeyEventKind::Release,
|
||||||
|
..KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)
|
||||||
|
};
|
||||||
|
widget.handle_key_event(release);
|
||||||
|
assert_eq!(widget.selection, None);
|
||||||
|
|
||||||
|
let press = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
|
||||||
|
widget.handle_key_event(press);
|
||||||
|
assert_eq!(widget.selection, Some(TrustDirectorySelection::DontTrust));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
190
codex-rs/tui/src/onboarding/windows.rs
Normal file
190
codex-rs/tui/src/onboarding/windows.rs
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
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"Windows Subsystem for Linux (WSL2) is required to run Codex.
|
||||||
|
|
||||||
|
To install WSL2:
|
||||||
|
1. Open PowerShell as Administrator and run: wsl --install
|
||||||
|
2. Restart your machine if prompted.
|
||||||
|
3. Launch the Ubuntu shortcut from the Start menu to complete setup.
|
||||||
|
|
||||||
|
After installation, reopen Codex from a WSL shell.";
|
||||||
|
|
||||||
|
pub(crate) struct WindowsSetupWidget {
|
||||||
|
pub codex_home: PathBuf,
|
||||||
|
pub selection: Option<WindowsSetupSelection>,
|
||||||
|
pub highlighted: WindowsSetupSelection,
|
||||||
|
pub error: Option<String>,
|
||||||
|
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::Continue,
|
||||||
|
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<Line> = vec![
|
||||||
|
Line::from(vec!["> ".into(), "Codex Windows Support".bold()]),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(
|
||||||
|
" Codex support for Windows is in progress. Full support for Codex on Windows requires Windows Subsystem for Linux (WSL2).",
|
||||||
|
),
|
||||||
|
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::Continue,
|
||||||
|
"Continue anyway",
|
||||||
|
));
|
||||||
|
lines.push(create_option(
|
||||||
|
1,
|
||||||
|
WindowsSetupSelection::Install,
|
||||||
|
"Exit and install Windows Subsystem for Linux (WSL2)",
|
||||||
|
));
|
||||||
|
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::Continue;
|
||||||
|
}
|
||||||
|
KeyCode::Down | KeyCode::Char('j') => {
|
||||||
|
self.highlighted = WindowsSetupSelection::Install;
|
||||||
|
}
|
||||||
|
KeyCode::Char('1') => self.handle_continue(),
|
||||||
|
KeyCode::Char('2') => self.handle_install(),
|
||||||
|
KeyCode::Enter => match self.highlighted {
|
||||||
|
WindowsSetupSelection::Continue => self.handle_continue(),
|
||||||
|
WindowsSetupSelection::Install => self.handle_install(),
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user