Skip to content
Merged
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
112 changes: 95 additions & 17 deletions src/pty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,13 @@ fn screen_thread(
let _ = w.write_all(&routed.app);
let _ = w.flush();
}
// Windows Terminal treats Quick Edit as ownership of ordinary
// mouse gestures: keep it while the shell is not asking for mouse
// reports (scrollback/right-click paste stay native), and release
// it only for a TUI that enables an xterm mouse protocol.
raw.sync_mouse_capture(
screen.parser.screen().mouse_protocol_mode() != vt100::MouseProtocolMode::None,
);
}
Some(Msg::ImageReady(ready)) => screen.image_ready(ready),
Some(Msg::Resize(rows, cols)) => screen.set_size(rows, cols),
Expand Down Expand Up @@ -1708,6 +1715,30 @@ impl RawMode {
fn restore(&self) {
self.leave();
}

fn sync_mouse_capture(&self, _capture: bool) {}
}

/// Derive the two Windows raw-input modes. Ordinary shell use preserves the
/// caller's Quick Edit bit so Windows Terminal owns wheel scrollback/right-click
/// paste; a child that requests xterm mouse reporting uses the capture variant.
#[cfg(windows)]
fn windows_raw_input_modes(mode: u32) -> (u32, u32) {
use windows_sys::Win32::System::Console::{
ENABLE_ECHO_INPUT, ENABLE_EXTENDED_FLAGS, ENABLE_INSERT_MODE, ENABLE_LINE_INPUT,
ENABLE_PROCESSED_INPUT, ENABLE_QUICK_EDIT_MODE, ENABLE_VIRTUAL_TERMINAL_INPUT,
ENABLE_WINDOW_INPUT,
};

let raw = (mode
& !(ENABLE_ECHO_INPUT
| ENABLE_LINE_INPUT
| ENABLE_PROCESSED_INPUT
| ENABLE_INSERT_MODE
| ENABLE_WINDOW_INPUT))
| ENABLE_EXTENDED_FLAGS
| ENABLE_VIRTUAL_TERMINAL_INPUT;
(raw, raw & !ENABLE_QUICK_EDIT_MODE)
}

/// Windows console-mode counterpart to termios. ConPTY itself is supplied by
Expand All @@ -1717,19 +1748,22 @@ impl RawMode {
struct RawMode {
input_orig: Option<u32>,
output_orig: Option<u32>,
/// Raw keyboard input while Windows Terminal retains ordinary mouse gestures.
input_raw: Option<u32>,
/// Same mode with Quick Edit disabled for a child TUI's mouse protocol.
input_mouse: Option<u32>,
output_raw: Option<u32>,
mouse_captured: std::cell::Cell<bool>,
raw_active: std::cell::Cell<bool>,
}

#[cfg(windows)]
impl RawMode {
fn acquire() -> RawMode {
use windows_sys::Win32::System::Console::{
DISABLE_NEWLINE_AUTO_RETURN, ENABLE_ECHO_INPUT, ENABLE_EXTENDED_FLAGS,
ENABLE_INSERT_MODE, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT, ENABLE_PROCESSED_OUTPUT,
ENABLE_QUICK_EDIT_MODE, ENABLE_VIRTUAL_TERMINAL_INPUT,
ENABLE_VIRTUAL_TERMINAL_PROCESSING, ENABLE_WINDOW_INPUT, GetConsoleMode, GetStdHandle,
STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, SetConsoleMode,
DISABLE_NEWLINE_AUTO_RETURN, ENABLE_PROCESSED_OUTPUT,
ENABLE_VIRTUAL_TERMINAL_PROCESSING, GetConsoleMode, GetStdHandle, STD_INPUT_HANDLE,
STD_OUTPUT_HANDLE, SetConsoleMode,
};

// SAFETY: standard handles are process-owned and remain valid here.
Expand All @@ -1746,17 +1780,13 @@ impl RawMode {
let output_orig =
(unsafe { GetConsoleMode(output, &mut output_mode) } != 0).then_some(output_mode);

let input_raw = input_orig.map(|mode| {
(mode
& !(ENABLE_ECHO_INPUT
| ENABLE_LINE_INPUT
| ENABLE_PROCESSED_INPUT
| ENABLE_QUICK_EDIT_MODE
| ENABLE_INSERT_MODE
| ENABLE_WINDOW_INPUT))
| ENABLE_EXTENDED_FLAGS
| ENABLE_VIRTUAL_TERMINAL_INPUT
});
// Keep the caller's Quick Edit bit for an ordinary shell: Windows
// Terminal uses it to retain host-side wheel scrollback and right-click
// paste. `sync_mouse_capture` clears it only while the child has explicitly
// enabled an xterm mouse-reporting protocol.
let input_modes = input_orig.map(windows_raw_input_modes);
let input_raw = input_modes.map(|modes| modes.0);
let input_mouse = input_modes.map(|modes| modes.1);
let output_raw = output_orig.map(|mode| {
mode | ENABLE_PROCESSED_OUTPUT
| ENABLE_VIRTUAL_TERMINAL_PROCESSING
Expand All @@ -1774,7 +1804,10 @@ impl RawMode {
input_orig,
output_orig,
input_raw,
input_mouse,
output_raw,
mouse_captured: std::cell::Cell::new(false),
raw_active: std::cell::Cell::new(true),
}
}

Expand All @@ -1787,13 +1820,20 @@ impl RawMode {
// SAFETY: the process-owned standard handle remains valid.
let _ = unsafe { SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), mode) };
}
self.raw_active.set(false);
}

fn enter(&self) {
use windows_sys::Win32::System::Console::{
GetStdHandle, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, SetConsoleMode,
};
if let Some(mode) = self.input_raw {
self.raw_active.set(true);
let input_mode = if self.mouse_captured.get() {
self.input_mouse
} else {
self.input_raw
};
if let Some(mode) = input_mode {
// SAFETY: the process-owned standard handle remains valid.
let _ = unsafe { SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), mode) };
}
Expand All @@ -1803,6 +1843,22 @@ impl RawMode {
}
}

fn sync_mouse_capture(&self, capture: bool) {
if self.mouse_captured.replace(capture) == capture || !self.raw_active.get() {
return;
}
use windows_sys::Win32::System::Console::{GetStdHandle, STD_INPUT_HANDLE, SetConsoleMode};
let mode = if capture {
self.input_mouse
} else {
self.input_raw
};
if let Some(mode) = mode {
// SAFETY: the process-owned standard handle remains valid.
let _ = unsafe { SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), mode) };
}
}

fn restore(&self) {
use windows_sys::Win32::System::Console::{
GetStdHandle, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, SetConsoleMode,
Expand All @@ -1815,6 +1871,7 @@ impl RawMode {
// SAFETY: as above, for stdout.
let _ = unsafe { SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), mode) };
}
self.raw_active.set(false);
}
}

Expand All @@ -1837,6 +1894,27 @@ mod tests {
}
}

#[cfg(windows)]
#[test]
fn windows_raw_mode_keeps_quick_edit_until_mouse_reporting() {
use windows_sys::Win32::System::Console::{
ENABLE_ECHO_INPUT, ENABLE_EXTENDED_FLAGS, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT,
ENABLE_QUICK_EDIT_MODE, ENABLE_VIRTUAL_TERMINAL_INPUT,
};

let original =
ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_QUICK_EDIT_MODE;
let (shell, mouse) = windows_raw_input_modes(original);
assert_ne!(shell & ENABLE_QUICK_EDIT_MODE, 0);
assert_eq!(mouse & ENABLE_QUICK_EDIT_MODE, 0);
assert_ne!(shell & ENABLE_EXTENDED_FLAGS, 0);
assert_ne!(shell & ENABLE_VIRTUAL_TERMINAL_INPUT, 0);
assert_eq!(
shell & (ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT),
0
);
}

#[test]
fn cursor_bridge_debounces_transient_hide() {
let mut b = CursorBridge::default();
Expand Down