# scrawl A shared ASCII doodle wall you connect to over SSH. Everyone connected right now is drawing on the same canvas, live. ``` ssh scrawl.goonk.se -p 23235 ``` Not aiming to be useful to anyone but me — built purely because a multiplayer canvas over raw SSH is a genuinely fun, weird shape for a toy, and the `wish`+`bubbletea` pattern from [delve-term](https://repo.explewd.com/explewd/delve-term) was still fresh enough to reuse without re-learning it. ## Controls - Arrow keys or `hjkl` — move your cursor - `space` / `enter` — paint - `backspace` / `delete` — erase - `,` / `.` — cycle the ink character (`[` / `]` also work) - `1`-`8` — pick a color - `c` — clear the whole canvas (yes, for everyone — it's a shared wall) - `q` / `ctrl+c` — quit ## Architecture Same security model as `delve-term`: the SSH server ([`charmbracelet/wish`](https://github.com/charmbracelet/wish)) hands each connection straight to a [bubbletea](https://github.com/charmbracelet/bubbletea) program — no shell, no exec, structurally can't reach a real shell on the host. The one thing this app needed beyond `delve-term`'s pattern: multiple sessions sharing live state. `wish`'s `bm.Middleware` helper hides the `*tea.Program` it creates behind its own closure, so this app builds the program directly instead, keeping a reference in a small registry (`game.Canvas`) that broadcasts a redraw signal to every *other* connected session the instant one of them paints. A real bug worth knowing about if you're extending this: `Program.Send()` blocks until that program's `Run()` event loop is actively reading from it. Broadcasting synchronously from inside `Join()`/`Paint()` deadlocked every session before it ever reached `Run()` — fixed by sending asynchronously (`go p.Send(...)`) everywhere the canvas notifies sessions of a change. A second, subtler bug: `lipgloss`'s default styles detect color support from the *server process's* `os.Stdout`, not any given session's actual terminal — and a server's stdout is typically redirected (a log file, a systemd journal), so every session silently lost all color/background styling at once. Fixed with a `lipgloss.Renderer` created per-session, bound to that session's actual output, forced to `TrueColor`. ## Local development ```bash ./run-local.sh # starts on :23235 ssh -p 23235 localhost -o StrictHostKeyChecking=no # in another terminal ``` ## Status Verified with two simultaneous SSH sessions (scripted, not just live poking) — one paints, the other sees it via the broadcast, peer count stays in sync. No persistence: canvas state lives in memory only and resets on restart, which is fine for what this is. ## License MIT.