How Ghostty Handles Focus Tracking and Focus-In/Focus-Out Events

Ghostty implements focus tracking through VT mode 1004, encoding focus changes as CSI sequences (\x1b[I for focus-in, \x1b[O for focus-out) written directly to the PTY when enabled by client applications.

Ghostty treats window focus as a first-class terminal state, exposing it to running applications through standard VT focus reporting. This cross-platform terminal emulator monitors native window system focus changes and translates them into escape sequences that console applications can consume through standard input.

How Focus Reporting Works in Ghostty

Enabling Focus Event Mode (VT 1004)

Applications enable focus tracking by sending the control sequence CSI ? 1004 h to standard output. Ghostty stores this state in the terminal mode table located in src/terminal/modes.zig using the flag .focus_event. Disabling the mode uses the sequence CSI ? 1004 l.

Detecting OS Window Focus Changes

Platform-specific window backends (macOS, GTK, etc.) monitor native focus events. When the window gains or loses focus, they invoke Termio.focusGained(td, focused) defined in src/termio/Termio.zig. This function serves as the central entry point for all focus state transitions in the terminal emulator.

Encoding and Delivering Focus Events

If the .focus_event mode is active, Ghostty constructs the appropriate CSI sequence. The encoding logic resides in src/terminal/focus.zig within the encode function:

  • \x1b[I represents focus-in
  • \x1b[O represents focus-out

These bytes are queued for PTY output using self.queueWrite(td, writer.buffered(), false), making the events available to the running application through its standard input stream.

Backend and Renderer Coordination

After writing to the PTY, Termio.focusGained forwards the change to the backend via self.backend.focusGained(td, focused) in src/termio/backend.zig. Simultaneously, the rendering layer maintains its own focus flag in src/renderer/generic.zig (renderer.generic.Self.focused) and receives updates through src/renderer/message.zig, enabling UI decorations like cursor visibility changes when unfocused.

Implementation Details: Key Components

The focus tracking system spans multiple modules across the Ghostty codebase:

  • src/terminal/modes.zig: Defines the .focus_event mode constant (VT 1004)
  • src/termio/Termio.zig: Central dispatch for focus changes and PTY writing
  • src/terminal/focus.zig: Contains the Event enum and encode function
  • src/termio/backend.zig: Propagates focus state to concrete backends via backend.focusGained
  • src/terminal/Terminal.zig: Stores the global focused: bool state
  • src/renderer/generic.zig: Maintains renderer-side focus awareness
  • src/renderer/message.zig: Carries focus state through the render pipeline via Message.focus

The src/terminal/Terminal.zig object stores focused: bool = true as a persistent property, allowing any component to query focus state without backend round-trips.

Practical Examples

Enabling Focus Reporting from a Child Process

Programs running inside Ghostty can enable focus reporting and react to events using standard input/output:

#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main(void) {
    /* Enable focus reporting (mode 1004) */
    write(STDOUT_FILENO, "\x1b[?1004h", 8);

    /* Read input and react to focus events */
    char buf[32];
    while (read(STDIN_FILENO, buf, sizeof(buf)) > 0) {
        if (memcmp(buf, "\x1b[I", 3) == 0) {
            printf("Focus gained\n");
        } else if (memcmp(buf, "\x1b[O", 3) == 0) {
            printf("Focus lost\n");
        }
    }
    return 0;
}

Querying Focus State Internally

Ghostty's internal components can check the current focus state through the terminal object:

// In renderer or UI code
if (self.renderer_state.terminal.modes.get(.focus_event)) {
    const isFocused = self.renderer_state.terminal.focused;
    // Use isFocused to adjust UI (e.g., dim when unfocused)
}

Summary

  • Ghostty implements VT 1004 focus reporting mode through the .focus_event flag in src/terminal/modes.zig
  • Platform backends trigger Termio.focusGained(td, focused) in src/termio/Termio.zig when window focus changes
  • Events encode as \x1b[I (focus-in) or \x1b[O (focus-out) via src/terminal/focus.zig
  • The PTY receives events through queueWrite, allowing child processes to read them from stdin
  • Backend propagation occurs through src/termio/backend.zig while the renderer tracks state in src/renderer/generic.zig
  • Applications enable reporting with CSI ? 1004 h and disable with CSI ? 1004 l

Frequently Asked Questions

How do I enable focus event reporting in Ghostty?

Applications enable focus tracking by writing the escape sequence CSI ? 1004 h (typically \x1b[?1004h) to stdout. This sets the .focus_event mode flag in Ghostty's terminal state table. To disable reporting, send CSI ? 1004 l according to the implementation in src/terminal/modes.zig.

What escape sequences does Ghostty send for focus events?

Ghostty sends \x1b[I when the window gains focus and \x1b[O when it loses focus. These CSI sequences are generated by the encode function in src/terminal/focus.zig and written to the application's stdin through the PTY via Termio.queueWrite.

How does Ghostty store the current focus state?

The focus state exists at multiple levels: the OS window manager reports changes to Termio.focusGained, src/terminal/Terminal.zig stores focused: bool as a persistent property, and src/renderer/generic.zig maintains a copy for UI rendering. The mode table in src/terminal/modes.zig tracks whether reporting is enabled via the .focus_event flag.

Why doesn't my application receive focus events in Ghostty?

Ensure your application sends the enable sequence (\x1b[?1004h) to stdout before expecting events. Ghostty only writes focus CSI sequences to the PTY when the .focus_event mode is active. Also verify your application reads from stdin, as focus events arrive as ordinary input data, not as signals or out-of-band messages.

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 →