5c38fd57
Update design spec with review feedback
a73x 2026-04-07 19:11
Commit message
docs/superpowers/specs/2026-04-07-waystty-design.md
| Old | New | ||
|---|---|---|---|
| @@ -36,18 +36,30 @@ renderer.zig — Vulkan setup, pipeline, instanced quad rendering | |||
| 36 | 36 | ||
| 37 | Protocols: | 37 | Protocols: |
| 38 | - `wl_compositor` + `wl_surface` — drawing surface | 38 | - `wl_compositor` + `wl_surface` — drawing surface |
| 39 | - `xdg_wm_base` + `xdg_surface` + `xdg_toplevel` — window management | 39 | - `xdg_wm_base` + `xdg_surface` + `xdg_toplevel` — window management (SSD only, no CSD) |
| 40 | - `wl_seat` → `wl_keyboard` + `wl_pointer` — input | 40 | - `wl_seat` → `wl_keyboard` + `wl_pointer` — input |
| 41 | - `wp_cursor_shape_v1` — set I-beam cursor over terminal area | ||
| 42 | - `wl_output` — monitor scale factor for DPI awareness | ||
| 43 | - `wp_fractional_scale_v1` — fractional DPI scaling (if available) | ||
| 41 | 44 | ||
| 42 | Vulkan surface via `VK_KHR_wayland_surface` (`VkSurfaceKHR` from `wl_display` + `wl_surface`). | 45 | Vulkan surface via `VK_KHR_wayland_surface` (`VkSurfaceKHR` from `wl_display` + `wl_surface`). |
| 43 | 46 | ||
| 44 | Keyboard input: keycodes from `wl_keyboard.key`, mapped via xkbcommon to keysyms/UTF-8, then handed to vt.zig's key encoder. | 47 | Keyboard input: keycodes from `wl_keyboard.key`, mapped via xkbcommon to keysyms/UTF-8, then handed to vt.zig's key encoder. |
| 45 | 48 | ||
| 49 | Key repeat: client-side implementation using `wl_keyboard.repeat_info` (rate + delay). Wayland does not repeat keys for you — we must track the last pressed key and fire repeats on a timer in the event loop. | ||
| 50 | |||
| 51 | Focus events: track `wl_keyboard.enter` / `wl_keyboard.leave`, encode via `ghostty_focus_encode()` when terminal has `GHOSTTY_MODE_FOCUS_EVENT` set. | ||
| 52 | |||
| 46 | Event loop: `wl_display_get_fd()` polled alongside PTY fd. Single-threaded. | 53 | Event loop: `wl_display_get_fd()` polled alongside PTY fd. Single-threaded. |
| 47 | 54 | ||
| 55 | #### Clipboard (phase 2) | ||
| 56 | |||
| 57 | Not implemented in v1. Requires `wl_data_device_manager`, `wl_data_device`, `wl_data_source`, `wl_data_offer` — significant protocol work. Will be the first feature added after the terminal is functional. Without clipboard, the terminal is usable but inconvenient. | ||
| 58 | |||
| 48 | ### pty.zig | 59 | ### pty.zig |
| 49 | 60 | ||
| 50 | - `forkpty()` to spawn child shell (`$SHELL` or `/bin/sh`) | 61 | - `forkpty()` to spawn child shell (`$SHELL` or `/bin/sh`) |
| 62 | - Set `TERM=xterm-256color` in child environment | ||
| 51 | - Non-blocking master fd | 63 | - Non-blocking master fd |
| 52 | - `read()` / `write()` helpers | 64 | - `read()` / `write()` helpers |
| 53 | - `SIGCHLD` handling for child exit | 65 | - `SIGCHLD` handling for child exit |
| @@ -55,18 +67,33 @@ Event loop: `wl_display_get_fd()` polled alongside PTY fd. Single-threaded. | |||
| 55 | 67 | ||
| 56 | ### vt.zig | 68 | ### vt.zig |
| 57 | 69 | ||
| 58 | Zig wrapper around `<ghostty/vt.h>` via `@cImport`: | 70 | Zig wrapper around `<ghostty/vt.h>` via `@cImport`. The C API is opaque-handle-based — all types are pointers created/freed with `_new`/`_free` pairs. Configuration uses a setter pattern: `ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_*, value)`. |
| 59 | - `Terminal` — init, feed bytes, resize, scroll, get render state | 71 | |
| 60 | - `KeyEncoder` / `MouseEncoder` — encode input events to VT sequences | 72 | Wrapped handle types: |
| 61 | - `RenderState` — snapshot, iterate rows/cells, get colors/cursor | 73 | - `Terminal` — `ghostty_terminal_new` / `_free`. Init with cols, rows, max_scrollback. Feed bytes via `ghostty_terminal_vt_write`. Resize via `ghostty_terminal_resize`. Scroll viewport via `ghostty_terminal_scroll_viewport`. |
| 62 | - Effect callbacks: write-to-pty, device-attributes, title-changed, etc. | 74 | - `RenderState` — `ghostty_render_state_new` / `_free`. Snapshot terminal state via `ghostty_render_state_update(render_state, terminal)`. Must be called after feeding PTY data, before iterating cells. Query colors via `ghostty_render_state_colors_get`. Check dirty flag via `GHOSTTY_RENDER_STATE_OPTION_DIRTY`. |
| 75 | - `RowIterator` — `ghostty_render_state_row_iterator_new` / `_free`. Iterate rows via `_next`. Each row yields a `RowCells`. | ||
| 76 | - `RowCells` — `ghostty_render_state_row_cells_new` / `_free`. Iterate cells via `_next`. Per-cell getters for: graphemes buffer, fg/bg colors, style flags (bold, italic, inverse, underline). Check row dirty via `GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY`. | ||
| 77 | - `KeyEncoder` — `ghostty_key_encoder_new` / `_free`. Before each encode, sync terminal modes via `ghostty_key_encoder_setopt_from_terminal`. Then create `KeyEvent` (`_new` / `_free`), populate, and encode. | ||
| 78 | - `MouseEncoder` — `ghostty_mouse_encoder_new` / `_free`. Before each encode, sync via `ghostty_mouse_encoder_setopt_from_terminal`. Create `MouseEvent` (`_new` / `_free`), populate, and encode. | ||
| 79 | |||
| 80 | Required effect callbacks (set via `ghostty_terminal_set`): | ||
| 81 | - `GHOSTTY_TERMINAL_OPT_WRITE_PTY` — terminal sends responses (device status, cursor position) back to shell. **Mandatory**. | ||
| 82 | - `GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES` — DA1/DA2/DA3 query responses. Reports VT220 conformance. | ||
| 83 | - `GHOSTTY_TERMINAL_OPT_SIZE` — reports terminal dimensions to querying applications. | ||
| 84 | - `GHOSTTY_TERMINAL_OPT_XTVERSION` — returns "waystty" for XTVERSION queries. | ||
| 85 | - `GHOSTTY_TERMINAL_OPT_TITLE_CHANGED` — updates the xdg_toplevel title. | ||
| 86 | - `GHOSTTY_TERMINAL_OPT_COLOR_SCHEME` — stub (returns false). | ||
| 87 | |||
| 88 | System init: call `ghostty_sys_set(GHOSTTY_SYS_OPT_DECODE_PNG, decode_png)` before creating terminal if Kitty graphics support is desired (phase 2). | ||
| 63 | 89 | ||
| 64 | ### font.zig | 90 | ### font.zig |
| 65 | 91 | ||
| 66 | - fontconfig: find system monospace font at startup | 92 | - fontconfig: find system monospace font at startup |
| 67 | - freetype: load font face, rasterize glyphs to bitmaps | 93 | - freetype: load font face, rasterize glyphs to single-channel (R8) bitmaps |
| 68 | - harfbuzz: shape text runs (ligatures, combining characters) | 94 | - harfbuzz: deferred to phase 2. For v1, render single codepoints per cell — libghostty already handles grapheme clustering and provides codepoints per cell via `GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF`. Ligature/shaping support can be added later by shaping runs of same-styled cells. |
| 69 | - Glyph atlas: single RGBA texture (e.g. 1024x1024), row-based packing, returns UV coords per glyph | 95 | - Glyph atlas: single R8 texture (e.g. 1024x1024), row-based packing, returns UV coords per glyph. Fragment shader applies fg color to the single-channel alpha. Colored emoji would need a separate RGBA atlas (phase 2). |
| 96 | - Cell metrics: `cell_width` derived from font advance width of 'M', `cell_height` from font ascent + descent + line gap. These are computed once at font load and used for grid calculations. | ||
| 70 | 97 | ||
| 71 | ### renderer.zig | 98 | ### renderer.zig |
| 72 | 99 | ||
| @@ -95,9 +122,12 @@ loop: | |||
| 95 | if pty_fd readable: | 122 | if pty_fd readable: |
| 96 | read pty → feed to terminal via ghostty_terminal_vt_write() | 123 | read pty → feed to terminal via ghostty_terminal_vt_write() |
| 97 | 124 | ||
| 98 | if terminal dirty or resize: | 125 | if key repeat timer expired: |
| 99 | snapshot render state | 126 | re-send last key event to pty |
| 100 | rebuild instance buffer | 127 | |
| 128 | if terminal dirty (GHOSTTY_RENDER_STATE_OPTION_DIRTY) or resize: | ||
| 129 | ghostty_render_state_update(render_state, terminal) | ||
| 130 | rebuild instance buffer (skip clean rows via ROW_OPTION_DIRTY) | ||
| 101 | render frame | 131 | render frame |
| 102 | 132 | ||
| 103 | if child exited: | 133 | if child exited: |
| @@ -136,8 +166,9 @@ Zig package dependencies (via `build.zig.zon`): | |||
| 136 | - zig-wayland — Wayland protocol bindings | 166 | - zig-wayland — Wayland protocol bindings |
| 137 | - vulkan-zig — Vulkan bindings generator | 167 | - vulkan-zig — Vulkan bindings generator |
| 138 | 168 | ||
| 169 | **libghostty-vt acquisition:** Built from source. The ghostty repo is fetched via `build.zig.zon` (pinned commit hash). A custom build step invokes `zig build lib-vt` within the fetched source to produce the shared library, similar to how ghostling's CMakeLists.txt handles it. | ||
| 170 | |||
| 139 | System C library linkage: | 171 | System C library linkage: |
| 140 | - libghostty-vt | ||
| 141 | - freetype2 | 172 | - freetype2 |
| 142 | - harfbuzz | 173 | - harfbuzz |
| 143 | - fontconfig | 174 | - fontconfig |
| @@ -201,3 +232,18 @@ End-to-end: spawn waystty with `echo "hello"; exit`, verify clean exit. Can run | |||
| 201 | 232 | ||
| 202 | - No premature optimization — get it working, then profile | 233 | - No premature optimization — get it working, then profile |
| 203 | - No threading unless profiling proves the single thread is the bottleneck | 234 | - No threading unless profiling proves the single thread is the bottleneck |
| 235 | |||
| 236 | ## Phased Features | ||
| 237 | |||
| 238 | Features deferred from v1 to keep initial scope minimal: | ||
| 239 | |||
| 240 | | Feature | Phase | Notes | | ||
| 241 | |---------|-------|-------| | ||
| 242 | | Clipboard (copy/paste) | 2 | `wl_data_device_manager` protocol work. First post-v1 priority. | | ||
| 243 | | Harfbuzz text shaping | 2 | Ligatures, complex scripts. v1 renders single codepoints per cell. | | ||
| 244 | | Kitty graphics protocol | 2 | Image display in terminal. Requires PNG decode callback + placement rendering. | | ||
| 245 | | Colored emoji | 2 | Separate RGBA atlas alongside the R8 glyph atlas. | | ||
| 246 | | Scrollbar rendering | 2 | Visual scrollbar. Viewport scrolling via `ghostty_terminal_scroll_viewport` is in v1. | | ||
| 247 | | Config file | future | `~/.config/waystty/config` for fonts, colors, keybindings. | | ||
| 248 | | URL detection/opening | future | Click-to-open URLs. | | ||
| 249 | | Multiple windows/tabs | future | Out of scope for a minimal terminal. | | ||