a73x

bddbc526

Add waystty design spec

a73x   2026-04-07 19:07

Minimal Wayland terminal emulator using libghostty-vt, Vulkan
rendering with glyph atlas + instanced quads, and
freetype/harfbuzz font stack.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

diff --git a/docs/superpowers/specs/2026-04-07-waystty-design.md b/docs/superpowers/specs/2026-04-07-waystty-design.md
new file mode 100644
index 0000000..2791c61
--- /dev/null
+++ b/docs/superpowers/specs/2026-04-07-waystty-design.md
@@ -0,0 +1,203 @@
# waystty Design Spec

A minimal, hackable Wayland terminal emulator written in Zig, using libghostty-vt for terminal emulation, Vulkan for rendering, and freetype/harfbuzz for font handling.

## Goals

- Light and minimal — a terminal you can hack on
- Comparable to foot in spirit: fast, simple, Wayland-native
- libghostty-vt handles VT parsing and terminal state; we own everything else

## Architecture

Six modules, each owning one concern:

```
main.zig          — event loop, glue, signal handling
wayland.zig       — Wayland protocol handling (zig-wayland)
vt.zig            — idiomatic Zig wrapper around libghostty-vt C API
pty.zig           — PTY spawn, read/write, child process management
font.zig          — fontconfig lookup, freetype rasterization, harfbuzz shaping, glyph atlas
renderer.zig      — Vulkan setup, pipeline, instanced quad rendering
```

### Data flow per frame

1. Wayland dispatches input events (keyboard/pointer) to main loop
2. Input encoded via vt.zig (key/mouse encoders), written to PTY
3. PTY output read, fed to terminal via `ghostty_terminal_vt_write`
4. Renderer snapshots render state from vt.zig, iterates cells
5. Missing glyphs rasterized on demand by font.zig into the atlas
6. Renderer builds instance buffer (cell position, atlas UV, fg/bg color), issues single instanced draw call

## Module Details

### wayland.zig

Protocols:
- `wl_compositor` + `wl_surface` — drawing surface
- `xdg_wm_base` + `xdg_surface` + `xdg_toplevel` — window management
- `wl_seat` → `wl_keyboard` + `wl_pointer` — input

Vulkan surface via `VK_KHR_wayland_surface` (`VkSurfaceKHR` from `wl_display` + `wl_surface`).

Keyboard input: keycodes from `wl_keyboard.key`, mapped via xkbcommon to keysyms/UTF-8, then handed to vt.zig's key encoder.

Event loop: `wl_display_get_fd()` polled alongside PTY fd. Single-threaded.

### pty.zig

- `forkpty()` to spawn child shell (`$SHELL` or `/bin/sh`)
- Non-blocking master fd
- `read()` / `write()` helpers
- `SIGCHLD` handling for child exit
- `ioctl(TIOCSWINSZ)` for resize

### vt.zig

Zig wrapper around `<ghostty/vt.h>` via `@cImport`:
- `Terminal` — init, feed bytes, resize, scroll, get render state
- `KeyEncoder` / `MouseEncoder` — encode input events to VT sequences
- `RenderState` — snapshot, iterate rows/cells, get colors/cursor
- Effect callbacks: write-to-pty, device-attributes, title-changed, etc.

### font.zig

- fontconfig: find system monospace font at startup
- freetype: load font face, rasterize glyphs to bitmaps
- harfbuzz: shape text runs (ligatures, combining characters)
- Glyph atlas: single RGBA texture (e.g. 1024x1024), row-based packing, returns UV coords per glyph

### renderer.zig

Vulkan setup:
- Instance → physical device → logical device → swapchain (FIFO present mode)
- Single render pass, single subpass, single graphics pipeline
- Vertex shader: per-instance data (cell x/y, atlas UV rect, fg color, bg color) → positioned quad
- Fragment shader: samples glyph atlas, applies fg color; bg drawn as untextured quads
- One instanced draw call per frame
- Swapchain recreation on resize

Shaders: GLSL compiled to SPIR-V via `glslc` at build time, embedded via `@embedFile`.

## Event Loop

```
init wayland → init vulkan → init font → init terminal → spawn pty

loop:
    poll(wayland_fd, pty_fd)

    if wayland_fd readable:
        wl_display_dispatch()
        → keyboard/pointer events → encode via vt.zig → write to pty

    if pty_fd readable:
        read pty → feed to terminal via ghostty_terminal_vt_write()

    if terminal dirty or resize:
        snapshot render state
        rebuild instance buffer
        render frame

    if child exited:
        break
    if xdg_toplevel close:
        break

cleanup: free terminal, destroy vulkan, disconnect wayland
```

### Resize handling

1. `xdg_toplevel` configure event gives new pixel dimensions
2. Recreate Vulkan swapchain
3. Recalculate grid: `cols = pixel_width / cell_width`, `rows = pixel_height / cell_height`
4. `ghostty_terminal_resize()` with new dimensions
5. `ioctl(TIOCSWINSZ)` on PTY fd

## Hardcoded Defaults

- Font: system monospace via fontconfig, 14px
- Colors: 16-color palette + fg/bg
- Scrollback: 1000 lines
- Window title: "waystty"
- Shell: `$SHELL` or `/bin/sh`

## Build System

`build.zig` only. No Makefile.

Targets:
- `zig build` — build
- `zig build run` — build and run

Zig package dependencies (via `build.zig.zon`):
- zig-wayland — Wayland protocol bindings
- vulkan-zig — Vulkan bindings generator

System C library linkage:
- libghostty-vt
- freetype2
- harfbuzz
- fontconfig
- xkbcommon
- wayland-client

Shader build step: `glslc` compiles `.vert`/`.frag` to SPIR-V, embedded via `@embedFile`.

### Project layout

```
waystty/
├── build.zig
├── build.zig.zon
├── src/
│   ├── main.zig
│   ├── wayland.zig
│   ├── vt.zig
│   ├── pty.zig
│   ├── font.zig
│   └── renderer.zig
└── shaders/
    ├── cell.vert
    └── cell.frag
```

## Testing

### Unit tests

Zig `test` blocks per module:
- `pty.zig` — spawn shell, write/read, verify child exit
- `font.zig` — load font, rasterize glyph, verify atlas packing and UV coords
- `vt.zig` — feed escape sequences, snapshot render state, verify cells/colors/cursor

`wayland.zig` and `renderer.zig` require a compositor/GPU — not unit tested.

Run with `zig build test`.

### Integration testing

End-to-end: spawn waystty with `echo "hello"; exit`, verify clean exit. Can run under `cage` or similar headless compositor in CI. Not day-one priority.

## Performance

### Measurement tools

1. **vtebench** — terminal throughput benchmark, compare against foot
2. **Frame timing** — `std.time.Timer` in render loop, warn in debug builds if frame > 16ms
3. **tracy** — Zig's `std.trace` for profiling key sections (pty read, VT parse, atlas rasterize, Vulkan draw)

### Performance by design

- Single instanced draw call per frame
- Glyph atlas caching (rasterize once, reuse)
- Dirty tracking from libghostty render state
- Single-threaded with `poll()` (no lock contention)
- FIFO vsync (no wasted frames)

### Non-goals

- No premature optimization — get it working, then profile
- No threading unless profiling proves the single thread is the bottleneck