Skip to content
5 min read

Blast Radius #3: Ghostty Ripped 213 Lines Out of a 237KB God File. The Graph Shows Why It Mattered.

Mitchell Hashimoto extracted mouse encoding from Ghostty's 237KB Surface.zig into a pure, testable module. 6 files changed. 729 files in the blast radius.

By CodeLayers Team

Blast Radius #3: Ghostty Ripped 213 Lines Out of a 237KB God File. The Graph Shows Why It Mattered.

Mitchell Hashimoto has a theory about terminal emulation: most programs that implement it get it wrong. IDEs, CI/CD platforms, multiplexers, log viewers. Hundreds of them, each rolling their own, and most end up "incomplete, buggy, and slow."

His fix is libghostty. Take the terminal core that already powers Ghostty and ship it as a reusable C library. Last week, he started actually doing the extraction. Seven PRs in four days.

But first he had to deal with Surface.zig.


The god file

Surface.zig is 237KB. Roughly 7,000 lines of Zig. It's the file where Ghostty handles everything a user can do to a terminal window: rendering, input, scrolling, selection, mouse events, IME, resize, focus, links, clipboard. If you click anywhere in Ghostty, your event passes through Surface.

Sitting inside that file: 213 lines that encode mouse events into terminal escape sequences. Five different wire protocols. X10. UTF-8. SGR. URxvt. SGR-Pixels. Each one has its own rules for button codes, coordinate limits, modifier encoding. All five living in a single function called mouseReport.

You can't ship a C library that depends on a 237KB god file. Surface knows about GTK windows and macOS views and cursor positions. A library consumer has none of that. So PR #11538 pulled the encoding out.

The function was doing three different jobs at once: figuring out whether an event should be reported at all (X10 only reports clicks, normal mode skips motion), computing the button code (translate a button enum into a protocol-specific integer, apply modifier bits), and formatting the actual escape sequence. All tangled together in ~230 lines.

// Before: deeply coupled to Surface, could fail
fn mouseReport(
    self: *Surface,
    button: ?input.MouseButton,
    action: MouseReportAction,   // Surface-local enum
    mods: input.Mods,
    pos: apprt.CursorPos,
) !void {
    // 230 lines mixing event filtering, button code math,
    // and five different wire format encoders
}

Mitchell pulled all of it into input/mouse_encode.zig. The new module takes explicit options (tracking mode, format, terminal size, pressed button state), writes to a generic writer, and has zero dependencies on Surface. You can test every protocol, every button code, every edge case without standing up a terminal.

// After: Surface just delegates
fn mouseReport(self: *Surface, button: ?input.MouseButton,
    action: input.MouseAction, mods: input.Mods, pos: apprt.CursorPos,
) void {  // can't fail anymore
    const opts: input.mouse_encode.Options = .fromTerminal(
        &self.io.terminal, self.size,
    );
    var data: termio.Message.WriteReq.Small.Array = undefined;
    var writer: std.Io.Writer = .fixed(&data);
    input.mouse_encode.encode(&writer, .{
        .button = button, .action = action,
        .mods = mods, .pos = .{ .x = pos.x, .y = pos.y },
    }, opts) orelse return;
}

There's a nice side effect here. mouseReport used to return !void (Zig's "can fail" return type). After the extraction, it returns plain void. The only failure mode was running out of buffer space, which is impossible with a fixed-size buffer. Seven try keywords disappeared from Surface.zig because of that one signature change.


6 files changed. 729 in the blast radius.

We ran the PR through Explore and the graph told a different story than the diff.

See it in 3D →

Surface.zig sits near the center of Ghostty's architecture. Every platform backend imports it. GTK, macOS AppKit, iOS UIKit, WASM. And Surface itself imports the terminal core, input handling, rendering, fonts, and I/O. In a codebase of 861 files and 13,464 dependency nodes, it's one of the most connected files in the entire project.

Full impact view showing 729 files lit up from 6 changed files across Ghostty's depth rings

When you click on Surface.zig in the 3D graph and expand the blast radius, the ripple goes in both directions. Upstream, the changed files touch the terminal core and input system. Downstream, Surface is consumed by every platform runtime. Change its API, even just the error set on one function, and every platform target is affected.

That's how 6 changed files cascade to 729. Almost half the codebase.

Zoomed in on Surface.zig with selected file panel showing imports and dependents

But the interesting part: the extraction actually shrinks the blast radius going forward. Before this PR, if you wanted to change how SGR mouse encoding works, you'd touch Surface.zig, and 729 files would light up. After this PR, you'd touch mouse_encode.zig, which has 2 imports. The graph quantifies what good refactoring does. Future changes to mouse encoding are now cheap.


The sprint

PR #11538 was just the first step. Two days later, Mitchell opened PR #11553 and built a C API on top of the extracted module: ghostty_mouse_encoder_new(), ghostty_mouse_event_set_button(), ghostty_mouse_encoder_encode(). 1,485 lines, 21 files, C headers, a build example, the Zig-to-C bridge. None of that would've been possible with the encoding logic still tangled inside Surface.

Then he did the same thing for focus encoding, terminal mode reporting, DECRPM reports, and size reports. Here's the full sprint:

PRWhat happenedLinesExplore
#11538Mouse encoding pulled into pure module+839/−217View in 3D
#11553Mouse encoding exposed as C API+1,485/−50View in 3D
#11577Focus encoding, C + Zig API+226/−16View in 3D
#11578DECRPM mode report extracted+132/−26
#11579Terminal modes exposed as C API+499/−1View in 3D
#11607Size report encoding, own file+394/−47View in 3D

Seven PRs. Four days. 4,000+ lines. Each one: extract, test, expose. You can trace the whole thing in the explore links above.


See it yourself

Explore ghostty-org/ghostty#11538 in 3D

Click Surface.zig. Expand the blast radius. Watch 729 files light up across every platform backend. Then click mouse_encode.zig and see how small its footprint is by comparison. That's the before and after of this refactor, visible in one view.

Want this on every PR your team opens? The CodeLayers GitHub Action posts an interactive 3D visualization as a comment. Two-minute setup, runs on every push.


Blast Radius is a weekly series where we look at real pull requests through 3D dependency graphs. Got a PR you want us to look at? Tell us on Bluesky.

Want to see your code in 3D?

Download on the App Store

Get notified about updates and new features: