How Ghostty Handles Mouse Input and XTerm/SGR Tracking Modes
Ghostty processes mouse input through a three-layer pipeline that converts raw OS events into terminal escape sequences using enums and encoding logic defined in src/input/mouse.zig and src/input/mouse_encode.zig, supporting five distinct formats (X10, UTF-8, SGR, URxvt, and SGR-pixels) and four event modes (x10, normal, button, any) controlled by terminal flags.
The Ghostty terminal emulator implements comprehensive mouse tracking compatible with the XTerm specification and modern extensions. According to the ghostty-org/ghostty source code, the architecture separates raw input definitions from terminal configuration and final encoding logic. This design allows the emulator to handle everything from legacy X10 mode (limited to three buttons) to SGR-pixels mode (1016) which reports coordinates in terminal pixels rather than grid cells.
Core Mouse Definitions in src/input/mouse.zig
The foundation of Ghostty’s mouse handling resides in src/input/mouse.zig, which declares C-compatible enums (enum(c_int)) for buttons, actions, and cursor shapes. The Button enumeration supports 11 distinct buttons, while the Action enum distinguishes between press, release, and motion events. These definitions ensure that the underlying GTK or macOS backends can communicate mouse state to the terminal core using integer values compatible with the ghostty.h public header.
The file also defines mouse.Shape, which maps W3C cursor names (and legacy XTerm aliases) to values consumed by the renderer. This abstraction allows the terminal to request specific cursor appearances when hovering over clickable regions.
Terminal Mouse Configuration in src/terminal/mouse.zig
Ghostty stores the active tracking behavior in two enums defined in src/terminal/mouse.zig: Event (the tracking mode) and Format (the encoding protocol). The terminal sets these via CSI escape sequences such as ?1000h (normal mode) and ?1006h (SGR mode).
The Event enum defines four reporting modes:
none– No mouse reportingx10– Legacy 9-button mode, reports only left/middle/right button pressesnormal– Reports button presses and releases, but not motionbutton– Reports motion only when a button is held (1002 mode)any– Reports all motion and button events (1003 mode)
The Format enum specifies the output protocol:
x10– Legacy\x1B[Mformat with character-encoded coordinatesutf8– Same header with UTF-8 encoded coordinatessgr–\x1B[<...M/\x1B[<...mformat (1006 mode)urxvt–\x1B[{...}Mformat (1015 mode)sgr_pixels– SGR format using terminal-pixel coordinates (1016 mode)
The helper function eventSendsMotion(event: Event) bool returns true for button and any modes, indicating that motion events should be processed.
The Encoding Pipeline in src/input/mouse_encode.zig
When the OS reports a mouse action, Ghostty constructs an Options struct to carry the terminal’s current configuration. Defined at lines 12-38 in src/input/mouse_encode.zig, this struct bundles:
event: The currentterminal.MouseEventmodeformat: The activeterminal.MouseFormatprotocolsize: Therenderer_size.Sizefor coordinate conversionany_button_pressed: Boolean state needed for out-of-viewport trackinglast_cell: Deduplication cache for motion events
The Options.fromTerminal(t, size) factory function (lines 39-49) extracts the current flags from the Terminal instance:
pub fn fromTerminal(t: *const Terminal, size: renderer_size.Size) Options {
return .{
.event = t.flags.mouse_event,
.format = t.flags.mouse_format,
.size = size,
};
}
This options object drives the entire encoding process, determining whether to report the event and how to format the output.
Event Filtering and Motion Tracking
The shouldReport function (lines 76-98 in src/input/mouse_encode.zig) implements the per-mode logic that filters raw input events based on the terminal’s current Event mode:
| Mode | Report Condition |
|---|---|
none |
Never reports |
x10 |
Only left/middle/right button presses |
normal |
Press and release actions (no motion) |
button |
Any action, but only when a button is held |
any |
All actions including motion |
The implementation checks the input action against these rules at the start of the encoding process. If shouldReport returns false, the event is silently discarded.
For out-of-viewport positions (detected by posOutOfViewport), Ghostty applies additional logic: release events are always reported to ensure drag operations complete correctly, while other actions are reported outside the viewport only if the terminal is in button or any mode and a button is currently pressed. Motion events are deduplicated using last_cell to prevent flooding the application with identical coordinates.
Coordinate Conversion: Grid Cells vs. Terminal Pixels
Ghostty performs two types of coordinate conversion depending on the selected format:
Grid Cell Conversion – Used by x10, utf8, sgr, and urxvt formats. The posToCell function (lines 56-66) converts surface pixel coordinates into terminal grid cells, clamping values to the visible grid dimensions.
Terminal Pixel Conversion – Used exclusively by sgr_pixels format. The posToPixels function (lines 68-78) converts surface coordinates to raw terminal pixels without clamping, enabling sub-cell precision for applications that require it.
Both functions rely on renderer_size.Coordinate to account for padding and surface scaling factors.
Button Code Calculation and Modifier Handling
The buttonCode function (lines 200-240) constructs the numeric value embedded in the final escape sequence. The calculation follows XTerm conventions:
Base button codes:
- Left = 0, Middle = 1, Right = 2
- Wheel up/down = 4/5, etc.
Modifier offsets (added for all modes except x10):
- Shift + 4
- Alt + 8
- Ctrl + 16
- Motion + 32
For legacy formats (x10, utf8), release events are forced to code 3 regardless of which button was released, while modern formats preserve button identity on release.
Output Format Examples
The encode function switches on opts.format to emit the correct CSI sequence. The following table demonstrates the encoding for a left-button press at grid position (5,6):
| Format | Escape Sequence | Example Bytes |
|---|---|---|
x10 |
\x1B[M + button+32 + x+33 + y+33 |
\x1B[M !! |
utf8 |
\x1B[M + UTF-8(x+33) + UTF-8(y+33) |
Variable width |
sgr |
\x1B[<code;col;rowM |
\x1B[<0;6;7M |
urxvt |
\x1B[code+32;col;rowM |
\x1B[60;6;7M |
sgr_pixels |
\x1B[<code;x_pixel;y_pixelM |
\x1B[<0;50;60M |
Note that sgr uses uppercase M for presses and motion, and lowercase m for releases, allowing applications to distinguish press-from-release without parsing the button code.
Practical Implementation Example
Below is a complete example showing how to encode a Shift-modified left-button press using Ghostty’s public API:
const mouse_encode = @import("input/mouse_encode.zig");
const term = @import("terminal/main.zig");
// Obtain configuration from terminal state and renderer size
const opts = mouse_encode.Options.fromTerminal(terminal_instance, renderer_size);
var buf: [32]u8 = undefined;
var writer = std.io.fixedWriter(&buf);
// Encode a Shift+LeftButton press at pixel coordinates (12, 20)
try mouse_encode.encode(&writer, .{
.action = .press,
.button = .left,
.mods = .{ .shift = true },
.pos = .{ .x = 12, .y = 20 },
}, opts);
// The buffer now contains the appropriate CSI sequence
The unit tests within src/input/mouse_encode.zig validate each format, including edge cases like SGR release sequences and UTF-8 coordinate encoding.
Summary
- Ghostty’s mouse handling splits concerns across
src/input/mouse.zig(definitions),src/terminal/mouse.zig(modes), andsrc/input/mouse_encode.zig(encoding). - Four Event modes (
x10,normal,button,any) control when events are reported, while five Format variants determine the escape sequence syntax. - The
shouldReportfunction filters events according to XTerm specifications, with special handling for out-of-viewport positions and motion deduplication. - Coordinates convert either to grid cells (traditional modes) or terminal pixels (
sgr_pixelsmode) usingposToCellandposToPixels. - Button codes combine base identifiers (0-2 for left/middle/right) with modifier flags (Shift, Alt, Ctrl) and a motion bit.
Frequently Asked Questions
What is the difference between X10 and SGR mouse modes in Ghostty?
X10 mode (src/terminal/mouse.zig Event.x10) limits reporting to left, middle, and right button presses only, using a legacy encoding where coordinates are passed as character values (button+32). SGR mode (Format.sgr) supports all buttons including the scroll wheel, reports modifier keys (Shift, Alt, Ctrl), and distinguishes button release events by using lowercase m instead of uppercase M in the escape sequence, while also supporting much larger coordinate values up to 32767.
How does Ghostty handle mouse events outside the visible terminal area?
Ghostty checks posOutOfViewport within src/input/mouse_encode.zig (lines 88-107). Release events are always reported even outside the viewport to ensure drag operations terminate correctly. Other events are reported outside the viewport only if the terminal is in button or any mode and a button is currently pressed, preventing spurious reports during casual mouse movement.
Which source files convert screen pixels to terminal grid cells?
Coordinate conversion happens in src/input/mouse_encode.zig using posToCell (lines 56-66) for grid-based formats and posToPixels (lines 68-78) for pixel-based reporting. These functions rely on renderer_size.Size defined in src/renderer/size.zig to account for surface padding, cell dimensions, and scaling factors.
Why does button release in X10 format lose button identity?
According to the implementation in src/input/mouse_encode.zig (lines 200-240), the X10 and UTF-8 formats follow historical XTerm behavior where release events are forced to button code 3. This limitation exists because the original protocol did not reserve bits to indicate which button was released, whereas the SGR format (Format.sgr) preserves the full button code on release by using a different terminator character.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →