Back to Blog
engineeringarchitecture

Keyboard Shortcut Mirroring: Your Terminal Shortcuts, From the Overlay

Callipso now mirrors Cmd+T, Cmd+W, Cmd+D and other shortcuts across iTerm2, Ghostty, Warp, Kitty, and Terminal.app — no window switching required.

Callipso TeamMarch 7, 20268 min read

Keyboard Shortcut Mirroring: Your Terminal Shortcuts, From the Overlay

Callipso sits on top of your terminal apps. You click terminals, send prompts, watch agents work. But there was one thing that always forced you back to the underlying app: keyboard shortcuts. Want to open a new tab? Cmd+T does nothing on the overlay — it is an Electron window, not iTerm2. You have to click through to iTerm2, hit Cmd+T there, then come back. Same for splitting panes, closing tabs, opening windows.

We fixed that. Callipso now intercepts your terminal app's keyboard shortcuts and executes them through the real adapter layer. Cmd+T on the overlay creates a tab in whatever app owns the currently selected terminal. Cmd+W closes it. Cmd+D splits it. All five terminal apps. Zero window switching.

How It Works

The system has three layers: shortcut maps, a key event interceptor, and per-app executors.

Shortcut Maps

Each terminal app has a static shortcut definition file that declares what keyboard shortcuts it supports:

iTerm2:       Cmd+T (new tab), Cmd+W (close), Cmd+D (split vertical),
              Cmd+Shift+D (split horizontal), Cmd+] / Cmd+[ (navigate panes)

Ghostty:      Cmd+T (new tab), Cmd+W (close), Cmd+D (split right),
              Cmd+Shift+D (split down), Cmd+N (new window)

Warp:         Cmd+T (new tab), Cmd+W (close)

Kitty:        Cmd+T (new tab), Cmd+W (close), Cmd+N (new window)

Terminal.app: Cmd+N (new terminal), Cmd+W (close)

Each shortcut is either supported (the adapter can execute it) or unsupported (the overlay shows a toast explaining it cannot be mirrored). This distinction is important — we do not pretend to support things we cannot do. Cmd+F (Find) requires the app's native UI; we cannot replicate that from the overlay, so we say so.

Key Event Interception

Electron's before-input-event on webContents fires before any default behavior. When you press Cmd+T, the KeybindingManager receives the raw input event, builds an accelerator string ("Cmd+T"), and checks it against the shortcut map for the active terminal's app.

The active app is resolved from the store: activeTerminalIdterminal.windowIdwindow.appId. If the active terminal belongs to iTerm2, the iTerm2 shortcut map is used. If it belongs to Ghostty, the Ghostty map is used. No manual switching.

Keyboard Event → buildAccelerator() → resolve active app
                                    → look up shortcut map
                                    → find matching shortcut
                                    → dispatch to per-app executor

Shortcuts that are not in any map pass through to Electron's default handling. This means Cmd+C, Cmd+V, and other system shortcuts work normally — we only intercept what we explicitly define.

Per-App Executors

Each app has its own executor method that translates the abstract action name into the correct coordinator call:

App"createTab""closeTab""splitRight"
iTerm2iterm2Manager.createSession(windowId)iterm2Manager.closeSession(sessionId)iterm2Manager.splitPane(sessionId, 'vertical')
GhosttyghosttyCoordinator.createTerminal()ghosttyCoordinator.closeTerminalByUniqueId(id)ghosttyCoordinator.splitRight()
WarpwarpCoordinator.createTerminal()warpCoordinator.closeTerminal()n/a
KittykittyCoordinator.createTerminal()kittyCoordinator.closeTerminalByUniqueId(id)n/a
Terminal.appterminalAppCoordinator.createTerminal()terminalAppCoordinator.closeTerminal(index)n/a

The executors handle the impedance mismatch between apps. iTerm2 close needs an AppleScript session ID. Ghostty and Kitty close need a uniqueId resolved from the terminal entity in the store. Warp close operates on the current tab. Terminal.app close uses a local index. The executor resolves the right identifier from the store and calls the right method.

The Cmd+K Palette

Alongside direct shortcut mirroring, we added a command palette. Press Cmd+K on the overlay and a searchable palette appears showing every registered shortcut, grouped by app.

The active app's shortcuts appear first, highlighted. You can arrow through the list, type to filter, and press Enter to execute. Clicking works too.

The palette fetches two IPC channels on open: get-all-shortcut-maps (every app's shortcuts) and get-shortcut-map (the active app). The active app header updates dynamically — select an iTerm2 terminal and reopen Cmd+K, and the header reads "Active: iTerm2" with iTerm2's shortcuts at the top.

This is useful for discovery. If you are new to Callipso and wondering which shortcuts work from the overlay, Cmd+K shows you everything in one place.

Config File Reading

Not everyone uses default keybindings. If you have remapped Cmd+T to something else in Ghostty or Kitty, the hardcoded shortcut map would be wrong.

Callipso reads your terminal app's config files at startup:

AppConfig PathFormat
Ghostty~/.config/ghostty/configkeybind = cmd+t=new_tab
Kitty~/.config/kitty/kitty.confmap cmd+t new_tab
iTerm2~/Library/Preferences/com.googlecode.iterm2.plistBinary plist (attempted, falls back to defaults)
Warpn/aNo user-configurable keybindings file
Terminal.appn/aNo user-configurable keybindings file

The config readers parse each line, map the app's key names to accelerator format (cmd+t becomes Cmd+T), and map the app's action names to Callipso's internal action names (new_tab becomes createTab). Custom keybindings are merged onto the hardcoded defaults — your config wins when there is a conflict.

If the config file does not exist, or parsing fails, the hardcoded defaults are used silently. No errors, no warnings to the user. The config reading is opportunistic.

The iTerm2 reader is a special case. iTerm2 stores keybindings in a binary plist with hex-encoded key codes — an undocumented format that is fragile to parse. We attempt to read it, but fall back to defaults if anything fails. Full iTerm2 config parsing may come later if there is demand.

The DevTools Conflict

There was a subtle bug in the MVP. Cmd+Shift+D was registered as a global shortcut for the DevTools toggle via Electron's globalShortcut.register(). Global shortcuts fire before before-input-event, which means the KeybindingManager never saw Cmd+Shift+D — it was consumed by the DevTools handler first.

This mattered because both iTerm2 (split horizontal) and Ghostty (split down) use Cmd+Shift+D. The fix was simple: move the DevTools toggle to Cmd+Shift+F12, freeing Cmd+Shift+D for the keybinding system. F12 is the standard DevTools key in browsers, so this is a more natural binding anyway.

What We Did Not Build

A few things we intentionally left out:

Live config watching. We read configs at startup, not on every change. If you remap a keybinding while Callipso is running, you need to restart for it to take effect. Adding fs.watch() with debouncing is straightforward, but we want to see if anyone actually needs it before adding the complexity.

Custom shortcut editor. There is no UI for remapping shortcuts within Callipso itself. The shortcuts mirror what your terminal app expects. If you want different shortcuts, change them in your terminal app's config — Callipso will pick them up.

Cross-app shortcut normalization. iTerm2 uses Cmd+D for vertical splits. Ghostty uses Cmd+D for right splits. These are conceptually similar but not identical. We mirror each app's actual behavior rather than trying to normalize across apps. The palette shows you exactly what each shortcut does for each app.

What We Learned

  1. Global shortcuts and before-input-event do not compose. Electron's globalShortcut fires first, always. If you need both systems, you must not register the same key in both. This is not documented prominently.

  2. Each terminal app has its own close semantics. iTerm2 needs an AppleScript session ID. Ghostty needs a unique surface ID. Kitty needs a unique window ID. Warp closes the current tab (no argument). Terminal.app needs a local index. There is no universal "close" — every adapter has its own interface.

  3. Unsupported shortcuts need explicit handling. Silently ignoring Cmd+F when a Ghostty terminal is selected is confusing — the user presses the shortcut and nothing happens. Showing a toast that says "Find — not available via overlay" is one line of code and eliminates the confusion entirely.

  4. Config file parsing is 90% mapping tables. The actual file reading is trivial. The work is mapping every app's key names (super, cmd, ctrl, control) and action names (new_tab, close_surface, close_tab, close_window) to a common format. Each app uses different names for the same concepts.

Fifteen files. 854 lines added. Five apps. One Cmd+K. Your muscle memory works from the overlay now.

Share: