TUIs vs. CLIs
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
This page was written against Ratatui 0.30.2, Textual 8.2.8, Bubble Tea v2, Ink 7.1.1, SwiftTUI 0.1.0, Spectre.Console 0.57.2 and the docsctl running example of this
sub-section, as checked on 2026-09-30. It contains no new code beyond short sketches; the language-specific details are on the framework pages linked below. The fictional docsctl
exists only on these pages, and its behaviour here is illustrative.
A command-line interface (CLI) reads arguments, does its work and exits, writing results to stdout. A terminal user interface (TUI) takes over the terminal, draws a screen of widgets, and reacts to keys until the person quits. Both run in a terminal, and they serve different audiences. This page is about how they fit together in one tool.
When a TUI helps people
A TUI is worth building when a person needs to explore rather than ask:
-
Browsing and choosing. Scrolling a list of search results, previewing a page and picking one is faster with arrow keys and a preview pane than with repeated
docsctl searchanddocsctl showcalls. -
Watching something change. Dashboards, log tails, build monitors and progress across many items benefit from a live-updating screen.
-
Reviewing before a risky action. Showing the plan of a
publishand letting a person move through the changes can be safer than a wall of text. -
Learning a tool. Discoverable menus and inline help lower the barrier for people who do not know the command names.
Whatever the framework, the point of the interface is a person watching and pressing keys, which is exactly what an agent’s tool call does not have.
Why agents still need a non-interactive mode
An agent drives a program through a tool call: it sends a command, waits for the process to finish, and reads stdout, stderr and the exit code. A TUI is incompatible with that on several counts:
-
It never finishes by itself. The event loop waits for a key. An agent’s call hangs until a timeout kills it.
-
It writes screen-control sequences, not data. A TUI draws with cursor movement and, usually, the alternate screen; captured output is noise that costs tokens and cannot be parsed.
-
It needs a terminal. TUI libraries switch the terminal into raw mode and query its size. Without a TTY they fail, misbehave or, as the Ink documentation notes, behave differently in CI-like contexts. Giving the agent a pseudo-terminal to make it work moves the problem rather than solving it.
-
It is not composable. A pipe,
--jsonandjqcannot reach data trapped inside a screen. -
It hides state and errors. Exit codes, error envelopes and dry-run plans, described on Streams & Exit Codes and Machine-Readable Output, have no place to live in a full-screen view.
The rule that follows: every operation a TUI offers must also be reachable as an ordinary command with structured output, and the TUI must never be the default when stdin or stdout is not a terminal. The prompting and detection rules on TTY, Colour & Interactivity apply here in full; a TUI is simply the most demanding kind of prompt.
Structuring a tool so the TUI is a view
The design that keeps both audiences happy has three layers:
-
A core library that performs the operations and returns typed results (search, show, plan, publish).
-
The CLI — commands and flags that call the core and print results as text or JSON, following the
docsctlcontract. -
An optional TUI — a separate subcommand such as
docsctl browsethat calls the same core and renders results interactively.
import sys
import typer
def browse() -> None:
"""Interactive browser; needs a terminal."""
if not (sys.stdin.isatty() and sys.stdout.isatty()):
typer.echo("browse needs a terminal; use `docsctl search --json`", err=True)
raise typer.Exit(2)
... # start the TUI here, calling the same core as `search`
Two guardrails make this safe. First, refuse to start the TUI without a terminal: check that stdin and stdout are TTYs, and otherwise exit with a usage error (2) that points at the equivalent command, for example
docsctl browse → "use docsctl search --json`". Second, keep destructive actions behind the same confirmation and `--dry-run rules as the CLI, so the TUI is not a route around them.
The docsctl schema --json command tree should list browse with a flag saying it is interactive-only, so an agent knows not to call it (see Self-Describing Help).
Comparing the libraries
The six libraries named in this sub-section take different approaches. The table compares only what their documentation states.
| Library | Language | Programming model | Notable points | Framework page |
|---|---|---|---|---|
Ratatui |
Rust |
Immediate-mode rendering: widgets are drawn into layout areas each frame; the crate root offers |
Charts, tables, gauges, progress bars and lists; constraint-based layout. |
|
Textual |
Python |
An |
Runs in a terminal or, through textual-serve, in a browser; a "pilot" for automated tests. |
|
Bubble Tea |
Go |
The Elm architecture: a model with |
Options such as |
|
Ink |
JavaScript and TypeScript |
React components ( |
Hooks such as |
|
SwiftTUI |
Swift |
SwiftUI-style |
macOS and Linux; latest tag 0.1.0 and last commit in 2024, so verify it builds before you depend on it. |
|
Spectre.Console |
C# |
Renderable widgets (tables, trees, panels, progress, status, prompts) written through |
Exposes capability information such as |
A practical reading of the table:
-
Ratatui, Bubble Tea, Ink and Textual are for full-screen applications; Spectre.Console is closer to Rich in Python (see Python CLI Frameworks), giving nicely formatted output and prompts without owning the screen.
-
Choose by language and team, not by feature list: all of them can draw a list, a table and a preview pane.
-
Testing differs. Textual ships a pilot, Bubble Tea lets you replace input and output, and the others need a terminal emulator or a thin abstraction over the view logic.
-
Java has no framework in this comparison; JLine provides terminal input and line editing, as described on Java CLI Frameworks.
A checklist for tools with a TUI
-
Every TUI action has an equivalent command with
--json. -
The TUI is its own subcommand, and never the default when there is no TTY.
-
Without a terminal, it exits immediately with exit code 2 and a message naming the non-interactive alternative.
-
It restores the terminal (leaves raw mode and the alternate screen) on exit, on
Ctrl-Cand on errors. -
Destructive actions use the same confirmation and dry-run flow as the CLI.
-
The command tree from
schema --jsonmarks the TUI as interactive-only.