Repository: denoland/deno
Pull request: fix(cli): fix arrow keys in interactive picker on Windows

## Summary

- Arrow keys, Enter, and Ctrl+C didn't work in `deno approve-scripts` and `deno outdated --interactive` on Windows (only j/k worked)
- Root cause: Windows Terminal enables `ENABLE_VIRTUAL_TERMINAL_INPUT` by default, which makes Windows deliver arrow keys as VT escape sequences (ESC[A/ESC[B) instead of `VK_UP`/`VK_DOWN` key events. Crossterm's Windows backend reads via `ReadConsoleInput` expecting `VK_*` codes, so those keys were silently dropped
- Fix: clear `ENABLE_VIRTUAL_TERMINAL_INPUT` when entering raw mode for the interactive picker, restore it on exit
- Regular character keys (j/k) were unaffected because they produce `u_char` values regardless of VT input mode

Fixes #32971

## Test plan

- [x] `cargo check -p deno` passes
- [ ] Manual testing on Windows with Windows Terminal
- [ ] CI

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Changed files:
filename: cli/Cargo.toml
patch: @@ -191,7 +191,7 @@ zip = { workspace = true, features = ["deflate-flate2"] }
 zstd.workspace = true
 
 [target.'cfg(windows)'.dependencies]
-winapi = { workspace = true, features = ["knownfolders", "mswsock", "objbase", "shlobj", "tlhelp32", "winbase", "winerror", "winsock2"] }
+winapi = { workspace = true, features = ["consoleapi", "knownfolders", "mswsock", "objbase", "processenv", "shlobj", "tlhelp32", "winbase", "winerror", "winsock2"] }
 deno_subprocess_windows.workspace = true
 
 [target.'cfg(unix)'.dependencies]

filename: cli/util/console.rs
patch: @@ -131,30 +131,98 @@ fn skip_str_seq(data: &[u8]) -> usize {
 
 pub struct RawMode {
   needs_disable: bool,
+  #[cfg(windows)]
+  original_mode: Option<u32>,
 }
 
 impl RawMode {
   pub fn enable() -> io::Result<Self> {
     terminal::enable_raw_mode()?;
+
+    #[cfg(windows)]
+    {
+      // Clear ENABLE_VIRTUAL_TERMINAL_INPUT so that arrow keys and
+      // special keys are delivered as VK_* key events via
+      // ReadConsoleInput, rather than as VT escape sequences.
+      // Windows Terminal enables this flag by default, which causes
+      // crossterm to miss arrow keys, Enter, and Ctrl+C.
+      let original_mode = windows_vt_input::clear_vt_input_flag();
+      Ok(Self {
+        needs_disable: true,
+        original_mode,
+      })
+    }
+
+    #[cfg(not(windows))]
     Ok(Self {
       needs_disable: true,
     })
   }
 
   pub fn disable(mut self) -> io::Result<()> {
     self.needs_disable = false;
+    #[cfg(windows)]
+    windows_vt_input::restore_mode(self.original_mode);
     terminal::disable_raw_mode()
   }
 }
 
 impl Drop for RawMode {
   fn drop(&mut self) {
     if self.needs_disable {
+      #[cfg(windows)]
+      windows_vt_input::restore_mode(self.original_mode);
       let _ = terminal::disable_raw_mode();
     }
   }
 }
 
+#[cfg(windows)]
+mod windows_vt_input {
+  use winapi::shared::minwindef::DWORD;
+  use winapi::um::consoleapi::GetConsoleMode;
+  use winapi::um::consoleapi::SetConsoleMode;
+  use winapi::um::processenv::GetStdHandle;
+  use winapi::um::winbase::STD_INPUT_HANDLE;
+
+  const ENABLE_VIRTUAL_TERMINAL_INPUT: DWORD = 0x0200;
+
+  /// Clear ENABLE_VIRTUAL_TERMINAL_INPUT on stdin and return the
+  /// original mode so it can be restored later.
+  pub fn clear_vt_input_flag() -> Option<u32> {
+    // SAFETY: GetStdHandle/GetConsoleMode/SetConsoleMode are safe
+    // Windows API calls with valid handle constants.
+    unsafe {
+      let handle = GetStdHandle(STD_INPUT_HANDLE);
+      if handle.is_null() {
+        return None;
+      }
+      let mut mode: DWORD = 0;
+      if GetConsoleMode(handle, &mut mode) == 0 {
+        return None;
+      }
+      if mode & ENABLE_VIRTUAL_TERMINAL_INPUT != 0 {
+        SetConsoleMode(handle, mode & !ENABLE_VIRTUAL_TERMINAL_INPUT);
+        Some(mode)
+      } else {
+        None // flag wasn't set, nothing to restore
+      }
+    }
+  }
+
+  pub fn restore_mode(original_mode: Option<u32>) {
+    if let Some(mode) = original_mode {
+      // SAFETY: restoring previously saved console mode.
+      unsafe {
+        let handle = GetStdHandle(STD_INPUT_HANDLE);
+        if !handle.is_null() {
+          SetConsoleMode(handle, mode);
+        }
+      }
+    }
+  }
+}
+
 pub struct HideCursorGuard {
   needs_disable: bool,
 }