a73x

bddbc526

Add waystty design spec

a73x   2026-04-07 19:07

Commit message
Add waystty design spec

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>

docs/superpowers/specs/2026-04-07-waystty-design.md
Old New
@@ -0,0 +1,203 @@
1 # waystty Design Spec
2
3 A minimal, hackable Wayland terminal emulator written in Zig, using libghostty-vt for terminal emulation, Vulkan for rendering, and freetype/harfbuzz for font handling.
4
5 ## Goals
6
7 - Light and minimal — a terminal you can hack on
8 - Comparable to foot in spirit: fast, simple, Wayland-native
9 - libghostty-vt handles VT parsing and terminal state; we own everything else
10
11 ## Architecture
12
13 Six modules, each owning one concern:
14
15 ```
16 main.zig — event loop, glue, signal handling
17 wayland.zig — Wayland protocol handling (zig-wayland)
18 vt.zig — idiomatic Zig wrapper around libghostty-vt C API
19 pty.zig — PTY spawn, read/write, child process management
20 font.zig — fontconfig lookup, freetype rasterization, harfbuzz shaping, glyph atlas
21 renderer.zig — Vulkan setup, pipeline, instanced quad rendering
22 ```
23
24 ### Data flow per frame
25
26 1. Wayland dispatches input events (keyboard/pointer) to main loop
27 2. Input encoded via vt.zig (key/mouse encoders), written to PTY
28 3. PTY output read, fed to terminal via `ghostty_terminal_vt_write`
29 4. Renderer snapshots render state from vt.zig, iterates cells
30 5. Missing glyphs rasterized on demand by font.zig into the atlas
31 6. Renderer builds instance buffer (cell position, atlas UV, fg/bg color), issues single instanced draw call
32
33 ## Module Details
34
35 ### wayland.zig
36
37 Protocols:
38 - `wl_compositor` + `wl_surface` — drawing surface
39 - `xdg_wm_base` + `xdg_surface` + `xdg_toplevel` — window management
40 - `wl_seat` → `wl_keyboard` + `wl_pointer` — input
41
42 Vulkan surface via `VK_KHR_wayland_surface` (`VkSurfaceKHR` from `wl_display` + `wl_surface`).
43
44 Keyboard input: keycodes from `wl_keyboard.key`, mapped via xkbcommon to keysyms/UTF-8, then handed to vt.zig's key encoder.
45
46 Event loop: `wl_display_get_fd()` polled alongside PTY fd. Single-threaded.
47
48 ### pty.zig
49
50 - `forkpty()` to spawn child shell (`$SHELL` or `/bin/sh`)
51 - Non-blocking master fd
52 - `read()` / `write()` helpers
53 - `SIGCHLD` handling for child exit
54 - `ioctl(TIOCSWINSZ)` for resize
55
56 ### vt.zig
57
58 Zig wrapper around `<ghostty/vt.h>` via `@cImport`:
59 - `Terminal` — init, feed bytes, resize, scroll, get render state
60 - `KeyEncoder` / `MouseEncoder` — encode input events to VT sequences
61 - `RenderState` — snapshot, iterate rows/cells, get colors/cursor
62 - Effect callbacks: write-to-pty, device-attributes, title-changed, etc.
63
64 ### font.zig
65
66 - fontconfig: find system monospace font at startup
67 - freetype: load font face, rasterize glyphs to bitmaps
68 - harfbuzz: shape text runs (ligatures, combining characters)
69 - Glyph atlas: single RGBA texture (e.g. 1024x1024), row-based packing, returns UV coords per glyph
70
71 ### renderer.zig
72
73 Vulkan setup:
74 - Instance → physical device → logical device → swapchain (FIFO present mode)
75 - Single render pass, single subpass, single graphics pipeline
76 - Vertex shader: per-instance data (cell x/y, atlas UV rect, fg color, bg color) → positioned quad
77 - Fragment shader: samples glyph atlas, applies fg color; bg drawn as untextured quads
78 - One instanced draw call per frame
79 - Swapchain recreation on resize
80
81 Shaders: GLSL compiled to SPIR-V via `glslc` at build time, embedded via `@embedFile`.
82
83 ## Event Loop
84
85 ```
86 init wayland → init vulkan → init font → init terminal → spawn pty
87
88 loop:
89 poll(wayland_fd, pty_fd)
90
91 if wayland_fd readable:
92 wl_display_dispatch()
93 → keyboard/pointer events → encode via vt.zig → write to pty
94
95 if pty_fd readable:
96 read pty → feed to terminal via ghostty_terminal_vt_write()
97
98 if terminal dirty or resize:
99 snapshot render state
100 rebuild instance buffer
101 render frame
102
103 if child exited:
104 break
105 if xdg_toplevel close:
106 break
107
108 cleanup: free terminal, destroy vulkan, disconnect wayland
109 ```
110
111 ### Resize handling
112
113 1. `xdg_toplevel` configure event gives new pixel dimensions
114 2. Recreate Vulkan swapchain
115 3. Recalculate grid: `cols = pixel_width / cell_width`, `rows = pixel_height / cell_height`
116 4. `ghostty_terminal_resize()` with new dimensions
117 5. `ioctl(TIOCSWINSZ)` on PTY fd
118
119 ## Hardcoded Defaults
120
121 - Font: system monospace via fontconfig, 14px
122 - Colors: 16-color palette + fg/bg
123 - Scrollback: 1000 lines
124 - Window title: "waystty"
125 - Shell: `$SHELL` or `/bin/sh`
126
127 ## Build System
128
129 `build.zig` only. No Makefile.
130
131 Targets:
132 - `zig build` — build
133 - `zig build run` — build and run
134
135 Zig package dependencies (via `build.zig.zon`):
136 - zig-wayland — Wayland protocol bindings
137 - vulkan-zig — Vulkan bindings generator
138
139 System C library linkage:
140 - libghostty-vt
141 - freetype2
142 - harfbuzz
143 - fontconfig
144 - xkbcommon
145 - wayland-client
146
147 Shader build step: `glslc` compiles `.vert`/`.frag` to SPIR-V, embedded via `@embedFile`.
148
149 ### Project layout
150
151 ```
152 waystty/
153 ├── build.zig
154 ├── build.zig.zon
155 ├── src/
156 │ ├── main.zig
157 │ ├── wayland.zig
158 │ ├── vt.zig
159 │ ├── pty.zig
160 │ ├── font.zig
161 │ └── renderer.zig
162 └── shaders/
163 ├── cell.vert
164 └── cell.frag
165 ```
166
167 ## Testing
168
169 ### Unit tests
170
171 Zig `test` blocks per module:
172 - `pty.zig` — spawn shell, write/read, verify child exit
173 - `font.zig` — load font, rasterize glyph, verify atlas packing and UV coords
174 - `vt.zig` — feed escape sequences, snapshot render state, verify cells/colors/cursor
175
176 `wayland.zig` and `renderer.zig` require a compositor/GPU — not unit tested.
177
178 Run with `zig build test`.
179
180 ### Integration testing
181
182 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.
183
184 ## Performance
185
186 ### Measurement tools
187
188 1. **vtebench** — terminal throughput benchmark, compare against foot
189 2. **Frame timing** — `std.time.Timer` in render loop, warn in debug builds if frame > 16ms
190 3. **tracy** — Zig's `std.trace` for profiling key sections (pty read, VT parse, atlas rasterize, Vulkan draw)
191
192 ### Performance by design
193
194 - Single instanced draw call per frame
195 - Glyph atlas caching (rasterize once, reuse)
196 - Dirty tracking from libghostty render state
197 - Single-threaded with `poll()` (no lock contention)
198 - FIFO vsync (no wasted frames)
199
200 ### Non-goals
201
202 - No premature optimization — get it working, then profile
203 - No threading unless profiling proves the single thread is the bottleneck