The CLI Landscape & Choosing a Stack

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

Before writing the first line of a command-line tool, two decisions shape everything that follows: what kind of terminal program it is, and which language and runtime it is built on. This page makes both decisions explicit. It describes the five kinds of terminal program, compares native and managed runtimes on the properties that matter for a tool people run from a shell, gives a decision matrix per audience (developers, system administrators and end users on Windows), a decision flow, and a map from every option to the page of this site that documents it.

The page was written on 25 October 2026 against these versions: Rust with Ratatui 0.30.2 and clap 4.6.7, Go 1.27.3 with Bubble Tea v2.1.0 and Cobra v1.10.2, Swift 6.4.1 with swift-argument-parser 1.8.2, .NET 10 (10.0.13) with System.CommandLine 2.0 and Terminal.Gui v2.5.0, Python 3.15.1 with Textual 8.2.8, Node.js 24 with Ink 8.0.0, JLine 4.4.8 and ncurses 6.6. The platforms are Windows 10 and 11, macOS and Linux. Section-wide versions are collected in the section landing page.

Every example in this section uses the Bookshelf scenario: a shelf tool with the subcommands shelf list, shelf add, shelf loan and shelf return, an interactive shelf repl, a full-screen shelf tui, and a background shelfd daemon that shelf can talk to.

Five kinds of terminal program

The word "CLI" covers programs that differ a lot in what they demand from the terminal. The kind of program you build, more than the language, decides which libraries you need and which platform problems you will meet.

Kind Bookshelf example What it needs from the terminal Typical building blocks

One-shot command

shelf list --json

Arguments, standard streams, an exit status. Only checks whether stdout is a terminal to decide on colour.

An argument parser, nothing else.

Command suite with subcommands

shelf add, shelf loan, shelf return

The same as a one-shot command, plus help, usage errors and shell completion for a tree of commands.

An argument parser with subcommands (clap, Cobra, swift-argument-parser, System.CommandLine, picocli).

REPL or interactive shell

shelf repl

Line editing, history, completion and prompts. Reads from a terminal in a mode where keys are delivered as they are typed.

A line editor (GNU Readline, rustyline, prompt_toolkit, JLine) or a prompt library.

Full-screen TUI

shelf tui

Raw mode, the alternate screen, cursor addressing, mouse and resize events, guaranteed restoration of the terminal on exit.

A TUI library (Ratatui, Bubble Tea, Textual, Terminal.Gui, ncurses, FTXUI, Ink).

Client of a daemon

shelf talking to shelfd over a local socket

A thin command that connects to a long-running process, prints the answer and exits; the daemon has its own lifecycle.

The one-shot building blocks, plus local IPC and a service definition.

The kinds are nested rather than exclusive. A tool such as shelf is usually a command suite whose repl and tui subcommands switch to a more demanding mode, and whose commands can run against shelfd. What changes between the kinds is how much of the terminal you take over:

The terminal mechanics underneath all five kinds (the TTY, the pseudo-terminal and the line discipline) are explained in Terminals, TTYs & Pseudo-Terminals.

The landscape at a glance

The figure below lays out the languages covered by this section against the three kinds of program that need a library. The first column also serves a daemon client. A green row is a language that compiles to a native executable; an amber row needs a managed runtime or interpreter on the machine unless the program is packaged with it.

Matrix of ten language runtimes against one-shot, REPL and full-screen TUI libraries

The libraries in the figure are the ones this section documents. Other libraries exist for every language; the selection favours those with current official documentation. Two rows deserve a comment before the comparison:

  • Swift has a strong argument-parser story and the weakest full-screen TUI story of the native languages: the TUI library listed is dormant (no activity since 2024), so a Swift TUI normally means calling ncurses through C interop or writing escape sequences directly.

  • Python, Node.js and the JVM have the richest high-level libraries, because a managed runtime makes it cheap to build large frameworks. Their cost is paid at distribution time, not at coding time.

Native versus managed runtimes

A native program is compiled ahead of time to machine code and runs as its own executable: C, C++, Rust, Go, Swift, Objective-C, .NET with Native AOT and Java through GraalVM Native Image. A managed program runs on a runtime that must be present: the JVM, the .NET runtime for framework-dependent applications, CPython and Node.js. The boundary is blurry in practice, because managed languages can be packaged with their runtime (PyInstaller and Nuitka for Python, Node.js single executable applications, dotnet publish with a self-contained deployment, jlink and jpackage for Java), and native languages can still depend on shared libraries.

Comparison on the properties that matter for a CLI

The official documentation of these languages does not publish comparable measurements of startup time or binary size, because the numbers depend on the program, the platform, the linking mode and the machine. The table therefore states direction and mechanism rather than figures, and a later section shows how to measure the real cost for your own tool.

Runtime Startup and size Cross-compilation Windows support TUI library maturity

C and C++

No runtime to start. Executables are small unless statically linked against large libraries.

Needs a cross toolchain (a clang or Zig based one, or a native build per OS). No built-in target switch.

Native on Windows with MSVC. POSIX-only libraries such as ncurses need a replacement there (PDCursesMod) or a different library (FTXUI).

ncurses 6.6 is mature and the reference for terminal capability handling. FTXUI v7.0.3 is active. PDCurses 3.9 is dormant, PDCursesMod v4.5.4 is active.

Rust

No runtime or garbage collector. Executables are self-contained apart from the system C library and grow with dependencies.

The compiler can target other platforms once the target’s standard library is installed with rustup, but a linker for the target is still needed. cross and cargo-zigbuild automate that.

First-class. crossterm supports Windows; termion does not.

Ratatui 0.30.2 is the standard, with a large ecosystem. It has no 1.0 release, so each 0.x release can break the API.

Go

No separate runtime to install; the runtime is linked into the executable, which is therefore larger than a comparable C program.

Built in: set GOOS and GOARCH. Stays simple while CGO_ENABLED=0; cgo reintroduces a C toolchain.

First-class for plain programs.

Bubble Tea v2.1.0 (v2 is generally available since 2026-02-24) with Bubbles, Lip Gloss and Huh. tview v0.42.0 and tcell v3.6.0 are the alternative.

Swift

Native executables. On Linux the Swift runtime libraries are needed unless the Static Linux SDK is used.

Through Swift SDKs. The Static Linux SDK produces statically linked Linux executables (musl) from another host.

A Swift toolchain for Windows is available from swift.org.

The weakest of the native options: SwiftTUI is dormant.

Objective-C

Native, very small startup cost. Foundation is part of the OS on Apple platforms.

Apple platforms natively, other platforms through GNUstep.

Only through GNUstep.

Uses the C libraries (ncurses).

.NET with Native AOT

Native executable that starts quickly and needs no runtime. Trimming limits reflection.

Per runtime identifier, and the target operating system must be the build operating system. There is no cross-OS Native AOT.

First-class, Windows being the home platform of the framework.

Terminal.Gui v2.5.0 (v2 is generally available since 2026-04-28) and Spectre.Console 0.57.2.

GraalVM Native Image (Java, Kotlin)

Native executable with fast startup, built with a long build step and configuration for reflection.

Build on each target operating system.

Supported.

Same libraries as the JVM (JLine, Lanterna, TamboUI), subject to native-image compatibility.

JVM and Kotlin (managed)

Needs a JVM; the JVM adds startup cost that matters for a command run many times.

Bytecode is portable. Per-OS images with jlink and jpackage.

Supported; JLine 4.4.8 needs Java 11.

JLine, Lanterna 3.1.5, TamboUI 0.6.0 and Mosaic 0.18.0 for Kotlin.

Python (managed)

Needs an interpreter; start-up includes importing the dependencies. PyInstaller 6.22.3 and Nuitka 4.3.1 bundle it.

Source is portable. Bundlers must run on each target OS.

Supported. The standard curses module is Unix only; windows-curses 2.4.2 fills the gap.

Textual 8.2.8 and Rich 15.0.0 are mature and popular. prompt_toolkit 3.0.53 for line editing.

Node.js (managed)

Needs Node.js; start-up includes module loading. Single executable applications embed the runtime.

Source is portable. A single executable must be built per OS.

Supported.

Ink 8.0.0 (React 19.3 or later, Node.js 22 or later). blessed and neo-blessed are abandoned.

Three observations follow from the table.

  1. Cross-compilation is where the languages differ most. Go and, to a good extent, Rust let one machine produce executables for every OS you ship to. Native AOT and GraalVM need one build machine per target operating system, which in practice means a CI matrix over ubuntu-latest, macos-latest and windows-latest. The recipes are in Native Binaries & Cross-Compilation.

  2. The TUI library decides the language, not the other way round. If the product is a full-screen tool, compare the libraries first. Ratatui, Bubble Tea, Textual and Terminal.Gui are all actively maintained, and each ties you to its language. For a command suite, nearly any language is adequate.

  3. A managed runtime moves cost to the user. The tool is easy to write and the end user must install a runtime, or you must bundle one, which turns the executable into a large file and moves the problem to packaging (Native Packages & Installers).

Measuring the cost for your own tool

Because no official source gives general numbers, measure. hyperfine runs a command repeatedly after a warm-up and reports mean and spread, and -N runs the command without an intermediate shell so that short commands are not dominated by shell start-up.

# Compare the start-up of the same `shelf --version` built in three stacks.
hyperfine --warmup 5 -N \
  './target/release/shelf --version' \
  './shelf-go --version' \
  'python3 shelf.py --version'

# Binary size on disk (macOS and Linux).
ls -l ./target/release/shelf ./shelf-go

The commands above were not run while this page was written, and the file names are placeholders for the binaries you built. Measure on the platforms you ship to, with a warm file cache and a cold one, because the first run after a download is what a new user experiences.

Cross-compiling the same tool from one machine

The following commands build the Bookshelf shelf executable for other targets from a single development machine. They show how different the mechanisms are; they are a sketch of each toolchain’s documented switch, not a complete release pipeline.

# Go: the target is two environment variables. Disable cgo to avoid needing a C toolchain.
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -o dist/shelf.exe ./cmd/shelf

# Rust: install the target's standard library, then build for it.
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl

# Swift: with the Static Linux SDK installed, build a static Linux executable.
swift build -c release --swift-sdk x86_64-swift-linux-musl

# .NET: a self-contained executable for another RID. Native AOT must be published on the target OS.
dotnet publish -c Release -r linux-x64 --self-contained

None of these commands was run while this page was written: the toolchains for Rust, Go and .NET were not installed on the machine used, and the Swift Static Linux SDK was not installed. Linking for the musl target in Rust, for example, may additionally require a musl-capable linker on the host; the rustup book and the Rust platform support page describe which targets need one.

Decision matrix per audience

The right stack depends on who installs and runs the tool. Three audiences cover most cases. The matrix lists what each cares about and which stacks fit. It is guidance derived from the properties above, not an official recommendation.

Audience What they care about Stacks that fit well Cautions

Developers

Installing with the toolchain they already have, fast iteration, shell completion, readable errors.

Rust (cargo install), Go (go install), a .NET global tool (dotnet tool), Python (pipx or uv tool), Node.js (an npm package with a bin entry), Swift on macOS (SwiftPM).

A tool that needs a runtime the developer lacks. Version drift between the runtime and the tool. Do not assume Windows developers have a C toolchain.

System administrators

One file that runs on servers they do not control, over SSH, in minimal containers and from cron or systemd units, with no runtime to install and predictable behaviour in a minimal environment.

Go (static with CGO_ENABLED=0), Rust (musl target), C, Swift with the Static Linux SDK, .NET Native AOT or self-contained for Linux.

Dynamic linking against a newer glibc than the server has. Interactive prompts in a script. Full-screen tools over slow or unusual terminals: check TERM and terminfo. Managed runtimes that are not installed.

End users on Windows

A double-click or winget install experience, no runtime or compiler to install, a signed executable that SmartScreen trusts, behaviour that is correct in Windows Terminal, cmd.exe and PowerShell.

.NET (Native AOT or self-contained, packaged as MSI or MSIX, see Windows Apps — .NET Publishing Options), Rust, Go, C and C++ with the MSVC runtime linked statically.

Unsigned executables. Libraries that assume a POSIX terminal. Argument parsing and code page differences described in Windows Console & Processes. A TUI library whose Windows backend is missing or lags.

For end users on macOS the equivalent concerns are Apple silicon and Intel in one download (a universal binary), Gatekeeper and notarization, and a Homebrew formula. They are covered in Signing, Notarization & OS Trust and Native Packages & Installers.

Differences by operating system that affect the choice

Concern Windows macOS Linux

Terminal

Windows Terminal and the console host. Microsoft recommends virtual-terminal sequences for new development.

Terminal.app and third-party terminals; zsh is the default shell.

Many emulators and shells; TERM and terminfo vary.

Portability trap

POSIX-only libraries and APIs (the curses module in Python, termion, ncurses). Console code pages.

Bare executables cannot be stapled after notarization, so they ship inside a package.

glibc versus musl: the system C library decides where a dynamically linked executable runs.

The Windows console direction is documented in the console ecosystem roadmap. The macOS notarization limit is covered in the section on signing.

A decision flow

Start with the kind of program, then narrow by audience. The flow is a starting point, not a verdict: team skills and existing code usually outweigh everything else.

flowchart TD A["Which kind of program is shelf?"] --> B{"Full-screen TUI needed?"} B -- "No: commands, REPL, daemon client" --> C{"Users have your runtime already?"} B -- "Yes" --> D{"Which language does the team use?"} C -- "Yes: developer tool" --> E["Use the language toolchain: Rust, Go, .NET tool, Python, Node.js"] C -- "No: admins or end users" --> F{"Single static file wanted?"} F -- "Yes, Linux and macOS" --> G["Go, Rust, C, or Swift with the Static Linux SDK"] F -- "Yes, Windows" --> H["Go, Rust, C, or .NET Native AOT or self-contained"] F -- "No, a runtime is acceptable" --> I["Bundle it: PyInstaller, Node.js SEA, jlink or GraalVM"] D -- "Rust" --> J["Ratatui"] D -- "Go" --> K["Bubble Tea"] D -- "Python" --> L["Textual"] D -- ".NET" --> M["Terminal.Gui"] D -- "C or C++" --> N["ncurses or FTXUI"] D -- "No preference" --> O["Pick by distribution target above, then the matching TUI library"]

The flow assumes you control the build. When you must add a command to an existing codebase, follow that codebase’s language; the extra cost of a second language runtime is nearly always larger than any difference in the table.

Pitfalls

  • Choosing on a hello-world benchmark. The start-up of an empty program says little about your tool once it parses arguments, reads configuration and loads dependencies. Measure the real tool.

  • Picking an unmaintained library. Several libraries in this ecosystem are dormant or superseded: SwiftTUI, tui-textarea (replaced by the ratatui-textarea fork), blessed and neo-blessed, PDCurses 3.9 (use PDCursesMod), and kotlinx-cli, whose README marks it obsolete. Check release activity before adopting.

  • Building on a pre-1.0 library without a pin. Ratatui has no 1.0 release and no published 1.0 roadmap, so pin the exact version and read the breaking-changes notes before upgrading.

  • Assuming POSIX on Windows. termion has no Windows support, and Python’s standard curses module is Unix only. Check each library’s Windows story before you commit.

  • Forgetting that cgo and Native AOT change the build. Both bring back a platform-specific toolchain and remove the one-machine cross-compile story.

  • Dynamic linking against a too-new glibc. An executable built on a recent distribution can fail on an older one. Static musl builds avoid it at the cost of other trade-offs, which the native binaries page explains.

  • Piping an installer to a shell. A curl | sh install script for your tool trains users into a risky habit. Prefer a package channel and signed artifacts; see Native Packages & Installers.

Where each option is documented

The middle column lists the pages of this section; the right column lists pages that already existed on this site and are linked rather than repeated.

Option In this section Elsewhere on this site

C

Command-Line Programs in C, C & C++ TUIs: ncurses, PDCurses & FTXUI

C — Program Structure

C++

Command-Line Programs in C++, C & C++ TUIs

C++ Reference

Rust

Command-Line Programs in Rust, Ratatui: Getting Started, Layout, Widgets & Styling, Events, Async & Architecture, Testing & Ecosystem

Rust — I/O, Files, Processes & Environment, Frameworks: Go & Rust

Go

Command-Line Programs in Go, Go TUIs with Bubble Tea

Go Reference, The Go Command, Builds & Cross-Compilation, Frameworks: Go & Rust

Swift

Command-Line Tools in Swift

Swift Package Manager, Frameworks: Swift & .NET, macOS Distribution Outside the App Store

Objective-C

Command-Line Tools in Objective-C

Objective-C Reference

.NET and C#

.NET Console Applications, .NET TUIs with Spectre.Console & Terminal.Gui

C# Build & Tooling, Windows Apps — .NET Publishing Options, Frameworks: Swift & .NET

JVM and Kotlin

Command-Line Programs on the JVM & Kotlin, Other TUI Libraries

Java Build & Tooling, Frameworks: Java, Quarkus Command Mode & CLI Applications

Python

Python TUIs with Textual, Rich & prompt_toolkit

Python Standard Library Tour, Frameworks: Python

Node.js

Other TUI Libraries (Ink and prompt libraries)

Building CLI Tools, Single Executable Applications, Frameworks: Node.js

The cross-cutting pages are Cross-Platform Portability for the differences between Windows, macOS and Linux, Native Binaries & Cross-Compilation for the build recipes, and Testing Terminal Apps & TUIs for verification across the matrix.