rtb-tui 0.6.5

Reusable TUI widgets: forms, wizards, prompts, tables. Part of the phpboyscout Rust toolkit.
Documentation
# AGENTS.md

This file provides guidance to AI coding agents (Claude Code, agy, codex, etc.) when working with code in this repository.

Ways of working are not repeated here. They live in the phpboyscout skills, and
naming a skill ages better than restating it — the restatement goes stale, the
skill does not.

## What this is

`rtb-tui` is a crate in the
[phpboyscout Rust toolkit](https://rust.phpboyscout.uk), extracted from
[rust-tool-base](https://gitlab.com/phpboyscout/rust-tool-base). That
decomposition is still in progress, so the split between the monorepo and the
extracted crates moves.

Despite the name it is not a full-screen terminal UI library: no panels, no
alternate screen, no key events, no event loop, no `ratatui` in the graph. It is
three line-oriented pieces in 323 lines of `src/` — `Wizard` (a step sequence over
`inquire`), `render_table` / `render_json` (the `--output text|json` pair) and
`Spinner` (a status line). There is no `[features]` table, so
`default-features = false` does nothing and every consumer takes all three
pieces and all eight runtime dependencies.
`docs/explanation/04-what-rtb-tui-does-not-do.md` has the rest of the absences.

At 0.6.3, extracted at 0.6.1 (2026-07-23) and unchanged in substance since.
Pre-1.0: the surface is settled but not frozen, and releases run through
colophon (`.colophon.yaml`), with cicd's `rust-release` publishing the tag. `src/lib.rs:16`
carries `#![forbid(unsafe_code)]`; `Cargo.toml:20` sets the lint to `deny` so a
test file could `#![allow]` if it needed to. Both hold here — do not assume it
of every toolkit crate, since `rust/forge` uses `#![deny(...)]` because `linkme`
needs a targeted `#[allow]`.

## What the tests actually exercise

This matters more than usual, because the development box has no display and
this is nominally a terminal UI crate. The suite is 18 integration tests across
five files in `tests/` — all headless, all offline, none driving a terminal.
There are no unit tests in `src/`, and the one doc example
(`src/wizard.rs:51`) is `rust,ignore`, so it is never compiled.

**The wizard tests never touch `inquire`.** `Wizard::run` (`src/wizard.rs:82`)
calls `WizardStep::prompt` and reads the `Result<StepOutcome, InquireError>` it
returns; everything interactive lives inside step implementations. The trait is
the seam, so every test implements `WizardStep` by hand with a
`Mutex<Vec<Result<..>>>` script it pops one outcome from per call. Under test is
the driver's index arithmetic and error mapping — advance, back, Esc-as-back,
first-step-back-as-cancel, Ctrl+C, other-error-wraps-in-`Step`. No PTY is
allocated and no golden buffer is compared. See
`docs/how-to/04-test-a-wizard-without-a-terminal.md`.

**The spinner tests assert absence of a panic, not presence of output.**
`Spinner::new` reads `console::Term::stderr().is_term()`; the harness captures
stderr, so that is false and the spinner is inert. There is no injection point
for the TTY check, so **the drawing path — `\r`, clear-line, the `⠋` frame — is
not covered at all**, and nothing asserts the no-TTY path writes nothing. Change
`src/spinner.rs` and the suite will not tell you whether it still renders
against a terminal; check by hand or not at all. That is a real limit, not a gap
to paper over.

So `just ci` — `fmt-check`, `lint`, `docs`, `test`, `audit` — is an honest
signal for the wizard driver and the render helpers, and no signal whatever for
anything needing a real terminal. Run it before raising a merge request. It
mirrors the MR gate rather than reproducing it: the pipeline assembles
`rust-lint`, `rust-test`, `rust-security` and `rust-docs` from
`phpboyscout/cicd`, and `rust-test` additionally runs cross-OS and coverage that
`just` does not. `just test` prefers `cargo nextest run`, falling back to
`cargo test`.

## Traps

- **`inquire` behaviour is assumed, not tested.** Esc mapping to
  `InquireError::OperationCanceled` is the hinge the navigation model turns on,
  and nothing here checks `inquire` still does it. A major bump is a behavioural
  change even when it compiles.
- **Going back re-runs a step against state as it stands now.** No snapshot, no
  undo, so a step that appends to state appends again on every visit. Steps must
  be idempotent and nothing enforces it.
- **Several source comments are wrong, and the repo knows.** The "Known
  documentation defects" table in `docs/reference/05-crate.md` lists six, among
  them a spec path that does not exist (`src/lib.rs:13`) and a `trybuild`
  rationale in `rust-toolchain.toml` for fixtures this crate has none of.
  Believe the code, and that table, over the comments.

## Its place in the decomposition

`rtb-tui` depends on no other toolkit crate — all eight runtime dependencies are
third-party — so it can move on its own. Three crates depend on it, verified
against `origin/main` on 2026-08-23:

| Consumer | Where | How |
|---|---|---|
| `rtb-cli` | `rust/cli`, `crates/rtb-cli/Cargo.toml:26` | required, no feature gate |
| `rtb-cli-bin` | `rust-tool-base`, `crates/rtb-cli-bin/Cargo.toml:89` | required, no feature gate |
| `rtb` (umbrella) | `rust-tool-base`, `crates/rtb/Cargo.toml:61` | optional, behind `tui`, which is in `default` |

The asymmetry is the point: the umbrella can compile the TUI out via its
feature, `rtb-cli` cannot. `rust/cli` is a four-crate workspace (`rtb-cli`,
`rtb-docs`, `rtb-mcp`, `rtb-update`), so a check reading only root manifests
misses it. All three resolve by published crates.io version rather than by path,
and all three pin `0.6.1` against this repo's `0.6.3` — a change here reaches
none of them until it is released and they are bumped.

**This crate carries its own `docs/`**, which is the Rust toolkit's pattern and
differs from the Go toolkit, where a family's documentation lives with its core.

## Which skills apply here

> **The estate has no Rust-specific skills yet**, so the table below is the
> language-agnostic set only. That is a known gap rather than an oversight, and
> the table is worth revisiting once it is filled.

| When | Skill |
|---|---|
| Starting a feature, a component, or any non-trivial change | `spec-driven-development` |
| Working in a repo other than the one you were invoked in | `cross-repo-worktree` |
| Building a feature or fixing a bug, given the seam above | `test-first-discipline` |
| Calling a change finished — code, tests and `docs/` land together | `definition-of-done` |
| Writing or restructuring this crate's `docs/` | `diataxis-docs`, `tutorial-voice` |
| Writing anything others will read and check | `checkable-claims` |
| Reporting a result, or trusting a checkout you did not just write | `verify-dont-trust` |
| Needing something from `rust/cli` or `rust-tool-base` | `raise-a-forge-issue` |
| Writing a commit message or a merge request description | `conventional-commits`, `pre-1-0-release-safety` |
| Committing, branching, merging, or opening a merge request | `forge-publish-workflow` |
| Releasing this crate and its consumers together | `release-train` |

`verify-frontend-headless` does **not** apply — it drives a browser, and nothing
here renders in one.

> Skills ship via the
> [phpboyscout marketplace]https://gitlab.com/phpboyscout/claude-code-plugins.
> An agent without them should treat a named skill as a topic to ask about.

## House rules

- Linear history. Rebase and fast-forward; never squash-merge from the UI.
- `feat`, `fix`, `perf` and `refactor` all cut a release. A change that repoints
  or removes a public interface is `feat`, not `refactor`, or it ships as a patch.
- No AI attribution in anything published, and never at-mention anyone.
- Never cut a release yourself. That is the maintainer's call, every time.