Keybindings are defined with hx.mux.keymap.set({...}) in your config.
local hx = require("hexe")
hx.mux.keymap.set({
{ key = { hx.key.ctrl, hx.key.alt, hx.key.q }, action = { type = hx.action.mux_quit } },
{ key = { hx.key.ctrl, hx.key.alt, hx.key.t }, action = { type = hx.action.tab_new } },
})The key field is a single array containing modifiers and the key, all using hx.key.*:
key = { hx.key.ctrl, hx.key.alt, hx.key.q }
key = { hx.key.ctrl, hx.key.alt, hx.key.shift, hx.key.p }
key = { hx.key.ctrl, hx.key.alt, hx.key.up }
key = { hx.key.ctrl, hx.key.alt, hx.key["1"] } -- number keys
key = { hx.key.ctrl, hx.key.alt, hx.key.dot }
key = { hx.key.ctrl, hx.key.alt, hx.key.comma }Modifiers:
hx.key.ctrlhx.key.althx.key.shifthx.key.super
Named keys:
- Letters:
hx.key.a…hx.key.z - Numbers:
hx.key["0"]…hx.key["9"] - Arrows:
hx.key.up,hx.key.down,hx.key.left,hx.key.right - Punctuation:
hx.key.dot,hx.key.comma,hx.key.space, etc.
Actions trigger mux operations. All available types:
| Action | Description |
|---|---|
hx.action.mux_quit |
Exit the mux |
hx.action.mux_detach |
Detach from session (leave running) |
hx.action.pane_disown |
Orphan current pane |
hx.action.pane_adopt |
Adopt an orphaned pane |
hx.action.pane_close |
Close current float or split pane |
hx.action.pane_select_mode |
Enter pane select/swap mode |
hx.action.split_h |
Split horizontally |
hx.action.split_v |
Split vertically |
hx.action.split_resize |
Resize split (requires dir) |
hx.action.tab_new |
New tab |
hx.action.tab_next |
Next tab |
hx.action.tab_prev |
Previous tab |
hx.action.tab_close |
Close current tab |
hx.action.float_toggle |
Toggle named float (requires float) |
hx.action.float_nudge |
Move float (requires dir) |
hx.action.focus_move |
Move focus (requires dir) |
hx.action.clipboard_copy |
Copy selection to clipboard |
hx.action.clipboard_request |
Paste from clipboard |
hx.action.system_notify |
Send a system notification |
hx.action.sprite_toggle |
Toggle Pokemon sprite overlay |
Actions that take parameters:
{ key = { ... }, action = { type = hx.action.float_toggle, float = "1" } }
{ key = { ... }, action = { type = hx.action.focus_move, dir = "left" } }
{ key = { ... }, action = { type = hx.action.split_resize, dir = "up" } }
{ key = { ... }, action = { type = hx.action.float_nudge, dir = "down" } }Controls what happens to the key after the bind fires:
| Mode | Description |
|---|---|
hx.mode.act_and_consume |
Run action, swallow the key (default) |
hx.mode.act_and_passthrough |
Run action AND forward key to pane |
hx.mode.passthrough_only |
Forward key to pane, no action |
-- default: key is consumed
{ key = { hx.key.ctrl, hx.key.alt, hx.key.t }, action = { type = hx.action.tab_new } }
-- passthrough: forward to pane, no action
{ key = { hx.key.ctrl, hx.key.alt, hx.key.up }, mode = hx.mode.passthrough_only,
when = function(ctx)
local p = ctx.pane(0)
return p and (p.process_name == "nvim" or p.process_name == "vim")
end }
-- both: run action and also send key into pane
{ key = { ... }, mode = hx.mode.act_and_passthrough, action = { type = hx.action.sprite_toggle } }Keys without any binding always pass through unchanged.
Optional condition that must be true for the bind to fire.
when is callback-only:
when = function(ctx)
return ctx.focus_split and ctx.process_name == "nvim"
endctx exposes the current focused pane state.
Pane lookup:
ctx.pane(0)(orctx.pane(nil)) → current focused panectx.pane(<number>)→ pane by runtime index inctx.panes(1-based)ctx.pane(<uuid_string>)→ pane by UUIDctx.pane("focused")/ctx.pane("current")→ current focused panectx.pane("last")→ previously focused pane (if available)ctx.pane("tab:<n>/focus")→ focused split pane for tabn(1-based)ctx.cache.get(key)/ctx.cache.set(key, value, ttl_ms)/ctx.cache.del(key)for callback caching
local p = ctx.pane(0)
if p and p.focus_float then
return true
end
return falsePrefer ctx.pane(0) (or hx.ctx.pane(0) when outside callback-local ctx).
Common pane fields:
| Field | Meaning |
|---|---|
focus_split |
Focused pane is a split |
focus_float |
Focused pane is a float |
process_name |
Foreground process name (for example nvim) |
process_running |
Whether a foreground process is present |
alt_screen |
Terminal is in alt-screen mode |
tab_count |
Number of open tabs |
active_tab |
Active tab index |
float_key |
Float key for focused float pane |
- Set
HEXE_LUA_TRACE=1to trace all callback evaluations. - Set
HEXE_LUA_TRACE=slowto trace only slow evaluations. - Optional threshold:
HEXE_LUA_TRACE_SLOW_MS(default8).
You can register runtime event callbacks through hx.events.
Supported events:
pane_focus_changedtab_changedcommand_finishedpane_shell_running_changedstatusbar_redraw(throttled, default 120ms)
Use the canonical helper API (hx.events.*):
hx.events.on("command_finished", function(ev)
-- ev.command, ev.cwd, ev.status, ev.duration_ms, ev.jobs, ev.pane_uuid
end)
hx.events.on("pane_shell_running_changed", function(ev)
-- ev.pane_uuid, ev.previous_running, ev.running, ev.phase, ev.command, ev.now_ms
end)
hx.events.on("statusbar_redraw", function(ev)
-- ev.now_ms, ev.term_width, ev.term_height, ev.active_tab, ev.tab_count, ev.interval_ms
end)
-- debounce helper (returns wrapped handler)
hx.events.on("statusbar_redraw", hx.events.debounce(250, function(ev)
-- runs at most every 250ms
end))
-- convenience helper
hx.events.once("pane_focus_changed", function(ev)
-- runs only once
end)Pass Ctrl+Alt+Arrow through to nvim/vim, otherwise move focus:
-- passthrough first (evaluated before the fallback)
{ key = { hx.key.ctrl, hx.key.alt, hx.key.up }, when = function(ctx) return ctx.process_name == "nvim" or ctx.process_name == "vim" end, mode = hx.mode.passthrough_only },
{ key = { hx.key.ctrl, hx.key.alt, hx.key.down }, when = function(ctx) return ctx.process_name == "nvim" or ctx.process_name == "vim" end, mode = hx.mode.passthrough_only },
{ key = { hx.key.ctrl, hx.key.alt, hx.key.left }, when = function(ctx) return ctx.process_name == "nvim" or ctx.process_name == "vim" end, mode = hx.mode.passthrough_only },
{ key = { hx.key.ctrl, hx.key.alt, hx.key.right }, when = function(ctx) return ctx.process_name == "nvim" or ctx.process_name == "vim" end, mode = hx.mode.passthrough_only },
-- fallback: move mux focus
{ key = { hx.key.ctrl, hx.key.alt, hx.key.up }, action = { type = hx.action.focus_move, dir = "up" } },
{ key = { hx.key.ctrl, hx.key.alt, hx.key.down }, action = { type = hx.action.focus_move, dir = "down" } },
{ key = { hx.key.ctrl, hx.key.alt, hx.key.left }, action = { type = hx.action.focus_move, dir = "left" } },
{ key = { hx.key.ctrl, hx.key.alt, hx.key.right }, action = { type = hx.action.focus_move, dir = "right" } },Binds are evaluated in order — first match wins.
-- split only when a split is focused
{ key = { hx.key.ctrl, hx.key.alt, hx.key.h }, when = function(ctx) return ctx.focus_split end, action = { type = hx.action.split_h } },
{ key = { hx.key.ctrl, hx.key.alt, hx.key.v }, when = function(ctx) return ctx.focus_split end, action = { type = hx.action.split_v } },{ key = { hx.key.ctrl, hx.key.alt, hx.key["1"] }, action = { type = hx.action.float_toggle, float = "1" } },
{ key = { hx.key.ctrl, hx.key.alt, hx.key["2"] }, action = { type = hx.action.float_toggle, float = "2" } },
{ key = { hx.key.ctrl, hx.key.alt, hx.key["0"] }, action = { type = hx.action.float_toggle, float = "p" } },The float value must match the key field of a float defined in your layout.
Hexa enables the kitty keyboard protocol on startup. Terminals that support it send structured key events (including modifiers on arrows, etc.). Terminals that don't fall back to legacy escape sequences — most binds still work.
Key forwarding for passthrough modes translates to legacy sequences:
- Arrow keys with mods →
ESC [ 1 ; <mod> A/B/C/D - Ctrl+letter → control character (0x01–0x1A)
- Alt+key → ESC prefix
- Shift+Tab →
ESC [ Z
when is also used in status bar segments and shell prompt segments. See statusbar for the full token list available in those contexts.