feat: mouse press and wheel from a non-leader client claim leadership#194
Open
TranscendOfSypherus wants to merge 1 commit into
Open
feat: mouse press and wheel from a non-leader client claim leadership#194TranscendOfSypherus wants to merge 1 commit into
TranscendOfSypherus wants to merge 1 commit into
Conversation
Non-leader input goes through isUserInput(), which excludes all mouse
reports — so scrolling or clicking in a second attached client is
silently dropped until a qualifying keystroke flips leadership. With
two terminals on one session (e.g. a web client and a native terminal),
the mouse is dead in whichever client isn't leader, and users learn to
'type something first' without knowing why.
A button press or a wheel tick is aimed at the pane, making it as
intentional as a keystroke, while motion is ambient (any-motion hover,
focus-follows-mouse sweeps) and a release always follows a press. So:
- isUserInput() becomes classifyInput() returning ignore / keyboard /
pointer: SGR (CSI < b;x;y M) button press and wheel — including
modifier variants — classify as pointer; motion (bit 32), releases
('m' final), focus reports, and legacy X10 mouse stay ignored (X10
also keeps the early return so its raw coordinate bytes can't be
misread as printable input).
- handleInput() claims leadership for pointer input but swallows the
claiming event: its coordinates were aimed at a screen laid out for
the previous leader's size, so forwarding could act at the wrong
position. Click-to-focus semantics — the user's next event flows as
leader. Keyboard input claims and forwards as before.
Verified with two differently-sized clients attached to one session:
the daemon log shows the leadership switch and resize land at
wheel-send time, the wheel bytes never reach the pty, and keyboard
behavior is unchanged.
TranscendOfSypherus
marked this pull request as ready for review
July 12, 2026 23:22
TranscendOfSypherus
marked this pull request as draft
July 12, 2026 23:32
TranscendOfSypherus
marked this pull request as ready for review
July 12, 2026 23:57
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
With two clients attached to one session, mouse input only works in the leader.
handleInput()routes non-leader input throughisUserInput(), which deliberately excludes mouse reports and focus events so a wheel tick or click from the other client is silently dropped, and nothing about it can flip leadership either.The user-visible symptom: attach a session in two terminals, run something mouse-aware (
less --mouse, tmux, any TUI with scroll support), and scrolling is dead in whichever client you didn't type in last. Users learn to "type something first" without knowing why.Repro:
zmx attach demoin two terminals of different sizeshandleInput), until any key is pressed in BChange
isUserInput()becomesclassifyInput()with three outcomes:~, modified arrows: claims leadership and is forwarded (behavior unchanged)CSI < b;x;y M) button press (base button 0–2, any modifiers) or wheel (button bit 64, any modifiers): claims leadership but the claiming event is swallowedmfinal), focus in/out, legacy X10 mouse (which also keeps the early return so its raw coordinate bytes can't be misread as printable input), and terminal query responsesThe dividing line: events the user aims at the pane (press, wheel) claim leadership; events the terminal emits on the user's behalf (motion, release, focus) don't. Motion in particular must not claim with any-motion tracking (1003) or focus-follows-mouse window managers, merely sweeping the pointer across a pane would steal leadership and cause resize storms.
Swallowing the claiming event is deliberate (click-to-focus semantics): its coordinates were aimed at a screen laid out for the previous leader's size, so forwarding it could act at the wrong position in the app. The user's next event flows normally as leader — and in practice a wheel gesture is many ticks, so scroll feels instant.
Legacy X10 is left non-claiming: its payload is three raw bytes after the CSI, which the streaming classifier would have to special-case, and every modern client negotiates SGR (DECSET 1006).
Testing
classifyInputexpectations plus new cases: SGR press/wheel with and without modifiers classify as pointer; motion, releases, wheel+motion, X10, and focus stay ignored; a graphics-resetCSI 0 mfollowed by text still counts as keyboard (regression case: onlyMmay stop the scan, for X10's raw bytes).zig fmt --checkandzig build testgreen; full bats suite 30/30.script, which the alpine CI image doesn't ship — happy to add one plus the apk package if you want the coverage.cat -v): the daemon log shows "setting new leader" + resize land at wheel-send time from the non-leader; the wheel bytes never reach the pty; a following key from the new leader forwards without a second claim; keyboard from a non-leader still claims and forwards.