mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
fix(tui): disable enhanced keys for VS Code WSL (#18741)
Fixes https://github.com/openai/codex/issues/13638 ## Why VS Code's integrated terminal can run a Linux shell through WSL without exposing `TERM_PROGRAM` to the Linux process, and with crossterm keyboard enhancement flags enabled that environment can turn dead-key composition into malformed key events instead of composed Unicode input. Codex already handles composed Unicode correctly, so the fix is to avoid enabling the terminal mode that breaks this path for the affected terminal combination. ## What Changed - Automatically skip crossterm keyboard enhancement flags when Codex detects WSL plus VS Code, including a Windows-side `TERM_PROGRAM` probe through WSL interop. - Add `CODEX_TUI_DISABLE_KEYBOARD_ENHANCEMENT` so users can force-disable or force-enable the keyboard enhancement policy for diagnosis. ## Verification - Added unit coverage for env parsing, VS Code detection, and the WSL/VS Code auto-disable policy. - `cargo check -p codex-tui` passed. - `./tools/argument-comment-lint/run.py -p codex-tui -- --tests` passed. - `cargo test -p codex-tui` was attempted locally, but the checkout failed during linking before tests executed because V8 symbols from `codex-code-mode` were unresolved for `arm64`.
This commit is contained in:
committed by
GitHub
Unverified
parent
ef071cf816
commit
1101dec9ae
+181
-9
@@ -57,10 +57,114 @@ mod job_control;
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
fn should_emit_notification(condition: NotificationCondition, terminal_focused: bool) -> bool {
|
||||
match condition {
|
||||
NotificationCondition::Unfocused => !terminal_focused,
|
||||
@@ -70,7 +174,10 @@ 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]
|
||||
@@ -96,6 +203,68 @@ 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<()> {
|
||||
@@ -108,14 +277,16 @@ 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.
|
||||
let _ = execute!(
|
||||
stdout(),
|
||||
PushKeyboardEnhancementFlags(
|
||||
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
||||
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES
|
||||
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
|
||||
)
|
||||
);
|
||||
if !keyboard_enhancement_disabled() {
|
||||
let _ = execute!(
|
||||
stdout(),
|
||||
PushKeyboardEnhancementFlags(
|
||||
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
||||
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES
|
||||
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let _ = execute!(stdout(), EnableFocusChange);
|
||||
Ok(())
|
||||
@@ -305,7 +476,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 = supports_keyboard_enhancement().unwrap_or(false);
|
||||
let enhanced_keys_supported =
|
||||
!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();
|
||||
|
||||
Reference in New Issue
Block a user