Build a Terminal Emulator with libghostty: Ghostling Architecture

2026-03-23 · 11 min read · gen:2m 35s · tok:19648
#libghostty #terminal-emulator #backend #c-programming #beginner-tutorial #english

Learn to build custom terminal emulators using libghostty’s C API. Explore PTY management, escape sequences, and terminal state machines with Ghostling.

Building Your Own Terminal Emulator with libghostty: A Deep Dive into Ghostling’s Architecture

Every time you open a terminal, a sophisticated dance occurs between your shell, a pseudo-terminal device, and a rendering engine that interprets thousands of escape sequences. Most developers take this complexity for granted. But understanding how terminal emulators work opens doors to building custom developer tools, embedded terminal widgets, and specialized CLI applications that go far beyond what off-the-shelf solutions offer.

Ghostling, a minimal terminal emulator built on libghostty, provides a unique learning opportunity. Unlike bloated implementations with decades of legacy code, Ghostling strips terminal emulation to its essential components while leveraging libghostty’s production-grade C API. This article dissects its architecture, showing you how to build terminal emulation capabilities into your own projects.

By the end, you’ll understand PTY lifecycle management, terminal state machines, escape sequence parsing, and how modern terminal emulators bridge the gap between shell processes and pixel rendering.

Prerequisites

Before diving in, ensure you have:

  • C/C++ fundamentals: Pointers, memory management, structs, and basic build systems (Make/CMake)
  • Linux/POSIX basics: File descriptors, process forking, signals
  • Development environment: GCC/Clang, libghostty headers and libraries installed
  • Optional: Familiarity with Zig (libghostty is written in Zig but exposes a C API)

Install libghostty on Ubuntu/Debian:

1
2
3
4
5
6
7
8
9
# Clone and build libghostty
git clone https://github.com/ghostty-org/ghostty.git
cd ghostty
zig build -Doptimize=ReleaseFast

# Install headers and library
sudo cp zig-out/lib/libghostty.* /usr/local/lib/
sudo cp -r include/ghostty /usr/local/include/
sudo ldconfig

⚠️ Note: The installation commands above are illustrative. Check the official Ghostty documentation for current build instructions, as the project structure and build process may change.

Architecture and Key Concepts

Terminal emulation involves three distinct layers working in concert. Understanding their boundaries is critical before writing any code.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        User Space                                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                           β”‚
β”‚  β”‚  Shell Process   │◄────────stdin─────────┐                   β”‚
β”‚  β”‚  (bash/zsh/fish) β”‚                       β”‚                   β”‚
β”‚  β”‚                  │─────stdout/stderr────►│                   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                       β”‚                   β”‚
β”‚           β”‚                                 β”‚                   β”‚
β”‚           β–Ό                                 β”‚                   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                       β”‚                   β”‚
β”‚  β”‚    PTY Slave     β”‚β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            β”‚ TTY Driver
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           β–Ό              Kernel Space                            β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                           β”‚
β”‚  β”‚    PTY Master    β”‚                                           β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           β–Ό         Terminal Emulator (Ghostling)                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚   Input Parser   │───►│  Terminal State │───►│   Screen   β”‚  β”‚
β”‚  β”‚   (libghostty)   β”‚    β”‚     Machine     β”‚    β”‚   Buffer   β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                                       β”‚         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                 β–Ό         β”‚
β”‚  β”‚ Keyboard/Mouse   β”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚     Input        │───►│  Input Encoder  β”‚    β”‚  Renderer  β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                   β”‚                   β”‚         β”‚
β”‚                                   β–Ό                   β–Ό         β”‚
β”‚                          To PTY Master            Display       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The PTY Layer

A pseudo-terminal (PTY) is a kernel-provided abstraction that emulates a hardware terminal. It consists of two endpoints:

  • Master: Controlled by the terminal emulator; receives output from and sends input to the slave
  • Slave: Appears as a regular terminal device (/dev/pts/N) to the shell process

When your shell writes ls output, it goes to the slave. The kernel routes it to the master, where your emulator reads and renders it.

The Parser Layer

Raw bytes from the PTY master contain a mix of printable text and control sequences. libghostty’s parser transforms this byte stream into structured events:

  • Print events: Regular characters to display
  • CSI sequences: Cursor movement, colors, scrolling (\x1b[1;31m for red text)
  • OSC sequences: Window titles, clipboard operations
  • DCS sequences: Device control, sixel graphics

The State Machine

Terminal state encompasses cursor position, active colors, scroll region, character attributes, and screen contents. libghostty maintains this state through a well-defined API, letting you query cells, handle reflows on resize, and manage alternate screen buffers.

Step-by-Step Implementation

Setting Up the PTY Infrastructure

Let’s start with the foundation: creating and managing the PTY pair. This code handles the fork/exec sequence that spawns a shell connected to our emulator.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
// pty_manager.c - PTY lifecycle management for Ghostling

#define _XOPEN_SOURCE 600
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>

typedef struct {
    int master_fd;                // File descriptor for PTY master
    pid_t child_pid;              // Shell process ID
    struct termios orig_termios;  // Original terminal settings for restoration
    int rows;
    int cols;
} PtyContext;

// Initialize PTY and spawn shell process
int pty_init(PtyContext *ctx, int rows, int cols, const char *shell) {
    ctx->rows = rows;
    ctx->cols = cols;
    ctx->master_fd = -1;
    ctx->child_pid = 0;
    
    // Open PTY master using POSIX interface
    ctx->master_fd = posix_openpt(O_RDWR | O_NOCTTY);
    if (ctx->master_fd < 0) {
        perror("posix_openpt failed");
        return -1;
    }
    
    // Grant access and unlock slave
    if (grantpt(ctx->master_fd) < 0 || unlockpt(ctx->master_fd) < 0) {
        perror("PTY setup failed");
        close(ctx->master_fd);
        ctx->master_fd = -1;
        return -1;
    }
    
    // Get slave device name
    char *slave_name = ptsname(ctx->master_fd);
    if (!slave_name) {
        perror("ptsname failed");
        close(ctx->master_fd);
        ctx->master_fd = -1;
        return -1;
    }
    
    // Fork child process for shell
    ctx->child_pid = fork();
    if (ctx->child_pid < 0) {
        perror("fork failed");
        close(ctx->master_fd);
        ctx->master_fd = -1;
        return -1;
    }
    
    if (ctx->child_pid == 0) {
        // Child process: set up slave PTY and exec shell
        close(ctx->master_fd);  // Child doesn't need master
        
        // Create new session, making child the session leader
        if (setsid() < 0) {
            _exit(1);
        }
        
        // Open slave PTY - this becomes our controlling terminal
        int slave_fd = open(slave_name, O_RDWR);
        if (slave_fd < 0) {
            _exit(1);
        }
        
        // Set window size before shell reads it
        struct winsize ws = {
            .ws_row = rows,
            .ws_col = cols,
            .ws_xpixel = 0,
            .ws_ypixel = 0
        };
        ioctl(slave_fd, TIOCSWINSZ, &ws);
        
        // Redirect standard streams to slave PTY
        dup2(slave_fd, STDIN_FILENO);
        dup2(slave_fd, STDOUT_FILENO);
        dup2(slave_fd, STDERR_FILENO);
        
        if (slave_fd > STDERR_FILENO) {
            close(slave_fd);
        }
        
        // Set up clean environment
        setenv("TERM", "xterm-256color", 1);
        setenv("COLORTERM", "truecolor", 1);
        
        // Execute shell
        const char *shell_path = shell ? shell : getenv("SHELL");
        if (!shell_path) shell_path = "/bin/sh";
        
        execlp(shell_path, shell_path, "-l", (char *)NULL);
        _exit(127);  // exec failed
    }
    
    // Parent: set master to non-blocking for async I/O
    int flags = fcntl(ctx->master_fd, F_GETFL);
    fcntl(ctx->master_fd, F_SETFL, flags | O_NONBLOCK);
    
    return 0;
}

// Handle terminal resize - notify shell via SIGWINCH
int pty_resize(PtyContext *ctx, int rows, int cols) {
    ctx->rows = rows;
    ctx->cols = cols;
    
    struct winsize ws = {
        .ws_row = rows,
        .ws_col = cols,
        .ws_xpixel = 0,
        .ws_ypixel = 0
    };
    
    // This triggers SIGWINCH in the shell
    if (ioctl(ctx->master_fd, TIOCSWINSZ, &ws) < 0) {
        perror("TIOCSWINSZ failed");
        return -1;
    }
    
    return 0;
}

// Clean shutdown
void pty_cleanup(PtyContext *ctx) {
    if (ctx->master_fd >= 0) {
        close(ctx->master_fd);
        ctx->master_fd = -1;
    }
    
    if (ctx->child_pid > 0) {
        // Send SIGHUP to shell (standard "hangup" signal)
        kill(ctx->child_pid, SIGHUP);
        ctx->child_pid = 0;
    }
}

πŸ’‘ Tip: Always set TERM=xterm-256color in the child environment. Many terminal features depend on correct terminfo detection, and xterm-256color provides broad compatibility.

⚠️ Warning: The posix_openpt approach shown here is the modern POSIX standard. Avoid the legacy BSD openpty() callβ€”it has portability issues and doesn’t work consistently across all systems.

Integrating libghostty’s Parser and State Machine

Now we connect libghostty to process the byte stream from the PTY master. This is where escape sequences become structured data.

⚠️ Important: The following code demonstrates the conceptual API structure. The actual libghostty API may differβ€”consult the official documentation and header files for the correct function signatures and types.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
// terminal_core.c - libghostty integration for terminal state management

#include <ghostty/ghostty.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>

// Forward declaration from pty_manager.c
typedef struct PtyContext PtyContext;

typedef struct {
    ghostty_surface_t surface;      // libghostty surface handle
    ghostty_config_t config;        // Terminal configuration
    PtyContext *pty;                // Our PTY context from above
    
    // Callbacks for rendering layer
    void (*on_damage)(int row, int col, int width, int height, void *userdata);
    void (*on_title_change)(const char *title, void *userdata);
    void *userdata;
    
    // Buffer for batching PTY reads
    uint8_t read_buffer[65536];
} TerminalCore;

// Callback: libghostty notifies us of screen damage
static void damage_callback(
    ghostty_surface_t surface,
    uint32_t x, uint32_t y,
    uint32_t width, uint32_t height,
    void *userdata
) {
    TerminalCore *core = (TerminalCore *)userdata;
    if (core->on_damage) {
        core->on_damage((int)y, (int)x, (int)width, (int)height, core->userdata);
    }
}

// Callback: window title changed via OSC sequence
static void title_callback(
    ghostty_surface_t surface,
    const char *title,
    void *userdata
) {
    TerminalCore *core = (TerminalCore *)userdata;
    if (core->on_title_change) {
        core->on_title_change(title, core->userdata);
    }
}

// Initialize terminal core with libghostty
int terminal_core_init(TerminalCore *core, PtyContext *pty, int rows, int cols) {
    memset(core, 0, sizeof(*core));
    core->pty = pty;
    
    // Create configuration
    ghostty_config_t config = ghostty_config_new();
    if (!config) {
        fprintf(stderr, "Failed to create ghostty config\n");
        return -1;
    }
    
    // Configure terminal parameters
    ghostty_config_set_int(config, "scrollback-lines", 10000);
    ghostty_config_set_bool(config, "mouse-tracking", true);
    ghostty_config_set_string(config, "term", "xterm-256color");
    
    core->config = config;
    
    // Create surface (the actual terminal state machine)
    ghostty_surface_options_t opts = {
        .rows = rows,
        .cols = cols,
        .config = config,
        .userdata = core,
        .damage_callback = damage_callback,
        .title_callback = title_callback,
    };
    
    core->surface = ghostty_surface_new(&opts);
    if (!core->surface) {
        fprintf(stderr, "Failed to create ghostty surface\n");
        ghostty_config_free(config);
        core->config = NULL;
        return -1;
    }
    
    return 0;
}

// Process incoming data from PTY - this is the main parsing entry point
int terminal_core_process_pty_data(TerminalCore *core) {
    ssize_t bytes_read = read(
        core->pty->master_fd,
        core->read_buffer,
        sizeof(core->read_buffer)
    );
    
    if (bytes_read < 0) {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            return 0;  // No data available (non-blocking)
        }
        return -1;  // Real error
    }
    
    if (bytes_read == 0) {
        return -2;  // EOF - shell exited
    }
    
    // Feed data to libghostty parser
    // This triggers the state machine and may invoke damage_callback
    ghostty_surface_feed(core->surface, core->read_buffer, (size_t)bytes_read);
    
    return (int)bytes_read;
}

// Send keyboard input to the PTY (and thus to the shell)
int terminal_core_send_input(TerminalCore *core, const char *data, size_t len) {
    ssize_t written = write(core->pty->master_fd, data, len);
    if (written < 0) {
        perror("write to PTY failed");
        return -1;
    }
    
    return (int)written;
}

// Handle keyboard events with proper encoding
int terminal_core_key_event(
    TerminalCore *core,
    uint32_t keycode,
    uint32_t modifiers,
    bool pressed
) {
    if (!pressed) return 0;  // We typically only care about key presses
    
    // Let libghostty encode the key sequence
    char encoded[32];
    size_t len = ghostty_surface_encode_key(
        core->surface,
        keycode,
        modifiers,
        encoded,
        sizeof(encoded)
    );
    
    if (len > 0) {
        return terminal_core_send_input(core, encoded, len);
    }
    
    return 0;
}

// Query a specific cell for rendering
ghostty_cell_t terminal_core_get_cell(TerminalCore *core, int row, int col) {
    return ghostty_surface_get_cell(core->surface, row, col);
}

// Handle resize
void terminal_core_resize(TerminalCore *core, int rows, int cols) {
    ghostty_surface_resize(core->surface, rows, cols);
    pty_resize(core->pty, rows, cols);
}

// Cleanup
void terminal_core_cleanup(TerminalCore *core) {
    if (core->surface) {
        ghostty_surface_free(core->surface);
        core->surface = NULL;
    }
    if (core->config) {
        ghostty_config_free(core->config);
        core->config = NULL;
    }
}

πŸ“ Note: The ghostty_surface_feed() function is the heart of terminal emulation. It parses the byte stream, updates internal state, and triggers callbacks for any visible changes. This single call handles hundreds of escape sequence types.

Building the Rendering Pipeline

The final piece connects terminal state to pixels. This example uses a simple framebuffer approach, but the pattern adapts to any graphics API.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
// renderer.c - Convert terminal state to visual output

#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>

// Forward declarations (these types come from libghostty)
typedef struct ghostty_cell ghostty_cell_t;
typedef struct ghostty_color ghostty_color_t;
typedef struct ghostty_cursor ghostty_cursor_t;

// Color type enumeration (simplified representation)
typedef enum {
    GHOSTTY_COLOR_DEFAULT_FG,
    GHOSTTY_COLOR_DEFAULT_BG,
    GHOSTTY_COLOR_INDEXED,
    GHOSTTY_COLOR_RGB
} ghostty_color_type_t;

// Cell flag for inverse video
#define GHOSTTY_CELL_INVERSE 0x01

typedef struct {
    TerminalCore *core;
    
    // Framebuffer
    uint32_t *pixels;
    int pixel_width;
    int pixel_height;
    
    // Font metrics
    int cell_width;
    int cell_height;
    int rows;
    int cols;
    
    // Dirty tracking for partial updates
    bool *dirty_rows;
    
    // Color palette (256 colors + default fg/bg)
    uint32_t palette[258];
} Renderer;

// Convert libghostty color to RGBA
static uint32_t color_to_rgba(ghostty_color_t color, uint32_t *palette) {
    switch (color.type) {
        case GHOSTTY_COLOR_DEFAULT_FG:
            return palette[256];
        case GHOSTTY_COLOR_DEFAULT_BG:
            return palette[257];
        case GHOSTTY_COLOR_INDEXED:
            return palette[color.index];
        case GHOSTTY_COLOR_RGB:
            return ((uint32_t)color.r << 24) | 
                   ((uint32_t)color.g << 16) | 
                   ((uint32_t)color.b << 8) | 0xFF;
        default:
            return 0xFFFFFFFF;
    }
}

// Initialize default xterm-256color palette
static void init_palette(uint32_t *palette) {
    // Standard 16 colors
    static const uint32_t base16[] = {
        0x000000FF, 0xCD0000FF, 0x00CD00FF, 0xCDCD00FF,
        0x0000EEFF, 0xCD00CDFF, 0x00CDCDFF, 0xE5E5E5FF,
        0x7F7F7FFF, 0xFF0000FF, 0x00FF00FF, 0xFFFF00FF,
        0x5C5CFFFF, 0xFF00FFFF, 0x00FFFFFF, 0xFFFFFFFF,
    };
    memcpy(palette, base16, sizeof(base16));
    
    // 216 color cube (6x6x6)
    for (int r = 0; r < 6; r++) {
        for (int g = 0; g < 6; g++) {
            for (int b = 0; b < 6; b++) {
                int idx = 16 + r * 36 + g * 6 + b;
                uint8_t rv = r ? (uint8_t)(r * 40 + 55) : 0;
                uint8_t gv = g ? (uint8_t)(g * 40 + 55) : 0;
                uint8_t bv = b ? (uint8_t)(b * 40 + 55) : 0;
                palette[idx] = ((uint32_t)rv << 24) | 
                               ((uint32_t)gv << 16) | 
                               ((uint32_t)bv << 8) | 0xFF;
            }
        }
    }
    
    // 24 grayscale
    for (int i = 0; i < 24; i++) {
        uint8_t v = (uint8_t)(i * 10 + 8);
        palette[232 + i] = ((uint32_t)v << 24) | 
                           ((uint32_t)v << 16) | 
                           ((uint32_t)v << 8) | 0xFF;
    }
    
    // Default foreground (white) and background (black)
    palette[256] = 0xFFFFFFFF;
    palette[257] = 0x000000FF;
}

int renderer_init(Renderer *r, TerminalCore *core, int rows, int cols) {
    memset(r, 0, sizeof(*r));
    r->core = core;
    r->rows = rows;
    r->cols = cols;
    r->cell_width = 8;   // Assuming