Terminal Setup for a Non-Mac Keyboard: Ghostty, tmux, and Zellij

Two independent things happened that drove me to reconsider my terminal workflow.
First, I started relying more and more on Claude Code as my AI coding assistant running inside the terminal. Shift+Enter (for inserting newlines without submitting) didn’t work inside tmux, so I had to use a workaround. It was annoying, but acceptable.
Then I switched to a 96-key keyboard with dual Bluetooth, because I wanted a single keyboard that can switch between two computers. That’s when my tmux scrollback navigation broke too. With two things broken, I decided to look deeper into both issues and understand what’s actually going on.
This post documents the problems, the dead ends, and the solutions I landed on, including eventually migrating from tmux to Zellij.
The Workflow
For years, I’ve used tmux and tmuxinator to manage my terminal sessions. The idea is simple: every project gets its own tmux session. When I start working on a project, I cd into it and run txs (an alias for tmuxinator start). If the session already exists, it attaches to it. If not, it creates a new one based on a .tmuxinator.yml config in the project root.
When I switch to a different project, I do the same thing: cd and txs. Each project lives in its own session with its own windows and panes. Projects I’m not actively working on stay in the background, invisible, creating no mental load.
For the terminal emulator, I use Ghostty. The issues described below are not Ghostty-specific though. Any modern terminal emulator that supports the kitty keyboard protocol (iTerm2, WezTerm, Kitty, etc.) would behave the same way.
On my MacBook’s built-in keyboard and the Magic Keyboard, everything worked fine with tmux.
Problem 1: Scrollback Navigation in tmux
I used to scroll through tmux history with Ctrl+B, then Page Up/Down. The new keyboard doesn’t have dedicated Page Up/Down keys, and fn+Arrow (which emulates them on Mac keyboards) doesn’t work on third-party keyboards.
The fix is to enable mouse scrolling and add Alt+Arrow bindings as a keyboard alternative. I eventually moved away from tmux (more on that below), but if you’re sticking with it, here are the changes to ~/.tmux.conf:
# Mouse scroll enters copy mode automatically
set -g mouse on
# Alt+Up enters copy mode and pages up in one keystroke
bind -n M-Up copy-mode \; send-keys -X page-up
# Alt+arrows for navigation inside copy mode
bind -T copy-mode M-Up send-keys -X page-up
bind -T copy-mode M-Down send-keys -X page-down
Problem 2: Shift+Enter in Claude Code Inside tmux
This one turned out to be unsolvable with tmux. To understand why, it helps to know how keyboard input works in terminals versus GUI applications. Here’s what I learned.
When you press Shift+Enter in a GUI app like a browser or a text editor, the app receives a rich event: “the Enter key was pressed, and the Shift modifier was held.” The operating system sends the full key identity and a bitmask of modifiers. The app can easily distinguish Shift+Enter from plain Enter and do something different with each.
Terminal emulators don’t work this way. They emulate hardware terminals from the 1970s, which communicated over serial lines. A serial line carries bytes, not key events. The bytes are encoded in ASCII, a standard from 1963 that defines 128 characters: printable ones (letters, digits, punctuation) and control characters (newline, tab, carriage return, etc.). When you press Enter, the terminal sends ASCII byte 0x0D (carriage return). When you press Shift+Enter, the terminal sends the same byte 0x0D. The Shift modifier is simply lost. The application on the other end has no way to tell the difference.
This limitation was recognized early on. Over the years, different terminals tried to fix it by sending escape sequences: strings of ASCII bytes starting with ESC [ that together encode a richer event. For example, ESC [ 1 ; 3 A means Alt+Up. The various protocols disagree on the format of these sequences. xterm introduced modifyOtherKeys, encoding Shift+Enter as ESC [ 27 ; 2 ; 13 ~. Paul LeoNerd Evans proposed a cleaner fixterms encoding: ESC [ 13 ; 2 u. The kitty terminal then built on fixterms to create the kitty keyboard protocol, which adds support for even more keys and modifiers. These approaches are partially interoperable at best. An app expecting kitty protocol won’t understand xterm’s encoding, and vice versa. For a deeper dive into why terminal input is so convoluted, Warp has a good blog post on the topic.
Modern terminals like Ghostty implement the kitty keyboard protocol: the terminal and the application negotiate a richer encoding where Shift+Enter gets a distinct sequence. Claude Code and Ghostty speak this protocol, so Shift+Enter works directly.
The problem is that tmux sits between Ghostty and Claude Code, and tmux doesn’t support the kitty keyboard protocol.
Keyboard → Ghostty → tmux → Claude Code
↑
doesn't speak
kitty keyboard protocol
I tried two tmux settings, both dead ends:
extended-keys on: tmux only forwards extended sequences if the app requests them using tmux’s own method. Claude Code requests them using the kitty protocol, which tmux ignores. Nothing happens.extended-keys always: tmux sends the xterm format (ESC [ 27 ; 2 ; 13 ~), but Claude Code expects the kitty format (ESC [ 13 ; 2 u). The raw escape sequence gets printed as garbage text.
There’s an open feature request and a pull request to add kitty keyboard protocol support to tmux, but it hasn’t landed yet.
Migrating to Zellij
Since the Shift+Enter issue is a fundamental tmux limitation, I decided to try Zellij, a modern terminal multiplexer written in Rust that supports the kitty keyboard protocol out of the box.
My tmuxinator workflow was simple: txs in a project directory starts a named session (or attaches to an existing one). The Zellij equivalent is a one-liner that I wrapped with a zsh alias:
alias zxs='zellij attach $(basename "$PWD") -c'
Shift+Enter in Claude Code works immediately, no configuration needed.
I’m still in the process of relearning years of tmux muscle memory. It helps, though, that Zellij supports mouse interaction for things like selecting panes and tabs, which makes the transition less painful.
Bonus: Forward Delete Without a Delete Key
My keyboard doesn’t have a dedicated Delete key. On a Mac keyboard, fn+Backspace sends Forward Delete, but on third-party keyboards the Fn key is typically handled internally by the keyboard firmware and never reaches macOS at all. I added a Ghostty keybinding instead:
keybind = ctrl+shift+backspace=text:\x1b[3~
Now Ctrl+Shift+Backspace sends Forward Delete everywhere.
Unresolved: Num Lock and the Numpad
My keyboard has a numpad, but it only types numbers. Num Lock doesn’t toggle it to send Page Up, Page Down, Home, End, etc. like it would on a full-size PC keyboard. macOS simply doesn’t support Num Lock in the traditional PC sense. The numpad keys always produce digits regardless of any Num Lock state. I haven’t found a fix for this yet, so the Ghostty keybind for Forward Delete above and mouse scrolling in Zellij are my workarounds for the missing keys.