Files
scrawl/README.md
Fredrik Johansson 039e131304
All checks were successful
Docker / build-and-push (push) Successful in 50s
Rebind char cycling to ,/. instead of [/]
[ and ] needed a reach/shift on most layouts and felt awkward next to
the otherwise all-unshifted control scheme (hjkl, space, digits). ,
and . are adjacent, unshifted, easy to hit without looking away from
the canvas -- a natural prev/next pair. Old bindings kept as a fallback
rather than removed.

Verified both in isolation (Update() with a KeyMsg) and live over a
real SSH session -- the status-line ink swatch visibly cycles from █
to ▓ on a fresh connection. An initial live test after the rebind
appeared to fail, but that traced to a stale session left over from
the server restart, not an actual bug -- confirmed working correctly
on a fresh connection.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 19:55:59 +02:00

72 lines
2.7 KiB
Markdown

# 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.