Quickstart
Install
Section titled “Install”No version has been released yet. The release pipeline is wired — goreleaser
builds six targets and generates a Homebrew cask
(brew install --cask deemwar-products/oss/device-bridge) and winget manifests —
but nothing has been tagged, so nothing is installable that way today. Build from
source.
The module has one dependency (gorilla/websocket) and needs cgo, which on macOS
means the system clang you already have.
git clone https://github.com/deemwar-products/device-bridgecd device-bridgego build -o device-bridge ./cmd/device-bridge./device-bridge --helpGo 1.24 or newer. macOS only today — see what actually works.
Grant Accessibility once
Section titled “Grant Accessibility once”Keyboard and mouse injection goes through CGEvent, which macOS gates behind
Accessibility. Without the grant, injection fails with an explicit error — it
never silently does nothing.
./device-bridge devicesinput available=trueIf it reports unavailable, add your terminal (or whatever binary is calling) under System Settings → Privacy & Security → Accessibility, then run it again.
Drive the desktop, no daemon
Section titled “Drive the desktop, no daemon”The one-shot verbs talk to the OS directly. No socket, no token, no serve.
./device-bridge type "hello world"./device-bridge key cmd+shift+t./device-bridge click --x 400 --y 300./device-bridge move-mouse --x 200 --y 200./device-bridge scroll --dy -120./device-bridge drag --x1 100 --y1 100 --x2 400 --y2 300Read the screen back
Section titled “Read the screen back”./device-bridge screenshot --out shot.pngwrote shot.png (3456x2234, 4812901 bytes)--display N picks a display (0 is the main one); --window ID captures a single
window by its CoreGraphics window id. A 1920×1080 one-shot measured 110 ms
cold in spike S5 — that figure is dominated by ScreenCaptureKit setup, so a
persistent stream costs far less per frame.
Open a stream
Section titled “Open a stream”Two shapes. The standalone one is a single stream on a fixed port:
export WINDOWCTL_TOKEN=$(openssl rand -hex 16)./device-bridge start video-out --port 9334Then connect and read frames. Each WebSocket binary message is one 16-byte header followed by the raw payload:
const ws = new WebSocket(`ws://127.0.0.1:9334/?token=${process.env.WINDOWCTL_TOKEN}`);ws.binaryType = 'arraybuffer';ws.onmessage = (ev) => { const h = new DataView(ev.data, 0, 16); const kind = h.getUint8(2); // 4 = screen const format = h.getUint8(3); // 11 = raw BGRA const seq = h.getUint32(4); const stamp = h.getUint32(8); // producer monotonic ms — subtract for latency const len = h.getUint32(12); const pixels = new Uint8Array(ev.data, 16, len);};The standalone video-out server captures at the display’s native size, 10 fps. Use the control plane if you need to negotiate a different shape.
Or run the control plane
Section titled “Or run the control plane”./device-bridge serve --port 9330One authenticated connection starts and stops every stream, and each start
reply carries the port that was actually allocated plus a one-time ticket:
{ "type": "hello", "token": "…" }{ "type": "start", "stream": "screen", "video": { "width": 1280, "height": 720, "fps": 30 } }{ "type": "stop", "stream": "screen" }{ "type": "list" }Full message set in Wire protocol; the auth rules are in Security.
What you cannot do yet
Section titled “What you cannot do yet”Nothing above feeds into the app. The two inbound lanes — virtual microphone
and virtual camera — do not work today: the audio driver is written but
coreaudiod will not load it without a notarized Developer ID build, and the
camera extension is not built at all. device-bridge install camera tells you so
and exits non-zero. See what actually works.