tuicov instruments terminal applications with the coverage runtime their language already trusts, then normalizes the evidence into cumulative line and widget reports. No tuicov runtime library is added to the application.
Think of it as an air-traffic controller for coverage: Rust, Go, Python, and TypeScript arrive in different formats; tuicov guides them into the same report.
| Language | TUI framework | Native line coverage | Live collection |
|---|---|---|---|
| π¦ Rust | ratatui | LLVM source-based coverage | Continuous profiles where LLVM supports them |
| π«§ Go | Bubble Tea | go build -cover integration coverage |
Complete counters at normal exit |
| π Python | Textual | coverage.py | SIGUSR2 snapshots where available |
| π TypeScript | Ink | Node.js V8 coverage + source maps | v8.takeCoverage() snapshots where available |
tuicov also discovers widget construction and render sites statically. A widget site is covered when its source span executes, giving line and widget coverage the same simple meaning: the code at this stable source location ran.
tuicov supports Python 3.9+ and has no mandatory Python runtime dependency outside the standard library.
uv sync --locked
uv run tuicov --versionuv creates the ignored .venv using .python-version; activation is optional.
uv run tuicov inspect ./path/to/appThat command detects the framework and prints the widget inventory. It is safe to use as an early configuration check on machines that do not have every native toolchain.
# Build the native instrumented target and persist its plan
uv run tuicov instrument ./path/to/app --types line,widget
# Run in a real terminal and collect cumulative snapshots every two seconds
uv run tuicov run ./path/to/app --types line,widget --interval 2s
# Rebuild JSON and HTML reports from persisted native artifacts
uv run tuicov report ./path/to/appUse instrument --dry-run to persist and print the complete plan without executing its
build commands.
flowchart LR
S["TUI source"] --> D["Framework detection"]
D --> A["Native adapter"]
A --> M["Manifest + widget inventory"]
M --> N["Native coverage runtime"]
N --> Z["Normalized snapshots"]
Z --> J["report.json"]
Z --> H["report.html"]
Each adapter owns detection, native build preparation, launch environment, snapshot requests, and result conversion. Everything after normalization is framework-neutral.
The ratatui adapter resolves the debug binary from Cargo metadata and honors
CARGO_TARGET_DIR. The target directory captured in the manifest is reused for later
builds and runs, preventing tuicov from instrumenting one binary and profiling another.
For unusual workspaces or renamed targets, pass the command explicitly.
The application still needs its own ecosystem:
- Rust:
llvm-profdataandllvm-cov, usually fromrustup component add llvm-tools-preview - Go: Go 1.20 or newer
- Python:
coverage.pyin the selected Python environment - Ink: a Node.js version with
NODE_V8_COVERAGE
TUIs need a real terminal. Run tuicov run interactively or place it inside a PTY
driver such as Microsoft shell-use.
Start interval and request-triggered collection together:
uv run tuicov run ./path/to/app \
--interval 2s \
--on-request \
--listen 127.0.0.1:0tuicov prints the chosen endpoint and writes it to .tuicov/service.json. While the
application is running:
curl -X POST http://127.0.0.1:PORT/collect
curl http://127.0.0.1:PORT/health
curl http://127.0.0.1:PORT/reportThe service accepts loopback addresses only. Snapshots are cumulative: later snapshots include everything observed earlier in the same work directory.
Native runtimes do not all flush on demand, and tuicov does not invent hits when they cannot. It keeps the interval with an availability note and records the complete data when the application exits normally.
| Adapter | During execution | At normal exit |
|---|---|---|
| ratatui | Continuous LLVM snapshots on supported platforms such as Darwin | Complete |
| Bubble Tea | Interval retained with an availability note | Complete |
| Textual | Generated launcher saves coverage.py counters on SIGUSR2 where available |
Complete |
| Ink | Generated preload calls v8.takeCoverage() on SIGUSR2 where available |
Complete |
Put --command last so every remaining argument belongs to the application:
uv run tuicov run ./path/to/app \
--interval 1s \
--command node dist/cli.js --demoFor exact argument transport from another program, use JSON:
uv run tuicov run ./path/to/app \
--command-json '["node","dist/cli.js","--demo"]'A replacement command must still activate the language's native coverage runtime. The built-in adapter commands do this automatically; custom commands are intentionally your responsibility.
Generated state stays under the selected work directory, .tuicov/ by default:
.tuicov/
βββ manifest.json detected framework, immutable run plan, widget inventory
βββ service.json active loopback endpoint, when enabled
βββ raw/ native profiles and counters
βββ generated/ launch hooks and converted profiles
βββ snapshots/ normalized cumulative snapshots
βββ report.json machine-readable overall + per-snapshot coverage
βββ report.html standalone human-readable report
The report includes:
- overall and per-file line totals, missing lines, and hit counts;
- overall widget totals plus covered and missing stable widget IDs;
- the same details for every interval, request, and exit snapshot;
- timestamps, trigger names, completeness, and native-runtime notes.
The extension boundary is deliberately small: implement framework detection and return a native run plan; tuicov handles persistence, collection, normalization, and reporting.
- Read
docs/architecture.mdfor the data model and lifecycle. - Read
docs/adding-an-adapter.mdfor the adapter contract. - Read
AGENTS.mdfor repository-specific contributor rules.
uv sync --locked
uv run python -m unittest discover -s tests -v
uv lock --checkWhen dependency metadata changes, use uv add --dev PACKAGE and commit both
pyproject.toml and uv.lock.
Self-terminating fixtures for all four frameworks live under tests/fixtures/e2e/.
The harness launches each one in a shell-use headless PTY, verifies the ordered
booting β loading β rendering screen flow and process exit, then checks interval,
line, and widget reports.
uv run python -m unittest tests.test_e2e_frameworks -vRun the Textual case with its isolated requirements layered over the locked project:
uv run --locked \
--with-requirements tests/fixtures/e2e/textual/requirements.txt \
python -m unittest \
tests.test_e2e_frameworks.FrameworkEndToEndTests.test_textual_application -vMissing native toolchains produce actionable skips. GitHub's
Framework end-to-end tests
workflow installs and runs the complete matrix.
tuicov is available under the MIT License.
If you use tuicov in your research, please cite:
@misc{peng2026llmstestterminaluser,
title = {Can LLMs Test Terminal User Interfaces?},
author = {Chao Peng and Ruida Hu and Ajitha Rajan and TegawendΓ© F BissyandΓ© and Jacques Klein and Cuiyun Gao},
year = {2026},
eprint = {2608.03743},
archivePrefix = {arXiv},
primaryClass = {cs.SE},
url = {https://arxiv.org/abs/2608.03743},
}
