Files
lumbridge-code/docs/decisions/0024-lumbridge-builds-its-own-toolkit.md
Metal AgentandClaude Opus 5 d7560d4b8b
CI / rust-headless (push) Successful in 7m31s
CI / rust-ui (push) Successful in 13m45s
Decide to build the toolkit, and say what would stop it
Lumbridge will build and open-source lumbridge-toolkit and ship on it, the same
arrangement Zed has with GPUI and for the same reason: nothing existing is built
for a terminal-grid-and-agent-panels product. GPUI stays as the backend until
parity, then goes.

What changed is not appetite. Writing a toolkit used to mean writing a GPU
renderer, a shaping and layout engine, and an accessibility layer from scratch;
vello, parley, accesskit, kurbo, peniko, winit and taffy now do that under a
permissive licence. Measured at ce48461e the GPUI family is 129,110 lines across
nine crates, and the large majority of it is those primitives. What is left --
the element model, layout composition, the paint pipeline, input routing, and
the terminal grid -- is what this project actually has to write.

Two claims justify building rather than adopting, and no other Rust toolkit
makes them. Accessibility is a crate the element model is built on rather than
an adapter added later, which matters because both candidates Lumbridge
evaluated failed that hard gate and 0017 shipped knowing it. And the terminal
grid is a first-class widget rather than an embedding problem, which matters
because this repository has already got cell measurement wrong twice.

M0 is a gate with its thresholds written before the run, so the result cannot be
rationalised afterwards: five panels of 200x60 styled cells no worse than the
GPUI baseline on frame p95 and key-to-present, an IME round trip that does not
split UTF-8, and an AccessKit tree driven by Orca and VoiceOver -- a passing
TreeUpdate unit test explicitly does not count. Failing it means staying on
pinned GPUI and superseding this record.

The failure mode being guarded against is a year with neither a finished toolkit
nor a finished product, so Phase 1's exit criterion is not deferred for toolkit
work, and no GPUI capability is removed before its replacement passes the same
gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SPYebLiN2w4TqnHUYGdECq
2026-09-01 14:03:56 -07:00

7.8 KiB

0024: Lumbridge builds its own UI toolkit, and GPUI becomes the interim backend

Status: accepted. Supersedes the framework selection in decisions 0017 and 0023. Those records' accounts of why the published GPUI crate is unusable, and the pinned revision Lumbridge runs on today, both stand — this record changes where the project is going, not where it currently is.

The decision

Lumbridge will build and open-source lumbridge-toolkit, a native Rust UI toolkit, and Lumbridge will ship on it. GPUI remains the backend until the toolkit reaches parity, and is then removed.

This is the same shape as Zed's own arrangement — an application and the toolkit it needed, developed together — and it is chosen for the same reason: no existing toolkit is built for what this product is. It is not a general-purpose GUI framework, and competing with GPUI on breadth is not the goal.

Why now, and what changed

The case against building a toolkit was that it means writing a GPU renderer, a text shaping and layout engine, and an accessibility layer from scratch, and that owning menus, IME, clipboard, drag-and-drop, multi-window and packaging on three platforms is where such projects die. Most of that is still true. What has changed is that the hardest primitives are now solved by permissively licensed crates that did not exist, or were not ready, when GPUI was started:

  • vello for GPU 2D rendering, wgpu underneath;
  • parley with swash/skrifa for shaping, layout and font fallback;
  • kurbo and peniko for geometry, paths and brushes;
  • accesskit for the platform accessibility tree;
  • winit for windowing, input and IME;
  • taffy for flex layout, which GPUI itself uses.

Measured at revision ce48461e, the GPUI family is 129,110 lines across nine crates — gpui alone is 75,995, with 15,391 for the Linux backend, 12,282 for Windows and 10,503 for macOS — carrying 108 non-Zed dependencies. The large majority of that is the primitives above. What remains, and what this project actually has to write, is the element model, layout composition, the paint pipeline, input routing, and the terminal grid.

What the toolkit is for, and what it is not

Two claims are worth making because nothing else in the Rust ecosystem makes them, and they are the reasons this is worth building rather than adopting:

Accessibility is structural. lt-a11y is a crate the element model is built on, not an adapter added later. Both candidates Lumbridge evaluated failed its accessibility hard gate — Floem has no AccessKit integration at its pinned revision, and published GPUI has no AccessKit dependency at all — and decision 0017 knowingly shipped with that gate unmet. A toolkit that cannot be retrofitted into that state is the point.

The terminal grid is a first-class widget, not an embedding problem. Every Rust application that puts a terminal on screen rebuilds cell measurement, styled run coalescing and viewport arithmetic, and docs/ARCHITECTURE.md records two ways this project already got it wrong: a cell advance hardcoded 13% wider than the face actually painted, and ws_xpixel/ws_ypixel reported as zero, so every program doing pixel arithmetic was told the window had no size.

It is explicitly not a general-purpose application framework, not a web target, and not a competitor for editor authors. Its first client is Lumbridge, and features arrive because Lumbridge needs them.

Repository and licence

A separate repository, consumed by Lumbridge as a revision-pinned Git dependency through the same guard that pins GPUI today.

Separate, rather than a directory in this tree, because this repository has twice shipped code that no build ever looked at: the GPUI shell in spikes/, which Cargo.toml excluded so that every test written into it silently never ran (decision 0017), and crates/lumbridge-devices, which was in neither members nor exclude and did not compile at all. A toolkit developed in an unlinted corner of an application's tree is the third instance waiting to happen. It also needs its own CI, its own issue tracker, and a release cadence that is not Lumbridge's.

Licensed MIT OR Apache-2.0, the Rust ecosystem's dual-licence norm. This is not only about adoption: an Apache-2.0 crate that unconditionally pulls GPL-3.0-or-later is precisely the trap that ended Lumbridge's use of upstream GPUI as a long-term dependency, and a permissive dual licence with a cargo deny source and licence gate from the first commit is how this project avoids setting the same trap for somebody else.

Shape

lt-core       ids, geometry, colour, units          (kurbo, peniko)
lt-text       shaping, font fallback, cell metrics  (parley, swash)
lt-layout     flex composition and measurement      (taffy)
lt-render     scene building, paint runs, damage    (vello)
lt-input      key encoding, IME, mouse, focus       (winit)
lt-a11y       the AccessKit tree
lt-platform   window, menus, clipboard, drag-and-drop
lt-element    the builder API applications write against
lt-terminal   the styled grid

M0 is a gate, not a formality

Before any port begins, one spike answers the three questions that would end this. Its thresholds are written here, in advance, so the result cannot be rationalised afterwards. The baseline is the current GPUI shell on the same machine and the same fixture.

  1. Density. Five panels of 200x60 styled cells consuming live PTY output. Frame p95 no worse than the GPUI baseline, and key-to-present no worse.
  2. Input. Composed accents and multi-codepoint graphemes survive an IME round trip without splitting UTF-8 — the same assertions spikes/gpui-accessibility-probe already makes.
  3. Accessibility. An AccessKit tree naming each pane, its selected state, its execution target and its waiting state, driven by Orca on Linux and VoiceOver on macOS. A passing unit test over a TreeUpdate is not an assistive-technology claim and does not satisfy this.

Failing M0 means Lumbridge stays on pinned GPUI and this record is superseded. Three weeks is the budget.

Migration, and what is not allowed to happen

Lumbridge ships on GPUI throughout. The port is a strangler, and the seam already exists: no crate outside apps/lumbridge imports a GPUI type, lumbridge-terminal already produces framework-neutral immutable snapshots and coalesced paint runs, lumbridge-theme derives colours with no framework types, and the a11y trait already abstracts the semantics away from the framework that renders them.

  • M1 — the lt-* crates, with lt-terminal rendering the grid.
  • M2lt-element, and the framework-neutral lumbridge-ui boundary that ARCHITECTURE.md has named since the beginning. One panel ports. GPUI renders the rest.
  • M3 — the shell builds on either backend behind a Cargo feature, both in CI, with the deterministic fixture replaying identically through each. That identical replay is the parity evidence; a screenshot is not.
  • M4 — GPUI is removed, and lumbridge-toolkit 0.1 is published.

The failure mode to guard against is a year spent with neither a finished toolkit nor a finished product. Two rules follow. Phase 1 of ROADMAP.md — "Lumbridge is worth using as a terminal multiplexer without AI features" — is not deferred for toolkit work. And no GPUI capability is removed before its lt- replacement passes the same gate, so main is never the worse option.

Known risks

vello is pre-1.0, and this trades one pre-1.0 dependency for several. macOS notarization and VoiceOver are where toolkit projects die, and there is currently no Mac in the development loop at all — every macOS row of UI_SPIKE_SCORECARD.md is still pending. Text is a tarpit; the mitigation is that a terminal grid needs a fraction of what a word processor does, and that scoping must be defended rather than allowed to erode.