# rtsp-streamer configurator (opentui) The interactive configurator, built with [opentui](https://opentui.com). It replaces the Bubble Tea TUI that lived in `internal/tui`. Launch it the usual way: ```bash rtsp-streamer tui ``` ## Why this is a separate executable opentui is a native core written in Zig with **TypeScript** bindings — there are no Go bindings. So this half of the tool is TypeScript compiled by Bun, and it ships as `rtsp-streamer-tui` next to the Go binary. `rtsp-streamer tui` finds and execs it. The trade-off is size: the Go binary is ~13 MB, and this one is ~120 MB because Bun embeds its own runtime plus opentui's native library. Nothing needs to be installed on the target, but it is a large artifact for an SD card. (`make tui-pi` comes out ~108 MB — defining `OPENTUI_LIBC` lets Bun embed only the glibc native package instead of both glibc and musl.) If it is missing or misbehaving, the previous Go implementation is still there: ```bash rtsp-streamer tui --legacy ``` ## Division of labour Everything that is not presentation stays in Go. This process holds no credentials and never writes the config file itself — it shells back to `rtsp-streamer` for three things: | Command | Purpose | | ------------------------------------------ | -------------------------------------------------------- | | `config export` | read the config, plus limits like `max_tiles` | | `config apply` (JSON on stdin) | merge edits, validate, save atomically, reload the daemon | | `discover --json [--enable-rtsp=high,low]` | Protect discovery, writing nothing | Two properties of that split are worth keeping: - **The controller password never crosses the bridge.** It is `json:"-"` on the way out, and `config apply` only merges `cameras`, `layouts` and `active_layout`, so it cannot be clobbered on the way back in either. - **`config apply` re-reads the file before merging.** A configurator left open for an hour can no longer overwrite a `views import`, a `layout set`, or a hand edit made in the meantime — it only replaces the keys it owns. Discovery is previewed rather than applied: `discover --json` writes nothing, the merge happens here, and nothing reaches disk until you press Save. ## Layout | Path | Contents | | ------------------ | -------------------------------------------------------------- | | `src/index.ts` | entry point: arg parsing, renderer setup, event loop | | `src/keys.ts` | keyboard routing (bindings carried over from the Go version) | | `src/state.ts` | the store — all state and mutations, ported from `internal/tui` | | `src/ui.ts` | renders the store into an opentui tree; grid mouse handling | | `src/bridge.ts` | subprocess calls into the Go binary | | `src/types.ts` | config schema + geometry helpers mirroring `internal/config` | | `src/theme.ts` | the lipgloss palette, resolved to hex | | `src/fixtures.ts` | sample config used by tests and the preview script | | `scripts/preview.ts` | render every screen to stdout as text | ## Working on it ```bash make run-tui # run from source against /tmp/rtsp-streamer.yaml, no compile make test-tui # bun test + tsc --noEmit make tui # compile bin/rtsp-streamer-tui for this machine make tui-pi # cross-compile bin/rtsp-streamer-tui-arm64 for a 64-bit Pi bun run scripts/preview.ts # dump every screen as text, no terminal needed ``` `preview.ts` is the quickest way to see a layout change — it uses opentui's test renderer, so it prints the screens without needing a TTY. ## Two things to know before changing the grid editor Both were bugs during the port, and neither is obvious from the code: **Do not overlap cell borders.** Sharing a border between neighbouring cells looks like it should work, but a box draws its own corners, so every shared edge renders as `┌` where a lattice needs `┬`. Each cell is a self-contained box that tiles edge-to-edge. **Do not rebuild the tree during a drag.** The renderer *captures* the renderable a drag started on and routes the rest of the gesture to it. `render()` destroys and recreates the whole subtree, so a rebuild mid-drag silently ends the drag after the first resize. Instead, `suppressRender` is set for the duration and the dragged tile's box is resized in place, with one full rebuild on release. For the same reason the grid's mouse handlers live on the persistent mount box, not on the grid box, and the grid's screen origin is captured on press rather than read live (a rebuilt box has no computed layout until the next frame, so `screenX` reads as 0).