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.
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-testand the display host's headless mode inSources/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: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
127.0.0.1, never bind to0.0.0.0.~/Library/Application Support/<App>/api-port— plus a fixed default port as a fallback.Authorization: Bearer …. That way no arbitrary locally running software (or a web page via DNS rebinding) can reach the controls.Originheader 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:Status query
Actions
POST /v1/output/startAppModel.start()POST /v1/output/stoprequestStop(message:)POST /v1/output/togglePOST /v1/preset{"index":1}selectPreset(_:)POST /v1/transform{"rotation":180,"mirrorH":true}transformPOST /v1/display{"id":3}selectedDisplayIDStart 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.