Skip to content

Quickstart

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.

Terminal window
git clone https://github.com/deemwar-products/device-bridge
cd device-bridge
go build -o device-bridge ./cmd/device-bridge
./device-bridge --help

Go 1.24 or newer. macOS only today — see what actually works.

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.

Terminal window
./device-bridge devices
input available=true

If it reports unavailable, add your terminal (or whatever binary is calling) under System Settings → Privacy & Security → Accessibility, then run it again.

The one-shot verbs talk to the OS directly. No socket, no token, no serve.

Terminal window
./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 300
Terminal window
./device-bridge screenshot --out shot.png
wrote 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.

Two shapes. The standalone one is a single stream on a fixed port:

Terminal window
export WINDOWCTL_TOKEN=$(openssl rand -hex 16)
./device-bridge start video-out --port 9334

Then 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.

Terminal window
./device-bridge serve --port 9330

One 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.

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.