Skip to content

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.

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.


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.

  • Language / license: C, MIT-ish (X/Open + ncurses license).
  • Mode: retained-ish (window/pad model with wnoutrefresh/doupdate batching).
  • 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.
  • 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.
  • 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.

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 and Components (hbox/vbox/border/text/flex), and FTXUI’s Renderer/Container model 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/flex composition 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’s useFocusManager. 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.
  • 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.

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 into Rect areas computed by a constraint-based Layout solver. Widget state (selection, scroll offset) lives in small …State structs 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 contractstateless 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-owned state and writes cells into a computed rect is a direct transliteration. (2) Ratatui’s constraint-based Layout (split a Rect by Length/Percentage/Min/Ratio constraints) 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.


LibraryLangLicenseModeVerdictWhat to borrow
termbox2C99MITimmediate (cell buffer)ADOPT (ratified, ADR-0027)— (it’s the substrate)
ncursesCMIT-ishretained-ishSKIPnothing (ADR-0027 §E)
tuiboxC99MITretained (box tree)PATTERN-ONLYUI→Screen→Box regions; dirty-bit (region-level)
AnbUICMITimmediate (dialogs)SKIPminimalist-menu idiom (glance)
FINAL CUTC++14LGPL-ishretainedSKIPnothing
FTXUIC++17MITdeclarative/reactivePATTERN-ONLYbox-composition combinators; focus traversal
imtuiC++MITimmediatePATTERN-ONLYimmediate-mode + app-owned-state discipline (paradigm anchor)
Tui WidgetsC++17retained (Qt-ish)SKIPnothing
RatatuiRustMITimmediatePATTERN-ONLYstateless-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 surfacesoftware/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.