Keyboard remapping¶
GISNIX ships one keyboard layer built on kanata, a userspace remapper that reads raw keyboard events and rewrites them before X11/Wayland ever sees them. It applies to every keyboard on the machine — there is nothing to configure per board unless you plug in something with a genuinely different physical layout (see Adding a second keyboard below).
kanata sits between the keyboard and every application, so the same remapping — and the same hold-to-talk speech-to-text — works everywhere:
It ships in the services-device-input-kanata bundle, which is on by
default on every GISNIX install — including a minimal one — so the
home-row modifiers work the moment you first log in, with nothing to set
up. You only lose it if you deliberately remove the line from
hosts/<name>/config.nix. It needs no vendor hardware and is a separate
bundle from services-device-input (Bazecor, OpenRazer, Piper), which
stays opt-in.
Home-row modifiers¶
Tap a home-row key and it types the letter. Hold it past 500ms and it becomes a modifier instead. Hold two together and they stack.
| Key | Tap | Hold |
|---|---|---|
a |
a | Super |
s |
s | Alt |
d |
d | Ctrl |
f |
f | Shift |
j |
j | Shift |
k |
k | Ctrl |
l |
l | Alt |
; |
; | Super |
Holding d and f together, for instance, gives you Ctrl+Shift — the two
modifiers combine the same way pressing two physical modifier keys would.
A held modifier applies to whatever key you press next on either hand, so
"hold d, tap c" is Ctrl+C.
The 500ms hold window is deliberate: it is well past the length of an
ordinary keystroke, so nothing you type in the normal course of typing
gets mistaken for a modifier. If you find yourself typing fast enough to
trip it — or slow enough that a real hold feels sluggish — the timeout is
modHoldTimeout in kanata-config.nix.
Bracket chords¶
Press two adjacent keys together — a genuine press-together within 40ms, not a fast roll — and you get a bracket instead of two letters.
| Chord | Types |
|---|---|
q+w |
{ |
o+p |
} |
a+s |
[ |
l+k |
] |
x+z |
< |
m+, |
> |
a+s and l+k double as home-row mods (Super/Alt, Shift/Ctrl) — press
them more than 40ms apart, which is how you'd normally hold a modifier
anyway, and they behave exactly as the mod table above describes. Only a
genuine press-together inside the window fires the bracket.
The chord lines live in chords-us.kbd / chords-pt.kbd next to
kanata-keyboard.nix, which picks between them by the host's
kanataLayout. Bigram-to-word expansion — another chord type kanata
supports, where typing io finishes it as ion — is not enabled by
default, since it fires mid-word on ordinary typing. Pass your own
expansionsFile to kanata-config.nix if you want it.
Clipboard holds¶
Hold x, c, or v instead of tapping it, and you get cut, copy, or
paste. Tap normally and you still get the letter.
| Key | Tap | Hold |
|---|---|---|
x |
x | Ctrl+X (cut) |
c |
c | Ctrl+C (copy) |
v |
v | Ctrl+Shift+V (paste) |
Paste is Ctrl+Shift+V, not the more common Ctrl+V, because Ctrl+Shift+V
works in a terminal and plain Ctrl+V doesn't. Cut and copy keep their
ordinary bindings, so holding c in a terminal still sends SIGINT via
Ctrl+C, same as tapping it always has.
Navigation layer¶
Hold Space and the layout underneath your left hand becomes a mouse; your right hand becomes arrow keys and paging.
| Key | Action |
|---|---|
e |
mouse up |
s |
mouse left |
d |
mouse down |
f |
mouse right |
w |
left click |
r |
right click |
t |
scroll up |
g |
scroll down |
h j k l |
left / down / up / right (arrow keys) |
n |
Home |
u |
Page Down |
i |
Page Up |
o |
End |
m, ,, . |
mouse speed: half, quarter, tenth |
Release Space or Menu and the layer disappears; every key underneath goes back to typing normally.
Push-to-talk (voxtype)¶
Hold physical right Ctrl and speak; release it and whatever you said gets typed at your cursor. This is voxtype, installed and running by default alongside kanata.
Right Ctrl is the trigger because it is on every keyboard, where the
Menu key is not — the Framework 16's built-in board, for one, has none —
so the same gesture works on any machine. Tapping right Ctrl still sends a
normal Ctrl press, so it stays usable as a modifier; only the hold is
repurposed. One trade-off comes with that: a fast Ctrl+<key> chord typed
specifically through the right Ctrl key can be read as a hold (another
key pressed while it's down) and start push-to-talk instead of applying
the modifier. Left Ctrl is untouched, so every shortcut still works
through that key — reach for the left one for chords.
Transcription runs entirely on the machine's CPU, via
whisper.cpp. No GPU or NPU is
needed or used, so it works the same on a plain laptop as on a workstation
with a graphics card, and nothing you say leaves the machine. (voxtype can
also send audio to a remote API, but GISNIX does not configure that mode,
so it is never in play here.) The default model, base.en, is chosen to
transcribe quickly on an ordinary CPU while staying accurate enough for
dictation; it is fetched once, the first time the machine has network
after install, by a voxtype-model-loader service that runs before the
daemon starts. Holding right Ctrl on a machine that has never been online
does nothing until that download finishes. After the model is cached,
everything is offline, including on later boots with no network at all.
A short sound plays on press (recording started) and a different one on release (recording stopped) — audible confirmation you don't have to watch the screen for, and a clear signal for when it's not recording (no sound on press means the daemon isn't running — see below).
If nothing happens when you hold Menu or right Ctrl, check both services:
A voxtype-model-loader stuck as activating (or restarting) means it's
still waiting on the network, or waiting on it to come back — it retries
every 30 seconds. voxtype itself won't start clean until the loader has
finished at least once.
See voxtype's own configuration reference for changing the speech model, language, or output behaviour — GISNIX ships it with upstream's defaults.
herdr layer¶
Hold Caps Lock and hjkl drive herdr. The base bundle installs herdr on
every machine, so this layer is there with nothing to turn on. A tap still
toggles Caps Lock as normal.
| Key | Action |
|---|---|
h |
previous tab |
l |
next tab |
j |
next workspace |
k |
previous workspace |
u |
down the agent list |
i |
up the agent list |
n |
new tab |
s |
edit scrollback — opens the pane's history in $EDITOR for keyboard-only selection and copy |
r |
toggle kanata's own macro recorder (not herdr's) — press once to start, again to stop; a click plays either way |
p |
play back the recorded macro |
e |
types your email address, if you've set one (see below) |
herdr's own previous agent/next agent binds ship unbound; the base
bundle's dotfiles/herdr/config.toml binds them to prefix+u and prefix+i
so u/i above have something to send. Leave that file alone if you
touch this layer — it is herdr's contract, read once at startup.
The email key¶
e is silent until you tell it whose keyboard this is. Add a line to your
own user file:
Rebuild, and holding Caps and pressing e types that address. Nothing is
hardcoded per machine: the key resolves the active login session to a
username at press time, then looks that username up in
kartoza.userEmails — so on a shared machine, tim's hold types
tim@example.com and alice's hold types whatever she set in
users/alice.nix, from the same physical key. An account with no entry
here gets silence when e is pressed.
Unmapped characters refuse rather than guess: the generated script only
emits keycodes for a-z, 0-9, ., @, and -, so an email address
using anything else won't type at all rather than typing something close
but wrong. @ and - sit on different physical keys under us vs pt,
and the script picks the right one from kanataLayout.
What is not here by default: an aerc (mail client) macro set —
compose, reply, file to folders, contacts. GISNIX does not install aerc,
so this stays a separate opt-in (kartoza.kanata.aercLayer = true;) for
a host that actually runs it, rather than shipping mail-client keybinds
to everyone by default.
aerc mode (opt-in)¶
A host with kartoza.kanata.aercLayer = true; gets a second thing the
herdr trigger key can reach: hold it, and — instead of herdr — you get
aerc commands on the same hjkl-shaped layout (switch account, switch
folder, file to spam/archive, compose, reply-all, and more).
Which one holding the trigger key reaches is a persistent choice, not something you pick each time: hold the trigger key, tap Space while still holding it, and release — that's the toggle. It doesn't change anything about the current hold; it changes which layer the trigger key reaches the next time you hold it, and plays a short beep so the switch has feedback beyond memory. Toggle again (same gesture, from inside the other layer) to go back.
Layout diagrams¶
Here is the whole layout drawn out, so you can see where everything sits.
There are three layers, and one set of diagrams per kanataLayout value —
US ANSI (the default) and pt-PT ISO. They are generated straight from the
same key tables kanata-config.nix uses (gisnix keyboard-diagrams), so
they always match what the machine actually does.
The base layer is what you type on normally. Its trick is the home-row modifiers: hold A for Super, S for Alt, D for Ctrl, F for Shift — and the mirror image on the right hand, J Shift, K Ctrl, L Alt, ; Super. Tap those keys and they type their letter as usual; only holding turns them into a modifier, so your fingers never leave the home row to reach for Ctrl or Alt. The diagram also shows the keys that reach the other layers on a hold: Space or Menu for navigation, Caps for the herdr layer, and right Ctrl for voxtype push-to-talk.
The navigation layer (hold Space or Menu) turns the right hand into arrow keys and mouse controls without leaving the keyboard.
The herdr layer (hold Caps) is the one the herdr clipboard-history
tool listens on.
Toggling it off¶
kanata-toggle # disable/re-enable remapping for every kanata instance
kanata-status # which instances are running
kanata-debug # service status, input devices, recent logs
A raw, unremapped keyboard is sometimes what you want — troubleshooting a
game that reads raw scancodes, or handing the machine to someone who
doesn't use this layout. kanata-toggle stops the daemon; the physical
keyboard reverts to whatever it would type without it, and toggling again
turns it back on.
Adding a second keyboard¶
The default instance matches every keyboard on the system, so a second ordinary, row-staggered board picks up the same home-row mods and navigation layer as the first the moment you plug it in.
Write a separate kanata instance only when a board's physical layout doesn't match a standard keyboard closely enough for the shared layer to make sense on it — an ortholinear or split board, one you want to keep at its factory layout, or one that should carry its own chord set. Run:
It lists connected keyboards with their device paths and prints a
services.kanata.keyboards.<name> block scoped to the one you pick, ready
to paste into your host's own configuration.