How the TUI thinks

The TUI keymap tells you what each key does. This page explains the two ideas that make the rest of it make sense: the modal interaction model and the token-counting pipeline that feeds weight sorting and size filtering.

A modal, vim-style surface

gnaw borrows vim's central idea: the same keys mean different things depending on what you're doing. There are three modes.

ModePurpose
NormalNavigate and act. Single keys move the cursor, toggle selection, switch tabs.
InsertEnter text โ€” a search query, a template variable value, the template body.
CommandThe : line, for actions that need an argument or are deliberately deliberate (like quitting).

The design choice worth knowing: only the command line is stored as state. The current mode isn't a variable the code sets and clears โ€” it's derived from context. If a text field is focused, you're effectively in Insert; if the : line is open, you're in Command; otherwise you're in Normal. Deriving mode from existing state rather than tracking a separate copy means the two can never drift out of sync, which is a recurring class of modal-UI bug.

The token-counting pipeline

Every number you see in the file tree โ€” per-file counts, directory weights, the denominator behind selection percentages โ€” comes from one background pipeline. Understanding it explains several behaviours that otherwise look like quirks.

Counting is per-file, lazy, and cached

Each selected file moves through a small state machine:

StateMeaning
PendingSelected and queued, not yet counted
CountingHanded to a background task
Done(n)Counted: n tokens
FailedBinary, empty, or unreadable

The state map does double duty: it's both the work queue (Pending entries are what get drained) and the cache (Done entries are kept). Deselecting a file doesn't immediately evict its count โ€” the entry lingers, so re-selecting the same file is instant rather than triggering a fresh count. Totals simply ignore entries for files that aren't currently selected.

Counting is debounced to quiescence

Counts don't fire on every keystroke or every toggle. After your last selection change there's a short quiet window (~200 ms); only then does a batch of Pending files get counted. Rapidly selecting twenty files schedules one count batch once you stop, not twenty cascading re-counts.

This is why a freshly selected file can briefly show no count, and why โ€” if a :size filter is active โ€” it can momentarily disappear: a file with no Done count can't satisfy a size comparison, so it's hidden until its count lands a fraction of a second later. It's the debounce settling, not a bug.

Weights aggregate bottom-up

Directories show the sum of the Done counts of the selected leaves beneath them. This aggregate is recomputed bottom-up whenever counting reaches quiescence: each file contributes its count (or zero if unselected), each directory sums its children. The same aggregate is the sort key for :sort tokens โ€” which is why weight sorting only becomes meaningful after counts have landed. Before then, everything weighs zero and the tree falls back to a stable path order rather than jumping around.

Determinism

Counting runs concurrently, but the results are order-independent: the aggregate for a directory is a sum, and ties in weight sorting fall back to the stable path ordering. Given the same repository state and the same selection, the tree settles into the same layout every time โ€” no matter what order the background counts happened to finish in.