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) }