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. Pick one of these eight and the page you are reading restyles itself from that theme's real colours — the same values the terminal paints with, read straight out of the binary.
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.
| Reading | Nominal | Degraded | Critical |
|---|---|---|---|
| Frames missed (render lag) | < 1% | 1–5% | ≥ 5% |
| Frames skipped (encode lag) | < 1% | 1–5% | ≥ 5% |
| Frame budget used | < 50% | 50–80% | ≥ 80% |
| Output FPS vs. session best | ≥ 95% | 80–95% | < 80% |
The panel reads the telemetry the daemon already polls for the header bar, so it costs no extra OBS traffic and cannot disagree with the numbers above it. Watch it fill in during the recording at the top of this page.
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
Fetches the static musl build for your architecture, checks it against the
release's SHA256SUMS.txt, and installs it to
~/.local/bin. A mismatched checksum installs nothing.
$ curl -fsSL https://worxbend.github.io/obsctl/install.sh | sh
Read it first if you like — it is install.sh in the repository, deployed here from the same file.
Static musl builds for linux-amd64 and
linux-arm64, published with
SHA256SUMS.txt — they do not depend on your glibc.
$ tar xzf obsctl-v0.5.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
# the password, typed into this shell only
$ read -rs OBS_WEBSOCKET_PASSWORD
$ export OBS_WEBSOCKET_PASSWORD
# one terminal: the daemon
$ obsctl server --headless
# another: the control room
$ obsctl
Config lives at ~/.config/obsctl/config.yml.
A plaintext password in it earns you a warning. The password belongs in the
daemon's environment — not in the config file, and not exported from your
~/.bashrc, where every program you
launch inherits it. To make it survive a reboot, read it from a password
manager at launch or from a chmod 600
EnvironmentFile in the systemd unit —
both are in the README.