TUI Library Shortlist — Embeddability Evaluation (Architecture)
This is an architect’s evaluation, not a captured project. It consumes the library index in awesome-tuis.md and the dedicated entries for tuibox, termui, ink, and ink-web, and renders a build recommendation for the KN-86 runtime’s UI layer.
The question
Section titled “The question”KN-86 renders its own 128×75 monochrome amber-on-black cell grid. Cartridges are authored in KEC Lisp (ADR-0001, ADR-0004). The runtime is C11 (no C++), and ADR-0027 (ratified 2026-06-07) already chose the substrate: termbox2 as the C-internal cell-grid display + input layer, with a constrained KN-86 cell API exposed to Fe (not raw termbox), a 128×75 grid, and a 128×150 half-block pseudo-pixel canvas. (See “CLAUDE.md Canonical Hardware Specification.”)
So the live architecture question is not “which TUI library should KN-86 build on” — that is settled. It is the narrower, sharper question this evaluation answers:
Of the broader TUI-library field (C/C++/Rust), is there anything KN-86 should adopt as a dependency, or is the right move — as ADR-0027 already implies — to borrow patterns and own the implementation? And what architecture model should govern the split between core logic and rendering?
Bottom line up front: Borrow patterns, do not adopt a library (beyond the already-ratified termbox2 single-header vendor). Every higher-level TUI library on the shortlist either fights KN-86’s bespoke-screen + constrained-FFI model, drags in a language/runtime KN-86 won’t carry, or duplicates what termbox2 + a thin Fe UI library already gives. The recommended architecture model is the cavacore core/render split: domain/logic in C, rendering thin and swappable across the SDL-audio-only emulator path and the termbox2 device path.
Evaluation criteria
Section titled “Evaluation criteria”For each library: language / license, immediate-mode vs retained-mode, how it would interop with the KEC Lisp control layer + the existing nOSh C renderer, and a verdict (adopt / pattern-only / skip).
A note on the two axes that decide most verdicts:
- C11, no C++. ADR-0027 §Constraints is explicit: “C11. No C++.” This alone reduces every C++ library (FTXUI, imtui, FINAL CUT, Tui Widgets, tvision) to pattern-only at best — they cannot be a runtime dependency without an FFI/ABI shim and a C++ toolchain in the Pi system image.
- Who owns the screen and input? ADR-0027’s load-bearing decision is that the runtime owns termbox; cartridges see a constrained cell API and pre-classified semantic events. Any library that wants to own the event loop, the input modes, or the whole screen (which is most of them) collides head-on with that boundary. KN-86 already rejected raw-termbox-to-Fe (ADR-0027 Option B) for exactly this reason; a fortiori it rejects a fatter library claiming the same authority.
C libraries
Section titled “C libraries”ncurses — SKIP
Section titled “ncurses — SKIP”- Language / license: C, MIT-ish (X/Open + ncurses license).
- Mode: retained-ish (window/pad model with
wnoutrefresh/doupdatebatching). - Interop: would replace termbox2 as the cell backend; Fe would still need a constrained wrapper over it.
- Verdict: SKIP. ADR-0027 §Option E already rejected ncurses for KN-86: terminfo dependency, larger API surface than termbox2, less amenable to single-file vendoring, and optimized for terminal-compatibility breadth KN-86 doesn’t need (the device targets exactly one terminal — the Linux console; the emulator targets modern terminals only). termbox2’s smaller modern MIT single-header surface won. Re-confirming, not re-deciding.
termbox2 — ADOPTED (already, by ADR-0027)
Section titled “termbox2 — ADOPTED (already, by ADR-0027)”- Language / license: C99, single-header
termbox2.h, MIT. - Mode: immediate-mode cell buffer — you write cells into a back buffer and
tb_present()diffs against the front buffer, emitting only changed cells. (This is the cell-byte diff that means KN-86 does not need to re-implement Ink-style output diffing — see ink.md.) - Interop: the C runtime binds termbox directly; Fe sees only the constrained cell API (
cell-set,cell-print,cell-clear-cart-region,half-block-set, …) and semantic input callbacks (on-key,on-tick, …). Input policy (31-key classification, hold detection, multi-tap, TERM/SYS chords) stays in nOSh. - Verdict: ADOPT — this is the ratified substrate. Listed here only to anchor the comparison: termbox2 is the floor; everything else is evaluated as “does it earn its place above termbox2.”
tuibox — PATTERN-ONLY (read the source; don’t depend)
Section titled “tuibox — PATTERN-ONLY (read the source; don’t depend)”- Language / license: C99, single-header
tuibox.h, MIT. (Full entry: tuibox.md.) - Mode: retained — a UI→Screen→Box hierarchy with a per-box dirty bit render cache; mouse-driven by default; click-handler/widget oriented.
- Interop: tuibox is built on termbox2, so it would sit between termbox2 and KN-86’s cell API — adding a box-tree + event-callback layer.
- Verdict: PATTERN-ONLY. ADR-0027 §Option D explicitly rejected tuibox as a dependency, and the reasoning holds: KN-86 has no mouse (31 keys), every screen is a bespoke draw, and cart authoring is “loop, render, yield” — not “declare a button, bind a handler.” tuibox’s value-add is mouse widgets KN-86 doesn’t use; its cost is a wider C surface and a layer that fights bespoke screens (mission board, REPL, nEmacs) when they don’t fit a widget tree. But two tuibox patterns are worth lifting: (1) the UI→Screen→Box hierarchy as the structural model for nOSh chrome regions (Row 0, rows 1..73 cart area, Row 74), and (2) the per-box dirty bit — though note termbox2 already diffs at the cell level, so the dirty bit only earns its keep at the region/component level (cf. the component-memoization point in ink.md). Read tuibox as a reference implementation; do not link it.
AnbUI — SKIP (pattern-curiosity only)
Section titled “AnbUI — SKIP (pattern-curiosity only)”- Language / license: C, MIT. A deliberately tiny “ANSI-only, no-ncurses” TUI toolkit (menus, message boxes, progress) aimed at embedding into small C programs with minimal footprint.
- Mode: immediate-mode, dialog/menu oriented.
- Interop: would overlap termbox2’s role with a much smaller, dialog-centric surface.
- Verdict: SKIP. AnbUI’s niche — “drop a menu/dialog into a C program with zero dependencies” — is real, but KN-86 already has termbox2 for the cell layer and will build its menus/dialogs as Fe-side widgets against the cell API (the focus-manager + select-input pattern from ink.md / ink-web.md). AnbUI’s dialog idioms are worth a glance as a minimalist menu reference; nothing to adopt.
FINAL CUT — SKIP (C++; widget-heavy)
Section titled “FINAL CUT — SKIP (C++; widget-heavy)”- Language / license: C++14, LGPL-ish. A full widget toolkit (windows, dialogs, scrollbars, a widget class hierarchy) — closer to a desktop GUI framework projected onto a terminal.
- Mode: retained, deep widget/object hierarchy, owns the event loop.
- Interop: C++ → violates ADR-0027’s “C11, no C++.” Would need a C ABI shim and a C++ toolchain in the system image. Its retained widget tree + event-loop ownership collide with the runtime-owns-input boundary.
- Verdict: SKIP. Wrong language, wrong authority model (it wants to own the screen and the event loop), wrong weight class (desktop-GUI-on-a-terminal vs. KN-86’s bespoke amber cells). Not even a strong pattern source — its abstractions assume a windowing metaphor KN-86 deliberately doesn’t have.
C++ libraries
Section titled “C++ libraries”FTXUI — PATTERN-ONLY (the declarative-composition idea is good; the dependency is wrong)
Section titled “FTXUI — PATTERN-ONLY (the declarative-composition idea is good; the dependency is wrong)”- Language / license: C++17, MIT. Used by caps-log and many others.
- Mode: declarative + functional-reactive — you compose
Elements andComponents (hbox/vbox/border/text/flex), and FTXUI’sRenderer/Containermodel handles layout and focus. Closest C++ analogue to Ink’s component model. - Interop: C++17 → cannot be a KN-86 dependency (ADR-0027 “no C++”). Its renderer owns the screen.
- Verdict: PATTERN-ONLY. FTXUI is the C++ proof that declarative element-composition + flex-style layout is pleasant for fixed-grid TUIs — the same thesis as Ink, from the C++ side. The pattern worth borrowing is its
hbox/vbox/border/flexcomposition combinators: a tiny set of box-combinators that resolve to cell rectangles. This is exactly the “borrow the box-composition model, not a flexbox engine” recommendation from ink.md, and it is what a Fe-side layout library should expose. FTXUI also has a clean focus-traversal model worth reading alongside Ink’suseFocusManager. Adopt neither library — but a Fe combinator set inspired by FTXUI+Ink+Lip-Gloss is the single highest-value piece of an optional KN-86 UI library.
imtui — PATTERN-ONLY (immediate-mode is the right paradigm; Dear ImGui is the wrong dependency)
Section titled “imtui — PATTERN-ONLY (immediate-mode is the right paradigm; Dear ImGui is the wrong dependency)”- Language / license: C++, MIT. An immediate-mode TUI built on the Dear ImGui paradigm/codebase, rendering ImGui-style UI into a terminal.
- Mode: immediate-mode — UI is re-declared every frame; no retained widget tree; widget state is keyed by call-site/ID. This is the purest example on the shortlist of the paradigm KN-86’s cart API already is.
- Interop: C++ + Dear ImGui → not a dependency candidate.
- Verdict: PATTERN-ONLY — and the most paradigm-aligned entry on the list. imtui matters because it validates that immediate-mode is the correct authoring paradigm for KN-86, which is precisely what ADR-0027 chose: a cart’s
(on-tick)redraws its region every frame; there is no retained scene graph the cart mutates. The immediate-mode discipline (re-declare the UI each frame, keep state in the application not the widget) maps perfectly onto Fe carts holding their own state and re-emitting cell writes per tick. The pattern to internalize: immediate-mode UI + application-owned state is the KN-86 cart model; any optional Fe component layer (the Ink-style one) must be a convenience on top of immediate-mode, never a retained graph the runtime has to reconcile and own. imtui is the reference for getting that boundary right.
Tui Widgets — SKIP (C++; Qt-flavored)
Section titled “Tui Widgets — SKIP (C++; Qt-flavored)”- Language / license: C++17, against
libtermpaint; a Qt-style widget library (signals/slots-ish, layouts, focus). - Mode: retained widget tree, owns the event loop.
- Verdict: SKIP. Same disqualifiers as FINAL CUT: C++, retained-widget authority model, desktop-GUI metaphor. FTXUI already covers the “good C++ pattern source” slot better (declarative + immediate-friendly vs. Tui Widgets’ retained-Qt model). Nothing additional to mine.
Rust libraries
Section titled “Rust libraries”Ratatui — PATTERN-ONLY (note its dominance; borrow its immediate-mode widget contract)
Section titled “Ratatui — PATTERN-ONLY (note its dominance; borrow its immediate-mode widget contract)”- Language / license: Rust, MIT. The successor to tui-rs and, per awesome-tuis.md, the dominant Rust TUI library — it shows up under more application projects across this research batch than any other single library (csvlens, logradar, and siblings).
- Mode: immediate-mode — the canonical pattern is
terminal.draw(|frame| { … }): every frame you construct stateless widgets (Block,Paragraph,List,Table,Gauge,Sparkline,Chart,Tabs) and render them intoRectareas computed by a constraint-basedLayoutsolver. Widget state (selection, scroll offset) lives in small…Statestructs the application owns — not in the widgets. - Interop: Rust → not a C11 runtime dependency. (A Rust static lib with a C ABI is possible but adds a Rust toolchain to the Pi system image and an FFI seam for zero payoff over termbox2.)
- Verdict: PATTERN-ONLY — and the best widget-contract reference on the shortlist. Two things to borrow: (1) Ratatui’s immediate-mode widget contract — stateless widget + application-owned state struct + render-into-a-Rect — is the cleanest expression of the model KN-86 should give its Fe widgets. A Fe
(list-widget items state rect)that reads a cart-ownedstateand writes cells into a computedrectis a direct transliteration. (2) Ratatui’s constraint-basedLayout(split aRectbyLength/Percentage/Min/Ratioconstraints) is a smaller, more KN-86-appropriate layout model than flexbox — it resolves integer cell rectangles from simple constraints, which is exactly what a fixed 128×75 grid wants. Borrow the widget contract and the constraint-split layout idea; do not add Rust to the stack. Its widget catalog (Sparkline, Gauge, Chart, Table, Tabs, List) also corroborates the termui.md + ink-web.md widget-inventory baseline.
The architecture model: cavacore (core/render split)
Section titled “The architecture model: cavacore (core/render split)”The most important positive recommendation here is not a library — it is a structural pattern KN-86 should adopt by name: the cavacore model from cava (the console audio visualizer). cava split itself into cavacore — a pure C library of the DSP + visualization logic (FFT, smoothing, frequency-bin → bar-height computation), with no I/O and no rendering — and a set of thin, swappable output backends (ncurses, raw terminal, SDL, framebuffer, …) that consume cavacore’s computed bar heights and draw them. The core knows nothing about how it’s displayed; you can re-target the renderer without touching the logic.
This is exactly the discipline KN-86 already commits to and should make explicit:
- Core (C, render-agnostic): the mission board, economy (credits/reputation), phase chain, Universal Deck State, CIPHER grammar/voice emission, cart capability dispatch, and the Fe VM itself. None of this knows whether it’s drawn by termbox2 on a Pi tty1 or by a terminal on a developer’s laptop. This is the nOSh runtime’s logic spine, and it maps cleanly onto the game-programming-patterns.md Update Method / State / Observer / Command patterns.
- Render layer (thin, swappable): termbox2 against the cell grid — one backend on the device (Linux-console tty1), the same backend in the emulator (a normal terminal). Audio is independently swappable (SDL audio in the emulator per ADR-0025; PSG→I2S→MAX98357A on device). The render/audio backends consume core state; they don’t own logic.
- The cell API is the seam. ADR-0027’s constrained Fe cell API is the cavacore boundary applied to cartridges: carts express what to draw (cells, half-blocks, semantic intent), the runtime decides how it reaches the panel. Swapping termbox2 for a hypothetical future backend is “a non-event for cartridges” — ADR-0027 says this in as many words.
Recommendation: name the cavacore split as the canonical architecture model in the nOSh runtime spec, cross-referenced to the capability model (logic in the runtime, carts as capability modules) and the FFI surface (ADR-0005, amended by ADR-0027 from 54 → ~30 primitives). The capability model already is a core/render split at the cartridge boundary; cavacore is the same discipline applied one layer down, at the runtime/renderer boundary. Making it explicit guards against the recurring failure mode ADR-0027 diagnosed — logic leaking into the renderer (pixel padding, letterbox math, dev-chrome in the device window) — by giving the boundary a name and a doctrine.
Synthesis verdict
Section titled “Synthesis verdict”| Library | Lang | License | Mode | Verdict | What to borrow |
|---|---|---|---|---|---|
| termbox2 | C99 | MIT | immediate (cell buffer) | ADOPT (ratified, ADR-0027) | — (it’s the substrate) |
| ncurses | C | MIT-ish | retained-ish | SKIP | nothing (ADR-0027 §E) |
| tuibox | C99 | MIT | retained (box tree) | PATTERN-ONLY | UI→Screen→Box regions; dirty-bit (region-level) |
| AnbUI | C | MIT | immediate (dialogs) | SKIP | minimalist-menu idiom (glance) |
| FINAL CUT | C++14 | LGPL-ish | retained | SKIP | nothing |
| FTXUI | C++17 | MIT | declarative/reactive | PATTERN-ONLY | box-composition combinators; focus traversal |
| imtui | C++ | MIT | immediate | PATTERN-ONLY | immediate-mode + app-owned-state discipline (paradigm anchor) |
| Tui Widgets | C++17 | — | retained (Qt-ish) | SKIP | nothing |
| Ratatui | Rust | MIT | immediate | PATTERN-ONLY | stateless-widget + state-struct contract; constraint-split layout; widget catalog |
The recommendation, in one line: Adopt no library beyond the already-ratified termbox2; borrow patterns (immediate-mode discipline from imtui/Ratatui, box-composition + focus from FTXUI/Ink, the dirty-bit region cache from tuibox), and structure the runtime on the cavacore core/render split. KN-86 renders its own grid — so the realistic and correct answer, which ADR-0027 already pointed at, is pattern-borrowing, not library adoption.
Does KN-86 adopt a component-model UI DSL (Ink-style) for its Lisp layer?
Section titled “Does KN-86 adopt a component-model UI DSL (Ink-style) for its Lisp layer?”Qualified yes — as an optional internal Fe library, not as the cart-facing substrate. The substrate stays imperative and constrained (ADR-0027’s cell API + semantic events), because that boundary protects chrome ownership and input policy and must not be re-opened. On top of it, an optional Fe UI library should borrow three patterns — (1) box-composition layout combinators (FTXUI/Ink/Ratatui-constraint, resolving to integer cell rects), (2) a focus-management system (Ink useFocusManager, consuming the on-key semantic events — the highest-value single idea for a 31-key device), and (3) component-level memoization (re-run a render lambda only when its inputs changed — the only part of Ink’s reconciler worth the Fe complexity, and only for tree-shaped chrome-heavy surfaces like the mission board, REPL, nEmacs, and bare-deck tabs). Bespoke gameplay carts keep drawing cells directly in immediate mode; structured surfaces opt into the component layer. A “Lisp-native Ink” is a worthwhile convenience library, never a requirement and never a retained scene graph the runtime has to own.
- Cross-link ink.md (the component-model architecture analysis), ink-web.md (the component catalog + the Row-74 status-bar reference + device/emulator parity), awesome-tuis.md (the source index for this candidate set), tuibox.md + termui.md (existing dedicated entries), and game-programming-patterns.md (the runtime-pattern vocabulary the cavacore core uses).
- Cross-link the capability model + FFI surface —
software/runtime/orchestration.md(the logic spine = cavacore core) and ADR-0005 (the FFI seam, amended by ADR-0027). The capability model is already a core/render split at the cart boundary; cavacore names the same discipline at the runtime/renderer boundary. - Grounding ADR: ADR-0027 — termbox2 substrate, 128×75 grid, constrained Fe cell API, half-block 128×150 canvas. This evaluation confirms and extends that decision; it does not re-open it.