🌟 whetū
An opinionated, zero-config status line and history picker for fish, bash and zsh, written in Zig.
Two things, not one. whetuu draws the status line above your cursor, and it puts your history on the up arrow.
Both live in one binary, so the picker knows where you are. The up arrow opens on what you ran in this directory, and Ctrl+G switches to all of it. Failed commands are never stored, so you do not scroll past your own typos.
Your own shell history stays exactly where it is. whetuu never reads or rewrites it, and keeps its own store alongside. See History for what the picker does and where it keeps things.
whetū is Māori for "star". A star is the character you type after, using the
Nerd Font glyph nf-md-star_face. The binary is installed as whetuu.
Pronounced feh-TOO (/fɛˈtuː/). In Māori wh is an f sound, not a w.
The macron in ū makes that vowel long, and a long vowel takes the stress, so
it falls on the second syllable. The ASCII name doubles the u to write that
same long vowel.
There is nothing to configure. One compiled binary renders one curated status
line, the same for everyone. The slowest module runs as a task via std.Io
while the rest run alongside it, so a render costs about what its slowest probe
costs. See Performance.
Needs a Nerd Font. whetuu draws the git branch, the language logos and the star with Nerd Font glyphs. Most terminal setups already run one, so try whetuu first. If the star and the branch glyph come out as empty boxes, switch your terminal font and they will be there.
Website · Install · Performance · Security
Modules
Left to right, each shown only when relevant:
| Module | Shows |
|---|---|
user_host |
user@host in bold green, only over SSH ($SSH_CONNECTION/$SSH_TTY) or when root, and then in bold red as a warning |
directory |
Current directory, with $HOME collapsed to ~. Keeps the anchor plus as many trailing directories as fit the width (~/…/projects/client) |
git branch |
Branch glyph and current branch (or (detached)), in magenta |
git state |
Any operation underway, in yellow: (rebasing 2/7), (merging), (cherry-picking), (reverting), (bisecting). Read straight from .git, with no extra subprocess |
git status |
[=conflicts $stashes +staged !modified ?untracked ⇡ahead ⇣behind] |
language |
Logo and toolchain version in the brand color, for 39 languages and tools. Detected from a project manifest (Cargo.toml, mix.exs, …), a source file extension (*.odin, *.rkt, …), or an infra marker (flake.nix, Dockerfile, *.tf for Terraform and OpenTofu) |
cmd_duration |
Timer glyph and <time> when the last command ran for 2 s or more |
update |
Cloud glyph and the tag of a newer release, when one has been published. Read from a file, never the network. Absent the rest of the time |
character |
A star, purple by default, or in the language brand color. Turns red after a failed command |
Performance
A status line runs before every command, so you pay its cost constantly.
Numbers from hyperfine --warmup 40 --runs 400 on a 13th gen i9-13900H,
ReleaseFast build, with the toolchain version cache warm, pinned to the
performance cores on an otherwise idle machine:
| Directory | Render | For comparison |
|---|---|---|
| No repo, no toolchain | 1.0 ms ± 0.2 | — |
| Zig repo, 49 files | 2.8 ms ± 0.4 | zig version alone: 3.5 ms |
| Monorepo, 10 000 files | 11.0 ms ± 1.8 | git status alone: 10.9 ms |
Two things do most of the work. The probes overlap, so a render costs about what
the slowest one costs rather than the sum of all of them. In the monorepo the
whole status line takes about as long as git status on its own.
Outside a repository neither of them runs. whetuu walks up from the current
directory looking for the repository, which is a handful of stat calls, and
only starts git once it has found one. That walk was already happening to read
the operation state and the stash count. Doing it first is what makes a
directory with no repository and no project cost a millisecond.
Toolchain versions are also cached, keyed on the binary path, mtime and size.
The first render in a project pays for the probe. Later ones read a small file
instead. Upgrading a toolchain changes its mtime, which drops the stale entry.
What that saves depends on the toolchain. A slow --version call is well worth
skipping. A fast one is already hidden behind the git probe running alongside
it, which is why the Zig repo above lands within noise of zig version itself.
Reproduce it with:
hyperfine --warmup 40 --runs 400 \
'whetuu render --shell fish --status 0 --duration-ms 0 --width 100'Pin the run on a laptop that mixes performance and efficiency cores, with
taskset -c 0-11 on Linux or its equivalent. Left to the scheduler, the same
measurement spreads across a factor of two and tells you nothing.
A slow repository cannot hang your shell. Both subprocesses are bounded. The
git call gets 250 ms and the toolchain probe gets 200 ms. They run at the same
time, so the worst case is the larger of the two, not the sum. Given a git
that hangs for 30 s, the status line still returns in 257 ms. It simply drops
the git segment.
And a status line stays out of git's way. The call passes
--no-optional-locks, so git reads the index without refreshing it. It never
takes index.lock, which a git commit in another terminal would then fail to
take, and it never writes the index back once per command forever.
In a large repository, almost all of that time is git status, and most of that
is the scan for untracked files. Speeding it up is git's job, not whetuu's.
Turning on git's untracked cache cut git status from 10.6 ms to 6.7 ms on the
10 000 file repository above:
git config core.untrackedCache trueSecurity
whetuu reads your repository and prints a line. Here is what that involves.
-
The network, in full. Every request whetuu can make is a
GETover TLS, to one of three GitHub hosts, of a URL anyone can open in a browser:Host What for Sent by api.github.comthe newest release tag upgrade,upgrade --checkgithub.comthe release tarball and its SHA256SUMSupgraderaw.githubusercontent.comCHANGELOG.mdat that tag, to print what changedupgrade,upgrade --checkwhetuu upgrademakes all three, and checks the download againstSHA256SUMSbefore writing anything.whetuu upgrade --checkmakes the first and the last, and writes the tag to a file, which is how the status line knows to show one. The status line starts that check for itself at most once a day, in a detached process whose output goes to/dev/null, and never asks anything itself. That is the whole of it: no telemetry, no account, no server, and nothing about you or your machine is sent — the requests carry a user agent ofwhetuu/<version>and nothing else. -
Every path is one the spec already names. The binary goes in
~/.local/bin, the history store under$XDG_DATA_HOMEand the version cache under$XDG_CACHE_HOME. whetuu creates no directory of its own in$HOME. Runwhetuu pathsto see both data locations, and whether each file exists yet. Uninstall lists what to remove. -
The installer edits one file, once. It appends an
initline to the config of the shell in$SHELL, guarded so a second run changes nothing. Not the config of a shell you do not use. APATHline joins it only when~/.local/binis not already on yourPATH. SetWHETUU_NO_MODIFY=1and it prints them instead. -
No config file. whetuu has none, so there is no config parser and no format for anything to smuggle through. Running, it writes three files. One is the history store. The others live under
~/.cache/whetuu, or under$XDG_CACHE_HOMEwhen that is set:versionsholds toolchain version strings, andreleaseholds the newest release tag and the time it was looked up. Delete either whenever you like. -
Three subprocesses, and nothing else.
git --no-optional-locks status --porcelain=2 --branch -z, and the version command of the detected toolchain (zig version,node --version, …), both bounded and neither run outside a repository or a project. The third is whetuu itself, aswhetuu upgrade --check, at most once a day: detached, in a process group of its own, with all three streams on/dev/nullso it can never write over your status line. Nothing else is executed. -
The history store is
0600, set again on every append. Command lines routinely contain paths and secrets. The store lives at~/.local/share/whetuu/history, or under$XDG_DATA_HOMEwhen that is set. It usually is not set, on macOS or Linux. -
A leading space keeps a command out of the store. Shells have used this convention for decades:
curl -H "Authorization: Bearer $TOKEN" https://api.example.comThis works in fish, zsh and bash. bash needs help, because its
historyoutput has already lost the indentation by the time whetuu sees the command. So the bash integration addsignorespaceto yourHISTCONTROLand keeps any value you already had. The command then stays out of bash's history too. -
Anything else is stored in plaintext. Paste a token into a
curlwithout that leading space and the whole line is written to the store, as long as the command succeeds. File permissions are the only protection. Nothing is redacted. Keep secrets in environment variables or a credentials file, as you would with your shell's own history.Only commands that exited
0are stored. Treat that as noise reduction for the picker, not a safeguard. It filters out your typos, not your workingcurl. -
A command that is not text is not stored either. Paste an image into your terminal and the shell keeps whatever survived of it on the command line. Run that as part of a line ending in a command that works and the shell reports the whole thing as having exited
0. whetuu drops it rather than fill the picker with a row nothing can run.
One thing to know. The language module picks which toolchain to probe from the
files in the current directory. So entering an untrusted repository can make
whetuu run something like node --version. It runs the binary your PATH
resolves, never one from the repository. If you keep . in your PATH that
distinction goes away, and it goes away for every other tool you run too.
Install
Two ways. Neither is more supported than the other.
Download the binary
Prebuilt binaries are on the
releases page, with a
SHA256SUMS file to verify them:
| Platform | Target |
|---|---|
| Linux x86-64 | x86_64-linux-musl, static, no runtime dependencies |
| Linux ARM64 | aarch64-linux-musl, static, no runtime dependencies |
| macOS Apple Silicon | aarch64-macos |
| macOS Intel | x86_64-macos |
sha256sum -c SHA256SUMS --ignore-missing tar -xzf whetuu-<version>-<target>.tar.gz mv whetuu ~/.local/bin/
Then add one line to your shell config, which Shell setup covers. That is the whole thing. The installer below does exactly this and nothing more.
The macOS binaries are unsigned. Download one in a browser and Gatekeeper
quarantines it, so the first run fails with "cannot be opened because the
developer cannot be verified". Clear the flag once with
xattr -d com.apple.quarantine "$(command -v whetuu)". Downloading with curl
or wget avoids the attribute entirely.
Run the installer
curl --proto '=https' --tlsv1.2 -fsSL https://yamafaktory.github.io/whetuu/install.sh | sh
It detects your platform, checks the download against the published
SHA256SUMS, puts the binary in ~/.local/bin, and adds the init line to the
config of the shell in $SHELL. A PATH line joins it only when ~/.local/bin
is not already on your PATH, which on most systems it is. Running it twice
changes nothing.
Read it first if you would
rather not pipe to a shell, or take the download route above instead. The script
saves you a uname and a checksum check. It is not a way to verify anything you
could not verify yourself, and if this repository were compromised the script
would be too.
WHETUU_NO_MODIFY=1 prints the lines instead of writing them.
WHETUU_INSTALL_DIR puts the binary somewhere else, and then the shell config
is left alone.
Upgrading
The status line tells you when to run it: a cloud glyph and the new version appear once a release is out, and go away once you are on it. To read what is waiting before installing it:
That names the release and prints its changelog entries, and installs nothing.
It replaces the running binary with the newest release, wherever that binary
lives, and prints every changelog entry between the two versions. The download
is checked against the published SHA256SUMS before anything is written, and
the new binary is renamed onto the old one, so an upgrade that fails halfway
leaves the one you have running.
whetuu does all of it itself. Your machine needs no curl, no tar and no shell for it. Running shells pick up the new binary on their next command. Open a new one to reload the init script.
Two cases it leaves alone. A binary you cannot write to, which is what a package
manager install looks like, so upgrade that the way you installed it. And a
build from source, which reports dev and has no release to compare itself
with. The installer above upgrades an install too, and running it twice changes
nothing.
Uninstall
rm ~/.local/bin/whetuu rm -rf ~/.local/share/whetuu ~/.cache/whetuu
Then delete the # whetuu block from your shell config. The first line removes
the program. The second removes the history store and both caches, which live
under the XDG directories rather than next to the binary. Run
whetuu paths before you delete anything and it prints all three locations, in
case $XDG_DATA_HOME or $XDG_CACHE_HOME moves them on your machine.
From source
Needs Zig 0.17 (dev), see minimum_zig_version in build.zig.zon for the exact
nightly:
git clone https://github.com/yamafaktory/whetuu.git cd whetuu zig build --release=fast mv zig-out/bin/whetuu ~/.local/bin/
Other build steps:
zig build test # run the unit tests zig build check # type check only zig build fmt # format all source files zig build run # build and run without installing
Maintainers: see RELEASING.md for cutting a release.
Shell setup
The installer already did this. This section is for a download or source
install, for WHETUU_NO_MODIFY=1, or for a shell whose config it could not
find.
Add the matching line to your shell config, then restart the shell. Add
~/.local/bin to your PATH first if it is not there already:
fish — ~/.config/fish/config.fish
whetuu init fish | sourcebash — ~/.bashrc (needs bash 5 or newer for command timing)
eval "$(whetuu init bash)"
zsh — ~/.zshrc
eval "$(whetuu init zsh)"
whetuu init <shell> prints the integration script. The shell hook then calls
whetuu render … before every command, passing the last exit status, the
command duration, and the terminal width.
Run whetuu init <shell> by hand and it prints the line above instead, with the
file it belongs in. Several hundred lines of shell answer nothing when you are
looking at a terminal. Pipe or substitute it, as the lines above do, and you get
the script. whetuu init fish | less reads it.
Usage
Day to day there is nothing to run. The shell hook drives everything, and the history picker is on the up arrow. The full command surface:
| Command | Does |
|---|---|
whetuu |
Print the command list |
whetuu --version |
Print the version |
whetuu init <fish|bash|zsh> |
Print the shell integration script, meant to be sourced or evaled. Prints the setup line instead when run straight into a terminal |
whetuu render |
Render one status line. Called by the shell hook, not by you |
whetuu history |
Open the interactive history picker |
whetuu history add -- <command> |
Record a finished command. Called by the shell hook |
whetuu paths |
Print where the history store and version cache live, and whether each file exists yet |
whetuu upgrade |
Replace the running binary with the newest release, and print what changed |
whetuu upgrade --check |
Say what release is waiting and what changed in it, and install nothing. Writes the tag down, which is what the status line reads. whetuu runs this for itself once a day |
render and history add take flags that only the init scripts pass, namely
exit status, duration and width. That is why they are left out here.
whetuu paths marks a file that is not there yet rather than hiding it. A fresh
install has none of them until the first command is recorded, the first toolchain
version is cached, and the first release check runs. With neither $HOME nor the matching XDG variable set it says
so, because then whetuu has nowhere to write.
History
whetuu keeps its own command history. It is one file, shared by all three
shells, at ~/.local/share/whetuu/history. It moves under $XDG_DATA_HOME when
that variable is set. macOS uses the same path rather than ~/Library, so the
store stays put when you share a dotfiles setup across machines.
Nothing is ever deleted from it. The picker reads the most recent few megabytes rather than the whole file, so it opens just as fast on a store built over years as on a fresh one. Everything you have run stays on disk either way, and on a store that large the commands past the window are ones you last ran years ago. How often you run a command is counted over that same window, so what breaks a tie between two equally good matches is how often you run it lately.
Your shell's own history file is untouched. whetuu never reads, writes or
truncates ~/.bash_history, ~/.zsh_history or fish's database. The two stores
run side by side. Delete the whetuu store and your shell history is exactly as
it was.
Two things it does take. The up arrow, which all three integrations bind to the
picker. And on bash only, ignorespace is added to your HISTCONTROL, keeping
whatever value you already had, so a space prefixed command stays out of both
stores. Ctrl+R and everything else your shell gives you keep working.
On bash the status line is drawn from a PROMPT_COMMAND hook. That is a list
the shell shares with everything else that wants to run each prompt, so whetuu
joins the front of it rather than replacing it. A terminal's own shell
integration and tools like direnv keep working.
A command is recorded once it finishes, and only when it exited with status 0. Typos and failed runs never enter the store. Prefix a command with a space to keep it out of the store entirely. Every command is stored together with the directory it ran in.
The command that just broke is not lost. When a command does not exit 0, it appears at the top of the picker, in red. Pick it to fix and run it again. Cancel and it is still there the next time you open the picker. It lives in memory until you run another command, and never reaches the store.
All three integrations bind the up arrow to the picker. Anything already
typed on the command line carries over into the search field. The picker opens
on the current directory's history, which is the set of commands you actually
run in this project. It falls back to all history when the directory has none
yet. A bar at the top names both scopes and highlights the active one, like
~/dev/whetuu | all.
- type to filter — the letters have to appear in order, not next to each
other, so
gcmfindsgit commit -m. Every word of the search must match, ignoring case - Backspace / Delete — drop the last character of the search. The cursor never leaves the end of it, so the two keys do the same thing
- Ctrl+W / Alt+Backspace — drop the last word of the search. A word here is what the search itself splits on, so one press takes one search word
- ↑ / ↓ — move the selection, where ↑ goes further back in time
- Home / End — jump to the newest command or to the oldest one on the list
- Ctrl+G — switch between this directory's history and all history
- Enter — run the selected command. A search that matches nothing runs as typed
- Tab — put the selected command on your shell's command line without running it, so you can edit it there before you press Enter
- Esc / Ctrl-C — cancel, leaving whatever you had typed on the command line
The picker behaves the same in all three shells.
Tab is for the command that is almost right. It closes the picker and hands the command to your shell, which means you edit it with the editor you already know. Arrow keys, word jumps, your own key bindings and fish's syntax highlighting all work, because at that point the line is an ordinary command line. whetuu is out of the way. Press Enter to run it, or Ctrl-C to drop it.
The list grows upward from the bottom. The most recent command sits just above
the search line and older ones climb from there. Type and the closest match
takes that bottom row instead, with weaker ones climbing away from it. Each row
is prefixed with how long ago it ran, like 5m, 2h or 3d. The selected row
is highlighted across the full width in the star purple of the status line.
Matches are ranked by how well they fit what you typed. Letters found in a run,
at the start of a word, or at the front of the command are worth more than the
same letters found scattered through the middle. So gcm puts git commit -m
above git checkout main, and both above a command that merely happens to
contain a g, a c and an m.
When two commands fit equally well, the one you run more often and more recently comes first. That is only ever a tiebreak. A command that fits what you typed better is always shown above one that fits it worse, however long ago you last ran it.
An empty search is not ranked at all. Open the picker and the list is in the order you ran things, newest first, so the up arrow still lands on the command you just ran. Nothing you run often can push it off that row.
Searching stays fast on a store of any size. Commands are scored several at a time using the vector instructions your processor already has, and a search is narrowed by which characters a command contains before any of it is scored. Against a full window of about 50 000 commands a keystroke costs 1 to 3 ms on the machine the Performance numbers come from, which is roughly a fifth of what the same code costs scoring one command at a time.
Commands are syntax highlighted. The program name, flags, paths, variables, quoted strings and operators each get their own color, so a long row reads at a glance. The colors come from your terminal theme rather than from whetuu, so the picker matches the palette you already run. The selected row switches to lighter tints of the same colors, which stay readable on the purple.
Paths are recognized by how they are written, like /tmp/out, ./build or
~/dev. A bare src stays plain. whetuu never touches the filesystem to render
a row, so it cannot know that one is a directory.
A command wider than the terminal loses its middle to a … rather than its end.
Both the program name and the tail stay on screen. That is what keeps a run of
commands sharing one long prefix apart, like several cd <long path> && git …
entries that differ only in the part a plain cut would drop.
Rows are drawn on one line. Runs of spaces, tabs and newlines each collapse to a single space, so a command written across several lines stays readable in the list. This changes the row only. Enter and Tab both give you back the command exactly as it was recorded.
The picker draws on /dev/tty, so nothing but the chosen command reaches
stdout. Duplicates are collapsed per directory, so the same command run in two
projects keeps its own recency in each.
