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

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 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) hands each connection straight to a 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

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

Description
Silly little toy.
Readme 47 KiB
Languages
Go 95.4%
Dockerfile 2.3%
Shell 2.3%