[ 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>
ssh in and draw on a live, shared doodle wall -- everyone connected
sees everyone else's edits in real time. Same wish+bubbletea security
model as delve-term (no shell, no exec, structurally can't reach a
real shell on the host), extended with the one thing delve-term didn't
need: multiple sessions sharing live state. wish's bm.Middleware helper
hides the *tea.Program it creates, so this builds the program directly
instead, keeping a registry (game.Canvas) that broadcasts a redraw
signal to every other connected session the instant one of them paints.
Banner generated via `figlet -f slant SCRAWL` rather than hand-drawn --
slant felt right for a doodle/scribble tool.
Two real bugs caught by actually running this with real SSH sessions,
not just unit-testing the game logic in isolation:
1. Program.Send() blocks until that program's Run() event loop is
actively reading from it. Broadcasting synchronously from inside
Join()/Paint() (including a session broadcasting to its own,
not-yet-running program on join) deadlocked every session before it
ever reached Run() -- the very first connection just hung with
nothing rendered. Fixed by sending asynchronously (go p.Send(...))
everywhere the canvas notifies sessions of a change.
2. Subtler: lipgloss's default package-level 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, systemd journal), so every connected session
silently lost all color/background styling at once, server-wide.
Manifested as painting a cell your own cursor already sat on being
invisible (the cursor glyph before/after looked identical, so
bubbletea's diffing renderer correctly sent zero bytes for a change
that produced no visual diff) -- confirmed via server-side debug
logging that painting itself worked correctly every time, isolating
the bug to rendering, then confirmed via a Go test that forcing a
real color profile was the difference between 0 and 1327 runes of
diff between two frames that should look different. Fixed with a
lipgloss.Renderer created per-session, bound to that session's
actual output, forced to TrueColor; cursors now highlight whatever's
already painted at that cell (background tint) rather than
replacing the character, so a session's own paint is never masked
by its own cursor marker sitting on top of it.
Verified end-to-end with two real, simultaneous SSH sessions (scripted
via pexpect): peer count syncs correctly, and one session's paint
genuinely arrives at the other via the live broadcast -- not just
unit-tested in isolation. Also verified the actual Docker image builds
and serves correctly over real SSH.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>