Skip to content

Local HTTP API: start/stop output and query status without the status menu #4

Description

@trsdn

Starting and stopping output today only works through the status menu in the menu bar. I can't reach it from outside — script, shortcut, Stream Deck — no URL scheme, no AppleScript, no CLI arguments besides --self-test and the display host's headless mode in Sources/TeleprompterMirror/main.swift.

For me this is the button I'd press most often: I sit in front of the prompter, not in front of the menu bar.

Why

I control my setup via a Stream Deck (now with OpenDeck). Every purchased device integrates cleanly there — the Elgato Key Lights, for example, via their local HTTP API on port 9123:

curl -s http://elgato-key-light-aeff.local:9123/elgato/lights
# {"numberOfLights":1,"lights":[{"on":0,"brightness":25,"temperature":154}]}

Of all things, my own apps are the ones I can't switch from the Deck. There's no CLI, no URL scheme, no scripting bridge, and no IPC endpoint. What's left is UI automation via Accessibility — fragile, slow, and broken by every layout change.

Proposal: a local HTTP API on 127.0.0.1

Of the possible approaches (URL scheme, AppleScript .sdef, XPC, DistributedNotificationCenter, HTTP), I think HTTP is the best trade-off:

URL scheme AppleScript XPC HTTP loopback
Trigger an action
Read state
Usable from a shell
Implementation effort low medium high low
Usable from OpenDeck actions only awkward no ✅ directly

The decisive point is reading state. A URL scheme alone isn't enough: a Deck button should show whether something is currently on or off. Without a status query, the button stays blind and falls out of sync as soon as something changes in the app itself.

Constraints

  • Only 127.0.0.1, never bind to 0.0.0.0.
  • Off by default, enabled via a setting.
  • Dynamic port, but written to a known file after startup so scripts can find it:
    ~/Library/Application Support/<App>/api-port — plus a fixed default port as a fallback.
  • A token in the same file, expected as Authorization: Bearer …. That way no arbitrary locally running software (or a web page via DNS rebinding) can reach the controls.
  • Reject the Origin header so browser pages can't get through in the first place.

Concretely for TeleprompterMirror

The state already exists in full in Sources/TeleprompterMirror/AppModel.swift:

@Published private(set) var isRunning = false
@Published private(set) var statusText = "..."
@Published private(set) var statusIsError = false
@Published private(set) var permissionGranted = ...

Status query

GET /v1/state
{
  "running": false,
  "busy": false,
  "status": "Ready.",
  "error": false,
  "permissionGranted": true,
  "preset": { "index": 0, "name": "Studio" },
  "presets": ["Studio", "Desk", "Mobile"],
  "display": { "id": 3, "name": "LG UltraFine" },
  "transform": { "rotation": 180, "mirrorH": true, "mirrorV": false }
}

Actions

Endpoint Effect existing hook
POST /v1/output/start Start output AppModel.start()
POST /v1/output/stop Stop output requestStop(message:)
POST /v1/output/toggle Toggle both
POST /v1/preset {"index":1} Switch preset selectPreset(_:)
POST /v1/transform {"rotation":180,"mirrorH":true} mirror/rotate transform
POST /v1/display {"id":3} Choose target monitor selectedDisplayID

Start and stop matter most here; preset and transform would be a bonus.

Use on the Deck: a button that glows green while output is running, and turns red on an error (statusIsError) — the error text already exists, I just don't see it when the menu is closed.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions