Repository navigation
Send mouse input to programs that track the mouse - #683
Merged
Merged
Conversation
coneilen
force-pushed
the
coneilen-terminal-mouse-reporting
branch
3 times, most recently
from
October 10, 2026 07:40
a341384 to
5bb9ac7
Compare
Every left press, drag and release in a terminal surface became a local text selection, and nothing forwarded mouse input to the pty, so mouse-aware programs (vim, tmux, htop, the Copilot CLI TUI) never received any, and Shift was not a way around it. Query the shell's VT state for the program's mouse tracking (DECSET 9, 1000, 1002, 1003). While it is on, encode press, release, motion and wheel with the pinned libghostty-vt mouse encoder in the format the program asked for (1005, 1006, 1015, 1016 or X10) and queue the bytes to the shell. Shift hands a gesture back to the local selection, and the right button opens the context menu only for the shell (or with Shift). A gesture keeps the route it began on until every button is up. A finer-than-notch wheel is accumulated to whole notches. With tracking off, behavior is unchanged. Not covered: Alt with the mouse, the side buttons, horizontal wheel (the provider does not distinguish it), and a drag that loses mouse capture is not reported to the program as released. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens <coneilen@microsoft.com>
A gesture owned by the program stayed on the program route until a later button-up, so a lost capture, a release outside the window, lost focus or a program that stopped tracking left the next click stuck on the program and the program with a held button. Track the buttons the program was told are down and where it last saw the pointer, release them when the capture or focus is lost or the next pointer event shows them up, and forget the gesture when tracking stops. Shift is the shell's override and is no longer reported, and a pointer leave now resets the same-cell motion filter. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens <coneilen@microsoft.com>
…releases Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens <coneilen@microsoft.com>
…put skips the release Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens <coneilen@microsoft.com>
coneilen
force-pushed
the
coneilen-terminal-mouse-reporting
branch
from
October 10, 2026 08:09
5bb9ac7 to
79791ec
Compare
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.
Summary
#679 turned every left press, drag and release in a terminal surface into a local text selection, and nothing in the terminal sources forwards mouse input to the pty. Mouse-aware programs (vim, tmux, htop, the Copilot CLI TUI in each loop's agent tab) therefore never received any, and Shift only extended the selection. This makes the mouse follow the program's own request: while the running program tracks the mouse, the shell sends it the events; otherwise nothing changes. One of a small series following the review of #676, #677 and #679; independent of the AltGr change in #681 and does not touch paste or focus routing; the one focus-related change is that the focus-lost callback now closes a held forwarded button (below).
Verified in code first:
TerminalSurface.onMouseonly ever fedselectionMouse, there was no query of the VT's mouse mode, and the winghostty provider only reports mouse events to the shell (it cannot write to the shell's pty). The wheel never did anything in the shell (scrollViewportwas not wired), so there is no existing scrollback behavior to preserve.Changes
TerminalVt.StategainsmouseTrackingEnabled()(DECSET 9/1000/1002/1003 viaGHOSTTY_TERMINAL_DATA_MOUSE_TRACKING) andencodeMouse(), which uses the pinned libghostty-vt mouse encoder, so the tracking mode and the format (X10, UTF-8 1005, SGR 1006, URXVT 1015, SGR-pixels 1016) come from the program's own requests. Motion within the cell of the last report is dropped by the shell (the encoder's own cell memory resets on every option refresh); positions are clamped to the surface.TerminalSurface.onMouseroutes each event: while the program tracks the mouse the left, middle and right buttons, motion and wheel are encoded and queued to the shell; with Shift held the gesture is a local selection instead (as in xterm and Windows Terminal), and with tracking off behavior is exactly as before. A gesture keeps the route it started on until every button is up, so a mode change mid-drag neither sends half a drag nor turns a forwarded press into a selection. A forwarded press clears any local selection.What each test proves
TerminalVt.zig(3 tests, no windows): DECSET 9/1000/1002/1003 each turnmouseTrackingEnabledon and off and 1006 alone does not;encodeMousereturns the exact hand-derived bytes for press, release, right/middle/Ctrl, wheel up and down, drag motion once per cell, hover under 1003, and the X10, SGR, URXVT, SGR-pixels, UTF-8 (a column above 95) formats and legacy mode 9 (press only); clamping at the edges; an empty geometry is refused.TerminalSurface.zig(1 test):routeMousedecisions without a parser, with tracking on/off, with Shift, for the wheel, a side button, and a tracking change mid-gesture.App.ziglive tests (10), each using a real Winghostty surface window inside the shell window with native mouse messages (WM_LBUTTONDOWN,WM_MOUSEMOVE,WM_MOUSEWHEEL, and so on) delivered to its window procedure and the bytes read from the shell's terminal input queue: all button, motion, wheel, format and mode combinations above arrive as exact bytes and no selection appears; Shift+drag still selectsGC-COPY-2and sends nothing, including hover and wheel with Shift; gestures keep their route and the right button's context-menu route; a forwarded drag that loses mouse capture (ReleaseCapture, delivering a real WM_CAPTURECHANGED) gets its release, then Shift selects and a new click is tracked; a right button released outside (seen as a later motion without it) is released at that position; WM_KILLFOCUS releases a held button; destroySurface (the first half of a recreate) releases a held button exactly once into a test sink standing in for the attach pipe when the press was delivered; a press still queued at teardown (nothing drained it) produces no release at all (RED against a version that always released: expected 0 sink calls, found 1); a pipe not in non-blocking mode skips the write; right button down then DECSET off then right-up opens no context menu and sends nothing, likewise after lost focus, and a release that never comes does not swallow the next gesture's; a program that stops tracking mid-press leaves the next gesture a normal selection; Shift pressed mid-drag adds no modifier bit; WM_MOUSELEAVE then re-entering the same cell in any-event mode is reported again; 60-unit wheel turns accumulate to one report and a reversal discards the leftover.RED before the fix, review round 2: the right-down/DECSET-off/right-up test failed against the previous commit (�xpected 0, found 1: the context menu opened); the teardown test could only fail to compile there, since it uses the new test sink (not behavioral RED), and the direct write to the real attach pipe is not tested. RED before the fix, round 1: the four live tests, and after review the two added for stuck gestures, Shift during a gesture and leave/re-enter (run against the previous commit's code: the release was missing at byte 19 and a Shift bit appeared at byte 13) (the VT and routing tests exercise the new API and were not run against the old code). They failed because nothing was sent: each tracking assertion expected bytes such as
ESC [ < 64 ; 4 ; 3 M(10 bytes) and found none. The parts of those tests that describe the old behavior (selecting with no tracking, Shift+drag selecting) passed before the fix and are guards, not RED. One expectation was corrected while writing the GREEN run: disabling DECSET 1002 or 1003 turns tracking off (as in real terminals), so the test re-enables 1000 before checking the next format; this was a mistake in my test, not in the code.What is NOT tested
SendMessageW. DPI scaling of pixel positions and cell metrics other than the fixture's are not tested.USERPROFILE,APPDATA,LOCALAPPDATA,TEMP,TMPandGRAPHCODE_SUPPORT_DIRin a scratch profile; no installed daemon, per-user zmx or real clipboard.Test plan
RED: zig test src\App.zig --test-filter "live terminal mouse" (pinned target and link flags, unfixed TerminalSurface) -> 3 passed, 4 failed: each tracking assertion expected 10 bytes such as ESC [ < 64 ; 4 ; 3 M and found len 0
GREEN: zig test src\App.zig --test-filter "live terminal mouse" (pinned flags, with the change) -> All 9 tests passed; zig test src\TerminalVt.zig --test-filter "mouse" -> All 3 tests passed
REGRESSION: zig test for TerminalKeys, TerminalVt, TerminalKeyEncoding, TerminalSelection, Clipboard, TerminalSurface and App roots (pinned flags) -> 10, 26, 43, 39, 15, 241 and 1052 tests (after rebasing on main with #684/#685) (after rebasing on main with the F6/F10 change) passed, 0 failed (baseline on main after the AltGr change: TerminalVt 22, TerminalSurface 223, App 1001)
Checklist
git commit -s) per the DCOmake test) - macOS target, not run: Windows-only change; the Windows roots above were run insteadmake check) - macOS lint target, not run: Windows-only change in Zig