Keymap

A Keymap is one controller model’s table from its odd controls to the kernel key names they should report, written once per model and shared by every Remote of that model. A model needs one only where the base table gets it wrong. The base passes every KEY_* code as itself, turns the hat axes into the arrows, and reads a gamepad’s south and east buttons as enter and back, so a Remote with no Keymap already works.

It is cluster-scoped, the way a DeviceClass and a StorageClass are, because one model’s table is the same in every namespace. A Remote in any namespace names it without a namespace qualifier.

Both sides of the table use evdev’s names, because every Linux controller driver reports the south face button as BTN_SOUTH, whatever is printed on the button, and KEY_VOLUMEUP means one step up of the room’s level from every controller. A Keymap renames a control and nothing more. The right side is a kernel key name, or none to drop the control, and each consumer holds its own table from key names to what they mean there.

apiVersion: media.liken.sh/v1alpha1
kind: Keymap
metadata:
  name: dualsense
spec:
  buttons:
    - press: BTN_NORTH
      key: KEY_VOLUMEUP
      repeat:
        delay: 400ms
        interval: 150ms
    - press: BTN_TR
      key: KEY_FASTFORWARD
      repeat:
        delay: 400ms
        interval: 250ms
    - press: BTN_THUMBR
      key: none
  axes:
    - axis: ABS_HAT0X
      value: 1
      key: KEY_RIGHT

Buttons and axes are separate lists because they bind differently: a button is a press, and an axis entry names a direction as well. A Keymap must bind at least one entry across the two lists.

One controller model’s mapping from the controls that its kernel driver names wrongly to the key names they should report. Write one Keymap per model that needs one, and share it through every Remote of that model.

spec

The table itself, in two lists: buttons for key presses, and axes for the hat directions.

Field Type Required Description
buttons []object no Button entries. Each one renames one control. A control with no entry reports the name the kernel gives it, and the release stops a repeat the press started.
axes []object no Axis entries. A gamepad’s d-pad arrives as the two hat axes rather than as buttons, so each d-pad direction is one entry here.

spec.buttons[]

Button entries. Each one renames one control. A control with no entry reports the name the kernel gives it, and the release stops a repeat the press started.

Field Type Required Description
press string yes The button, by its evdev key name: BTN_SOUTH on a gamepad, KEY_PLAYPAUSE on a media remote. Every Linux driver reports the same button under the same name, so the name works across models. Any name in the kernel’s EV_KEY space is accepted; the pattern catches an obvious typo, and the operator refuses a name that is not in its own table of evdev key names when it compiles the Keymap. A Remote in discovery logs the name of every code its controller reports, so the names come from the log, not from a vendor document. Pattern: ^(BTN|KEY)_[A-Z0-9_]+$.
key string yes The kernel key name this control reports instead, or none to drop it. Both sides are the kernel’s own names. A consumer binds this name and not the control, and none is how a Keymap silences a control that the default mapping would otherwise pass. Pattern: ^((BTN|KEY)_[A-Z0-9_]+|none)$.
repeat object no When present, the Remote’s pod synthesises the repeat while the button is held: it publishes the press, waits the delay, then publishes a repeat every interval until the release. A gamepad button never autorepeats in the kernel, so it needs this block. A keyboard key autorepeats on its own and needs none. One repeat is capped at 30 seconds, because a controller that sleeps mid-hold publishes no release.

spec.buttons[].repeat

When present, the Remote’s pod synthesises the repeat while the button is held: it publishes the press, waits the delay, then publishes a repeat every interval until the release. A gamepad button never autorepeats in the kernel, so it needs this block. A keyboard key autorepeats on its own and needs none. One repeat is capped at 30 seconds, because a controller that sleeps mid-hold publishes no release.

Field Type Required Description
delay string no How long to hold before the repeat starts, as a duration like 400ms. A tap shorter than this does not repeat. Defaults to 400ms. Pattern: ^[0-9]+(\.[0-9]+)?(ms|s)$.
interval string no How often to publish the repeat while the button is held, as a duration like 300ms. Defaults to 300ms. Pattern: ^[0-9]+(\.[0-9]+)?(ms|s)$.

spec.axes[]

Axis entries. A gamepad’s d-pad arrives as the two hat axes rather than as buttons, so each d-pad direction is one entry here.

Field Type Required Description
axis string yes One of the two hat axes, X across and Y down. The analog sticks are not bindable: a resting thumb reports hundreds of times a second, and no key name carries an analog value. One of: ABS_HAT0X, ABS_HAT0Y.
value integer yes Which direction of the axis this entry binds. The hat reports -1 and 1 as its two presses and 0 as the release, and the release stops a repeat the press started. One of: -1, 1.
key string yes The kernel key name this direction reports, or none to drop it. The default mapping already names both directions of both hats as the arrows, so an entry here is for a pad that means something else by them. Pattern: ^((BTN|KEY)_[A-Z0-9_]+|none)$.
repeat object no The same repeat block the buttons carry, for a hat direction. The default mapping already repeats the four arrows, so an entry needs this only where it names a key of its own.

spec.axes[].repeat

The same repeat block the buttons carry, for a hat direction. The default mapping already repeats the four arrows, so an entry needs this only where it names a key of its own.

Field Type Required Description
delay string no How long to hold before the repeat starts, as a duration like 400ms. Defaults to 400ms. Pattern: ^[0-9]+(\.[0-9]+)?(ms|s)$.
interval string no How often to publish the repeat while the direction is held, as a duration like 300ms. Defaults to 300ms. Pattern: ^[0-9]+(\.[0-9]+)?(ms|s)$.

No status

Nothing reports on a Keymap, so it has no status subresource, and kubectl get keymaps shows each table’s age. The operator compiles each table on every pass. A press, axis, or key name that is not an evdev name fails the compile, the operator logs the failure, and every Remote that names the Keymap keeps the last good table.

On the bus

A Keymap owns no topic of its own. The operator folds the base table with the Keymap and publishes the result retained on the keys topic of each Remote that names it, under the Remote’s tree . A Keymap that does not compile publishes nothing and leaves the last good table on each of those topics. A deleted Keymap leaves each of its Remotes on the base alone, and the operator republishes their tables on the next pass.