Skip to content

Make the TUI yours

You’ve spent a few days in the feedmill TUI now, and the rough edges have stopped being abstract. There’s one action you reach for constantly — for you it’s the session switcher, because a week-long build means you’re forever jumping between the thread where you’re fixing the JSON parser and the one where you’re auditing dedupe — and it’s still ctrl+x then l, a leader chord, every single time. Meanwhile half the default keymap is bound to things you’ve never once wanted, and a couple of those chords are close enough to the ones you do use that you keep firing them by accident. And the theme that shipped is fine at your desk and unreadable on the couch at night.

None of that is a flaw in OpenCode. It’s just that the defaults are defaults — a reasonable starting point, not a fit to your hands. From the leader-and-palette lesson you already know the two facts that make fixing this cheap: the keymap lives in tui.json, and keybinds is merged over the built-ins rather than replacing them. This lesson is about actually using that — turning the keymap from someone else’s into yours, and settling on a theme you can live inside for the week.

First, learn the name of the thing you want to bind

Section titled “First, learn the name of the thing you want to bind”

Every action in the TUI has an internal name — session_list, theme_list, command_list, session_new, and so on. Rebinding is just pointing one of those names at a different key. So before you edit anything, the move is to find the name of the action you keep reaching for, and you already have the tool for that: the command palette tells you both the human label and the chord, and the chord is the name made visible.

The cleaner way to read off every name at once is to ask the agent — it can see the schema:

> what are the keybind action names in tui.json, and which one opens the session list?
The TUI keymap is keyed by action name in tui.json under "keybinds".
The session switcher is "session_list" (default <leader>l). A few others
you'll likely touch: "session_new" (<leader>n), "command_list" (ctrl+p),
"theme_list" (<leader>t), "model_list" (<leader>m).

Now you’re not guessing at key combos in the dark. You know that the thing you fire forty times a day is called session_list, and rebinding it is one line.

Here’s the change that pays for the whole lesson. A leader chord is the right default for most actions — it keeps OpenCode’s keymap out of your terminal’s way — but the one or two actions you trigger constantly don’t deserve to sit behind a prefix. If you’re jumping sessions all day, make it a single keystroke.

You only write the bindings you’re changing — everything you leave out keeps its default, because the merge is additive:

{
"$schema": "https://opencode.ai/tui.json",
"keybinds": {
"session_list": "ctrl+s"
}
}

That’s it. session_list is now a bare ctrl+s — one stroke, no leader — and every other binding in the TUI is exactly as it shipped. Save the file and the change is live. (One caveat on this particular key: ctrl+s is the classic terminal flow-control stop — XOFF — so on some setups it may get swallowed before OpenCode sees it; if it feels dead, pick a different free chord. The mechanism is the point, not the exact key.) The discipline here is restraint: don’t sit down and remap the whole keymap to your imagined ideal on day one. You’ll get it wrong, because you don’t yet know which actions you actually reach for. Promote a chord to a single key only after you’ve felt yourself wanting it — let real friction, not a planning session, drive what you bind.

You can also bind through the leader explicitly with the <leader> token, which is how you move an action onto a different leader chord rather than off the leader entirely:

{
"keybinds": {
"session_new": "<leader>k"
}
}

The <leader> token resolves to whatever your leader key is, so if you later move the leader off ctrl+x, this binding follows it instead of breaking.

Unbind what you never use — and what bites you

Section titled “Unbind what you never use — and what bites you”

The other half of making the keymap yours is subtraction. Some default chord sits one key away from one you use constantly, and every so often your fingers miss and fire it. The clean fix isn’t to be more careful — it’s to take the binding away. Set it to "none" (or false) and it stops existing:

{
"keybinds": {
"session_new": "<leader>k",
"messages_half_page_up": "none"
}
}

Now the chord you kept tripping over is inert. This reads as a small thing and isn’t — on a multi-day feedmill session, an accidental action at the wrong moment is exactly the kind of papercut that erodes your trust in driving by keyboard. Unbinding the two or three chords you fat-finger is how the TUI stops surprising you.

If ctrl+x itself is the thing that clashes — it’s a common terminal binding, and you may have trained your fingers on it elsewhere — move the leader once and every <leader> chord moves with it:

{
"keybinds": {
"leader": "ctrl+a"
}
}

The leader lives inside the keybinds object, alongside the action names — not at the top level. (leader_timeout, confusingly, is a top-level key; leader is not. Easy to conflate.) That single change re-homes the entire leader keymap. You don’t touch the individual bindings; they’re all expressed relative to leader, so they ride along.

Keys are the half of “make it yours” that’s about speed. The theme is the half that’s about not hating your screen by Thursday. You met the picker already — /theme, or its leader chord <leader>t — and that’s the right tool for browsing: it opens a list you can arrow through with a live preview, so you can audition tokyonight against your room’s lighting before you commit.

> /theme
┌─ theme ─────────────────────────────────┐
│ opencode (current) │
│ tokyonight │
│ catppuccin │
│ gruvbox │
└─────────────────────────────────────────┘

Picking from that list switches the theme for the session. To make it stick across restarts, set it in config — and this is where the file boundary matters. The theme lives in tui.json under the theme key, the same file as your keybinds:

{
"$schema": "https://opencode.ai/tui.json",
"theme": "tokyonight",
"keybinds": {
"session_list": "ctrl+s"
}
}

The default theme is opencode. If none of the built-ins fit, OpenCode reads custom themes from JSON files in its theme directories (~/.config/opencode/themes/ for a user-wide theme, .opencode/themes/ for one scoped to feedmill) — but that’s a deeper cut than most people need, and the built-in set is broad enough that you’ll usually just be choosing.

One thing worth keeping straight, because it’s a recurring source of confusion: tui.json is not opencode.json. Your model and provider config — the bring-your-own-model wiring from chapter two — lives in opencode.json. The TUI’s keys, leader, timing, and theme live in tui.json. Two files, two jobs; editing the wrong one is the most common reason a change you made “doesn’t take.”

Step back from the JSON. What you’ve done is close the gap between the tool’s defaults and the way you actually work: the action you fire constantly is now one key, the chords that bit you are gone, the leader sits where your fingers expect it, and the screen is one you can look at for a week of feedmill work without flinching. That’s the whole point of a keyboard-first TUI being configurable — it lets the interface disappear, which is exactly what you want when the thing you’re actually here to do is fix a thicket of feed parsers, not fight a keymap.

That fluency — driving the TUI by hand, holding your place across sessions, bending it to your grip — is the foundation the rest of the course stands on. The next thing to install isn’t another interface skill; it’s memory the agent carries on its own, so it stops reintroducing the same feedmill bugs you keep fixing. Next: Rules (AGENTS.md).