CLI vs. TUI: Terminal Interfaces, Automation, and Interaction Contracts
Understand the difference between command-line and text user interfaces, when terminal programs combine them, and how to make automation reliable.
CLI and TUI describe different aspects of a terminal program, not two mutually exclusive kinds of executable. CLI usually means a command-line interface: commands and options are passed as arguments, with data and diagnostics exchanged through standard input, standard output, standard error, and an exit status. TUI means a text user interface that presents a navigable screen inside a terminal, usually with keyboard input, cursor positioning, screen updates, and terminal capability handling.
One program can provide both. Debian’s package-management reference describes aptitude as both a command-line tool and a full-screen interactive text interface; the same ecosystem separately recommends apt for interactive command-line use and apt-get for scripts. This is a useful counterexample to the idea that “CLI” must mean automation or that every interactive terminal application is a TUI.
Think in terms of input, output, and control flow
A command-line operation is usually easy to compose because its contract can be made explicit:
grep -n 'TODO' src/main.c >todo-report.txt
status=$?
if [ "$status" -gt 1 ]; then
printf 'grep failed with status %s\n' "$status" >&2
exit "$status"
fi
This example uses a stream-oriented interface: output is redirected and the shell observes an exit code. For grep, status 0 means a match, 1 means no match, and a value above 1 signals an error, so the script preserves the difference between “nothing to report” and an operational failure. In longer pipelines, also choose the shell’s pipeline-status policy and quote data correctly. A CLI is not automatically script-safe; it needs stable output, documented exit statuses, predictable prompts, and options that avoid human-only behavior.
A TUI instead owns a screen region or full terminal display. The user moves focus, inspects a list, expands details, and confirms an action using keys. Libraries such as curses and ncurses abstract terminal capabilities, keyboard input, and screen updates so an application can draw a logical interface without hard-coding one escape-sequence dialect for every terminal.
The distinction is about interaction shape, not whether text is involved. A shell prompt is interactive but not generally a TUI. A command such as aptitude can be invoked with arguments and can also enter a full-screen mode. A --help page does not make a tool a TUI, and a line-oriented confirmation prompt does not turn every CLI invocation into one.
TTY detection is a capability check, not a human detector
Programs sometimes need to know whether a file descriptor is connected to a terminal. POSIX isatty() checks that property; it does not prove a human is present, that keyboard input is safe to request, or that standard output is visible. A process can have terminal stdin and redirected stdout, or receive stdin from a pipe while stderr remains a terminal.
#include <stdio.h>
#include <unistd.h>
int main(void) {
if (isatty(STDIN_FILENO)) {
fprintf(stderr, "stdin is attached to a terminal\n");
} else {
fprintf(stderr, "stdin is not attached to a terminal\n");
}
return 0;
}
The message is intentionally narrow: isatty() reports attachment, not intent. For a destructive action, require an explicit flag such as --yes or --non-interactive and define the safe behavior when the flag is absent. Do not silently infer consent because a descriptor happens to be a TTY. Likewise, avoid switching machine-readable output into a progress bar solely because stdout is a terminal if callers depend on stable records.
TUI programs have terminal-state obligations
A full-screen interface must account for more than painting its first screen. It should handle terminal resize, narrow dimensions, key mappings, nonstandard terminal types, interrupted sessions, and cleanup after errors. Screen libraries can restore terminal modes and map logical drawing operations to terminal capabilities, but the application still needs clear behavior when standard input or output is redirected or when $TERM is missing or inaccurate.
Well-behaved TUI tools should leave the user’s terminal usable after normal exit and common interruptions. Test success, cancellation, invalid input, resize, and a command launched from a terminal multiplexer or remote shell. If a tool changes terminal modes and is killed abruptly, recovery may require running stty sane from a working shell; that is emergency recovery, not a substitute for the application’s cleanup path.
TUI screens are valuable for inspecting complicated state because they can show context, dependencies, and proposed changes before applying them. Debian’s aptitude, for example, exposes package browsing and planned actions in a full-screen text interface while retaining command-line operations. That visual planning surface does not make the final transaction harmless: review removals, confirm repository state, and use a simulation or test host when the change is high-impact.
Choose the interface that fits the operator and execution environment
Prefer a CLI for repeatable jobs, pipelines, remote administration, scheduled work, and CI. Provide explicit arguments for paths and identifiers, stable exit codes, machine-readable output where needed, and a documented noninteractive mode. A script should not hang forever waiting for an answer that no one can provide.
Prefer a TUI when a person needs to browse many records, compare candidate actions, or repeatedly navigate related information over a long-lived terminal session. A TUI is not inherently safer or more accessible: it can hide important details behind menus, assume keyboard conventions, or fail in a constrained terminal. Preserve a noninteractive route for automation and recovery.
When supporting both modes, make the boundary obvious:
tool list --format=json # stable output for scripts
tool install package-id # explicit command with a reviewable target
tool # optional interactive launcher, if documented
tool install package-id --yes # only when the operator deliberately opts in
Do not make automation depend on scraping a TUI, parsing localized prompts, or sending simulated keystrokes to a full-screen program. Use the documented CLI/API, a configuration file, or a dedicated library. If a TUI is the only interface, treat that as a deployment constraint and isolate the workflow behind a tested operator procedure rather than pretending it is deterministic automation.
A practical interface test matrix
For every command that may run unattended, verify:
- Terminal attached: expected prompt and progress behavior are clear.
- Input redirected: the program consumes the intended data and never waits for an unannounced answer.
- Output redirected: output stays parseable and diagnostics go to the appropriate stream.
- No color or pager: flags or environment controls produce clean logs.
- Failure: exit status distinguishes success, validation failure, and operational failure.
- TUI cancellation: leaving or cancelling the screen does not apply a transaction accidentally or corrupt terminal state.
The strongest tools make both the human workflow and the automation contract explicit. CLI and TUI are complementary presentation models; pick the one that fits the task, and verify the exact behavior of the particular command rather than inferring it from the category name.
Related:
Sources: