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 reporting
  • x10 – Legacy 9-button mode, reports only left/middle/right button presses
  • normal – Reports button presses and releases, but not motion
  • button – 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[M format with character-encoded coordinates
  • utf8 – Same header with UTF-8 encoded coordinates
  • sgr – \x1B[<...M / \x1B[<...m format (1006 mode)
  • urxvt – \x1B[{...}M format (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 current terminal.MouseEvent mode
  • format: The active terminal.MouseFormat protocol
  • size: The renderer_size.Size for coordinate conversion
  • any_button_pressed: Boolean state needed for out-of-viewport tracking
  • last_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), and src/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 shouldReport function 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_pixels mode) using posToCell and posToPixels.
  • 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →