Skip to content

The terminal UI

Running way with no flags opens the terminal UI: every task in the repository, in one searchable list, with the one thing a browser cannot do — pressing Enter runs it, in this terminal.

It is dependency-free: raw mode, ANSI escapes and one frame string per keystroke, no TUI toolkit. Collection is asynchronous, so a refresh runs in the background while you keep typing; the details of that are in Refresh below.

Type to filter. Search is fuzzy by default: terms are whitespace-separated and ANDed, so photo dev narrows the list to a photo app’s dev server — every term has to match somewhere on the row. A leading ' on a term forces that term to match as an exact substring instead of a fuzzy subsequence. Matching is case-insensitive throughout.

Ctrl+E switches the whole query to exact mode, where every term must match as a substring rather than a fuzzy subsequence. The current mode is shown in the top-right corner of the screen, next to the tab row.

| Key | Action | | ----------------------- | ---------------------------------------------------------------------------------------------------- | | type to filter | narrow the list by fuzzy (or exact) match | | Enter | run the selected task, handing it this terminal — or, for a task with parameters, ask for them first | | Ctrl+Y | copy the selected task’s command | | Tab / Shift+Tab | next / previous runner tab | | Ctrl+E | toggle exact search instead of fuzzy | | Ctrl+T | show only services — tasks that run until you stop them | | Ctrl+S | trust this repository, after a y/n confirmation | | Ctrl+R | refresh — re-probe the repository | | , | this project’s configuration, read-only | | ↑ / ↓ | move the selection by one row | | Page Up / Page Down | move the selection by a screenful | | Home / End | jump to the first / last row | | Backspace | delete the last character of the query | | Ctrl+U | clear the query | | Esc / Ctrl+C | quit |

Ctrl+S is offered only when runners were actually withheld for want of trust; press it when nothing is withheld and it says so and does nothing. It arms a question rather than acting — the status line asks trust this repository? y/n, and only y goes through, because Ctrl+S is flow control on many terminals and arrives unintentionally. Answering y writes a real, persistent grant to the trust store, exactly as the trust command does, then re-probes in place so the runners appear where the note was. See Trust & execution.

The primary actions — run, copy, tab, exact, services, trust, refresh, quit — are always shown in the footer line at the bottom of the screen. Movement (arrows, Page Up/Down, Home/End) and line editing (Backspace, Ctrl+U) aren’t listed there, but both work as described above.

Tab and Shift+Tab cycle through a tab per runner plus a combined “all” view, each labelled with how many tasks it holds. Switching tabs narrows the list to that runner without touching the query.

Ctrl+T toggles a filter for services — tasks marked persistent, meaning they run until you stop them rather than finishing on their own (a dev server, for example). Toggling it on hides everything else; toggling it off restores the full list. A * next to a row in the list marks it as a service. Ctrl+T is the one keystroke worth remembering here: press it, then type the app’s name.

The command Enter would execute is always shown on screen, above the key line, along with any parameters it takes — so running a task is never a surprise. Tasks are spawned from an argument vector, never through a shell, so a recipe with <placeholders> cannot be mangled into something else on the way. Ctrl+Y copies that same command to the clipboard instead of running it.

A recipe like just deploy <env> [tag] cannot be run as it stands: its argument vector holds just deploy and nothing else, because the placeholders are not values. Enter on such a task therefore collects them first. The key line becomes a command line with the command already on it and the cursor after it:

$ just deploy ▮
takes <env> [tag] enter run esc back

Type the arguments, Enter runs the whole line, Esc goes back to the list with nothing run. Backspace deletes a character and Ctrl+U clears what you have typed.

Only the tail is yours to type — the command itself is fixed. Quoting works as a shell’s word splitting does, so "two words" is one argument, but nothing else a shell does happens: no globbing, no variables, no operators. *.log reaches the command as those five characters.

A task that takes no parameters is unaffected: Enter runs it, with no line to dismiss.

, replaces the list with this project’s configuration as waypost resolved it — runners, execution, discovery, excluded paths, in the config schema’s own key order, each section naming which layer supplied its value. Esc goes back.

It is the same four sections the web view shows, so the two surfaces describe a project the same way.

Reading only. The page can edit because it has a server to POST to and a pointer to click with; the terminal points you at way config set instead.

The first frame comes from the stored probe, not from a new one, so a repository whose slowest runner takes seconds shows its tasks at once instead of an empty frame. A real probe runs behind it and the rows update in place. Nothing is painted from a cold store: with execution off every exec runner already contributes a withheld report, so a cold store would yield a catalog of nothing but withheld rows — an empty list dressed up as an answer, which is worse than the empty frame it replaced.

Rows that came out of the store and are out of date are marked at the runner’s TAB with a leading ~, not per row: a whole runner’s rows are as old as each other, and marking each one would repeat a single fact hundreds of times.

The status line beside the task count then says one of three things:

  • Fresh — nothing. There is nothing to report, and a line that is always there is a line nobody reads.
  • Stale — cached rows — re-probing just (~149ms) while the probe is in flight, or cached rows: just 35s old — ^r to re-probe once it is over and the rows on screen are still the stored ones.
  • Stale and slow to probe here — painted red, because this is the case the cache exists for: cached rows — moon (~2.4s) is slow to probe here, so this takes a moment. Warning about a runner this repository has MEASURED at several seconds in the same voice as a 200ms refresh would waste the one warning that matters.

Slowness and refresh cost are measured in THIS repository, from a rolling window of the last five probe durations reported as their median. A runner that is quick here is never called slow because it is slow somewhere else, and where nothing has been measured yet the line says never timed here rather than inventing a number.

If a task you run turns out not to exist — a recipe renamed since the stored probe — the spawn failure is followed by a line naming the cached listing and its age, so an unexplained failure becomes one you can act on.

Ctrl+R re-probes the repository. It is ALWAYS a real probe, never a read of the store: a refresh that could be answered from a cache would make the one key that guarantees accuracy indistinguishable from doing nothing. It writes the store on the way out, so the next start still paints instantly. While a refresh is in flight, “refreshing” appears next to the task count; a refresh that fails reports the error in the same place instead of losing the list you already had.

The selection is held by the task’s identity, not its row number, so a refresh that reorders or drops rows cannot silently move the cursor onto a different task than the one that was on screen.