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.
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
# vtabs
|
||||
|
||||
A proof-of-concept terminal emulator with **vertical tabs**, built on
|
||||
[libghostty-vt](https://github.com/ghostty-org/ghostty) 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
|
||||
|
||||
```sh
|
||||
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.
|
||||
Reference in New Issue
Block a user