Greyson Parrelli 738cead680 vtabs: terminal with vertical tabs on libghostty-vt + GTK4
Uses libghostty-vt (the API Ghostty documents for external embedders) for
the terminal core. Ghostty's other C API, ghostty.h, exposes a full terminal
surface but only supports macOS and iOS platform tags, so it cannot be
embedded on Linux.

We supply the layers libghostty-vt deliberately leaves out: PTY and process
management, a Cairo/Pango cell renderer, and a GTK4/libadwaita UI with a
Zen-style vertical tab sidebar.

Nix pins the whole toolchain (Zig 0.16 via zig-overlay, GTK 4.22, libadwaita)
so no host setup is needed.
2026-08-11 08:36:10 -04:00

vtabs

A proof-of-concept terminal emulator with vertical tabs, built on libghostty-vt and GTK4/libadwaita.

The sidebar holds the window controls, a new-tab button, and one row per tab — the layout Zen Browser uses for vertical tabs — with the terminal inset to its right.

Quick start

nix develop          # Zig 0.16 + GTK4 + libadwaita, no host toolchain needed
zig build run

Everything is pinned by flake.nix; nothing needs to be installed on the host.

Which "libghostty" this uses, and why

Ghostty ships two different C APIs, and only one of them is usable here:

include/ghostty.h include/ghostty/vt.h
name "libghostty-internal" libghostty-vt
scope whole terminal incl. renderer VT core only
platforms GHOSTTY_PLATFORM_MACOS, GHOSTTY_PLATFORM_IOS any
intended for Ghostty's own macOS app external embedders

ghostty.h is the one that would hand you a ready-made terminal surface, but it has no Linux platform tag at all — its ghostty_platform_e enum only knows macOS and iOS, and its own header says it is "tailored to the needs of the macOS app". On Linux, Ghostty does not embed itself through that API; it builds its GTK apprt directly into the binary.

So this project uses libghostty-vt, the API Ghostty documents for external embedders. That means we get, for free and battle-tested:

  • escape sequence parsing and full terminal state
  • screen, scrollback, line wrapping, and reflow on resize
  • key and mouse event encoding (including the Kitty keyboard protocol)
  • paste safety checking and bracketed paste encoding

and we supply everything above it: process management, rendering, and the UI.

We consume it as a Zig module rather than through the C ABI. Ghostty's build.zig detects that it is being used as a dependency and, in that mode, builds only libghostty-vt — no GTK, no executable — so zig build pulls in just the VT core.

Architecture

main.zig       AdwApplication, CSS loading
Window.zig     sidebar + GtkStack of terminals, tab management, shortcuts
Terminal.zig   GtkDrawingArea: Cairo/Pango renderer + input handling
Session.zig    libghostty-vt Terminal + parser, fed by the PTY
Pty.zig        openpt/fork/exec, controlling terminal setup
key.zig        GDK keyval -> libghostty-vt key mapping
theme.zig      colors libghostty-vt has no opinion about

Two design choices worth calling out:

No IO thread. The PTY is read on the GLib main loop through a unix fd watch (g_unix_fd_add). Terminal state is therefore only ever touched from the main thread, so the renderer reads the screen with no locking. Ghostty itself uses a dedicated IO thread; that is the right answer for a real terminal, but this is dramatically simpler and is not a bottleneck at interactive speeds.

No GPU renderer. Ghostty rasterizes glyphs into an atlas and draws on the GPU. Here, each frame walks the visible rows, groups cells into runs of identical style, and hands each run to Pango. That is far more work per frame in principle, but a terminal grid is small.

What works

  • Real shell on a real PTY, with a controlling terminal so job control, Ctrl-C and SIGWINCH behave
  • Full SGR rendering: 16/256/true color, bold, italic, underline, strikethrough, inverse, and the bright-on-bold convention
  • Block / bar / underline / hollow cursor styles
  • Scrollback via mouse wheel; typing snaps back to the prompt
  • Resize reflows the grid and notifies the child
  • Window title (OSC 0/2) becomes the tab label
  • Tabs: create, close, switch; closing the last one closes the window; a child exiting closes its own tab

Shortcuts

Ctrl+Shift+T new tab
Ctrl+Shift+W close tab
Ctrl+Shift+V paste (bracketed-paste aware, refuses unsafe pastes)
Ctrl+PageUp/PageDown previous / next tab
Alt+1..Alt+8 jump to tab N, Alt+9 jumps to the last

Not implemented

This is a proof of concept, and the following are deliberately absent:

  • Mouse selection and copy. libghostty-vt provides the selection and mouse-encoding primitives, but nothing here is wired to them yet, so there is no way to copy text. Paste works.
  • Ligatures and complex shaping. Each run is drawn independently at a fixed grid offset, so text that needs shaping across cell boundaries won't look right.
  • Kitty graphics, hyperlinks, tab reordering, split panes, config file.
  • Custom terminfo. TERM is reported as xterm-256color rather than ghostty, since we don't install a terminfo entry.

Development

./b.sh wraps nix develop --command zig build and strips the enormous command line Zig prints on failure.

./shot.sh out.png "text to type" runs the app inside a throwaway headless Sway and screenshots it. This keeps UI checks entirely out of your real Wayland session — nothing appears on screen, and it works while the session is locked.

Screenshots use $SHELL like the real app does; set VTABS_SHOT_SHELL=/bin/sh for output uncluttered by your shell's rc files.

One caveat when driving it: wtype loses the first keystroke of every invocation while the compositor adopts its freshly uploaded keymap, so scripted input should begin with a throwaway key. That is a quirk of the injection tool, not of the terminal.

S
Description
No description provided
Readme
875 KiB
Languages
Zig 74.4%
TypeScript 16.9%
CSS 6.1%
Python 1.4%
Shell 0.8%
Other 0.4%