kb
A small terminal UI for looking up keybindings across the tools you actually use — one tab per app, opens on the tab matching whatever's currently in focus, free-text search across all of them.

Why
Keybindings live scattered across config files, plugin docs, and muscle
memory. kb puts them in one place: a fast TUI you bind to a shortcut,
that already knows what you were doing when you opened it.
Features
- Tabs per app — one per tool, switch with
←/→orTab/Shift+Tab - Auto-focus detection — guesses which app you were just in (via Hyprland window class + process tree) and opens directly on that tab
- Search —
/filters keys and actions in the active tab; multiple words match in any order and fuzzily (like fzf), so "tab next", "next tab", and "nxt tb" all find a row whose action is "Next tab" - Aliases and
@tabjump — tabs carry extra search terms beyond their name (Tridactyl also answers to "vim", "browser"), and typing@nameat the start of a search jumps straight to that tab (@vim scrollhops to Neovim/Tridactyl and filters for "scroll") - Non-interactive mode —
kb --list/--jsonprints every bind for scripting or feeding an external picker (rofi/walker/fzf),kb --conflictsflags keys reused across apps - Live sourcing where possible — some tabs are parsed straight from the app's real config/state instead of a hand-maintained list (see below)
- Light/dark theme, auto-switched by time of day
Supported apps
| App | Source |
|---|---|
| Hyprland | live — parses bind/bindd/bindm lines from ~/.config/hypr/*.conf |
| Neovim | live — headless nvim dump of vim.api.nvim_get_keymap(), loaded in the background |
| Herdr | static defaults + live overrides read from ~/.config/herdr/config.toml |
| Tridactyl | static (config lives in browser sync storage, no local file to read) |
| Spotify (spotify_player) | static (no local keymap override found) |
| Lazygit | static (no local keymap override found) |
| Yazi | static (defaults are compiled into the binary) |
| Glow | static |
| Tuicr | static |
"Static" tabs are hand-copied from each tool's documented defaults — swap
them for a live source in src/sources/ if the app you use has one and you
want it parsed instead.
Install
Or build from source:
Binary lands at target/release/kb (the crate is keybinds-tui, the binary is kb).
Usage
Run kb in a terminal, or bind it to a key. On launch it tries to guess
which app tab to open based on the window you were just in.
| Key | Action |
|---|---|
h/← / l/→ / Tab |
Switch tabs |
j/↓ / k/↑ |
Move selection |
Ctrl+d / Ctrl+u |
Half page down / up |
gg / G |
Jump to top / bottom of the list |
/ |
Search / filter |
Esc |
Cancel search |
q / Esc (outside search) |
Quit |
Focus detection
kb reads hyprctl activewindow -j at startup to guess the tab. If you
launch it from a wrapper script that opens kb in its own window (e.g. a
scratchpad terminal), that lookup would just see kb's own window instead —
pass the previously-focused window down explicitly instead:
KB_FOCUS_CLASS=<class> KB_FOCUS_PID=<pid> kb
An example Hyprland scratchpad launcher is not included here since it's
specific to each setup, but the pattern is: capture
hyprctl activewindow -j before opening kb's window, then pass those
two values through the env vars above.
Everything else (window-class matching, process-tree walking for terminal apps) works standalone with no extra setup.
Non-interactive / picker mode
--list is meant for piping into whatever fuzzy picker you already have,
instead of opening the TUI:
|
|
|
Neovim dump cache
The headless nvim dump takes a few seconds, so its result is cached at
$TMPDIR/kb-nvim-cache.json for 5 minutes — reopening kb shortly after is
instant. Delete that file (or just wait out the TTL) if you edited your
Neovim config and want a fresh dump sooner.
Adding an app
Without forking, drop a ~/.config/kb/tabs.toml and it's merged in at
startup:
[[]]
= "MyApp"
= ["myapp"]
= ["alt-name-people-search-for"]
[[]]
= "General"
[[]]
= "Ctrl+X"
= "Do something"
In the source tree, each built-in lives in src/sources/<app>.rs and
returns a model::Tab. Add a live parser if the app has a config/dump you
can read; otherwise a static table following the existing modules (e.g.
tridactyl.rs, built via Tab::from_raw) works fine. Wire it into the
tabs vec in main.rs and, if it can run inside a generic terminal window,
add its process name to focus.rs.
Development
CI runs both on every push/PR.