A Keymap is one controller model’s table from buttons and axes to
named actions, written once per model and shared by every
Remote of that model. 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.
The left side of the table uses evdev’s names, because every Linux
controller driver reports the south face button as BTN_SOUTH,
whatever is printed on the button. The right side names what a
person means, pause or seek, never an mpv command, so a
different player program can implement the same table later.
apiVersion: media.liken.sh/v1alpha1
kind: Keymap
metadata:
name: dualsense
spec:
buttons:
- press: BTN_SOUTH
action: pause
- press: BTN_TR
action: seek
amount: 30
repeat:
delay: 400ms
interval: 300ms
axes:
- axis: ABS_HAT0X
value: 1
action: 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 table from buttons and axes to named actions. Write one Keymap per model, and share it through every Remote of that model.
spec
The table itself, in two lists: buttons for key presses, and axes for the d-pad’s hat directions.
| Field | Type | Required | Description |
|---|---|---|---|
buttons |
[]object | no | Button entries. Each one answers the press. A held button fires once unless it names a repeat, 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 answers the press. A held button fires once unless it names a repeat, and the release stops a repeat the press started.
| Field | Type | Required | Description |
|---|---|---|---|
press |
string | yes | The button, by its evdev key name, such as BTN_SOUTH. Every Linux controller driver reports the same position under the same name, so the name works across models. The names this operator accepts: BTN_SOUTH, BTN_EAST, BTN_C, BTN_NORTH, BTN_WEST, BTN_Z, BTN_TL, BTN_TR, BTN_TL2, BTN_TR2, BTN_SELECT, BTN_START, BTN_MODE, BTN_THUMBL, and BTN_THUMBR. |
action |
string | yes | What the press does, in this operator’s vocabulary. The words name what a person means, never a player program’s own command. Most actions command the player. The navigation actions, up, down, left, right, select, and back, drive the on-screen display. cycle-focus switches which unit a shared controller holds. One of: pause, mute, seek, volume, chapter, subtitles, audio, info, cycle-focus, up, down, left, right, select, back. |
amount |
integer | no | How far the action moves: seconds for seek, a step for volume and chapter. The sign is the direction, so one action serves both bumpers. Exactly these three actions take an amount; the rest refuse one. |
repeat |
object | no | When present, the action repeats while the button is held. The player pod fires it on the press, waits the delay, then re-fires it every interval until the release. Omit it and the button fires once per press. A repeat works on any action, so a held seek scrubs and a held volume ramps. One repeat is capped at 30 seconds, because a controller that sleeps mid-hold publishes no release. |
spec.buttons[].repeat
When present, the action repeats while the button is held. The player pod fires it on the press, waits the delay, then re-fires it every interval until the release. Omit it and the button fires once per press. A repeat works on any action, so a held seek scrubs and a held volume ramps. 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 re-run the action 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 action takes 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. |
action |
string | yes | What the press does, in this operator’s vocabulary. The words name what a person means, never a player program’s own command. Most actions command the player. The navigation actions, up, down, left, right, select, and back, drive the on-screen display. cycle-focus switches which unit a shared controller holds. One of: pause, mute, seek, volume, chapter, subtitles, audio, info, cycle-focus, up, down, left, right, select, back. |
amount |
integer | no | How far the action moves: seconds for seek, a step for volume and chapter. The sign is the direction. Exactly these three actions take an amount; the rest refuse one. |
repeat |
object | no | When present, the action repeats while the hat direction is held. The player pod fires it on the press, waits the delay, then re-fires it every interval until the release. Omit it and the direction fires once per press. One repeat is capped at 30 seconds, because a controller that sleeps mid-hold publishes no release. |
spec.axes[].repeat
When present, the action repeats while the hat direction is held. The player pod fires it on the press, waits the delay, then re-fires it every interval until the release. Omit it and the direction fires once per press. 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 re-run the action while the hat 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
a table before it creates any pod. A press or action name
outside the vocabulary above fails the Play that uses the table,
and the reason appears on that Play’s status.
On the bus
Each Keymap owns one retained topic on the
bus, under the cluster’s topic base:
keymaps/<name>
The topic drops the namespace segment because a Keymap is
cluster-scoped. The operator is the only writer. It compiles the
table’s names down to numbers and publishes the whole table as one
JSON array, retained, so a translator reads the current table the
instant it connects, and a Keymap edit reaches a running
translator with no pod restart. The example above compiles to:
[{"type": 1, "code": 304, "value": 1, "action": "pause"},
{"type": 1, "code": 311, "value": 1, "action": "seek", "amount": 30,
"repeatDelay": 400, "repeatInterval": 300},
{"type": 3, "code": 16, "value": 1, "action": "right"}]
Each row is one binding: an evdev event type, code, and value
on the left, an action and its amount on the right. A button
compiles to EV_KEY (type 1) with value 1, the press alone. An axis
compiles to EV_ABS (type 3) with the value the entry states.
repeatDelay and repeatInterval are milliseconds, and both are
absent on a binding that fires once. The translator matches numbers
and parses no name.
The operator republishes a topic only when the compiled table
differs from the last one it wrote, because a new subscriber reads
the retained value from the broker. A Keymap that does not compile
publishes nothing and leaves the last-good table in place, so a
broken edit does not empty a running translation, and the operator
logs the failure. When a Keymap is deleted, the operator clears the retained
value with an empty publish, so a deleted table leaves nothing
behind on the bus.