Skip to content

feat: support pixel-precision mouse reporting (CSI ?1016h) - #1077

Merged
gdamore merged 3 commits into
gdamore:mainfrom
ImGajeed76:feat/pixel-mouse
Apr 29, 2026
Merged

gdamore merged 3 commits into
gdamore:mainfrom
ImGajeed76:feat/pixel-mouse

Conversation

@ImGajeed76

@ImGajeed76 ImGajeed76 commented Apr 27, 2026 •

Copy link
Copy Markdown
Contributor

Demo

demo

This is from glife, a Conway's Game of Life renderer that uses braille (2x4 dots per cell) for its grid. With pixel mouse mode the cursor resolves to the individual dot under it instead of snapping to a whole cell. This PR is the upstream counterpart of a local patch that glife had been carrying to make this work.

Motivation

xterm's SGR-Pixel mouse mode (CSI ?1016h, PmMouseSgrPixel) reports mouse coordinates in terminal pixels rather than character cells. It is the only way for an application to resolve where the cursor is within a cell, which is useful for things like braille-grid drawing tools, mouse-driven sub-cell selection, and pixel-accurate widgets in terminals that report xpixel/ypixel via TIOCGWINSZ.

tcell already knows the mode exists; PmMouseSgrPixel is defined in vt/mode.go. But Screen has no way to ask for it, and the input parser unconditionally clamps mouse coordinates to the cell-based screen size in handleMouse, which destroys pixel coordinates before the app ever sees them. Today, applications that want this have to either fork tcell or emit CSI ?1016h themselves and live with the clip silently breaking drag events outside the visible grid.

Supported by Kitty, WezTerm, iTerm2, Konsole, recent xterm, VS Code's integrated terminal, Alacritty, and Ghostty. tmux and older terminals fall back to cell coordinates, so the existing flags continue to work unchanged in those cases.

Change

Adds a new MouseFlags value, MousePixelEvents, that acts as a modifier on top of the existing MouseButtonEvents, MouseDragEvents, and MouseMotionEvents. When set:

  • tScreen.enableMouse emits PmMouseSgrPixel instead of PmMouseSgr.
  • The input parser skips its cell-size clamp for mouse reports, so pixel coordinates pass through to EventMouse.Position() unchanged.
  • EnableMouse and DisableMouse also disable PmMouseSgrPixel on reset, mirroring the existing handling for PmMouseSgr.

The flag is purely additive (value 8, no overlap with the existing flags) and does nothing on its own. At least one of the other mouse flags must still be set for any events to be delivered. Default behavior is unchanged.

EventMouse.Position() documents that the unit is pixels when the screen was started with MousePixelEvents, otherwise cells (today's behavior).

Usage

screen.EnableMouse(
    tcell.MouseButtonEvents,
    tcell.MouseDragEvents,
    tcell.MousePixelEvents,
)
// ev.Position() now returns terminal pixels

Tests

Two new tests in input_test.go:

  • TestInputMouseSgrClipsToScreen pins the existing clip behavior as a regression guard.
  • TestInputMouseSgrPixelNoClip exercises SetPixelMouse(true) with a 1006-format SGR report whose coordinates exceed the screen, and verifies they pass through unchanged. It also toggles the flag back off and confirms clipping resumes.

go test ./... is green across all packages.

Note on the VT emulator

The vt terminal emulator used by mouse_test.go's integration tests currently has no branch for PmMouseSgrPixel; it always emits 1006-style cell coordinates, even when 1016 is enabled. Because of that, exercising the new flag end-to-end through MockScreen would require the emulator to track a synthetic cell-pixel size, which felt out of scope for this PR. The unit-level tests above cover both code paths in the input parser directly. Happy to extend the emulator in a follow-up if you would prefer richer integration coverage.

Summary by CodeRabbit

  • New Features

    • Support for mouse pixel-event reporting and a runtime toggle to switch pixel vs. cell reporting.
  • Behavior

    • With pixel mode enabled, coordinates are reported in terminal pixels; when disabled, coordinates are clamped to character-cell bounds. Reporting is consistent across input paths.
  • Documentation

    • Clarified coordinate origin and unit behavior for mouse events.
  • Tests

    • Added tests for SGR mouse coordinate handling in both modes.

Adds a new MouseFlags value, MousePixelEvents, that acts as a modifier
on top of MouseButtonEvents/MouseDragEvents/MouseMotionEvents. When set,
EnableMouse emits PmMouseSgrPixel instead of PmMouseSgr and the input
parser skips its cell-size clamp on mouse coordinates so pixel
positions pass through to EventMouse.Position() unchanged. Default
behavior is unchanged.

Includes tests pinning both the existing clip behavior and the new
pixel-mode passthrough including the toggle-off transition.
@ImGajeed76
ImGajeed76 requested a review from gdamore as a code owner April 27, 2026 19:11
@coderabbitai

coderabbitai Bot commented Apr 27, 2026 •

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 961103e9-ec4d-4667-a63c-4234051ec194

📥 Commits

Reviewing files that changed from the base of the PR and between 8f60fe7 and c9b94c8.

📒 Files selected for processing (1)
  • input.go

📝 Walkthrough

Walkthrough

Adds pixel-coordinate mouse reporting support by introducing a MousePixelEvents flag, an inputParser.SetPixelMouse(bool) method and state, SGR mouse parsing that uses params[i]-1 and skips cell-clamping when pixel mode is enabled, and tscreen logic to negotiate/enable pixel vs standard SGR mouse modes.

Changes

Cohort / File(s) Summary
Input parser & tests
input.go, input_test.go
Adds pixelMouse state and func (ip *inputParser) SetPixelMouse(on bool); SGR parsing computes coords as params[1]-1/params[2]-1 and only clamps when pixel mode is off; nested parser initialization captures pixelMouse; tests added for legacy clamping and pixel-mode pass-through.
Screen flags
screen.go
Adds MousePixelEvents MouseFlags = MouseFlags(8) and documentation that enabling it switches reported units to terminal pixels and requires a mouse action flag for delivery.
Terminal screen integration
tscreen.go
enableMouse() derives pixel preference from MousePixelEvents, calls SetPixelMouse(pixel), and enables either vt.PmMouseSgrPixel or vt.PmMouseSgr accordingly.
Mouse API docs
mouse.go
Clarifies (*EventMouse).Position() docstring: origin upper-left; units are character cells by default or terminal pixels when MousePixelEvents is enabled.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant Term as Terminal Screen
    participant Parser as Input Parser
    participant TTY as Terminal / TTY

    App->>Term: EnableMouse(with/without MousePixelEvents)
    Term->>Term: Determine pixel requested (MousePixelEvents)
    Term->>Parser: SetPixelMouse(pixel)
    Term->>Term: Enable vt.PmMouseSgrPixel or vt.PmMouseSgr
    TTY->>Parser: SGR mouse event (CSI ... M / m) with params
    Parser->>Parser: Compute x = params[1]-1, y = params[2]-1
    alt pixelMouse = true
        Parser->>Parser: Do not clamp coordinates (pixel units)
    else
        Parser->>Parser: Clamp coordinates to [0..cols-1]/[0..rows-1]
    end
    Parser->>App: Emit mouse event with Position()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • gdamore

Poem

🐰 I hopped through CSI and code,
I nudged the mouse from cell to node,
Pixels freed from clamp and fence,
Now clicks travel fine and dense,
A tiny rabbit’s pixel ode.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: support pixel-precision mouse reporting (CSI ?1016h)' directly and accurately summarizes the main change: adding support for xterm's SGR-Pixel mouse mode. It is specific, clear, and conveys the primary objective of the changeset.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@input.go`:
- Around line 125-136: SetPixelMouse sets ip.pixelMouse and forwards to
ip.nested when nested exists, but the lazy constructor in handleWinKey that
creates ip.nested copies evch, rows, cols, and advanced but forgets to copy
pixelMouse; update the nested-creation code in handleWinKey to initialize the
new nested parser's pixelMouse field from ip.pixelMouse (and ensure any mutex
locking around ip.pixelMouse is respected, e.g., read it under
ip.l.Lock()/Unlock()) so the lazily-created nested parser uses the same
pixelMouse setting as the parent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9af5f262-c4fc-499e-b814-c39014843f20

📥 Commits

Reviewing files that changed from the base of the PR and between 5d0b84b and ac79adc.

📒 Files selected for processing (5)
  • input.go
  • input_test.go
  • mouse.go
  • screen.go
  • tscreen.go

Comment thread input.go
handleWinKey constructs a nested inputParser the first time it sees a
doubly-encoded win32-input-mode mouse event (a workaround for Alacritty
and WezTerm). It copied evch/rows/cols/advanced but not pixelMouse, so
events routed through the nested parser would still be clipped to cell
bounds even after EnableMouse(MousePixelEvents). Carry pixelMouse over
on construction so the nested parser inherits the same setting.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@input.go`:
- Around line 128-136: SetPixelMouse currently returns early when ip.nested !=
nil which leaves the parent inputParser.pixelMouse unchanged; change it to
update the parent's pixelMouse under ip.l lock and then call
ip.nested.SetPixelMouse(on) (mirroring the SetSize pattern) so both parent
(field pixelMouse) and nested parser stay in sync; ensure locking around
ip.pixelMouse assignment and avoid race conditions when accessing ip.nested and
calling its SetPixelMouse.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aa5f6b10-769c-4d56-afa2-c74016a01519

📥 Commits

Reviewing files that changed from the base of the PR and between ac79adc and 8f60fe7.

📒 Files selected for processing (1)
  • input.go

Comment thread input.go
SetPixelMouse previously short-circuited to the nested parser when one
existed, leaving the parent parser's pixelMouse field unchanged. Since
normal SGR mouse events are processed by the parent (the nested parser
is only used for the ASCII-byte workaround in handleWinKey), the parent
needs the flag set too. Set the parent's pixelMouse under lock and then
forward to the nested parser if present, so both stay consistent.
@gdamore

gdamore commented Apr 28, 2026

Copy link
Copy Markdown
Owner

It really would be nice to have this in the vt layer as well. One problem is that applications will need to know how to map pixels to character cells.

@codecov

codecov Bot commented Apr 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.16667% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.92%. Comparing base (5d0b84b) to head (c9b94c8).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
tscreen.go 57.14% 1 Missing and 2 partials ⚠️
input.go 88.23% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1077   +/-   ##
=======================================
  Coverage   87.92%   87.92%           
=======================================
  Files          46       46           
  Lines        5449     5466   +17     
=======================================
+ Hits         4791     4806   +15     
+ Misses        502      501    -1     
- Partials      156      159    +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gdamore
gdamore merged commit 91fb035 into gdamore:main Apr 29, 2026
10 of 11 checks passed
ImGajeed76 added a commit to ImGajeed76/glife that referenced this pull request Apr 29, 2026
…nstall

Renames the module path so users can run
go install github.com/ImGajeed76/glife@latest. Updates README install
section and replaces the old "Patched tcell" note (the fork is gone)
with a pointer to gdamore/tcell#1077.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants