Skip to content

Makima State — Frontend Integration Guide

Overview

Makima writes $XDG_RUNTIME_DIR/makima-state.json atomically after every relevant input event. The file is updated via rename() so reads are always consistent — no partial writes.

The file moved

Until 0.4 this lived at /tmp/makima-state.json. /tmp is mode 1777, so any local process could create the path first and decide what every reader believed — the same reason the control socket sits in $XDG_RUNTIME_DIR, a per-user tmpfs with mode 0700. The tray and the HUD still fall back to the old path when they find a file there and none in the runtime directory, so a makima from before the move is still read correctly. makima itself falls back to /tmp only when there is no runtime directory at all, which is a bare TTY or a container started without one.

Watch for changes using inotify on the directory, not the file itself (atomic rename creates a new inode each time, so watching the file directly loses events):

inotifywait -m -e moved_to /tmp/ 2>/dev/null | grep --line-buffered "makima-state.json" | while read _; do
  python3 -c "
import json, datetime
d = json.load(open('$XDG_RUNTIME_DIR/makima-state.json'))
t = datetime.datetime.now().strftime('%H:%M:%S.%f')[:-3]
lp = d['trackpads']['lpad']; rp = d['trackpads']['rpad']
ls = d['sticks']['lstick'];   rs = d['sticks']['rstick']
print(f\"{t}  lpad({lp['x']:+.3f},{lp['y']:+.3f}{'T' if lp['touching'] else ' '}{'P' if lp['pressed'] else ' '})  rpad({rp['x']:+.3f},{rp['y']:+.3f}{'T' if rp['touching'] else ' '}{'P' if rp['pressed'] else ' '})  lstick({ls['x']:+.3f},{ls['y']:+.3f}{'*' if ls['active'] else ' '})  rstick({rs['x']:+.3f},{rs['y']:+.3f}{'*' if rs['active'] else ' '}\")
" 2>/dev/null
done

Full Schema

{
  "lifecycle": "ready",
  "errors": {
    "base_config": "parse error in Steam Deck Base.toml line 12: unexpected token"
  },
  "configs": [
    { "name": "Steam Deck Base",          "enabled": true,  "status": "ok",      "errors": [] },
    { "name": "Steam Deck Trackpads", "enabled": true, "status": "ok",    "errors": [] },
    { "name": "Firefox",             "enabled": true,  "status": "warning", "errors": [] },
    { "name": "Konsole",             "enabled": false, "status": "ok",      "errors": [] }
  ],
  "config_roots": {
    "system": "/usr/share/deckery/configs",
    "user": "/home/user/.config/deckery"
  },
  "context": {
    "active_app": "org.mozilla.firefox",
    "config_stack": ["Steam Deck Base", "org.mozilla.firefox"],
    "paused": false,
    "gaming_mode": false,
    "held_modifiers": ["BTN_TL"],
    "active_buttons": ["BTN_TL", "BTN_SOUTH"],
    "active_outputs": [
      { "key": "KEY_LEFTCTRL", "silent": false }
    ],
    "available_modifiers": { "BTN_MODE": { "has_app_combos": false } },
    "analog_state_export": false
  },
  "bindings": {
    "BTN_SOUTH": {
      "action": ["KEY_ENTER"],
      "kind": "remap",
      "label": null,
      "origin": "Steam Deck Base",
      "silent": false
    },
    "BTN_TL-BTN_GRIPR2": {
      "action": ["KEY_LEFTCTRL", "KEY_PAGEDOWN"],
      "kind": "remap",
      "label": "Next Tab",
      "origin": "Steam Deck Base",
      "silent": false
    },
    "BTN_THUMBL": {
      "action": ["deckery-hud-toggle"],
      "kind": "command",
      "label": "Toggle HUD",
      "origin": "Steam Deck Base",
      "no_pause": true
    }
  },
  "modifier_active": {
    "BTN_GRIPR2": {
      "action": ["KEY_LEFTCTRL", "KEY_PAGEDOWN"],
      "kind": "remap",
      "label": "Next Tab",
      "origin": "Steam Deck Base"
    }
  },
  "gaming_mode_trigger": {
    "key": "BTN_BASE",
    "label": "Toggle Gaming Mode"
  },
  "last_action": {
    "type": "keys",
    "value": ["KEY_ENTER"],
    "label": null,
    "ts": 1234567890.123
  },
  "trackpads": {
    "lpad": {
      "mode": "mt-trackpad",
      "x": 0.170,
      "y": 0.039,
      "touching": true,
      "pressed": false
    },
    "rpad": {
      "mode": "mt-trackpad",
      "x": 0.0,
      "y": 0.0,
      "touching": false,
      "pressed": false
    }
  },
  "sticks": {
    "lstick": {
      "mode": "disabled",
      "x": 0.023,
      "y": 0.006,
      "deadzone": 0.031,
      "active": false
    },
    "rstick": {
      "mode": "cursor",
      "x": -0.002,
      "y": 0.002,
      "deadzone": 0.092,
      "active": false
    }
  },
  "imu": {
    "x": 0.512,
    "y": 0.489
  }
}

Fields

lifecycle

String describing the current startup/reinitialisation phase of makima.

Value Meaning
"ready" Normal operation — fully initialised and processing input
"starting" First startup in progress — device grabs not yet complete
"reinitializing" Reinitializing after a device reconnect, device error, or config file change — temporarily amber in the tray
"" File absent or written by an older makima build that doesn't export this field

The tray shows an amber icon whenever lifecycle is "starting" or "reinitialising", even if all services report active.


errors

Object mapping error slot names to human-readable error strings. Empty {} when there are no errors.

Key When present
"base_config" A config file failed to parse — escalated to a top-level error so the tray shows red, not just a marker in the submenu
"no_device" No compatible input device was found — makima is waiting for one to appear

When any key is present the tray shows a red icon, regardless of service state.


configs

Array of all config files known to makima, in the order they appear on disk. Updated whenever configs are loaded, reloaded, or their enabled state changes.

Each entry:

Field Type Meaning
name string Config identifier — the file base name without .toml (e.g. "Steam Deck Base", "Steam Deck Trackpads", "Firefox"). There is no naming convention to decode
enabled bool Whether this config is active. The base config (the one declaring [device]) is always enabled and cannot be toggled by the user.
exclusive_group string \| null Set when this config belongs to a set of mutually exclusive modules. At most one member of a group is enabled at a time; the tray draws them as radio buttons. No member enabled means the whole group is switched off — there is no separate field for that
status string Derived from errors: "ok" when it is empty, "error" when any entry has severity error, "warning" otherwise
errors [{severity, message}] Parse or load problems for this config; empty exactly when status is "ok". Each entry: { "severity": "error" \| "warning", "message": "..." }

"error" means the config is not loaded — nothing it declares is in effect. "warning" means it is loaded and something about it is worth saying. The common warning is a user file that failed to parse: the shipped config of the same name stays in effect, enabled stays true, and the message names the file that was skipped. from_user is not exported, so a consumer cannot tell that case apart from any other warning by the fields alone — the message is what says it.

The tray's Controller Bindings submenu is driven directly from this array. Toggling a config via the tray sends a config enable/disable <name> IPC command, which updates enabled and rewrites this field. Enabling a member of an exclusive_group disables its siblings in the same step. The resulting state is persisted to ~/.config/deckery/preferences.toml, which is re-read on every reload — so a restart and a reload always agree about what is active.


config_roots

The two directories the configs above were read from. Written once at startup and unchanged for the rest of the process; null until then.

Field Type Meaning
system string The shipped configs — /usr/share/deckery/configs for an RPM install, the checkout's configs/ for a git install, or whatever DECKERY_SYSTEM_CONFIG names
user string The user's own configs, normally ~/.config/deckery

Published so a frontend can offer to open either folder without re-deriving the resolution rules — which depend on the install method and on two environment variables, and would drift the moment either changes. The tray hides its Open shipped configs item while this field is null rather than opening a guessed path.


context

Field Type Meaning
active_app string Active app class, e.g. "org.mozilla.firefox". "default" when no app-specific config is loaded.
config_stack [string] Active config name(s). One entry = base config only; two entries = base + app override.
paused bool Makima is paused — no output is emitted. Set when HUD opens.
gaming_mode bool Gaming Mode is active — all remaps suppressed, raw input passed through.
held_modifiers [string] Modifier buttons currently physically held (e.g. ["BTN_TL"]). Empty when no modifier is held. Use this to switch between normal and modifier view.
active_buttons [string] All buttons currently physically held, including non-modifiers. Use this to highlight buttons on the gamepad layout.
active_outputs [{key, silent}] System-level output keys currently being held, resolved from active_buttons + current modifiers. Each entry: { "key": "KEY_LEFTCTRL", "silent": false }.
available_modifiers object Modifier buttons that, if pressed next, would unlock additional combo bindings. Keys are button names; value is { "has_app_combos": bool }. has_app_combos: true means at least one qualifying combo for that modifier comes from an app-specific config override — the HUD signals this with a distinct accent. Use to hint which modifiers are worth showing.
analog_state_export bool Whether analog data (sticks, trackpads) is currently being written into this file.

bindings

Complete map of all configured button actions for the current config.

Key format: - "BTN_SOUTH" — plain binding, no modifier - "BTN_TL-BTN_GRIPR2" — combo: BTN_TL held, BTN_GRIPR2 pressed

Value fields:

Field Type Meaning
action [string] Output keys (remap) or shell commands (command)
kind string "remap" / "command" / "movement"
label string\|null Human-readable binding name, or null if not configured
origin string Config file this binding comes from
silent bool If true, this binding is intentionally hidden from the HUD display
no_pause bool (command only) Fires even when makima is paused

This map is static while the config doesn't change. Reload it when context.config_stack changes.


modifier_active

Subset of bindings — only the combos reachable with the currently held modifiers, keyed by trigger button name only (modifier prefix stripped).

Example: if BTN_TL is held, modifier_active contains all BTN_TL-* entries, keyed as "BTN_GRIPR2" etc.

Empty {} when no modifier is held.

Use this to replace bindings in the display when held_modifiers is non-empty.


gaming_mode_trigger

The configured Gaming Mode toggle button. null if the trigger is disabled (trigger = { key = "disabled" } in config).

Field Type Meaning
key string Button name, e.g. "BTN_BASE"
label string Always "Gaming Mode"

Use this to label the QAM / three-dot button in the HUD without hardcoding the key name.

See Gaming Mode for the full Gaming Mode reference.


last_action

The most recently processed discrete user action. null until the first action occurs.

Field Type Meaning
type string "keys" / "command" / "movement"
value any Output keys array, command string array, or movement description
label string\|null Human-readable label. Set from binding label = config, or generated (e.g. "Gaming Mode On" / "Gaming Mode Off" for Gaming Mode changes).
ts float Unix timestamp (seconds since epoch, millisecond precision)
silent bool When true, the HUD suppresses the toast for this action.

Gaming Mode changes (double-click trigger, IPC commands, Steam auto-detection) always set last_action with type: "command" — even in pause/preview mode, so the HUD can show the action in its preview toast without actually toggling the mode.


trackpads

Always present. Both lpad and rpad are always included regardless of mode.

All position values are normalized to −1.0 … +1.0, rounded to 3 decimal places.

Field Type Meaning
mode string "mt-trackpad", "disabled" — value of [trackpad.left/right] mode
x float Horizontal position −1.0…+1.0. 0.0 when not touching.
y float Vertical position −1.0…+1.0. 0.0 when not touching. Positive = up.
touching bool true when finger is on the pad.
pressed bool true when the pad is physically clicked.

sticks

Always present. Both lstick and rstick are always included regardless of mode.

Field Type Meaning
mode string "disabled" / "cursor" / "scroll" / "bind"
x float Horizontal position −1.0…+1.0.
y float Vertical position −1.0…+1.0.
deadzone float Configured deadzone in normalized space (0.0…1.0). Use as circle radius.
active bool true when either axis exceeds the deadzone.

imu

Gyroscope/accelerometer axes, normalized to 0.0…1.0.

Field Type Meaning
x float ABS_HAT2X axis normalized
y float ABS_HAT2Y axis normalized

Only populated when analog_state_export is active.


Frontend Logic

Which bindings to display

if held_modifiers is non-empty and modifier_active is non-empty:
    display = modifier_active     ← only combos reachable from current modifier
else:
    display = bindings            ← all bindings

Gaming Mode state

if context.gaming_mode:
    show_badge("Gaming Mode")
    // bindings are suppressed — raw input goes to game

QAM / trigger button label

if gaming_mode_trigger != null:
    label(gaming_mode_trigger.key, gaming_mode_trigger.label)

Button highlighting

for each button in layout:
    if button in active_buttons:
        highlight(button, "held")
    elif last_action.value contains mapped_key(button) and recent:
        highlight(button, "just_pressed")
    else:
        unhighlight(button)

Stick visualization

// Draw deadzone circle with radius = stick.deadzone
// Place dot at (stick.x, stick.y)
// Tint ring or dot when stick.active == true

Paused state

if context.paused:
    show_overlay("preview mode — no output")

Update Frequency

  • On every button press and release
  • On modifier state change (held_modifiers changes)
  • On config switch (active window changes)
  • On pause/resume or Gaming Mode change via IPC socket
  • On trackpad position change (when finger is on pad), rate-limited to ~60 Hz
  • On trackpad touch/release — always immediate
  • On trackpad click (press/release)
  • On stick movement, rate-limited to ~60 Hz

Analog writes are skipped entirely if no rounded value has changed.

The file is not polled — only updated on events. Use inotify, not a timer.


Button Name Reference (Steam Deck)

bindings key Physical button
BTN_SOUTH A
BTN_EAST B
BTN_NORTH X
BTN_WEST Y
BTN_TL L1
BTN_TR R1
BTN_TL2 L2 (digital only)
BTN_TR2 R2 (digital only)
BTN_THUMBL L3 (left stick click)
BTN_THUMBR R3 (right stick click)
BTN_SELECT Select / View
BTN_START Start / Menu
BTN_MODE Steam button
BTN_BASE QAM / three-dot button
BTN_GRIPL L5 (upper left back paddle)
BTN_GRIPL2 L4 (lower left back paddle)
BTN_GRIPR R5 (upper right back paddle)
BTN_GRIPR2 R4 (lower right back paddle)
BTN_DPAD_UP/DOWN/LEFT/RIGHT D-Pad