Rebuild the interactive configurator on opentui behind a JSON bridge
The TUI is now a TypeScript/opentui app in tui/ rather than Bubble Tea.
opentui is a Zig core with TypeScript bindings and no Go bindings, so this half
of the tool can't live in the Go binary; it compiles with Bun into a sibling
executable (rtsp-streamer-tui) that `rtsp-streamer tui` execs.
Everything that isn't presentation stays in Go, reached over three JSON
commands. The configurator holds no credentials and never writes the config
itself:
config export the config, plus limits like max_tiles
config apply (stdin) merge cameras/layouts/active_layout, validate, save
atomically, reload the daemon
discover --json Protect discovery, writing nothing
Two properties of that split are deliberate:
- The controller password never crosses the bridge. It's json:"-" on the way
out, and apply only merges the three keys the TUI edits, so it can't be
clobbered on the way back in either.
- apply re-reads the file before merging, so an editor left open for an hour can
no longer overwrite a `views import`, a `layout set`, or a hand edit made in
the meantime.
Discovery is previewable as a result: `discover --json` writes nothing, the
merge happens in the TUI, and nothing reaches disk until you save. Only
--enable-rtsp has a side effect, and it's on the controller.
Config structs gain json tags mirroring their yaml ones so the config
round-trips through the bridge under the same key names it has on disk, and
maxGridDim moves to config.MaxGridDim so the CLI and both configurators enforce
one ceiling. The write path is byte-for-byte identical to `layout set`, checked
against a copy of a live config.
Visible change: the grid editor draws real bordered boxes, so a spanning tile is
one box instead of an origin cell plus "·" continuation marks, and the
header-offset arithmetic in mouse.go is gone — the framework hit-tests list
rows. Keybindings, the lipgloss palette and the screen flow are carried over
unchanged; S now saves from anywhere.
The Bubble Tea version stays as `tui --legacy`. It's compiled into the Go binary
and needs no Bun, and on a headless Pi the TUI is the only config UI there is,
so a fallback is worth its weight. The cost of the new one is size: ~120 MB
against ~13 MB, since Bun embeds its runtime and opentui's native library.
Tests: 67 bun tests drive the real (in-memory) opentui renderer, including mouse
click and drag, plus tsc --noEmit. `make test-tui` runs both, and
scripts/preview.ts dumps every screen as text without needing a terminal.
Three bugs found during the port are documented in tui/README.md, since none are
apparent from the code: overlapping cell borders render as ┌ where a lattice
needs ┬; a drag dies after the first resize if the tree is rebuilt, because the
renderer captures the press-target renderable; and a rebuilt box has no computed
layout until the next frame, so its screenX reads 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
129
README.md
129
README.md
@@ -49,6 +49,20 @@ Arch: `sudo pacman -S sway mpv foot`
|
||||
To build: **Go 1.23+**. You can build on your dev machine and copy the binary,
|
||||
or build on the Pi itself.
|
||||
|
||||
The interactive configurator is a second binary built with
|
||||
[Bun](https://bun.sh) (it's written in TypeScript — see
|
||||
[the TUI section](#the-tui)). It is **optional**: `make` builds only the Go
|
||||
binary, and everything except `rtsp-streamer tui` works without it. Install Bun
|
||||
only if you want the configurator:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://bun.sh/install | bash # then: make tui
|
||||
```
|
||||
|
||||
Neither Bun nor Node is needed at *run* time — `make tui` produces a standalone
|
||||
executable. And `rtsp-streamer tui --legacy` is built into the Go binary, so a
|
||||
box without Bun still has a configurator.
|
||||
|
||||
> ⚠️ Debian / Raspberry Pi OS ships **Go 1.19** via `apt`, which is too old
|
||||
> (`log/slog` needs 1.21+). Install a current Go into `~/.local/go` and put it
|
||||
> first on `PATH` — do **not** rely on the system `go`:
|
||||
@@ -71,6 +85,21 @@ make deploy # install + restart the kiosk session
|
||||
rtsp-streamer version # prints the baked-in git commit / build date
|
||||
```
|
||||
|
||||
The configurator is built separately (needs Bun; skip it and use
|
||||
`rtsp-streamer tui --legacy`):
|
||||
|
||||
```sh
|
||||
make tui # host arch -> bin/rtsp-streamer-tui (standalone, ~120 MB)
|
||||
make tui-pi # arm64 -> bin/rtsp-streamer-tui-arm64 (the `make pi` analogue)
|
||||
make test-tui # bun test + tsc --noEmit
|
||||
make run-tui # run it from source against /tmp/rtsp-streamer.yaml
|
||||
```
|
||||
|
||||
`make install` copies `bin/rtsp-streamer-tui` alongside the main binary when it
|
||||
exists, which is where `rtsp-streamer tui` looks for it. If you cross-compiled
|
||||
with `make tui-pi`, rename the `-arm64` artifact to `rtsp-streamer-tui` on the
|
||||
target, or point `$RTSP_STREAMER_TUI` at it.
|
||||
|
||||
## Configure
|
||||
|
||||
```sh
|
||||
@@ -97,23 +126,35 @@ may not overlap. See `main-plus` in [`config.example.yaml`](config.example.yaml)
|
||||
|
||||
### The TUI
|
||||
|
||||
`rtsp-streamer tui` is a Bubble Tea configurator meant to be run over SSH:
|
||||
`rtsp-streamer tui` is an [opentui](https://opentui.com) configurator meant to be
|
||||
run over SSH:
|
||||
|
||||
- **Cameras** — review discovered cameras, `d` disable, `x` delete.
|
||||
- **Layouts** — edit an arrangement on a live ASCII grid preview.
|
||||
- **Layouts** — edit an arrangement on a live grid preview.
|
||||
- **Set active layout** — choose what the wall shows.
|
||||
- **Discover** / **Discover + enable RTSP (high)** — pull cameras from Protect.
|
||||
- **Discover** / **Discover + enable RTSP (hi+lo)** — pull cameras from Protect.
|
||||
- **Save** — writes the config and tells a running daemon to reload live.
|
||||
|
||||
In the **layout editor** the grid is drawn live as you edit:
|
||||
It is a separate executable (`rtsp-streamer-tui`) built by Bun, because opentui is
|
||||
a TypeScript library — see [`tui/README.md`](tui/README.md) for the architecture
|
||||
and the Go↔TS bridge. Build it with `make tui`; `make install` puts it alongside
|
||||
the main binary. The previous Bubble Tea implementation is still available as
|
||||
`rtsp-streamer tui --legacy`, which needs no Bun runtime.
|
||||
|
||||
In the **layout editor** the grid is drawn live as you edit, with a spanning tile
|
||||
shown as a single box:
|
||||
|
||||
```
|
||||
+----------+----------+----------+----------+
|
||||
|Front Door| · | · |Driveway |
|
||||
|3x3 | · | · | |
|
||||
+----------+----------+----------+----------+
|
||||
| · | · | · |Back Yard |
|
||||
...
|
||||
┌─Driveway─────────────────────────────────────┐┌─Front Door───┐
|
||||
│ 3x2 hi ││ lo │
|
||||
│ ││ │
|
||||
│ │└──────────────┘
|
||||
│ │┌──────────────┐
|
||||
│ ││ │
|
||||
└──────────────────────────────────────────────┘└──────────────┘
|
||||
┌──────────────┐┌──────────────┐┌──────────────┐┌─Back Yard────┐
|
||||
│ ││ ││ ││ auto │
|
||||
└──────────────┘└──────────────┘└──────────────┘└──────────────┘
|
||||
```
|
||||
|
||||
- **mouse (over SSH):** click a cell to assign a camera; **press on a tile and
|
||||
@@ -127,6 +168,7 @@ In the **layout editor** the grid is drawn live as you edit:
|
||||
- `esc` back
|
||||
|
||||
Elsewhere: arrows / `j`/`k` move (or click), `enter` selects, `esc` back, `q` quits.
|
||||
`S` saves from any screen.
|
||||
|
||||
### Stream quality per tile
|
||||
|
||||
@@ -155,6 +197,44 @@ wall — stream URLs, tile geometry, player settings — against what's already
|
||||
running and leaves the streams untouched when nothing material changed, so
|
||||
saving an unrelated edit never blanks the screen.
|
||||
|
||||
### JSON interface
|
||||
|
||||
Three commands speak JSON. The configurator in `tui/` is built on them, and
|
||||
they're the supported way to script config changes:
|
||||
|
||||
```sh
|
||||
rtsp-streamer config export # whole config as JSON on stdout
|
||||
rtsp-streamer config apply < edits.json # merge, validate, save, reload
|
||||
rtsp-streamer discover --json # Protect discovery, writes nothing
|
||||
rtsp-streamer discover --json --enable-rtsp=high,low
|
||||
```
|
||||
|
||||
`config apply` reads an object with any of `cameras`, `layouts` and
|
||||
`active_layout`, and **merges** it into a fresh read of the file — absent keys are
|
||||
left alone, and the `controller` section is never touched. So a one-key edit needs
|
||||
nothing else in the payload:
|
||||
|
||||
```sh
|
||||
echo '{"active_layout": "quad"}' | rtsp-streamer config apply
|
||||
```
|
||||
|
||||
(That is exactly what `layout set quad` does, and it leaves cameras and layouts
|
||||
untouched. `--json` output pairs well with `jq` if you have it.)
|
||||
|
||||
Two deliberate properties:
|
||||
|
||||
- **The controller password never appears in `config export`,** and `apply` can't
|
||||
overwrite it. Secrets stay in the file (or `$RTSP_STREAMER_PASSWORD`).
|
||||
- **`apply` re-reads before merging,** so a long-running editor can't clobber a
|
||||
`views import`, a `layout set`, or a hand edit made in the meantime.
|
||||
|
||||
`discover --json` writes nothing, which makes discovery previewable — only
|
||||
`--enable-rtsp` has a side effect, and it is on the *controller* (it switches
|
||||
RTSP on for those channels in Protect).
|
||||
|
||||
Every one of the three prints a JSON document on stdout even when it fails, with
|
||||
an `ok` field, and exits non-zero on failure.
|
||||
|
||||
## Clock overlay
|
||||
|
||||
An optional always-on-top clock can be rendered at a screen edge:
|
||||
@@ -282,6 +362,14 @@ Then `sudo reboot` and the wall comes up on boot.
|
||||
git pull && make deploy
|
||||
```
|
||||
|
||||
`make deploy` covers the daemon and the CLI. It does **not** rebuild the
|
||||
configurator — that is a separate, slower build, and it only matters when
|
||||
something under `tui/` changed:
|
||||
|
||||
```sh
|
||||
git pull && make tui && make deploy
|
||||
```
|
||||
|
||||
`make deploy` builds natively, copies to `/usr/local/bin` (auto-sudo — run it
|
||||
*without* `sudo` so `go build` keeps your `PATH`), then runs
|
||||
`rtsp-streamer restart`, which tells the running daemon to **re-exec itself in
|
||||
@@ -306,7 +394,7 @@ rtsp-streamer discover --enable-rtsp=high # enable the High channel where miss
|
||||
rtsp-streamer discover --enable-rtsp=low # or the Low substream
|
||||
```
|
||||
|
||||
In the TUI, the menu item **"Discover + enable RTSP (high)"** does the same.
|
||||
In the TUI, the menu item **"Discover + enable RTSP (hi+lo)"** does the same.
|
||||
Enabling preserves each channel's other encoder settings — it only flips the
|
||||
`isRtspEnabled` flag. Plain `discover` (no flag) never modifies your controller;
|
||||
it just skips cameras with no RTSP-enabled channel and reports which ones.
|
||||
@@ -399,6 +487,22 @@ pegged, work through:
|
||||
- **Build error `package log/slog is not in GOROOT`** — you're on Debian's Go
|
||||
1.19; install Go 1.23 into `~/.local/go` and prepend it to `PATH` (see Build).
|
||||
|
||||
- **`rtsp-streamer tui` says `rtsp-streamer-tui not found`** — the configurator
|
||||
binary isn't built or isn't installed. Either build it (`make tui && make
|
||||
install`, needs Bun) or use `rtsp-streamer tui --legacy`, which is compiled into
|
||||
the Go binary and always available. `$RTSP_STREAMER_TUI` overrides the path.
|
||||
|
||||
- **`rtsp-streamer tui` exits immediately with a config error** — the configurator
|
||||
loads the config through `rtsp-streamer config export` *before* taking over the
|
||||
terminal, so an invalid config surfaces as a plain error rather than a broken
|
||||
screen. Run `rtsp-streamer config validate` to see the same problem.
|
||||
|
||||
- **The configurator can't reach the Go binary** — it shells back to
|
||||
`rtsp-streamer` for every config and Protect operation, resolved from
|
||||
`$RTSP_STREAMER_BIN` (set automatically by `rtsp-streamer tui`) and otherwise
|
||||
from `$PATH`. Running the compiled TUI directly from a shell where
|
||||
`rtsp-streamer` isn't on `PATH` is the usual cause.
|
||||
|
||||
## Caveats & scope
|
||||
|
||||
- **Unofficial API.** UniFi Protect has no public API; the local endpoints used
|
||||
@@ -431,6 +535,7 @@ internal/player/ one supervised mpv per stream, JSON IPC, restart/backoff
|
||||
internal/compositor/ sway control + tile geometry
|
||||
internal/daemon/ orchestrator + control socket + health loop
|
||||
internal/ipc/ control-socket protocol shared by daemon and CLI/TUI
|
||||
internal/tui/ Bubble Tea configurator (grid editor, mouse, quality)
|
||||
internal/tui/ legacy Bubble Tea configurator (`tui --legacy`)
|
||||
tui/ opentui configurator (TypeScript/Bun) — see tui/README.md
|
||||
deploy/ sway kiosk config, autologin, install.sh
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user