Track terminal mouse ownership accurately
ci / test (push) Canceled after 0s
ci / fuzz-smoke (push) Canceled after 0s
ci / macos-client (macos-aarch64, macos-14) (push) Canceled after 0s
ci / macos-client (macos-x86_64, macos-13) (push) Canceled after 0s
ci / windows-client (push) Canceled after 0s
ci / package-release (linux-x86_64, ubuntu-latest, , , ) (push) Canceled after 0s
ci / package-release (macos-aarch64, macos-14, , , ) (push) Canceled after 0s
ci / package-release (macos-x86_64, macos-13, , , ) (push) Canceled after 0s
ci / package-release (windows-aarch64, windows-latest, aarch64, windows, aarch64-pc-windows-msvc) (push) Canceled after 0s
ci / package-release (windows-x86_64, windows-latest, , , ) (push) Canceled after 0s
ci / remote-bench (push) Canceled after 0s
ci / publish-gitea-release (push) Canceled after 0s
ci / test (push) Canceled after 0s
ci / fuzz-smoke (push) Canceled after 0s
ci / macos-client (macos-aarch64, macos-14) (push) Canceled after 0s
ci / macos-client (macos-x86_64, macos-13) (push) Canceled after 0s
ci / windows-client (push) Canceled after 0s
ci / package-release (linux-x86_64, ubuntu-latest, , , ) (push) Canceled after 0s
ci / package-release (macos-aarch64, macos-14, , , ) (push) Canceled after 0s
ci / package-release (macos-x86_64, macos-13, , , ) (push) Canceled after 0s
ci / package-release (windows-aarch64, windows-latest, aarch64, windows, aarch64-pc-windows-msvc) (push) Canceled after 0s
ci / package-release (windows-x86_64, windows-latest, , , ) (push) Canceled after 0s
ci / remote-bench (push) Canceled after 0s
ci / publish-gitea-release (push) Canceled after 0s
This commit is contained in:
+149
-58
@@ -6801,8 +6801,7 @@ async fn run_terminal(
|
||||
let before_mouse_strip = bytes.len();
|
||||
let (stripped_bytes, stripped_unowned_mouse) = strip_unowned_terminal_reports(
|
||||
bytes,
|
||||
predictor.alternate_screen,
|
||||
predictor.mouse_tracking,
|
||||
predictor.mouse_tracking.enabled(),
|
||||
);
|
||||
bytes = stripped_bytes;
|
||||
if stripped_unowned_mouse {
|
||||
@@ -8426,19 +8425,14 @@ fn trace_wake_repaint_retry_arm(trigger: &str, elapsed: Duration) {
|
||||
);
|
||||
}
|
||||
|
||||
fn should_strip_unowned_terminal_reports(alternate_screen: bool, mouse_tracking: bool) -> bool {
|
||||
// Only a full-screen app that explicitly enabled mouse reporting owns these
|
||||
// bytes. In every other state, local terminal mouse reports are stale UI
|
||||
// noise and forwarding them can type SGR fragments at a normal shell prompt.
|
||||
!(alternate_screen && mouse_tracking)
|
||||
fn should_strip_unowned_terminal_reports(mouse_tracking: bool) -> bool {
|
||||
// Mouse mode, not alternate-screen mode, establishes ownership. Inline TUIs
|
||||
// and multiplexers can legitimately request mouse reports on the main screen.
|
||||
!mouse_tracking
|
||||
}
|
||||
|
||||
fn strip_unowned_terminal_reports(
|
||||
bytes: Vec<u8>,
|
||||
alternate_screen: bool,
|
||||
mouse_tracking: bool,
|
||||
) -> (Vec<u8>, bool) {
|
||||
if !should_strip_unowned_terminal_reports(alternate_screen, mouse_tracking) {
|
||||
fn strip_unowned_terminal_reports(bytes: Vec<u8>, mouse_tracking: bool) -> (Vec<u8>, bool) {
|
||||
if !should_strip_unowned_terminal_reports(mouse_tracking) {
|
||||
return (bytes, false);
|
||||
}
|
||||
let before = bytes.len();
|
||||
@@ -9424,17 +9418,35 @@ const GLITCH_FORCE_MS: u128 = 250;
|
||||
/// without retaining arbitrary terminal output.
|
||||
const TERMINAL_OUTPUT_PARSE_TAIL: usize = 64;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
enum TerminalMouseMode {
|
||||
#[default]
|
||||
None,
|
||||
X10,
|
||||
PressRelease,
|
||||
Highlight,
|
||||
ButtonMotion,
|
||||
AnyMotion,
|
||||
}
|
||||
|
||||
impl TerminalMouseMode {
|
||||
fn enabled(self) -> bool {
|
||||
self != Self::None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
struct TerminalModeChanges {
|
||||
alternate_screen: Option<bool>,
|
||||
mouse_tracking: Option<bool>,
|
||||
mouse_tracking_enable: Option<TerminalMouseMode>,
|
||||
mouse_tracking_disable_mask: u8,
|
||||
}
|
||||
|
||||
fn terminal_modes_after_output(
|
||||
mut alternate_screen: bool,
|
||||
mut mouse_tracking: bool,
|
||||
mut mouse_tracking: TerminalMouseMode,
|
||||
bytes: &[u8],
|
||||
) -> (bool, bool) {
|
||||
) -> (bool, TerminalMouseMode) {
|
||||
let mut offset = 0usize;
|
||||
while offset < bytes.len() {
|
||||
let Some((next, changes)) = terminal_private_mode_changes(bytes, offset) else {
|
||||
@@ -9444,8 +9456,12 @@ fn terminal_modes_after_output(
|
||||
if let Some(enable) = changes.alternate_screen {
|
||||
alternate_screen = enable;
|
||||
}
|
||||
if let Some(enable) = changes.mouse_tracking {
|
||||
mouse_tracking = enable;
|
||||
if let Some(mode) = changes.mouse_tracking_enable {
|
||||
mouse_tracking = mode;
|
||||
} else if terminal_mouse_mode_mask(mouse_tracking) & changes.mouse_tracking_disable_mask
|
||||
!= 0
|
||||
{
|
||||
mouse_tracking = TerminalMouseMode::None;
|
||||
}
|
||||
offset = next.max(offset + 1);
|
||||
}
|
||||
@@ -9481,13 +9497,15 @@ fn terminal_private_mode_changes(
|
||||
b'h' | b'l' => {
|
||||
let params = &bytes[params_start..cursor];
|
||||
let enable = byte == b'h';
|
||||
let (mouse_tracking_enable, mouse_tracking_disable_mask) =
|
||||
terminal_private_mouse_tracking_changes(params, enable);
|
||||
return Some((
|
||||
cursor + 1,
|
||||
TerminalModeChanges {
|
||||
alternate_screen: terminal_private_params_include_alt_screen(params)
|
||||
.then_some(enable),
|
||||
mouse_tracking: terminal_private_params_include_mouse_tracking(params)
|
||||
.then_some(enable),
|
||||
mouse_tracking_enable,
|
||||
mouse_tracking_disable_mask,
|
||||
},
|
||||
));
|
||||
}
|
||||
@@ -9504,15 +9522,45 @@ fn terminal_private_params_include_alt_screen(params: &[u8]) -> bool {
|
||||
.any(|param| matches!(param, b"47" | b"1047" | b"1049"))
|
||||
}
|
||||
|
||||
fn terminal_private_params_include_mouse_tracking(params: &[u8]) -> bool {
|
||||
params
|
||||
.split(|byte| matches!(byte, b';' | b':'))
|
||||
.any(|param| {
|
||||
matches!(
|
||||
param,
|
||||
b"1000" | b"1001" | b"1002" | b"1003" | b"1005" | b"1006" | b"1015" | b"1016"
|
||||
)
|
||||
})
|
||||
fn terminal_private_mouse_tracking_changes(
|
||||
params: &[u8],
|
||||
enable: bool,
|
||||
) -> (Option<TerminalMouseMode>, u8) {
|
||||
let mut enabled_mode = None;
|
||||
let mut disabled_modes = 0u8;
|
||||
for param in params.split(|byte| matches!(byte, b';' | b':')) {
|
||||
let Some(mode) = terminal_mouse_mode(param) else {
|
||||
continue;
|
||||
};
|
||||
if enable {
|
||||
enabled_mode = Some(mode);
|
||||
} else {
|
||||
disabled_modes |= terminal_mouse_mode_mask(mode);
|
||||
}
|
||||
}
|
||||
(enabled_mode, disabled_modes)
|
||||
}
|
||||
|
||||
fn terminal_mouse_mode(param: &[u8]) -> Option<TerminalMouseMode> {
|
||||
match param {
|
||||
b"9" => Some(TerminalMouseMode::X10),
|
||||
b"1000" => Some(TerminalMouseMode::PressRelease),
|
||||
b"1001" => Some(TerminalMouseMode::Highlight),
|
||||
b"1002" => Some(TerminalMouseMode::ButtonMotion),
|
||||
b"1003" => Some(TerminalMouseMode::AnyMotion),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn terminal_mouse_mode_mask(mode: TerminalMouseMode) -> u8 {
|
||||
match mode {
|
||||
TerminalMouseMode::None => 0,
|
||||
TerminalMouseMode::X10 => 1 << 0,
|
||||
TerminalMouseMode::PressRelease => 1 << 1,
|
||||
TerminalMouseMode::Highlight => 1 << 2,
|
||||
TerminalMouseMode::ButtonMotion => 1 << 3,
|
||||
TerminalMouseMode::AnyMotion => 1 << 4,
|
||||
}
|
||||
}
|
||||
|
||||
/// One speculatively-echoed character on the current line.
|
||||
@@ -9535,7 +9583,7 @@ struct Predictor {
|
||||
/// True while the server-side program has requested terminal mouse reports.
|
||||
/// When false, SGR mouse bytes from the local terminal are stale UI noise and
|
||||
/// must not be forwarded into the shell prompt.
|
||||
mouse_tracking: bool,
|
||||
mouse_tracking: TerminalMouseMode,
|
||||
/// Tail of recent terminal output, retained so alternate-screen transitions
|
||||
/// split across UDP frames are still detected.
|
||||
output_parse_tail: Vec<u8>,
|
||||
@@ -9585,7 +9633,7 @@ impl Predictor {
|
||||
mode,
|
||||
enabled: enabled && mode != PredictMode::Off,
|
||||
alternate_screen: false,
|
||||
mouse_tracking: false,
|
||||
mouse_tracking: TerminalMouseMode::None,
|
||||
output_parse_tail: Vec::new(),
|
||||
cells: Vec::new(),
|
||||
cursor: 0,
|
||||
@@ -9609,7 +9657,7 @@ impl Predictor {
|
||||
self.epoch += 1;
|
||||
self.confirmed_epoch = self.epoch - 1;
|
||||
self.alternate_screen = false;
|
||||
self.mouse_tracking = false;
|
||||
self.mouse_tracking = TerminalMouseMode::None;
|
||||
self.output_parse_tail.clear();
|
||||
self.oldest_pending_at = None;
|
||||
}
|
||||
@@ -9787,7 +9835,7 @@ impl Predictor {
|
||||
"client.terminal_modes",
|
||||
&[
|
||||
("alt", self.alternate_screen.to_string()),
|
||||
("mouse", self.mouse_tracking.to_string()),
|
||||
("mouse", self.mouse_tracking.enabled().to_string()),
|
||||
("bytes", dosh::trace::bytes_summary(bytes)),
|
||||
],
|
||||
);
|
||||
@@ -11883,29 +11931,84 @@ mod tests {
|
||||
|
||||
predictor.observe_output(b"\x1b[?1000;1006h");
|
||||
assert!(!predictor.alternate_screen);
|
||||
assert!(predictor.mouse_tracking);
|
||||
assert!(predictor.mouse_tracking.enabled());
|
||||
|
||||
predictor.observe_output(b"\x1b[?1006;1000l");
|
||||
assert!(!predictor.mouse_tracking);
|
||||
assert!(!predictor.mouse_tracking.enabled());
|
||||
|
||||
predictor.observe_output(b"\x1b[?1000;1049h");
|
||||
assert!(predictor.alternate_screen);
|
||||
assert!(predictor.mouse_tracking);
|
||||
assert!(predictor.mouse_tracking.enabled());
|
||||
|
||||
predictor.observe_output(b"\x1b[?1049l");
|
||||
assert!(!predictor.alternate_screen);
|
||||
assert!(predictor.mouse_tracking);
|
||||
assert!(predictor.mouse_tracking.enabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predictor_tracks_pixel_mouse_mode() {
|
||||
fn main_screen_tui_mouse_input_survives_until_mode_is_disabled() {
|
||||
let mut predictor = Predictor::new(true);
|
||||
let report = b"\x1b[<0;12;8M".to_vec();
|
||||
|
||||
predictor.observe_output(b"\x1b[?1000;1006h");
|
||||
assert!(!predictor.alternate_screen);
|
||||
let (preserved, changed) =
|
||||
strip_unowned_terminal_reports(report.clone(), predictor.mouse_tracking.enabled());
|
||||
assert!(!changed);
|
||||
assert_eq!(preserved, report);
|
||||
|
||||
predictor.observe_output(b"\x1b[?1006;1000l");
|
||||
let (stripped, changed) =
|
||||
strip_unowned_terminal_reports(report, predictor.mouse_tracking.enabled());
|
||||
assert!(changed);
|
||||
assert!(stripped.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_encoding_alone_does_not_claim_input_and_cannot_disable_tracking() {
|
||||
let mut predictor = Predictor::new(true);
|
||||
|
||||
predictor.observe_output(b"\x1b[?1016h");
|
||||
assert!(predictor.mouse_tracking);
|
||||
assert!(!predictor.mouse_tracking.enabled());
|
||||
|
||||
predictor.observe_output(b"\x1b[?1003h");
|
||||
assert!(predictor.mouse_tracking.enabled());
|
||||
|
||||
predictor.observe_output(b"\x1b[?1016l");
|
||||
assert!(!predictor.mouse_tracking);
|
||||
assert!(predictor.mouse_tracking.enabled());
|
||||
|
||||
predictor.observe_output(b"\x1b[?1003l");
|
||||
assert!(!predictor.mouse_tracking.enabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabling_replaced_mouse_protocol_does_not_clear_active_mode() {
|
||||
let mut predictor = Predictor::new(true);
|
||||
|
||||
predictor.observe_output(b"\x1b[?1000h\x1b[?1003h");
|
||||
predictor.observe_output(b"\x1b[?1000l");
|
||||
assert!(predictor.mouse_tracking.enabled());
|
||||
|
||||
predictor.observe_output(b"\x1b[?1003l");
|
||||
assert!(!predictor.mouse_tracking.enabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_protocol_state_survives_sequences_split_across_frames() {
|
||||
let mut predictor = Predictor::new(true);
|
||||
|
||||
predictor.observe_output(b"\x1b[?10");
|
||||
assert!(!predictor.mouse_tracking.enabled());
|
||||
predictor.observe_output(b"00;1006h");
|
||||
assert!(predictor.mouse_tracking.enabled());
|
||||
|
||||
predictor.observe_output(b"\x1b[?10");
|
||||
predictor.observe_output(b"06l");
|
||||
assert!(predictor.mouse_tracking.enabled());
|
||||
|
||||
predictor.observe_output(b"\x1b[?100");
|
||||
predictor.observe_output(b"0l");
|
||||
assert!(!predictor.mouse_tracking.enabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -11925,11 +12028,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unowned_mouse_reports_are_stripped_unless_tui_owns_mouse() {
|
||||
assert!(should_strip_unowned_terminal_reports(false, false));
|
||||
assert!(should_strip_unowned_terminal_reports(false, true));
|
||||
assert!(should_strip_unowned_terminal_reports(true, false));
|
||||
assert!(!should_strip_unowned_terminal_reports(true, true));
|
||||
fn mouse_tracking_alone_establishes_terminal_report_ownership() {
|
||||
assert!(should_strip_unowned_terminal_reports(false));
|
||||
assert!(!should_strip_unowned_terminal_reports(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -13856,24 +13957,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unowned_terminal_mouse_reports_strip_unless_tui_owns_mouse() {
|
||||
fn unowned_terminal_mouse_reports_strip_unless_application_owns_mouse() {
|
||||
let input = b"\x1b[<35;10;1Mcmd\r".to_vec();
|
||||
|
||||
let (stripped, changed) = strip_unowned_terminal_reports(input.clone(), false, false);
|
||||
let (stripped, changed) = strip_unowned_terminal_reports(input.clone(), false);
|
||||
assert!(changed);
|
||||
assert_eq!(stripped, b"cmd\r");
|
||||
|
||||
let (stripped, changed) = strip_unowned_terminal_reports(input.clone(), false, true);
|
||||
assert!(changed);
|
||||
assert_eq!(stripped, b"cmd\r");
|
||||
|
||||
let (stripped, changed) =
|
||||
strip_unowned_terminal_reports(b"\x1b[<35;10;1M".to_vec(), true, false);
|
||||
assert!(changed);
|
||||
assert!(stripped.is_empty());
|
||||
|
||||
let (preserved, changed) =
|
||||
strip_unowned_terminal_reports(b"\x1b[<35;10;1M".to_vec(), true, true);
|
||||
let (preserved, changed) = strip_unowned_terminal_reports(b"\x1b[<35;10;1M".to_vec(), true);
|
||||
assert!(!changed);
|
||||
assert_eq!(preserved, b"\x1b[<35;10;1M");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user