Everything on one screen
Scenes, audio matrix, profiles, collections, output state, telemetry, stream health, a live log feed, and a command palette — in one responsive view.
Your OBS control room — right inside the terminal.
A Crystal TUI, an automation-friendly CLI, and a local daemon that owns the single OBS WebSocket connection — so your scripts, your dashboard, and your stream never fight over it.
obsctl ships 29 built-in themes, and custom themes can override any subset of the palette. These eight are rendered above — and the page you are reading restyles itself from the same values, because there is only one palette and both are using it.
Press Ctrl-T
or F2
in the running TUI to open the appearance lab.
Scenes, audio matrix, profiles, collections, output state, telemetry, stream health, a live log feed, and a command palette — in one responsive view.
A local daemon owns the WebSocket and holds authoritative state. The TUI and every CLI call are thin clients over a Unix socket. No competing handshakes.
Stable command grammar, one JSON envelope per invocation, canonical error codes, and exit statuses that mean something in a shell conditional.
Bounded reconnect backoff, explicit reconnect control, state subscriptions, and diagnostics that redact passwords and auth strings from every sink.
29 themes, custom colours, Unicode and ASCII modes, English and Ukrainian UI surfaces, and keyboard-first navigation throughout.
An in-tree immediate-mode Crystal library inspired by Ratatui, tested against memory, ANSI, and real PTY backends.
The same command grammar drives the palette and the shell. Aliases, shortcuts, exact OBS names, and case-insensitive names all resolve; quote anything with spaces.
# Scenes, audio, outputs
$ obsctl scene main
$ obsctl volume "Desktop Audio" 70
$ obsctl toggle-mute mic
$ obsctl record start
$ obsctl stream
# Follow daemon state from a script
$ obsctl watch --topics state \
| jq -r '.data.current_scene'
Main Camera
# Run it as a user service
$ obsctl service install
$ obsctl service start
# One envelope per invocation, on stdout
$ obsctl scene main --json
{
"ok": true,
"result": { "message": "scene set: Main Camera" },
"error": null,
"exit_code": 0
}
# Failures are structured too
$ obsctl scene nope --json; echo $?
{
"ok": false,
"result": null,
"error": { "code": "SCENE_NOT_FOUND", … },
"exit_code": 4
}
4
| Code | Class | Meaning |
|---|---|---|
| 0 | Success | The command did what it said |
| 1 | Failure | Unclassified failure |
| 2 | Config | Missing or invalid configuration |
| 3 | Connection | Daemon or OBS unavailable |
| 4 | ObsRequest | OBS refused the request |
| 5 | CommandParse | The command could not be parsed |
| 6 | Ipc | Local IPC protocol error |
While the stream is live, a stats panel opens beside the logs with the numbers that actually predict a bad broadcast: frames missed to rendering lag, frames skipped to encoding lag, and how much of each frame's budget the compositor is burning.
Drop ratios are scored the way the OBS stats dock scores them — under 1% nominal, 5% or more critical — and FPS is judged against the best rate seen this session, so a 30 fps profile reads as healthy at 30.
One process owns the OBS connection. Everything else is a local client, so state, reconnects, validation, and secret handling all live in one place — and control never leaves your machine.
obs-websocket 5.x, on localhost.
↓ WebSocketHolds the single socket and the authoritative state snapshot. Supervises reconnects.
↓ Unix socket · newline-delimited JSONSubscribes to state, events, and logs. Opens no OBS connection of its own.
↑ subscribeShort-lived processes. One request, one envelope, one exit code.
↑ command
Static musl builds for linux-amd64 and
linux-arm64, published with
SHA256SUMS.txt — they do not depend on your glibc.
$ tar xzf obsctl-v0.4.0-linux-amd64.tar.gz
$ install -Dm755 obsctl ~/.local/bin/obsctl
$ git clone https://github.com/worxbend/obsctl.git
$ cd obsctl && shards install
$ make release
Needs Linux, Crystal 1.21+, and Shards.
$ obsctl init
$ obsctl validate-config
$ export OBS_WEBSOCKET_PASSWORD='…'
# one terminal: the daemon
$ obsctl server --headless
# another: the control room
$ obsctl
Config lives at ~/.config/obsctl/config.yml.
Secrets belong in environment variables — a plaintext password in the file
earns you a warning.