Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

CLI design — lexicon & options

Goal: a lexicon a new user understands without a glossary, and options that fix what repo/west users always missed.

Lexicon (canonical since v0.1)

TermMeaningReplaces / rejected
repoone Git repository in the workspace ([repo.NAME])brick (accepted alias), project (repo-tool jargon)
stacka named composition of repos ([stack.NAME], repos = [...])product (accepted alias)
overlaynamed per-repo overrides applied at lock timeprofile, variant
changesetone feature across N repos (branch + PR/MRs)topic, issue
groupfree-form label on a repo, used to filter commandskept from repo tool, now actually wired
revwhat you ask for: branch, tag, or SHA — kind auto-detectedrevision, refspec
lock / pinresolved SHA in haw.lockfreeze (planned rename: haw pin / haw unpin)
driftHEAD differs from the locked SHA

Old spellings (brick, product, bricks, --product, --bricks) parse forever as aliases; serialization and docs use the new words only.

Verbs (commands)

Rule: one guessable verb per action, git-adjacent, no jargon. Old names kept as hidden aliases so nothing breaks.

VerbDoesAlias (accepted)
haw init <path>bootstrap a workspace from a manifest
haw syncmaterialize the tree to haw.lock (writes lock if absent)
haw treeprint the stack → repo treegraph
haw statusfleet status: branch, head, dirty, drift per repost
haw run '<cmd>'run a command in every repo, in parallel (positional)forall (with -c)
haw lockresolve every repo’s rev → SHA into haw.lock
haw pinpin haw.lock to current checkouts (no network)freeze
haw unpinrestore haw.lock to manifest revsunfreeze
haw switch <stack>record a stack as current and sync it
haw repo add|remove|listedit the repos of the manifestbrick
haw stack add|remove|listedit the stacks of the manifestproduct
haw change start|status|listcross-repo feature (changeset) workflow
haw grep <pat>fan-out grep across every repo
haw verifydrift gate — exit 3 if the tree diverges from haw.lock
haw buildrun each repo’s manifest build command across the fleet
haw testrun each repo’s manifest test command across the fleet
haw hooks installinstall lifecycle hooks from .haw/hooks/
haw evidencebundle SBOM / provenance / signatures into haw-evidence.tar.gz
haw publish <files> --to <registry>upload artifacts to a private registry (see DISTRIBUTION.md)
haw import --from <west.yml|default.xml>convert a west / Google-repo manifest to haw.toml
haw merge plan|resolve|status|cleanup|abortparallel collaborative merge (per-slice)
haw completions <shell>print a shell completion script to stdout
haw plugins new|list|installscaffold, discover, and install haw-<name> plugins
haw (no args) / haw dashopen the TUI cockpittui

haw run takes the command positionally (haw run 'git fetch'); -c/--command still works via the forall alias. Running haw with no subcommand opens the dashboard (like htop, k9s).

Rev handling (user-friendly by default)

  • One field: rev = "main" | "v6.1.2" | "<40-hex sha>". No type = key; the kind is detected (refs/heads > peeled tag > tag > full SHA).
  • Display: SHAs are shown 8 chars everywhere; haw.lock stores the full 40.
  • Never detached: branch revs check out on a same-name branch, tags/SHAs on haw/<rev>.

Groups (implemented)

  • groups = ["firmware", "ci"] on a repo.
  • haw sync --group firmware, haw status --group ci, haw forall --group firmware -c ... (repeatable; empty filter = everything; a filter excludes ungrouped repos).
  • Groups are recorded in haw.lock so filtering works offline.

Options grid

OptionCommandsNote
--stack <S>sync, treealias --product; default: last switch, else the only stack
--overlay <O>lock, sync*, treerepeatable, later wins; *sync only when generating the lock
--group <G>sync, status, runrepeatable
--repos a,bchange startalias --bricks
--slug <S>repo addrepo path under --remote (alias --repo); with --remote, not --url
-j, --jobs <N>sync, switch, rundefault min(cores, 8)
--recurse-submodulessyncclone/update each repo’s git submodules, pinned to the superproject
--skip-branchchange startadopt current branches (RepoFleet)
--branch <B>change startdefault change/<id>

TUI keymap

k9s/lazygit-style, keyboard-first. Three mechanisms carry everything:

  • digits 17 switch views (from any top-level list view),
  • a opens the current view’s context actions (a lazygit-style menu),
  • : is the command bar for the rest — its verbs mirror the CLI (learn one, know both).

Data loads on a background worker — the UI never blocks. The fleet grid auto-refreshes every ~5s while idle (never while you’re typing, in an overlay, a confirm, or a job is in flight); F5 / ctrl-r refresh on demand.

Global (frozen — these keys mean the same thing in every view)

KeyAction
/ or k/jmove cursor (in a drill-in: scroll one line)
enterdrill in (stack → fleet → repo/PR/CI detail) · confirm a y/n prompt
esc / b / clear an active filter, else go back one level
qquit · ctrl-c force-quit
/fuzzy filter the grid (live, case-insensitive: /knlkernel)
:command bar (mirrors the CLI verbs, see below)
?help overlay
F5 / ctrl-rrefresh now
ctrl-d / ctrl-uhalf-page down / up · PageUp / PageDown full page
ggoto — quit and print the cursor repo’s path (cd "$(haw dash)")
wtoggle watch — auto-refresh the fleet & the open PR/CI view
spacemark / unmark the cursor repo (Fleet & Changeset only; shown as )

View jumps (17) — from any list view

KeyView: alias
1fleet:fleet
2changesets:changesets
3PR/MRs:prs
4CI runs:ci
5tree:tree
6governance:governance
7plugins:plugins

Digits are inert in the scroll/detail views (repo/PR/CI detail, files, grep) — jump from a list. Sorting (</>/.) applies to the Fleet, PR/MR, and CI tables.

Fleet view

KeyAction
ssync — the marked repos if any, else the cursor repo, else the stack
spacemark / unmark the cursor repo (shown as )
rrun a command — across the marked repos if any, else the whole fleet
pproblems-only filter (⚠ dirty / drift / behind / missing)
xdrop into a shell in the cursor repo (exits the cockpit)
fbrowse the cursor repo’s files (local disk or forge)
!run one shell command in the cursor repo (in its detail view)
enterdrill into the cursor repo’s git detail (branch, SHA, status, log, diffstat, remotes)

Switch-stack, lock, and git-fetch moved to the command bar: :stack (picker) / :stack NAME, :lock, :fetch. Pinning the lock is p in the Stacks view (or :pin).

Marks persist across the Fleet and Changeset views; with marks set, both s (sync) and r/:run act on just the marked set.

Fleet PR/MR view (3) and CI view (4)

KeyAction
enterdrill in — PR/MR: reviewers, checks, body, url · CI run: jobs, steps, conclusion
aactions menu — PR/MR: m merge · a approve · c checkout (each asks y/n)
dread the PR/MR’s diff (scrollable)
lread the CI run/pipeline’s logs (scrollable)
fbrowse the PR/MR’s changed files (PR-files view)
oopen the cursor row in your browser
< > .sort the table
b / escback

a (actions) and d are also available from within a PR/MR drill-in. Refetch is now just F5 / ctrl-r.

Files view (f from a repo)

A read-only browser: view or pick a file at any ref, on local disk or the forge (GitHub / GitLab / Bitbucket). It never stages/commits — it only reads. Two modes share the same repo / ref / scope context and toggle with T: a flat one-directory list (default) and a navigable expandable tree.

KeyAction
enteropen a directory, or view a file’s content (scrollable)
Ttoggle to the tree view (and back)
rref picker — read files AS OF a chosen branch / tag / SHA
eedit the file under the cursor in $EDITOR (local files only)
Rtoggle between the local-disk tree and the forge view
b / escup a directory, then back to the fleet
xdrop into a shell in the repo

Tree view (T from Files)

KeyAction
enter / expand the directory (or open the file) under the cursor
collapse the directory (or jump to and collapse its parent)
rref picker (same as Files)
Tback to the flat list
Rtoggle local ⇄ forge
b / escback to the fleet

The tree fetches every file path of the repo at the active ref once, then expands/collapses client-side. Collapsed dirs show , expanded ; files are indented under their parents.

Ref picker (r in either mode)

r opens a popup listing the repo’s branches then tags (j/k + enter to pick), plus an input row to type an arbitrary ref or commit SHA. Selecting a ref reloads the current view AS OF that ref (the flat list re-roots at the repo root; the tree re-fetches its paths). The panel title shows the active ref honestly: @ main, @ v1.0.0, @ a1b2c3d, or @ HEAD (local) / @ default (remote) when none is pinned. Local refs come from git for-each-ref / git ls-tree / git show <ref>:<path>; forge refs and trees come from each forge’s REST API.

e suspends the cockpit, hands the current TTY to $VISUAL/$EDITOR (falling back to nvim/vim/vi) on the file’s absolute path, then resumes and reloads the listing. It is declined on the forge view (R) and on directories; if the repo isn’t on disk it prompts to sync.

Errors view, Plugins view, Governance view

Reach them from a list view (Errors via :errors/:err, Plugins via 7/:plugins, Governance via 6/:governance). In Governance, o opens the cursor plugin’s artifact (SBOM / provenance / …). Refetch is F5 / ctrl-r; b / esc go back.

Changeset view

KeyAction
nnew changeset
spaceselect / deselect a repo
aactions menu — r request cross-linked PR/MRs (selected, or all if none) · l land in dependency order (each asks y/n)
ggoto the cursor repo

Actions menu (a)

a opens a bordered actions popup listing the current view’s context actions, each with its sub-key. Pressing a listed sub-key fires that action — write actions keep their y/n confirm gate. esc (or any unlisted key) cancels. Views with no actions report so.

Command bar (:)

Verbs mirror the CLI, and the status line echoes the exact command each one runs, so the TUI doubles as a way to discover the CLI.

CommandAction
:stackopen the switch-stack picker (alias :stacks)
:stack NAME / :switch NAMEswitch to a stack
:lockcommit the lock (resolve revs → SHA)
:fetchgit fetch the cursor repo
:errors / :errerrors view — failures collected across the fleet
:fleet / :changesets / :treeview jumps (same as 1 / 2 / 5)
:prs / :cifleet-wide PR/MR / CI views (same as 3 / 4)
:governance / :pluginsgovernance / plugins view (same as 6 / 7)
:syncsync the current stack
:run CMDrun a command (across marked repos in the Fleet, else the fleet)
:build / :test / :verifyfleet build / test / drift-verify
:pin / :lockpin HEADs / commit the lock
:change [ID | start ID | land ID | request ID]changeset workflow
:merge [cleanup <repo> | abort <repo>]list / seal / abort in-progress merges
:grep <pat>fan-out grep across every repo
:sh CMDrun a shell command in the cursor repo
:problemstoggle the problems-only filter (⚠ dirty/drift/behind/missing)
:watchtoggle watch auto-refresh (same as w)
:<repo>jump the fleet cursor to a repo whose name matches
:theme [NAME]switch skin live (no arg opens the theme picker; a chosen theme persists)
:editor [CMD]set the e-key editor (no arg opens a picker of editors on PATH; persists)
:compacttoggle the collapsed one-line header (persists)
:helphelp overlay

Themes / skins

Seven built-in skins: classic, catppuccin (default), dracula, nord, gruvbox, solarized, monochrome. classic is a neutral 16-color-ANSI look that reads on both light and dark terminals. :theme with no argument opens an interactive picker (highlighting the active skin); pressing enter applies the skin live and persists it to [ui].theme in the config. :theme <name> still switches (and persists) directly.

User config — ~/.config/haw/config.toml

Optional. A missing file, a partial table, or an unknown key all fall back to sane defaults (the cockpit never errors on absence). HAW_CONFIG overrides the path.

[ui]
theme = "classic"        # startup theme (a built-in name)
editor = "nvim"          # editor for the `e` key
compact_header = false   # start with the header collapsed
refresh_secs = 5         # idle auto-refresh cadence (clamped 2–60)

[keys]                   # remap a SAFE subset of action keys (single chars)
sync = "s"               # remappable: sync, goto, run, shell, files, problems, watch
goto = "g"

Precedence:

  • Theme: NO_COLOR (non-empty) → monochrome; else HAW_THEME (env) if it names a built-in; else [ui].theme; else the default catppuccin.
  • Editor: $VISUAL$EDITOR (env always wins) → [ui].editor → the first of nvim/vim/vi on PATHvi.

Custom keybindings ([keys])

Each entry maps an action name to a single key char. Only a safe subset is remappable — sync, goto, run, shell, files, problems, watch. The frozen globals (j, k, :, /, ?, q, b, space, g, w, and the digit view-jumps 17) can never be the target of a remap; a remap that targets a global, duplicates another remap, collides with a default action key, or isn’t a single char is dropped with a startup warning. When a remap is active, the header hints show the active key so they stay honest. The original default key keeps working too (a remap adds, never removes).

Shipped since this design was written

  • haw pin / haw unpin (aliases freeze/unfreeze).
  • --label <L> on change start, forwarded to PR/MRs at change request.
  • forge = "github" | "gitlab" key on [remote.X] for hosts the URL heuristic misses.
  • deps = [...] on a repo — change land merges in stable topological order.
  • haw verify, haw sync --locked, --format json on status/tree, exit 3 on drift.
  • haw build / haw test (per-repo commands in the manifest), lifecycle hooks in .haw/hooks/, haw hooks install, haw evidence, haw-<name> plugins.
  • Lexicon nuance: --slug on repo add accepts --repo as alias; haw run takes the command positionally (forall -c still works).
  • TUI g (goto) quits and prints the repo path — cd "$(haw dash)" — instead of spawning a nested shell.
  • TUI: live idle auto-refresh (~5s), fuzzy / filter (nucleo), column sorting (</>/.), marks + bulk s/r, drill-ins for repos/PRs/CI runs, the a actions menu (merge / approve / checkout in PR/MR; request-PR / land in Changeset), fleet-wide governance (6) view, the file browser (f) with a navigable tree (T), ref picker (r), and local edit (e), and seven themes including classic (HAW_THEME, NO_COLOR, live :theme with an interactive picker).
  • TUI config file ~/.config/haw/config.toml: [ui] startup theme / editor / compact_header / refresh_secs, plus [keys] custom keybindings for a safe subset of action keys. Interactive :theme / :editor pickers persist a choice; :compact toggles the collapsed header.

Planned (not yet implemented)

  • Tag conveniences: haw lock --as-of <tag>; haw status marking rev kind (branch/tag/sha).
  • haw auth login — OAuth device flow + OS keychain (see ARCHITECTURE DR-14).
  • TUI: mouse support.