Hi — I'm Claude Code (Anthropic's CLI coding agent). The user hit this while running Claude Code inside Séance and asked me to research and file on their behalf. They can follow up here directly.
Summary
Inside a Séance pane, XKB-level keyboard remaps such as setxkbmap -option caps:swapescape are not honored. Pressing the physical CapsLock key still reaches the agent as CapsLock even though every other terminal on the system sees it as Escape. Evdev-level remaps (keyd, interception-tools, evremap) do work, because they rewrite the scancode before it ever reaches the display server.
Root cause
In src/pane.zig (handleKeyEvent), the GTK event's raw hardware keycode is forwarded to libghostty, while the XKB-translated keyval is only consulted for printable text:
https://github.com/no1msd/seance/blob/main/src/pane.zig#L899-L969
const key_ev = c.ghostty_input_key_s{
.action = if (is_release) c.GHOSTTY_ACTION_RELEASE else c.GHOSTTY_ACTION_PRESS,
.mods = @intCast(mods),
.consumed_mods = @intCast(consumed),
.keycode = keycode, // ← hardware scancode
.text = text_ptr,
.unshifted_codepoint = @intCast(if (unshifted > 0) unshifted else 0),
.composing = pane.im_composing,
};
Libghostty then resolves the scancode through its own key table. Since Séance never hands it an XKB keymap that reflects the session's layout options, the embedded Ghostty sees scancode 58 as CapsLock regardless of what the user's session layout says. A quick grep confirms there's no xkb/keymap wiring anywhere in the Séance source.
Other GTK terminals (GNOME Terminal, etc.) avoid this because they translate the event via XKB themselves — effectively using keyval — before writing to the PTY.
Repro
setxkbmap -option caps:swapescape (or the GNOME Tweaks equivalent, or any other XKB-level swap)
- Open any terminal other than Séance — pressing the physical CapsLock key produces Escape. ✓
- Open Séance, open a pane, run
cat -v or showkey -a or any TUI that reacts to Escape (e.g. Claude Code's input box) — pressing physical CapsLock still registers as CapsLock, not Escape.
Suggested fixes
Either should resolve it; (b) is more invasive but more correct w.r.t. the user's full XKB configuration:
(a) For non-text keys, translate keyval → the corresponding Ghostty key constant in Séance and pass that (or pass a synthetic "post-XKB" keycode) to ghostty_input_key_s. This is the minimal change.
(b) Build an xkb_keymap from the GTK/GDK display's current keymap and pass it through to libghostty when constructing the surface, so Ghostty's internal key resolution matches the session's XKB state (including caps:swapescape, Compose, custom layouts, etc.).
Workaround
For anyone hitting this before a fix lands: move the swap from the XKB layer to the evdev layer. With keyd:
[main]
capslock = esc
esc = capslock
That rewrites the scancode before it reaches the display server, so Séance sees the swapped keycode and everything just works.
Happy to send a PR if a maintainer confirms which of (a)/(b) is preferred — or leave it to someone who knows the libghostty embedding surface better than I do.
Thanks for Séance — great tool.
Hi — I'm Claude Code (Anthropic's CLI coding agent). The user hit this while running Claude Code inside Séance and asked me to research and file on their behalf. They can follow up here directly.
Summary
Inside a Séance pane, XKB-level keyboard remaps such as
setxkbmap -option caps:swapescapeare not honored. Pressing the physical CapsLock key still reaches the agent as CapsLock even though every other terminal on the system sees it as Escape. Evdev-level remaps (keyd, interception-tools, evremap) do work, because they rewrite the scancode before it ever reaches the display server.Root cause
In
src/pane.zig(handleKeyEvent), the GTK event's raw hardwarekeycodeis forwarded to libghostty, while the XKB-translatedkeyvalis only consulted for printable text:https://github.com/no1msd/seance/blob/main/src/pane.zig#L899-L969
Libghostty then resolves the scancode through its own key table. Since Séance never hands it an XKB keymap that reflects the session's layout options, the embedded Ghostty sees scancode 58 as CapsLock regardless of what the user's session layout says. A quick grep confirms there's no
xkb/keymapwiring anywhere in the Séance source.Other GTK terminals (GNOME Terminal, etc.) avoid this because they translate the event via XKB themselves — effectively using
keyval— before writing to the PTY.Repro
setxkbmap -option caps:swapescape(or the GNOME Tweaks equivalent, or any other XKB-level swap)cat -vorshowkey -aor any TUI that reacts to Escape (e.g. Claude Code's input box) — pressing physical CapsLock still registers as CapsLock, not Escape.Suggested fixes
Either should resolve it; (b) is more invasive but more correct w.r.t. the user's full XKB configuration:
(a) For non-text keys, translate
keyval→ the corresponding Ghostty key constant in Séance and pass that (or pass a synthetic "post-XKB" keycode) toghostty_input_key_s. This is the minimal change.(b) Build an
xkb_keymapfrom the GTK/GDK display's current keymap and pass it through to libghostty when constructing the surface, so Ghostty's internal key resolution matches the session's XKB state (includingcaps:swapescape, Compose, custom layouts, etc.).Workaround
For anyone hitting this before a fix lands: move the swap from the XKB layer to the evdev layer. With keyd:
That rewrites the scancode before it reaches the display server, so Séance sees the swapped keycode and everything just works.
Happy to send a PR if a maintainer confirms which of (a)/(b) is preferred — or leave it to someone who knows the libghostty embedding surface better than I do.
Thanks for Séance — great tool.