A command palette for tmux. It runs on Bun, has no runtime dependencies, and opens quickly enough to use as a regular tmux binding.
Type a few letters, pick a command, hit enter: split a pane, jump to a window,
detach a session, open a popup tool, or switch to a custom palette. User config
lives in ~/.config/tmux-palette/*.json, so local changes survive repo updates.
Commands — main palette for panes, windows, sessions, and built-in tmux actions.
demo.mp4
Themes + plugins — theme picker with live preview, plus custom palettes powered by shell commands.
2026.05.13.08.37.16.mp4
The tool is in beta. It's stable enough to use day-to-day, but the surface area is still settling — I want the next few weeks to be about running it myself, gathering feedback, and shaking out bugs rather than growing the feature set.
If you'd like to contribute, the most useful work right now is anything that improves the existing base (refactors, perf, polish, docs) or fixes a bug. New features will likely get closed — no hard feelings, but please open an issue first to discuss before writing code.
- Fast startup — designed for frequent use from a tmux key binding
- Custom palettes — define your own with a single JSON file, bind to any key
- Hide built-ins — declutter the default palette via
hidden.json - Mobile-aware — auto-fullscreens on narrow terminals (Moshi / Blink on iOS)
- Curated themes — 12 built-in themes including Shades of Purple, Dracula, Tokyo Night, Catppuccin, Gruvbox, Nord, and Solarized. Pick one with live preview, or drop your own
- Popup tools — use
{ "popup": "htop" }to open tools likebtop,lazygit, log tails, orfzfscripts in a tmux popup - Scriptable sources — point a palette at a shell command that prints JSON or one item per line. Examples live in
examples/ - Small codebase — roughly 2k LOC, so it is easy to audit, fork, or patch locally
- No fork required — every customization lives in
~/.config/tmux-palette/*.json
- Open
lazygit,htop,btop, or log tails in a centered tmux popup. - Create a PR explorer from
gh pr listthat opens PRs in your browser. - Create a GitHub Actions picker that jumps straight to a workflow run.
- Pick a Docker container and tail its logs in a popup.
- List npm scripts from the current project and run one from tmux.
- Bind focused palettes to separate keys, like
M-tfor Tools orM-afor Appearance. - Keep a personal favorites palette without editing the repo.
See examples/ for drop-in palettes you can copy into
~/.config/tmux-palette/palettes/.
Use TPM if you already use it; otherwise manual install is the simplest path. The AI-agent option is just a guided setup flow.
- Bun
- tmux 3.4+ recommended (
display-popup -Esupport) - Optional tools for examples only:
gh,jq,docker,npm,git, etc.
Manual install
Requires Bun: https://bun.sh
git clone https://github.com/eduwass/tmux-palette ~/Sites/tmux-palette
cd ~/Sites/tmux-palette
bun installBind it to a tmux key in your .tmux.conf — Ctrl+Space gives the most "Raycast-feel" since it skips the prefix:
bind -n C-Space run-shell "~/Sites/tmux-palette/bin/tmux-palette.sh"Or if you'd rather go through the tmux prefix:
bind p run-shell "~/Sites/tmux-palette/bin/tmux-palette.sh"Reload: tmux source-file ~/.tmux.conf and hit your binding.
Install via TPM (Tmux Plugin Manager)
Requires Bun: https://bun.sh
Add to your .tmux.conf:
set -g @plugin 'eduwass/tmux-palette'
set -g @palette-key 'C-Space' # optional, default: C-Space (no-prefix)
set -g @palette-find-pane-key 'M-f' # optional, no binding by default
set -g @palette-move-pane-key 'M-m' # optional, no binding by defaultThen prefix + I (TPM's install key) to install. TPM clones the repo,
runs bun install on first load, and binds the keys for you. Set
@palette-key 'off' to skip the main binding and bind it yourself.
Guided onboarding with an AI agent
This is optional; it is just a guided setup flow. Choose it if you want an agent to drive the onboarding experience: install the repo, set up the tmux binding, test that it opens, and optionally create your first custom commands or theme.
Paste the prompt below into Claude Code, Codex, opencode, Cursor, or any AI coding agent.
You are helping a user onboard tmux-palette, a small command palette for tmux. Repo: https://github.com/eduwass/tmux-palette
Goal: get the palette installed, bound to a key, tested inside tmux, and leave the user with one useful next customization if they want it.
Follow steps in order. Confirm with the user before any change that modifies their files.
1. Prerequisites
- Run `bun --version`. If Bun is missing, point them to https://bun.sh/docs/installation and stop — do not auto-install.
- Run `tmux -V`. If lower than 3.4, warn that `display-popup -E` may not work, then proceed.
2. Clone and install
- Default path: `~/Sites/tmux-palette`. Ask the user if they want a different location.
- If the path already exists and contains the repo, run `git -C <path> pull` and skip cloning.
- Otherwise: `git clone https://github.com/eduwass/tmux-palette <path> && cd <path> && bun install`.
3. Bind it to a tmux key (required — the palette doesn't open without one)
- Default suggestion: `bind -n C-Space run-shell "<absolute-path>/bin/tmux-palette.sh"` (no-prefix, opens with Ctrl+Space). Ask the user if they want a different key.
- Append the bind line to `~/.tmux.conf` (create it if missing).
- Run `tmux source-file ~/.tmux.conf` to reload (or tell them to do it).
4. Pick a theme (optional)
Ask: "Want to choose a built-in theme now, or should I try to match your terminal's theme?"
If they want a built-in theme:
- Tell them they can open the palette and choose **Switch Theme...** for live preview.
- Mention the curated built-ins: Shades of Purple, Dracula, Tokyo Night, Catppuccin Mocha, Gruvbox Dark, Rosé Pine, Nord, Solarized Dark, Kanagawa Wave, GitHub Dark, One Dark, Ayu Dark.
If they want to match their terminal, detect it:
- Check $TERM_PROGRAM and $TERM. Common values: ghostty, iTerm.app, vscode, WezTerm, Apple_Terminal.
- Read the relevant config:
- Ghostty: ~/.config/ghostty/config
- Alacritty: ~/.config/alacritty/alacritty.toml (or .yml)
- Kitty: ~/.config/kitty/kitty.conf (follow `include` lines)
- WezTerm: ~/.wezterm.lua or ~/.config/wezterm/wezterm.lua
- iTerm2 / others: ask the user for hex codes; their configs are hard to parse.
- Extract: background → `bg`, foreground → `fg`, cursor color → `accent`, selection bg → `selected`. Derive `panel` (slightly lighter than bg) and `muted` (fg dimmed).
- Write `~/.config/tmux-palette/theme.json` with `{ bg, panel, selected, fg, muted, accent }`. The palette reads this at runtime; do NOT edit source files.
- Report the colors you picked.
5. Test
Tell the user to press their binding. Ask what they see.
6. Offer one useful follow-up
When it works, ask:
- "Want a quick custom command, like opening lazygit or htop in a popup?" — write items to `~/.config/tmux-palette/commands.json` (array of Items). Action types: `{ "tmux": "..." }`, `{ "shell": "..." }`, `{ "popup": "..." }`, `{ "palette": "name" }`. Do NOT edit source files.
- "Want a focused palette for PRs, Docker logs, npm scripts, or git branches?" — copy an example from `examples/` into `~/.config/tmux-palette/palettes/` and bind it.
- "Want custom shortcut labels?" — write `~/.config/tmux-palette/shortcuts.json` mapping item titles to label strings.
Only do one follow-up unless the user asks for more.
Constraints
- Prefer `~/.config/tmux-palette/*.json` over source edits. The user's config survives upstream pulls; source edits don't.
- Do not push to git or modify files outside the user's home directory.
- Do not auto-install Bun or any other system package.
- If anything fails, stop and explain what went wrong.
- Type to filter. Multi-word search is supported (
split horiz). - Up/Down arrows or Ctrl+P / Ctrl+N to move selection.
- Enter to run the selected command.
- Esc to cancel.
- Mouse works too — click rows, scroll the wheel.
Auto-aliases: initials of multi-word titles match automatically. Type nw for "New
Window", cs for "Choose Session", sh for "Split Horizontal", etc.
- CI runs
bun test, TypeScript, Fallow dead-code, and Fallow duplication checks. - The codebase is intentionally small and has no runtime package dependencies.
- Custom palettes are local JSON files, but they can run shell commands. Only copy palette examples you understand, especially if they come from outside this repo.
- User config lives under
~/.config/tmux-palette/; normal customization should not require editing source files.
- Requires tmux popup support; tmux 3.4+ is recommended.
- Plugin commands run each time their palette opens. Add your own cache layer for slow commands.
- This is currently installed from the repo or via TPM, not a packaged npm release.
{ "shell": "..." }and{ "popup": "..." }actions execute through the user's shell by design.
Drop-in user config lives in ~/.config/tmux-palette/. One JSON file per
concern — no source edits, no fork, survives upstream pulls.
Path: ~/.config/tmux-palette/palettes/<name>.json
Define a brand-new palette and bind any key to its name:
bind -n M-q run-shell "~/Sites/tmux-palette/bin/tmux-palette.sh my-favs"from— array of item titles to pull from the main commands palette (built-ins + yourcommands.json)fromCategory— pull every item from one categorycommand— shell command that prints a JSON array ofItemobjects to stdout (see plugins below)items— brand-new items defined inlinetitle/grouped/emptyText— same as built-in palettes
All keys optional. Resolution order: from → fromCategory → command → items.
Drop-in ready-to-use palettes live in
examples/— git branches, GitHub PRs (with color-coded status dots), Docker container logs, npm scripts, file picker. Copy one and bind a key.
The command field is the plugin escape hatch — fzf-style. Anything
that prints to stdout becomes a palette. Two output modes:
JSON mode — full control. Print a JSON array of Item objects:
// ~/.config/tmux-palette/palettes/github-prs.json
{
"title": "GitHub PRs",
"command": "gh pr list --json number,title,url --jq '[.[] | {icon: \"\", title: ((.number|tostring) + \" \" + .title), action: {shell: (\"gh pr view \" + (.number|tostring) + \" --web\")}}]'"
}Plain-text mode — fzf-style. Print one item per line, define a
default action template at the palette level with {} substituted
for the selected line:
// ~/.config/tmux-palette/palettes/git-branches.json
{
"title": "Git Branches",
"command": "git branch --format='%(refname:short)'",
"action": { "tmux": "send-keys 'git checkout {}' Enter" }
}This means most "command that prints lines" tools you'd pipe through fzf can become tmux-palette palettes with no scripting — just the JSON config and an action template.
Write the command in any language, distribute it however you want (gist, repo, copy-paste). The plugin runs every time the palette opens (no caching), so for expensive calls add your own cache layer. Errors are surfaced as a single item in the palette so failures stay visible without crashing the popup.
hidden.json — hide built-in items
Drop a JSON array of item titles to skip them in the main commands palette:
["Toggle Status Bar", "Reload Config", "Toggle OpenTUI Top Bar"]Items still appear if you reference them by title in a custom palette
(see above) — hidden.json is just about decluttering the default.
Append items to the commands palette without editing source:
[
{
"icon": "",
"title": "Toggle Diff Viewer",
"category": "Tools",
"action": { "tmux": "run-shell '/path/to/script.sh'" }
},
{
"icon": "",
"title": "Open Project in Cursor",
"category": "Tools",
"action": { "shell": "cursor /path/to/project" }
},
{
"icon": "",
"title": "htop",
"category": "Tools",
"action": { "popup": "htop" }
}
]Action types: { "tmux": "..." }, { "shell": "..." }, { "popup": "..." }, { "palette": "find-pane" }.
{ "popup": "htop" } opens the given command in a centered tmux popup
(80% × 80%, closes when the command exits). Handy for log viewers,
htop, btop, less, fzf-driven tools, etc.
{
"maxHeight": 28,
"width": 90,
"padX": 3,
"mobileWidth": 80,
"border": "none",
"popupBorder": "none"
}All keys optional. maxHeight caps how tall the popup gets when you
have lots of commands. width is the fixed popup width. padX is the
horizontal padding inside the popup.
mobileWidth is the client-width threshold for auto-fullscreen mode:
when the terminal is narrower than this many columns (iOS terminals
like Blink or Moshi typically run 50-60 cols), the popup goes
edge-to-edge with padX=1. Defaults to 80, set to 0 to disable.
border is the main palette border, popupBorder is the border for
{ "popup": "..." } action popups. Both default to none. Accepted
values: none, single, double, heavy, rounded, padded,
simple — passed straight to tmux display-popup -b. rounded works
but its corner glyphs can leave small visual gaps against the
surrounding terminal, so it is not the default.
Open the main palette and pick Switch Theme... (under Appearance). Arrow-key through the list — every theme lives-previews instantly so you see the colors apply before you commit. Enter saves it and returns you to the previous palette with the new theme on; Esc cancels.
The theme picker also includes Add custom theme..., which opens this README's custom-theme instructions in your default browser.
Bundled themes are intentionally limited to a small curated set: Shades of
Purple, Dracula, Tokyo Night, Catppuccin Mocha, Gruvbox Dark, Rosé Pine, Nord,
Solarized Dark, Kanagawa Wave, GitHub Dark, One Dark, and Ayu Dark. Their
panel/selected/muted/accent colors are tuned for readable contrast. To tweak a
theme without editing the repo, add a custom theme file or override individual
colors in theme.json.
Two forms. Pick a bundled (or custom) theme by name:
{ "name": "tokyo-night" }Or full color override (applied on top of the resolved theme):
{
"bg": "#1a1b26",
"panel": "#16161e",
"selected": "#283457",
"fg": "#c0caf5",
"muted": "#565f89",
"accent": "#7aa2f7"
}The theme switcher writes the { "name": "..." } form for you. You can
also mix: pick a name, then add individual keys to nudge specific colors.
Drop one JSON file per theme into ~/.config/tmux-palette/themes/:
// ~/.config/tmux-palette/themes/my-theme.json
{
"bg": "#0d0f12",
"panel": "#171a1f",
"selected": "#2c3038",
"fg": "#e6e8eb",
"muted": "#7d8590",
"accent": "#ff6b6b"
}Custom themes show up in the Switch Theme... picker alongside the
bundled ones (tagged custom). The filename becomes the slug — drop
my-theme.json → reference it as { "name": "my-theme" } in
theme.json, or just pick it from the switcher.
Pass --category=<name> to open the main palette filtered to one
category, Raycast-favorites style:
bind -n M-t run-shell "~/Sites/tmux-palette/bin/tmux-palette.sh commands --category=Tools"
bind -n M-a run-shell "~/Sites/tmux-palette/bin/tmux-palette.sh commands --category=Appearance"The popup title auto-updates to the category name and the category header gets hidden (since everything is the same category anyway).
When your terminal has a key-remap layer (Ghostty / iTerm2 / Karabiner) that
translates something like Cmd+D into a tmux binding, tmux only sees the
tmux side and doesn't know the original key. Use this to show what you
actually press:
{
"Split Horizontal": "Cmd+D",
"Find Pane": "Cmd+Shift+P",
"Choose Session": "Cmd+S"
}Keys are item titles; values are whatever text you want on the right side.
{
"Split Horizontal": ["sh"],
"Find Pane": ["fp"]
}Auto-aliases (initials like nw) still work for free, invisibly.
For things JSON can't express — custom row rendering, dynamic item
generators, custom filter logic — edit the TS source. Items in
src/palettes/commands.ts have this shape:
{
icon: "", // any nerd-font glyph
title: "Find Pane",
description?: "...", // optional, dimmed text after title
shortcut?: "Cmd+Shift+P", // optional, right-aligned label
category?: "Panes", // optional, groups items under a header
aliases?: ["fp"], // optional, visible chip + searchable
action: { tmux: "..." } // see Actions below
}{ tmux: "split-window -h" } // runs `tmux <cmd>` after the popup closes
{ shell: "echo hi" } // runs a shell command after the popup closes
{ popup: "htop" } // opens cmd in a centered 80% tmux popup
{ palette: "find-pane" } // chains into another palette
{ run: (ctx) => { ... } } // custom JS, runs in-process, then exits
{ apply: (ctx) => { ... } } // custom JS, runs in-process, then pops
// back to the previous palette (used by
// the theme switcher to "apply + return"){ tmux } is special: it dispatches after the popup closes, so interactive
tmux prompts (confirm-before, command-prompt) actually get keyboard
input. Without this, prompts hang because the popup still owns stdin.
The bash wrapper opens a tmux display-popup running the palette. When you
pick an item, the palette writes the encoded command to a tempfile and exits.
The wrapper then reads the tempfile and runs the command — after the
popup is gone. This matters because interactive tmux commands like
confirm-before need stdin, which is captured by the popup while it's open.
MIT