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[Irepresents focus-in\x1b[Orepresents 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_eventmode constant (VT 1004)src/termio/Termio.zig: Central dispatch for focus changes and PTY writingsrc/terminal/focus.zig: Contains theEventenum andencodefunctionsrc/termio/backend.zig: Propagates focus state to concrete backends viabackend.focusGainedsrc/terminal/Terminal.zig: Stores the globalfocused: boolstatesrc/renderer/generic.zig: Maintains renderer-side focus awarenesssrc/renderer/message.zig: Carries focus state through the render pipeline viaMessage.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_eventflag insrc/terminal/modes.zig - Platform backends trigger
Termio.focusGained(td, focused)insrc/termio/Termio.zigwhen window focus changes - Events encode as
\x1b[I(focus-in) or\x1b[O(focus-out) viasrc/terminal/focus.zig - The PTY receives events through
queueWrite, allowing child processes to read them from stdin - Backend propagation occurs through
src/termio/backend.zigwhile the renderer tracks state insrc/renderer/generic.zig - Applications enable reporting with
CSI ? 1004 hand disable withCSI ? 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →