Files
scrawl/game/view.go
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

120 lines
3.9 KiB
Go

package game
import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
)
// Generated via `figlet -f slant SCRAWL` — the slant felt right for a
// doodle/scribble tool, not hand-tuned character by character.
const banner = ` _____ __________ ___ _ ____
/ ___// ____/ __ \/ | | / / /
\__ \/ / / /_/ / /| | | /| / / /
___/ / /___/ _, _/ ___ | |/ |/ / /___
/____/\____/_/ |_/_/ |_|__/|__/_____/`
func (m Model) View() string {
if m.state == stateIntro {
return m.renderIntro()
}
return m.renderCanvas()
}
// style is a small accessor so every render call goes through m.renderer
// (bound to this specific SSH session's actual output) rather than
// lipgloss's package-level default styles, which detect color support
// from the *server process's* os.Stdout -- not any given session's
// terminal. That's a real bug this app hit: the server's stdout is
// typically redirected (a log file, a systemd journal, anything that
// isn't a TTY), so the default global renderer silently disabled all
// color/background styling for every connected session at once,
// regardless of what terminal they were actually using.
func (m Model) style() lipgloss.Style {
return m.renderer.NewStyle()
}
func (m Model) renderIntro() string {
title := m.style().Bold(true).Foreground(lipgloss.Color("#ff6b6b")).Render(banner)
dim := m.style().Foreground(lipgloss.Color("#777777"))
body := title + "\n" +
dim.Render("a shared doodle wall, one keystroke at a time") + "\n\n" +
"Everyone connected right now is drawing on the same canvas.\n" +
"Move with arrow keys or hjkl, space/enter to paint, backspace to\n" +
"erase. , and . cycle the ink character, 1-8 pick a color, c clears\n" +
"everything for everyone (yes, really — it's a shared wall).\n\n" +
dim.Render("press any key to start")
return m.boxStyle().Render(body)
}
func (m Model) boxStyle() lipgloss.Style {
return m.style().Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("#555555")).Padding(1, 2)
}
func (m Model) renderCanvas() string {
cells, cursors := m.canvas.Snapshot()
// Cursors are a background highlight on whatever's actually painted at
// that cell, not a glyph that replaces it -- a glyph-replace bug meant
// painting a cell your own cursor already sat on was invisible: the
// cursor marker before and after painting rendered as the exact same
// character in the exact same color, so bubbletea's diffing renderer
// (correctly) sent zero bytes, since nothing had visually changed.
type painted struct {
ch rune
color string
bg string
}
grid := make([][]painted, Height)
for y := 0; y < Height; y++ {
grid[y] = make([]painted, Width)
for x := 0; x < Width; x++ {
c := cells[y][x]
if c.Char == 0 {
grid[y][x] = painted{ch: ' '}
} else {
grid[y][x] = painted{ch: c.Char, color: c.Color}
}
}
}
for id, cur := range cursors {
if id == m.id {
continue
}
grid[cur.Y][cur.X].bg = cur.Color
}
if self, ok := cursors[m.id]; ok {
grid[self.Y][self.X].bg = inkPalette[m.inkColorIdx]
}
var b strings.Builder
for y := 0; y < Height; y++ {
for x := 0; x < Width; x++ {
p := grid[y][x]
style := m.style()
if p.color != "" {
style = style.Foreground(lipgloss.Color(p.color))
}
if p.bg != "" {
style = style.Background(lipgloss.Color(p.bg)).Foreground(lipgloss.Color("#000000"))
}
ch := p.ch
if ch == 0 || ch == ' ' {
ch = ' '
}
b.WriteString(style.Render(" "))
b.WriteString(style.Render(string(ch)))
}
b.WriteString("\n")
}
inkSwatch := m.style().Foreground(lipgloss.Color(inkPalette[m.inkColorIdx])).Render(string(charPalette[m.charIdx]))
dim := m.style().Foreground(lipgloss.Color("#777777"))
status := fmt.Sprintf("ink %s (1-8 color, , . char) peers: %d %s",
inkSwatch, m.canvas.PeerCount(), dim.Render("space paint · c clear · q quit"))
return m.boxStyle().Render(b.String() + "\n" + status)
}