Build minimal terminal emulators using the libghostty-vt C API with Raylib for windowing and rendering
triggers
["build a terminal emulator with libghostty","use libghostty-vt C API","embed terminal emulation in my app","ghostling terminal example","integrate ghostty terminal library","libghostty render state API","minimal terminal emulator in C","ghostty libghostty-vt bindings"]
Ghostling is a minimal viable terminal emulator built on libghostty-vt, the embeddable C library extracted from Ghostty. It uses Raylib for windowing/rendering and lives in a single C file. The project demonstrates how to wire libghostty-vt's VT parsing, terminal state, and render-state API to any 2D or GPU renderer.
What libghostty-vt Provides
VT sequence parsing (SIMD-optimized)
Terminal state: cursor, styles, text reflow, scrollback
Render state management (what cells changed and how to draw them)
All implementation lives in main.c. Below are the key patterns extracted from it.
1. Initialize the Terminal
#include"ghostty.h"// provided by libghostty-vt via CMake// Create terminal with cols x rows cellsghostty_terminal_t *terminal = ghostty_terminal_new(
&(ghostty_terminal_config_t){
.cols = cols,
.rows = rows,
}
);
if (!terminal) { /* handle error */ }
2. Write Data from PTY into the Terminal
// Read from PTY fd, feed raw bytes to libghostty-vtssize_t n = read(pty_fd, buf, sizeof(buf));
if (n > 0) {
ghostty_terminal_write(terminal, buf, (size_t)n);
}
ghostty_terminal_resize(terminal, new_cols, new_rows);
// libghostty-vt handles text reflow automatically// Send SIGWINCH to the child process after thisstructwinsizews = { .ws_col = new_cols, .ws_row = new_rows };
ioctl(pty_fd, TIOCSWINSZ, &ws);
kill(child_pid, SIGWINCH);
6. Render: Walk the Render State
The render state API tells you exactly which cells changed and how to draw them — no need to redraw everything every frame.
// Get render state handleghostty_render_state_t *rs = ghostty_terminal_render_state(terminal);
// Begin a render pass (snapshot current state)
ghostty_render_state_begin(rs);
// Iterate dirty cellsghostty_render_cell_iter_t iter = {0};
ghostty_render_cell_t cell;
while (ghostty_render_state_next_cell(rs, &iter, &cell)) {
// cell.col, cell.row — grid position// cell.codepoint — Unicode codepoint (0 = empty)// cell.fg.r/g/b — foreground RGB// cell.bg.r/g/b — background RGB// cell.attrs.bold — bold flag// cell.attrs.italic — italic flag// cell.attrs.reverse — reverse video// Example: draw with Raylib
Color fg = { cell.fg.r, cell.fg.g, cell.fg.b, 255 };
Color bg = { cell.bg.r, cell.bg.g, cell.bg.b, 255 };
Rectangle rect = {
.x = cell.col * cell_width,
.y = cell.row * cell_height,
.width = cell_width,
.height = cell_height,
};
DrawRectangleRec(rect, bg);
if (cell.codepoint != 0) {
char glyph[8] = {0};
// encode codepoint to UTF-8 yourself or use a helper
encode_utf8(cell.codepoint, glyph);
DrawText(glyph, (int)rect.x, (int)rect.y, font_size, fg);
}
}
// End render pass (marks cells as clean)
ghostty_render_state_end(rs);
7. Scrollback
// Scroll viewport up/down by N rows
ghostty_terminal_scroll(terminal, -3); // scroll up 3
ghostty_terminal_scroll(terminal, 3); // scroll down 3// Scroll to bottom
ghostty_terminal_scroll_bottom(terminal);
// Set master_fd non-blocking
fcntl(master_fd, F_SETFL, O_NONBLOCK);
// In your main loop:uint8_t buf[4096];
ssize_t n;
while ((n = read(master_fd, buf, sizeof(buf))) > 0) {
ghostty_terminal_write(terminal, buf, (size_t)n);
}
// EAGAIN means no data available — not an error
Raylib Key → ghostty_key_t Mapping
ghostty_key_traylib_key_to_ghostty(int rl_key) {
switch (rl_key) {
case KEY_A: return GHOSTTY_KEY_A;
case KEY_ENTER: return GHOSTTY_KEY_ENTER;
case KEY_BACKSPACE: return GHOSTTY_KEY_BACKSPACE;
case KEY_UP: return GHOSTTY_KEY_UP;
case KEY_DOWN: return GHOSTTY_KEY_DOWN;
// ... etcdefault: return GHOSTTY_KEY_INVALID;
}
}