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 search and docsctl show calls.

  • 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 publish and 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, --json and jq cannot 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:

  1. A core library that performs the operations and returns typed results (search, show, plan, publish).

  2. The CLI — commands and flags that call the core and print results as text or JSON, following the docsctl contract.

  3. An optional TUI — a separate subcommand such as docsctl browse that calls the same core and renders results interactively.

Refusing to start the TUI without a terminal (Python, Typer)
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.

Table 1. TUI and rich-output libraries compared
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 ratatui::run, which sets up and restores the terminal.

Charts, tables, gauges, progress bars and lists; constraint-based layout.

Go & Rust

Textual

Python

An App class, widgets and CSS-style styling with events and screens.

Runs in a terminal or, through textual-serve, in a browser; a "pilot" for automated tests.

Python

Bubble Tea

Go

The Elm architecture: a model with Init, Update and View, run by tea.NewProgram(…​).

Options such as tea.WithInput, tea.WithOutput and tea.WithoutRenderer() for non-TTY runs.

Go & Rust

Ink

JavaScript and TypeScript

React components (<Text>, <Box>) rendered to the terminal with flexbox layout.

Hooks such as useInput and useStdin; ESM-only; behaves differently outside a TTY.

Node.js

SwiftTUI

Swift

SwiftUI-style View types with @State and Application(rootView:).start().

macOS and Linux; latest tag 0.1.0 and last commit in 2024, so verify it builds before you depend on it.

Swift & .NET

Spectre.Console

C#

Renderable widgets (tables, trees, panels, progress, status, prompts) written through AnsiConsole.

Exposes capability information such as Profile.Capabilities.Interactive; a rendering library rather than a full-screen framework.

Swift & .NET

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-C and on errors.

  • Destructive actions use the same confirmation and dry-run flow as the CLI.

  • The command tree from schema --json marks the TUI as interactive-only.