Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions codex-rs/tui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1583,7 +1583,7 @@ pub(crate) async fn resolve_cwd_for_resume_or_fork(
reason = "TUI should no longer be displayed, so we can write to stderr."
)]
fn restore() {
if let Err(err) = tui::restore() {
if let Err(err) = tui::restore_after_exit() {
eprintln!(
"failed to restore terminal. Run `reset` or restart your terminal to recover: {err}"
);
Expand All @@ -1602,7 +1602,7 @@ impl TerminalRestoreGuard {
#[cfg_attr(debug_assertions, allow(dead_code))]
fn restore(&mut self) -> color_eyre::Result<()> {
if self.active {
crate::tui::restore()?;
crate::tui::restore_after_exit()?;
self.active = false;
}
Ok(())
Expand Down
239 changes: 46 additions & 193 deletions codex-rs/tui/src/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ use crossterm::event::DisableFocusChange;
use crossterm::event::EnableBracketedPaste;
use crossterm::event::EnableFocusChange;
use crossterm::event::KeyEvent;
use crossterm::event::KeyboardEnhancementFlags;
use crossterm::event::PopKeyboardEnhancementFlags;
use crossterm::event::PushKeyboardEnhancementFlags;
use crossterm::terminal::EnterAlternateScreen;
use crossterm::terminal::LeaveAlternateScreen;
use crossterm::terminal::supports_keyboard_enhancement;
Expand Down Expand Up @@ -55,115 +52,16 @@ mod frame_rate_limiter;
mod frame_requester;
#[cfg(unix)]
mod job_control;
mod keyboard_modes;

/// Target frame interval for UI redraw scheduling.
pub(crate) const TARGET_FRAME_INTERVAL: Duration = frame_rate_limiter::MIN_FRAME_INTERVAL;
const DISABLE_KEYBOARD_ENHANCEMENT_ENV_VAR: &str = "CODEX_TUI_DISABLE_KEYBOARD_ENHANCEMENT";

/// A type alias for the terminal type used in this application
pub type Terminal = CustomTerminal<CrosstermBackend<Stdout>>;

fn keyboard_enhancement_disabled() -> bool {
let disable_env = std::env::var(DISABLE_KEYBOARD_ENHANCEMENT_ENV_VAR).ok();
let is_wsl = running_in_wsl();
let is_vscode_terminal = is_wsl && running_in_vscode_terminal();
keyboard_enhancement_disabled_for(disable_env.as_deref(), is_wsl, is_vscode_terminal)
}

fn keyboard_enhancement_disabled_for(
disable_env: Option<&str>,
is_wsl: bool,
is_vscode_terminal: bool,
) -> bool {
if let Some(disabled) = parse_bool_env(disable_env) {
return disabled;
}

// VS Code running a WSL shell can hide TERM_PROGRAM from the Linux process
// environment, so `running_in_vscode_terminal` also probes the Windows-side
// environment through WSL interop.
is_wsl && is_vscode_terminal
}

fn parse_bool_env(value: Option<&str>) -> Option<bool> {
match value.map(str::trim) {
Some("1") => Some(true),
Some(value) if value.eq_ignore_ascii_case("true") => Some(true),
Some(value) if value.eq_ignore_ascii_case("yes") => Some(true),
Some("0") => Some(false),
Some(value) if value.eq_ignore_ascii_case("false") => Some(false),
Some(value) if value.eq_ignore_ascii_case("no") => Some(false),
_ => None,
}
}

fn running_in_wsl() -> bool {
#[cfg(target_os = "linux")]
{
crate::clipboard_paste::is_probably_wsl()
}

#[cfg(not(target_os = "linux"))]
{
false
}
}

pub(crate) fn running_in_vscode_terminal() -> bool {
vscode_terminal_detected(
std::env::var("TERM_PROGRAM").ok().as_deref(),
windows_term_program().as_deref(),
)
}

fn vscode_terminal_detected(
linux_term_program: Option<&str>,
windows_term_program: Option<&str>,
) -> bool {
term_program_is_vscode(linux_term_program) || term_program_is_vscode(windows_term_program)
}

fn term_program_is_vscode(value: Option<&str>) -> bool {
value.is_some_and(|value| value.eq_ignore_ascii_case("vscode"))
}

fn windows_term_program() -> Option<String> {
#[cfg(target_os = "linux")]
{
static WINDOWS_TERM_PROGRAM: std::sync::OnceLock<Option<String>> =
std::sync::OnceLock::new();
WINDOWS_TERM_PROGRAM
.get_or_init(read_windows_term_program)
.clone()
}

#[cfg(not(target_os = "linux"))]
{
None
}
}

#[cfg(target_os = "linux")]
fn read_windows_term_program() -> Option<String> {
let output = std::process::Command::new("cmd.exe")
.args(["/d", "/s", "/c", "set TERM_PROGRAM"])
.stdin(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.output()
.ok()?;

if !output.status.success() {
return None;
}

String::from_utf8_lossy(&output.stdout)
.lines()
.find_map(|line| {
line.trim_end_matches('\r')
.strip_prefix("TERM_PROGRAM=")
.map(str::to_string)
})
.filter(|value| !value.trim().is_empty())
keyboard_modes::running_in_vscode_terminal()
}

fn should_emit_notification(condition: NotificationCondition, terminal_focused: bool) -> bool {
Expand All @@ -175,10 +73,7 @@ fn should_emit_notification(condition: NotificationCondition, terminal_focused:

#[cfg(test)]
mod tests {
use super::keyboard_enhancement_disabled_for;
use super::parse_bool_env;
use super::should_emit_notification;
use super::vscode_terminal_detected;
use codex_config::types::NotificationCondition;

#[test]
Expand All @@ -204,68 +99,6 @@ mod tests {
/*terminal_focused*/ false
));
}

#[test]
fn keyboard_enhancement_env_flag_parses_common_values() {
assert_eq!(parse_bool_env(Some("1")), Some(true));
assert_eq!(parse_bool_env(Some("true")), Some(true));
assert_eq!(parse_bool_env(Some("YES")), Some(true));
assert_eq!(parse_bool_env(Some("0")), Some(false));
assert_eq!(parse_bool_env(Some("false")), Some(false));
assert_eq!(parse_bool_env(Some("NO")), Some(false));
assert_eq!(parse_bool_env(Some("unexpected")), None);
assert_eq!(parse_bool_env(/*value*/ None), None);
}

#[test]
fn keyboard_enhancement_auto_disables_for_vscode_in_wsl() {
assert!(keyboard_enhancement_disabled_for(
/*disable_env*/ None, /*is_wsl*/ true, /*is_vscode_terminal*/ true
));
}

#[test]
fn keyboard_enhancement_auto_disable_requires_wsl_and_vscode() {
assert!(!keyboard_enhancement_disabled_for(
/*disable_env*/ None, /*is_wsl*/ true, /*is_vscode_terminal*/ false
));
assert!(!keyboard_enhancement_disabled_for(
/*disable_env*/ None, /*is_wsl*/ false, /*is_vscode_terminal*/ true
));
}

#[test]
fn keyboard_enhancement_env_flag_overrides_auto_detection() {
assert!(!keyboard_enhancement_disabled_for(
Some("0"),
/*is_wsl*/ true,
/*is_vscode_terminal*/ true
));
assert!(keyboard_enhancement_disabled_for(
Some("1"),
/*is_wsl*/ false,
/*is_vscode_terminal*/ false
));
}

#[test]
fn vscode_terminal_detection_uses_linux_and_windows_term_program() {
assert!(vscode_terminal_detected(
Some("vscode"),
/*windows_term_program*/ None
));
assert!(vscode_terminal_detected(
/*linux_term_program*/ None,
Some("vscode")
));
assert!(!vscode_terminal_detected(
/*linux_term_program*/ None,
Some("WindowsTerminal")
));
assert!(!vscode_terminal_detected(
/*linux_term_program*/ None, /*windows_term_program*/ None
));
}
}

pub fn set_modes() -> Result<()> {
Expand All @@ -278,16 +111,7 @@ pub fn set_modes() -> Result<()> {
// Some terminals (notably legacy Windows consoles) do not support
// keyboard enhancement flags. Attempt to enable them, but continue
// gracefully if unsupported.
if !keyboard_enhancement_disabled() {
let _ = execute!(
stdout(),
PushKeyboardEnhancementFlags(
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
)
);
}
keyboard_modes::enable_keyboard_enhancement();

let _ = execute!(stdout(), EnableFocusChange);
Ok(())
Expand Down Expand Up @@ -335,29 +159,58 @@ impl Command for DisableAlternateScroll {
}
}

fn restore_common(should_disable_raw_mode: bool) -> Result<()> {
// Pop may fail on platforms that didn't support the push; ignore errors.
let _ = execute!(stdout(), PopKeyboardEnhancementFlags);
execute!(stdout(), DisableBracketedPaste)?;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RawModeRestore {
Disable,
Keep,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum KeyboardRestore {
PopStack,
ResetAfterExit,
}

fn restore_common(
raw_mode_restore: RawModeRestore,
keyboard_restore: KeyboardRestore,
) -> Result<()> {
match keyboard_restore {
KeyboardRestore::PopStack => keyboard_modes::restore_keyboard_enhancement_stack(),
KeyboardRestore::ResetAfterExit => keyboard_modes::reset_keyboard_reporting_after_exit(),
}

let mut first_error = execute!(stdout(), DisableBracketedPaste).err();
let _ = execute!(stdout(), DisableFocusChange);
if should_disable_raw_mode {
disable_raw_mode()?;
if matches!(raw_mode_restore, RawModeRestore::Disable)
&& let Err(err) = disable_raw_mode()
{
first_error.get_or_insert(err);
}
let _ = execute!(stdout(), crossterm::cursor::Show);
Ok(())
match first_error {
Some(err) => Err(err),
None => Ok(()),
}
}

/// Restore the terminal to its original state.
/// Inverse of `set_modes`.
pub fn restore() -> Result<()> {
let should_disable_raw_mode = true;
restore_common(should_disable_raw_mode)
restore_common(RawModeRestore::Disable, KeyboardRestore::PopStack)
}

/// Restore the terminal after Codex is exiting.
///
/// Uses a stronger keyboard reset than [`restore`] so the parent shell recovers even if a
/// terminal missed the stack pop that normally pairs with [`set_modes`].
pub fn restore_after_exit() -> Result<()> {
restore_common(RawModeRestore::Disable, KeyboardRestore::ResetAfterExit)
}

/// Restore the terminal to its original state, but keep raw mode enabled.
pub fn restore_keep_raw() -> Result<()> {
let should_disable_raw_mode = false;
restore_common(should_disable_raw_mode)
restore_common(RawModeRestore::Keep, KeyboardRestore::PopStack)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -437,7 +290,7 @@ pub fn init() -> Result<Terminal> {
fn set_panic_hook() {
let hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
let _ = restore(); // ignore any errors as we are already failing
let _ = restore_after_exit(); // ignore any errors as we are already failing
hook(panic_info);
}));
}
Expand Down Expand Up @@ -485,8 +338,8 @@ impl Tui {

// Detect keyboard enhancement support before any EventStream is created so the
// crossterm poller can acquire its lock without contention.
let enhanced_keys_supported =
!keyboard_enhancement_disabled() && supports_keyboard_enhancement().unwrap_or(false);
let enhanced_keys_supported = !keyboard_modes::keyboard_enhancement_disabled()
&& supports_keyboard_enhancement().unwrap_or(false);
// Cache this to avoid contention with the event reader.
supports_color::on_cached(supports_color::Stream::Stdout);
let _ = crate::terminal_palette::default_colors();
Expand Down
Loading
Loading