Uniview

Comparison

What uniview's TUI stack is missing next to ink, opentui, and Textual

uniview's TUI stack (@uniview/tui-core + @uniview/tui-react/@uniview/tui-solid) is new. ink has been the default React-in-terminal renderer since 2017; opentui is a newer engine with the same React-and-Solid-on-terminal shape as uniview, backed by a native (Zig) renderer and a Yoga layout; Textual is the most feature-complete TUI framework in any language, and its Python API doesn't map onto React/Solid, but its feature list is a useful checklist of what a mature TUI framework eventually grows.

This page is an honest gap list, not a sales pitch. Every claim about ink/opentui/Textual below was checked against the checked-out source in references/ink, references/opentui, and references/textual (or against uniview's own source in packages/) — not against general knowledge of these projects, which drifts across versions.

Layout & primitives

FeatureuniviewinkopentuiTextual
Layout engine✅ Custom pure-TS flexbox (computeLayout) + a pluggable LayoutEngine seam with an opt-in yogaLayoutEngine (real yoga-layout WASM, selected via the layoutEngine root option)Yoga (yoga-layout)Yoga (lib/yoga.options.ts)Custom (_layout_resolve.py, box_model.py) — DOM/CSS box model, not flexbox
Box/container<Box><box>Container/Vertical/Horizontal/Grid
TextText<Text> (wraps, truncates, transforms)<text>Static/Label
Pre-styled rich text (spans in one node)RichText⚠️ via nested <Text> composition only<span>/<b>/<i>/<u>/<a> inline modifiersContent/markup ([bold]...[/])
height:"100%" under flexGrow✅ not a bug — bringing up yogaLayoutEngine proved Yoga produces the same geometry (content-sized parent, flex-basis:auto correct); a flexBasis:0 child fills instead✅ (Yoga)✅ (Yoga)
Absolute positioningposition:"absolute" + zIndex❌ no absolute positioning✅ (Yoga position: "absolute")✅ (layer, offset)
<Static> (append-only, never-repainted log region)<Static> (React + Solid) — commits finalized lines to a scrollback channel above the live frame<Static>⚠️ RichLog/Log are closer to a scrollback widget, not a static-append region
<Spacer> (flex-grow filler)<Spacer> (React + Solid)<Spacer>❌ (same flexGrow workaround)❌ (same workaround)
<Transform> (per-line output transform, e.g. gradient text)<Transform> (React + Solid) — per-line output transform<Transform>⚠️ via custom render_line overrides
<Newline><Newline> (React + Solid)<Newline><br>N/A (markup handles it)
<Masked> (mask child text, e.g. secrets)<Masked> (React + Solid)

Focus & input

FeatureuniviewinkopentuiTextual
Focus managerFocusManager — Tab/Shift-Tab cycling, digit-jump via nextFocususeFocusManager (.focusNext(), .focusPrevious(), .focus(id))✅ (Yoga tree + KeyHandler)App.focused, focus_next()/focus_previous()
Per-node input hookuseInput((input, key) => …) global hook (React + Solid), resolved host-side via InputRouter.subscribeInput; onKeyDown prop still bubblesuseInput((input, key) => …) global hook, auto-scoped to focus/raw modeuseKeyboard() hook (React), onKeyDown eventson_key handler + Binding/BINDINGS declarative keymap
Key bubbling model✅ DOM-style: focused node → nearest ancestor onKeyDown; Enter/Space activate focused node first⚠️ no bubbling — useInput is effectively global, ordering is registration order⚠️ event-based, stopPropagation() exists (see KeyHandler.stopPropagation.test.ts) but no ancestor-chain bubbling model✅ message bubbling through the DOM-like widget tree (bubble=True on messages)
Click → focus target resolution✅ nearest focusable ancestor of the hit nodeN/A (no mouse support)✅ (mouse-aware, hit-tested against Yoga tree)
Mouse modes✅ off / click / drag / motion via TerminalDriver❌ no mouse support at all✅ click/drag/move, mock-mouse.ts test harness✅ click, MouseMove, MouseScroll
Hit-testingOwnerTable (cell → node)N/A✅ Yoga-tree hit test✅ compositor-based (_compositor.py)
Kitty keyboard protocol (disambiguated key events)kitty-keyboard.ts, opt-in flagsparse.keypress-kitty.ts⚠️ partial via _keyboard_protocol.py
Paste eventusePaste (React + Solid) — every paste reaches subscribers host-sideusePaste⚠️ raw input only, no dedicated paste hook foundPaste message
Terminal resize eventresize in TuiInputEventuseStdout().columns/rows + resize listener, useWindowSizeuseResize, useTerminalDimensionsResize message

Text/content widgets

FeatureuniviewinkopentuiTextual
Panel (titled/footered bordered box)❌ (compose from Box + Text yourself)⚠️ Box supports title-ish styling but no dedicated titled-panel component✅ (border titles/subtitles on any widget)
List (selection, full-row highlight, click-select)❌ (userland only)⚠️ Select covers single-select lists; no plain scrolling list with full-row highlight as a named componentListView/OptionList
VirtualList (windowed rendering)⚠️ ScrollBox virtualizes via viewport culling (objects-in-viewport.ts) but there's no list-specific windowed componentDataTable and ListView virtualize rows
Select (keyboard-navigable single-select)Select, plus TabSelect for tab-strip selectionSelect, RadioSet
ScrollView✅ (wheel + key scroll, scrollbar, controlled/uncontrolled) + a standalone <Scrollbar> (React + Solid) extracted for reuseScrollBox + ScrollBarScrollView base class on every scrollable widget
Markdown✅ headings/lists/quotes/tables/inline emphasis/links/fenced code+highlightMarkdown renderable + markdown-parser.tsMarkdown, MarkdownViewer (with TOC)
Code (syntax highlighting)✅ via lowlight/highlight.js, line numbers, filename→language detectionCode renderable, tree-sitter-backed (tree-sitter-styled-text.ts), LineNumberRenderableTextArea has syntax highlighting (tree-sitter)
Diff (unified diff viewer)✅ gutters, sign column, hunk headers, per-line highlightDiff renderable (Diff.ts, Diff.regression.test.ts)❌ no built-in diff widget
StreamingMarkdown (stable/tail split for cheap re-parses of growing text)❌ (no equivalent found in Markdown.ts)
Editable multi-line text areaTextarea — full editor: selection, undo/redo, highlights, visual-line wrapping (Textarea.*.test.ts)TextArea — same class of feature, tree-sitter highlighting
Single-line text input<TextInput> (React + Solid) — controlled, host-driven editing (TextInputMachine) with rendered caret, mask/placeholder, onChange/onSubmit❌ (no built-in <Input>, common to build on useInput)Input renderableInput widget, with Suggester/validators
Tables<Table> (React + Solid) — column layout, header, virtualized rows, per-row cursor, optional sort (editable cells still a gap)DataTable — sortable, editable cells, cursor modes
Tree view<Tree> / <DirectoryTree> (React + Solid) over TreeMachine — expand/collapse, indent guides (│ ├─ └─), keyboard navTree, DirectoryTree
Calendar / date picker<Calendar> (React + Solid) over a clock-free CalendarMachine — month grid, selectable date, arrows = day, PgUp/PgDn = month❌ (no core calendar widget)
Tabs<Tabs> (React + Solid) — tab strip + active panel, roving arrow nav (wraparound, disabled-skip) via TabsMachineTabSelect renderableTabbedContent/Tabs/TabPane

Charts & graphics

FeatureuniviewinkopentuiTextual
Bar chart / histogram✅ eighth-block glyphs (▁-█, 8 steps/row); grouped & stacked multi-series (BarDatum.value accepts a per-series array)❌ (no chart renderables in renderables/index.ts)❌ (Sparkline widget only)
SparklineSparkline widget
Gauge<Gauge> (centered label on bar) + <LineGauge> (ratatui-style label · bar · percent on one line)
Chart axes (ticks, numeric labels, axis titles)frameChart/axisTicks — Y gutter + X rule + tick labels + xTitle/yTitle, wired into <LineChart>/<Scatter> via options.axes
Chart legend (swatch + series name, positionable)renderLegend — horizontal/vertical, top/bottom, colored swatch, via options.legend
Line chart / scatter plot✅ braille 2×4 sub-cell canvas (SubcellCanvas, 8x character-grid resolution)
Sub-cell / braille rendering✅ (packages/tui-core/src/canvas/subcell.ts)❌ (no braille/subcell canvas found in core/src)
Public <Canvas> / shapes API (ratatui Canvas)<Canvas> (React + Solid) with a draw(cv) callback — line/rect/circle/points, braille/dot/block/half-block markers, cv.project (shared dataToPixel), optional world Map; emits styled lines (no new primitive)
2D/3D scene rendering, sprites, particles, physicspackages/core/src/3d/ — WebGL-in-terminal: sprites, textures, phong lighting, rapier/planck physics demos
Raster imageshalf-block path<Image> (React + Solid) decodes-agnostic RGBA → box-downsample → cells (fg=upper px, bg=lower px), the universal chafa-style fallback that works on any truecolor terminal, no new primitive (canvas/image.ts). ❌ graphics protocols (Kitty/Sixel/iTerm2) — would need an opaque escape-passthrough region that breaks the one-grapheme-per-cell model (deferred by design)⚠️ detects the Kitty keyboard protocol but has no image rendering path⚠️ terminal-capability-detection.ts parses Kitty graphics protocol responses (capability probing), but no image renderable was found in renderables/index.ts❌ no built-in image widget in core (third-party textual-image exists, out of scope here)

Rendering/output targets

FeatureuniviewinkopentuiTextual
Live ANSI terminal outputAnsiCellSurface, truecolor, partial-repaint diffing (diffFrames)log-update.ts, ANSI diffing via string reconciliation✅ native (Zig) buffer + diffing (buffer.ts, renderer.ts)✅ Rich-based compositor, partial repaint
Color-depth degradation (truecolor → 256 → 16)nearestNamedColor/rgbToAnsi256✅ (via chalk/ansi-styles downstream, not in ink core itself)terminal-palette.ts✅ (Rich color downsampling)
Headless/in-memory surface for testsMemoryCellSurface (.text(), per-cell .cells()/styles)renderToStringtest-renderer.ts (headless renderer)App.run_test() returns a Pilot driving an in-memory app
SVG/static export (for docs, screenshots)SvgCellSurface/renderSvg❌ (no SVG surface found in core/src)⚠️ App.export_screenshot() produces SVG
String-render for CI logs✅ (via MemoryCellSurface.text())renderToString✅ (test-renderer.ts output capture, console.ts)✅ (Pilot/App.run_test + export_text)

Testing & automation

FeatureuniviewinkopentuiTextual
Query rendered tree by role/labelsemantics.ts (SemanticNode, RoleQuery)❌ (no role/label query API found — tests assert on buffer content)✅ every widget exposes .query() (CSS-selector-like), plus explicit ARIA-ish roles are not modeled, but ids/classes are queryable
Programmatic drive without a real terminalAutomationSession + automation-runner.ts⚠️ possible via renderToString + manual stdin.write, no first-class drivermock-keys.ts, mock-mouse.ts, test-recorder.ts (can record and replay real sessions)Pilotpilot.click(), pilot.press(), pilot.hover(), async, first-class
Snapshot testing⚠️ possible via MemoryCellSurface.text() + your own snapshot tool, no built-in harness__snapshots__/ — dedicated snapshot test setup across renderables/, tests/pytest-textual-snapshot (separate package, but Textualize-maintained)
Session recording/replaytest-recorder.ts

Styling & theming

FeatureuniviewinkopentuiTextual
Theme object (named colors, semantic roles)defaultTheme/ThemeColors❌ (no theme concept — colors are per-prop)⚠️ colors are per-prop (RGBA.ts), no shared theme object foundTheme class, App.theme, built-in dark/light themes, live theme switching
Syntax highlighting themeSyntaxTheme✅ (syntax-style.ts)✅ (_text_area_theme.py)
CSS-like stylesheet (external, cascading, selectors)❌ (styling is inline props, no selector/cascade system found)✅ TCSS — DEFAULT_CSS, CSS_PATH, class/id selectors, cascading, hot-reloadable at dev time
Per-node style props (color, bold, border, etc.)✅ (both TCSS and inline)

Animation

FeatureuniviewinkopentuiTextual
Frame/tick hook for building your own animationuseAnimation(){frame, time, delta, reset} (React + Solid) over a host-side FrameClock; a Timeline class — duration, ease, loop, alternate, onUpdate/onComplete, sequenceable via .add()useAnimation({interval}){frame, time, delta, reset}Timeline class — duration, ease, loop, alternate, onUpdate/onComplete, sequenceable via .add()_animator.py + _easing.py — animate any reactive attribute (self.animate("opacity", 0.0))
Named easing functionsease.ts — linear/quad/cubic/sine/expo/bounce (In/Out/InOut) + resolveEasing❌ (bring your own math)✅ (easingFunctions referenced in Timeline.ts)✅ (_easing.py)
Declarative "animate this style prop" APIanimate("w", to, { duration, ease }) (React + Solid) — returns the current value to drop into a node's style/props; per-frame writes are local re-renders, never per-frame RPC⚠️ via Timeline targeting renderable properties, not declarative JSX propswidget.animate(attribute, value, duration=…)

Accessibility

FeatureuniviewinkopentuiTextual
Screen-reader detectionuseIsScreenReaderEnabled⚠️ no first-class screen-reader integration; Textual is a terminal renderer, same limitation as the others
Semantic role modelSemanticNode/RoleQuery — but built for automation/testing, not for actual assistive tech output⚠️ AccessibilityContext exists but is thin⚠️ has widget-level "role"-ish concepts for query purposes only

Terminal UIs generally have poor screen-reader story across the board — this is a weak category for every framework compared here, not a uniview-specific gap.

Dev ergonomics

FeatureuniviewinkopentuiTextual
React DevTools integrationconnectReactDevTools/connectSolidDevTools — dynamically imports react-devtools-core/solid-devtools behind a devtools flagdevtools.ts — connects to react-devtools-core over WebSocket when DEV=true-equivalentreconciler/devtools.ts — same pattern, dynamically imported when DEV=trueN/A (not a React renderer)
Error overlay (catch render errors, show a readable overlay instead of a crash)ErrorBoundary+ErrorOverview (React), TuiErrorBoundary+ErrorOverview (Solid, byte-identical SVG); core withTerminalRestore/installCrashGuard restore raw mode on crashErrorBoundary.tsx + ErrorOverview.tsxcomponents/error-boundary.tsx✅ Textual shows a formatted traceback screen on unhandled exceptions
Hot reload (bundler watch, not state-preserving HMR)⚠️ tsdown --watch / custom --watch flag in example build scripts, plain rebuild-and-restart⚠️ no TUI-specific HMR found; standard bundler watch⚠️ lib/singleton.ts mentions surviving "Bun hot reloads" for a specific singleton, not general component HMR⚠️ CSS_PATH stylesheets do hot-reload at dev time (textual run --dev); component code does not
CLI/dev tooling package❌ (ink itself has none; ink-testing-library is separate)✅ own scripts/, dev/ tooling per packagetextual CLI — textual run, textual console, textual colors, textual keys

What uniview does that the others don't

  • Framework-agnostic host, proven byte-identical. @uniview/tui-react and @uniview/tui-solid sit on the same @uniview/tui-core plus the internal host layer, and the SVG output for the same app built in React vs. Solid is byte-identical. ink is React-only; opentui ships separate packages/react and packages/solid bindings but they are two independent reconcilers over the same core renderables — there's no cross-framework output-identity guarantee comparable to what uniview's test suite asserts.
  • Sub-cell rendering for charts specifically. uniview's LineChart/Scatter use a braille 2×4 canvas for 8x the addressable resolution of the character grid (packages/tui-core/src/canvas/subcell.ts). Neither ink nor opentui nor Textual ship any chart primitives at all in their core — Textual's Sparkline is the closest, and it doesn't use sub-cell addressing.
  • SVG export as a first-class render target. SvgCellSurface/renderSvg turns a frame into a standalone SVG file, which is how this docs site's own screenshots (lazygit-solid.svg, charts-react.svg, 2048-solid.svg) are generated. opentui has no SVG surface; Textual's export_screenshot() is the nearest equivalent elsewhere.
  • Automation-first semantic querying built into the core package, not bolted on as a separate testing library. host-tui/src/automation.ts and semantics.ts let you query by role/label and drive the tree without a real terminal, shipped alongside the renderer rather than as an ink-testing-library-style add-on.

Prioritized gap list

Ordered by (impact on "can I build a real app") × (how cheap it is given uniview's current architecture):

  1. ✅ Done (Phase 3): useInput/useKeyboard-style hook. useInput((input, key) => …) and usePaste ship in both tui-react and tui-solid, subscribing to the host InputRouter directly via subscribeInput — a global hotkey is handled regardless of focus, resolved host-side with no per-event RPC and no workaround Box at the root.
  2. ✅ Done (Phase 4): Bundled <TextInput> component. <TextInput> (React + Solid) wires the host-owned TextInputMachine to a visible controlled field with a rendered caret, mask/placeholder, and onChange/onSubmit — the host still owns editing (no per-event RPC on a cursor move).
  3. ✅ Done (Phase 4): Bundled <Tabs> component. <Tabs> (React + Solid) renders a tab strip + active panel with roving arrow navigation through TabsMachine. A standalone <Scrollbar> and a <LineGauge> shipped in the same phase.
  4. ✅ Done (Phase 3): Error boundary + overlay. ErrorBoundary + ErrorOverview (React) and TuiErrorBoundary + ErrorOverview (Solid, on Solid's own ErrorBoundary) render a readable overlay instead of a crash — the two overlays are asserted byte-identical. Core withTerminalRestore/installCrashGuard guarantee raw mode is restored on crash.
  5. ✅ Done (Phase 9): Yoga layout adapter. yogaLayoutEngine (real yoga-layout WASM) implements the pluggable LayoutEngine seam and is selectable via the layoutEngine root option (React + Solid). Bringing it up also settled the supposed height:"100%" "bug": Yoga produces the same geometry as the custom engine (correct flex-basis:auto behavior), so it was never a bug. Yoga is a stricter flexbox — it honors explicit cross-axis sizes the custom engine stretches — so customLayoutEngine remains the default and Yoga is opt-in, keeping the zero-dependency (Worker/Deno/Bun) path WASM-free.
  6. A titled/scrolling Tree/DataTable-class widget. Textual's DataTable and Tree cover a large slice of real TUI apps (file explorers, log tables) that uniview currently has no answer for beyond composing List/VirtualList by hand. Medium-large effort — needs its own selection/scroll/column-layout model, though it can reuse VirtualList's windowing.
  7. Raster image support (Kitty graphics protocol). Confirmed absent in uniview, and only capability-detected (not rendered) in opentui; genuinely absent everywhere except third-party Textual plugins. Worth building specifically because none of the comparison frameworks have solved it well — but it's a real terminal-capability-detection
    • protocol-encoding project. Large effort — needs a new CellSurface capability (raw escape-sequence passthrough anchored to a cell region) plus per-terminal capability probing (a starting point already exists in opentui's terminal-capability-detection.ts for reference).
  8. ✅ Done (Phase 8): Animation/tween API. A framework-neutral Timeline + FrameClock + named easings (ease.ts) live in @uniview/tui-core, and useAnimation / animate ship in both tui-react and tui-solid over a host-side clock — per-frame value writes are local re-renders, never per-frame RPC. The React and Solid renders of the same animation are asserted byte-identical.
  9. CSS-like stylesheet system. Textual's TCSS (external file, selectors, cascade, hot-reload) is the most structurally different feature here — neither ink nor opentui have it either, so this is the lowest-priority item measured against the closest competitors, but it's the biggest usability win for large apps if uniview ever wants to compete with Textual specifically. Large effort — a real parser/cascade/selector engine, largely orthogonal to the existing per-prop styling model.

On this page