Skip to content

fix(input): parse x10 mouse reports and re-assert host sgr - #2311

Closed
zhang17-24 wants to merge 1 commit into
herdrdev:masterfrom
zhang17-24:fix/x10-mouse-garbage
Closed

fix(input): parse x10 mouse reports and re-assert host sgr#2311
zhang17-24 wants to merge 1 commit into
herdrdev:masterfrom
zhang17-24:fix/x10-mouse-garbage

Conversation

@zhang17-24

Copy link
Copy Markdown

Summary

  • Parse X10 (ESC[M) mouse reports on Unix so they are treated as mouse input instead of being typed into the focused pane as garbage.
  • When the client detects X10 (the host terminal dropped to DEFAULT mouse encoding), force the host back to SGR (?1006h), debounced, so the encoding drop cannot persist.

Reproduction

  1. Run herdr in a host terminal that reports X10 (ESC[M), e.g. VS Code integrated terminal after the mouse encoding drops to DEFAULT (SGR off: printf '\033[?1006l' > $(tty) from the host, or after a terminal reset).
  2. Move the mouse. Before: the focused pane input filled with garbage like CN1CQ2CS3.... After: reports parse as mouse events.

Test plan

  • Unit tests for X10 parsing, incomplete-report handling, detection, and the SGR re-assert debounce.
  • Verified end-to-end in VS Code: X10 reports parse as Mouse (no garbage) and ?1006h is re-asserted automatically.

refs #1537

When the host terminal drops to DEFAULT mouse encoding (xterm.js reports
X10, ESC[M), herdr typed each report into the focused pane as text. Parse
X10 reports as mouse events and, on detecting them, force the host back to
SGR (?1006h) so the encoding drop cannot persist.

refs herdrdev#1537
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The raw input layer now parses X10 mouse reports, buffers incomplete sequences, and discards expired tails. The Unix client path detects X10 reports and focus gains, then reasserts SGR mouse capture with a 500 ms debounce.

Changes

X10 mouse input and capture recovery

Layer / File(s) Summary
X10 report parsing and decoding
src/raw_input.rs
The parser recognizes complete X10 reports, buffers incomplete reports, converts coordinates to zero-based values, and maps buttons and modifiers. Tests cover motion, scrolling, button presses, and report detection.
Incomplete X10 framing and timeouts
src/raw_input.rs, src/client/input.rs
Framers expose incomplete X10 detection. X10 sequences use the extended mouse timeout and are discarded when they expire. Tests verify buffering and timeout behavior.
Debounced host capture reassertion
src/client/mod.rs
The Unix input path reasserts SGR mouse capture after X10 reports or outer-focus gains. Reassertion clears existing host reporting and is limited to once every 500 ms. A unit test verifies suppression of an immediate repeat.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant UnixInputPath
  participant RawInputFramer
  participant HostTerminal
  UnixInputPath->>RawInputFramer: receive X10 report or focus-gain input
  RawInputFramer-->>UnixInputPath: report event or focus-gain event
  UnixInputPath->>HostTerminal: clear host mouse reporting
  UnixInputPath->>HostTerminal: enable SGR mouse capture
Loading

Possibly related PRs

  • herdrdev/herdr#2285: Extends the related host mouse-capture reassertion and raw-input focus/mouse detection mechanisms.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the two main changes: X10 mouse report parsing and host SGR re-assertion.
Description check ✅ Passed The description directly explains the X10 parsing, SGR re-assertion, reproduction, and test coverage in the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@kangal-bot kangal-bot added the ai-review Trigger automated AI reviews for pull requests admitted by the PR gate label Aug 4, 2026
@greptile-apps

greptile-apps Bot commented Aug 4, 2026

Copy link
Copy Markdown

Greptile Summary

The PR adds Unix-side parsing and framing for legacy X10 mouse reports, then debounces host-terminal SGR mouse-mode reassertion when X10 or focus-gain input is detected.

  • Recognizes complete and partial ESC[M reports and maps them to mouse events.
  • Extends input timeouts and timeout cleanup for incomplete X10 reports.
  • Reasserts SGR encoding after detecting a host encoding drop or terminal recreation.

Confidence Score: 4/5

The mouse-capture state bypass should be fixed before merging because delayed or stray X10 input can re-enable capture after it was disabled.

The X10 branch invokes the unconditional host SGR reassertion without checking mouse_capture_active, while the focus-gain branch performs that check and the reassertion does not update the cached state.

Files Needing Attention: src/client/mod.rs

Important Files Changed

Filename Overview
src/raw_input.rs Adds X10 report recognition, semantic parsing, incomplete-sequence framing, detection, and focused unit coverage.
src/client/input.rs Applies the longer mouse-sequence idle timeout while an incomplete X10 report is buffered.
src/client/mod.rs Adds debounced SGR reassertion, but the X10 trigger can re-enable host capture while the client’s capture state is disabled.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Host stdin bytes] --> B[RawInputByteFramer]
  B --> C{Complete X10 report?}
  C -->|Yes| D[Parse MouseEvent]
  C -->|Partial| E[Wait with mouse timeout]
  D --> F[Client loop detects X10]
  F --> G[Debounce]
  G --> H[Clear host mouse modes]
  H --> I[Enable SGR mouse capture]
  D --> J[Forward raw bytes to server]
Loading

Reviews (1): Last reviewed commit: "fix(input): parse x10 mouse reports and ..." | Re-trigger Greptile

Comment thread src/client/mod.rs
Comment on lines +1475 to +1479
if crate::raw_input::contains_x10_mouse_report(&data)
|| (host_capture_active
&& events
.iter()
.any(|event| matches!(event, RawInputEvent::OuterFocusGained)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 X10 bypasses disabled capture state

When an X10 report remains queued after mouse capture is disabled, this condition calls the unconditional SGR reassertion without checking state.mouse_capture_active, re-enabling host mouse reporting while the cached state remains false and causing subsequent mouse input to be captured unexpectedly.

Knowledge Base Used: Client attach and raw input pipeline

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cb162512-ab5a-46f0-aafc-27e2a9d2d13e

📥 Commits

Reviewing files that changed from the base of the PR and between 1997b88 and 97d58e0.

📒 Files selected for processing (3)
  • src/client/input.rs
  • src/client/mod.rs
  • src/raw_input.rs

Comment thread src/client/mod.rs
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
use std::time::{Duration, Instant};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n --hidden \
  -g 'Cargo.toml' -g 'Justfile' -g 'justfile' -g '*.yml' -g '*.yaml' \
  '(deny|forbid)\(warnings\)|-D[[:space:]]*warnings|--deny[[:space:]]+warnings|cargo (check|clippy|test).*--target' .

Repository: herdrdev/herdr

Length of output: 345


Compile-gate the Unix-only reassertion code.

If RawInputEvent, the debounce constant, helpers at lines 570–592, and loop state at line 1428 are Unix-only, add #[cfg(unix)] to each. The Windows just recipe runs Clippy with -D warnings, so unused items fail validation.

Source: Coding guidelines

Comment thread src/client/mod.rs
Comment on lines +1474 to +1486
let host_capture_active = state.mouse_capture_active;
if crate::raw_input::contains_x10_mouse_report(&data)
|| (host_capture_active
&& events
.iter()
.any(|event| matches!(event, RawInputEvent::OuterFocusGained)))
{
// X10 (host fell to DEFAULT encoding) or focus regained
// with capture active (terminal recreated): force SGR back
// on. Debounced. Focus-gain re-assert is gated on capture
// being active so it cannot enable capture the user turned off.
reassert_host_sgr_mouse_capture(&mut last_sgr_reassert)
.map_err(ClientError::ConnectionFailed)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Gate X10 recovery on active mouse capture.

contains_x10_mouse_report(&data) is outside the host_capture_active condition. A matching stdin chunk calls EnableMouseCapture even when state.mouse_capture_active is false. This overrides the user's disabled mouse-capture setting.

Proposed fix
-                    if crate::raw_input::contains_x10_mouse_report(&data)
-                        || (host_capture_active
-                            && events
-                                .iter()
-                                .any(|event| matches!(event, RawInputEvent::OuterFocusGained)))
+                    if host_capture_active
+                        && (crate::raw_input::contains_x10_mouse_report(&data)
+                            || events
+                                .iter()
+                                .any(|event| matches!(event, RawInputEvent::OuterFocusGained)))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let host_capture_active = state.mouse_capture_active;
if crate::raw_input::contains_x10_mouse_report(&data)
|| (host_capture_active
&& events
.iter()
.any(|event| matches!(event, RawInputEvent::OuterFocusGained)))
{
// X10 (host fell to DEFAULT encoding) or focus regained
// with capture active (terminal recreated): force SGR back
// on. Debounced. Focus-gain re-assert is gated on capture
// being active so it cannot enable capture the user turned off.
reassert_host_sgr_mouse_capture(&mut last_sgr_reassert)
.map_err(ClientError::ConnectionFailed)?;
let host_capture_active = state.mouse_capture_active;
if host_capture_active
&& (crate::raw_input::contains_x10_mouse_report(&data)
|| events
.iter()
.any(|event| matches!(event, RawInputEvent::OuterFocusGained)))
{
// X10 (host fell to DEFAULT encoding) or focus regained
// with capture active (terminal recreated): force SGR back
// on. Debounced. Focus-gain re-assert is gated on capture
// being active so it cannot enable capture the user turned off.
reassert_host_sgr_mouse_capture(&mut last_sgr_reassert)
.map_err(ClientError::ConnectionFailed)?;

Comment thread src/raw_input.rs
Comment on lines +345 to +351
if starts_with_incomplete_x10_mouse_sequence(&self.buffer) {
tracing::debug!(
len = self.buffer.len(),
"discarding incomplete X10 mouse tail after input timeout"
);
self.buffer.clear();
return chunks;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Discard the remaining X10 bytes after a timeout.

Line 350 clears the partial report but does not retain how many coordinate bytes remain. If ESC[M C times out and '$ arrives later, the framer forwards those coordinate bytes to the pane as text.

Track the remaining 6 - buffer.len() bytes in discard state. Consume exactly that tail before parsing subsequent input. Add a regression test that appends normal input after the delayed tail and verifies that only the normal input is forwarded.

@ogulcancelik

Copy link
Copy Markdown
Collaborator

superseeded by #2312

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Trigger automated AI reviews for pull requests admitted by the PR gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants