Skip to Content
DocsBuilding extensions

Building extensions

The Deck itself is coming soon — it isn’t part of the Gatecaster beta, which ships the touch driver. The manifest format below is stable and you can author against it today; the host that renders your tiles arrives in a later release.

Extensions add tiles to the Deck. An extension is a folder containing a JSON manifest (plus any optional assets it references), installed at:

~/Library/Application Support/Gatecaster/Extensions/<id>/manifest.json

<id> is a reverse-DNS identifier — e.g. com.gatecaster.nowplaying. The folder name should match the manifest’s id. Extensions run only inside the Deck, but they are free to author and test locally: drop a folder into the directory above and reload.

What ships today vs. what’s on the roadmap. The declarative layer is available now: the manifest (schema v1 and v2), fields, buttons, named actions, and refresh polling. The push provider runtime, WebView presentation, OAuth / secrets injection, the capability enforcement model, and the registry / distribution flow are designed but not yet implemented — they are specified in the app repo’s docs/Plugins/PLATFORM_SPEC.md and are called out inline below. A manifest may declare the roadmap fields (the loader decodes them tolerantly), but the host does not yet act on them. Do not ship a pack that depends on them.

The manifest

The current schema is v2, a superset of v1. The manifest’s v field records the version: absent or 1 means v1, 2 means v2. Every v2 field is additive — a v1 manifest decodes with all of them absent and behaves exactly as before. On load the host runs a normalized() migration step that promotes a v1 manifest to the v2 shape (wraps it as a declarative view, treats a refresh with no parse as JSON), so v1 manifests keep working with no author action.

Core fields

FieldTypeNotes
idstringRequired. Reverse-DNS, unique.
namestringRequired. Tile header title.
symbolstringSF Symbol name for the header icon.
colorHexstringHeader icon tint, e.g. "#32D74B".
minW / minHintSmallest span (in grid cells) the tile renders well at.
defaultW / defaultHintSpan when first dropped (defaults to min).

A complete declarative tile (this is the bundled Apple Music example, a v1 manifest that auto-migrates):

{ "id": "com.gatecaster.nowplaying", // required, reverse-DNS "name": "Apple Music", // required, header title "symbol": "music.note", // SF Symbol "colorHex": "#32D74B", "minW": 2, "minH": 1, // smallest sensible span "defaultW": 3, "defaultH": 2, // span on first drop "fields": [ { "label": "Track", "refreshKey": "title" }, { "label": "Artist", "refreshKey": "artist" } ], "buttons": [ { "symbol": "backward.fill", "action": { "kind": "media", "value": "previous" } }, { "symbol": "playpause.fill", "action": { "kind": "media", "value": "playpause" } }, { "symbol": "forward.fill", "action": { "kind": "media", "value": "next" } } ], // Poll a shell command every 5s; its JSON stdout feeds the refreshKeys above. "refresh": { "command": "osascript -e 'tell application \"Music\" to set t to name of current track & \"|\" & artist of current track' 2>/dev/null | awk -F'|' '{printf \"{\\\"title\\\":\\\"%s\\\",\\\"artist\\\":\\\"%s\\\"}\", $1, $2}'", "everySeconds": 5 } }

Fields

A fields array renders labelled values down the tile. Each entry:

KeyNotes
labelThe row label.
refreshKeyKey into the refresh JSON (or provider state). The live value renders.
valueStatic text shown when there is no refreshKey.
type"text" (default) · "image" · "range" · "slider" · "dial". Unknown → text.
size"small" · "regular" · "large".
min / maxRange for range / slider / dial (default 0100).
orientation"vertical" (default) · "horizontal"slider only; dial ignores it.
action / runFired when the control is set (see below).

text shows the value as a string. image renders the value as a tile image (data URI, file path, or a provider-pushed PNG — the last is roadmap). range draws a read-only bar from 0 to max.

slider and dial are interactive: the user drags to pick a value in [min, max], and the dragged integer is substituted as the $value token into the field’s inline action or named run (with run taking precedence). The set is fired throttled. A slider/dial with no action simply displays.

{ "label": "Brightness", "type": "slider", "size": "large", "min": 0, "max": 100, "orientation": "horizontal", "refreshKey": "bri", // current value to display "action": { "kind": "shell", "value": "set-brightness $value" } }

Buttons

A buttons array renders a grid of action chips. Each entry:

KeyNotes
labelCaption under the icon.
symbolSF Symbol.
states[]Multi-state cycle — each tap advances and fires the next state’s action.
toggleDeprecated on/off form; superseded by states[].
action / runInline action, or a named action by id. run wins when both are present.

Actions

There are two ways to attach behavior: an inline action object, or a named action declared in the manifest’s actions map (v2) and referenced by run. An action’s kind is one of:

kindvalue semantics
appApp name ("Spotify") or full path — opens it.
urlA URL, opened in the default browser.
keystrokeA shortcut, e.g. "cmd+shift+m", "fn+f8".
shortcutThe name of an Apple Shortcut to run.
shellA zsh command.
volumeOutput volume as a percent, "0""100".
media"playpause" · "next" · "previous".
pageSwitch the Deck to a page — "2" or a page name.
activateBring an app to front / launch it (app name).
providerForward the action to the tile’s provider. Roadmap (needs the provider runtime).
interpreterRun a multi-line script. See v2 extras below.

v2 action extras (on entries in the actions map):

  • params[] — parameter names, resolved from $config.<key> or the caller.
  • then"refresh" re-polls the tile after the action fires; "none" (default) does not.
  • interpreter ("osascript" | "zsh") + script — a multi-line script as an alternative to kind + value.
"actions": { "ping": { "kind": "provider", "value": "ping", "then": "refresh" }, "mute": { "interpreter": "osascript", "script": "set volume with output muted", "then": "refresh" } }

Polling refresh

A refresh object pulls data periodically. Its command’s stdout feeds the refreshKeys declared on your fields.

KeyNotes
commandA zsh command. Its stdout is parsed into a flat key→value dict.
everySecondsPoll interval; clamped to a minimum of 2s.
parsev2. How to read stdout (see below). Default JSON.
transformv2. Per-key value remap (see below).

parse is { "kind": "json" } by default — stdout must be a flat JSON object. Alternatively { "kind": "delimiter", "delimiter": "|", "fields": [...], "trim": true } splits the first non-empty line into the named keys positionally. ("parse": "json" as a bare string is also accepted.)

transform remaps a key’s raw value: either a $value template string ("Volume: $value%") or an exact-match lookup map ({ "true": "Muted", "false": "" }).

Wiring a refreshKey to refresh output:

{ "fields": [ { "label": "Volume", "refreshKey": "vol" }, { "label": "Status", "refreshKey": "muted" } ], "refresh": { "command": "osascript -e 'output volume of (get volume settings)' && ...", "everySeconds": 3, "parse": { "kind": "delimiter", "delimiter": "|", "fields": ["vol", "muted"], "trim": true }, "transform": { "muted": { "true": "Muted", "false": "On" } } } }

A tile is either poll (refresh) or push (provider), never both. The poll command runs with your user’s privileges — the only code an extension executes is the actions and refresh command you installed.

Config (per-instance settings)

A configSchema array renders a settings form for each placed tile. The values the user enters surface as $config.<key> tokens, substituted into commands and action params. Each row:

typeRenders
textA text field.
toggleA switch.
sliderA slider (min / max).
selectA picker from static options[].
device-pickerA picker. With source: "provider:<key>", options are pushed live by a provider — roadmap (needs the provider runtime).
connect-buttonFires a named action (often pairing / OAuth) and stores the result in a secret. Roadmap (needs provider + OAuth).
secretA keychain-backed token field. Roadmap (needs the secrets store).

Common keys: key (required; the $config.<key> name), label, default, and the type-specific fields above.

Example packs

The gatecaster-examples repo ships runnable manifests under extensions/ — including:

  • com.gatecaster.nowplaying/ — a v1 declarative tile with a polling refresh (auto-migrated to v2 on load).
  • com.gatecaster.audio/, com.gatecaster.spotify/, com.gatecaster.zoom/, com.figma.shortcuts/v2 declarative tiles (sliders, transport buttons, keystroke pads).
  • com.gatecaster.heartbeat/ — a v2 manifest paired with a providerroadmap; it parses and loads today, but the provider does not yet run.

To try one, copy its folder into the Extensions directory and reload the Deck:

cp -R extensions/com.gatecaster.nowplaying \ ~/Library/Application\ Support/Gatecaster/Extensions/

See Examples for the full list and the Touch API clients.

Roadmap

Everything in this section is designed but not yet implemented. It is specified in the app repo’s docs/Plugins/PLATFORM_SPEC.md. Manifests may declare these fields (the loader is tolerant), but the host does not act on them yet. Do not present a pack as working if it depends on them.

Push provider runtime

A provider declares a long-lived process ({ command, args, caps }) the host spawns on demand and speaks to over stdio NDJSON. Direction and message types:

  • host → provider: { "action", "params" } and { "action": "refresh" }.
  • provider → host: hello (announces caps), patch (shallow-merges into the tile’s state dict), image (a base64 PNG for an image field), options (feeds a device-picker), and error.

The host manages the process lifecycle: lease-by-close, spawn-on-demand, reap-on-idle, and crash-restart — mirroring the Touch API’s self-healing-lease discipline.

WebView presentation

A view of { "kind": "webview", "entry": "<html>" } loads HTML from the pack and exposes a gatecaster.* JavaScript bridge for richer tile UI.

Secrets and OAuth

Keychain-backed secrets injected into the provider/command environment as GATECASTER_SECRET_<KEY>, plus an OAuth redirect catcher (a loopback HTTP listener or a registered x-gatecaster:// URL scheme) that captures a token and stores it.

Capability model

Each pack declares capabilities[] — a subset of shell · network · process · secrets · native-binary — enforced at runtime as the pack’s ceiling.

touch and suppress are deliberately not grantable to extensions. That isolation is intentional — the Deck and the input-event surface are kept separate. If you need raw contacts, gestures, or input suppression, use the Touch API instead, which is out-of-process and ships free in the beta.

Distribution

.gatecaster pack files (a zip of the manifest + assets), x-gatecaster://install?src=… deep links, an install disclosure sheet that surfaces every declared capability before the user agrees, Ed25519-signed verified packs (CryptoKit), and a curated registry.

Versioning

Every message and manifest carries a v. It bumps only on breaking changes — additive fields don’t bump it. Always ignore unknown fields and unknown kind values so a forward manifest still loads and renders something.