# How Ghostty Handles Mouse Input and XTerm/SGR Tracking Modes

> Discover how Ghostty's three-layer pipeline processes mouse input, supporting XTerm and SGR tracking modes and five distinct formats for seamless terminal interaction.

- Repository: [Ghostty/ghostty](https://github.com/ghostty-org/ghostty)
- Tags: internals
- Published: 2026-05-01

---

**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`](https://github.com/ghostty-org/ghostty/blob/main/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:

```zig
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:

```zig
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.