- Extract StatusBarStyle, CursorStyle, CursorWarmStyle to game/style.go (eliminates 8 identical statusBarStyle and 6 cursorStyle definitions) - Extract shared BorderChar/HBorderRow helpers to game/border.go (deduplicates nearly identical functions between takuzu and hitori) - Remove unused game.ResetMsg and duplicate handlers in hitori/shikaku (root model already calls Reset() directly) - Cache sudoku conflicts in Model instead of recomputing on every View() - Replace panic in View() default case with error string - Replace panics in debug renderer with graceful fallback - Fix non-adaptive debug border color to use AdaptiveColor
- commit
- 6de7d24fbbdd9bf764352fd260e7593bb6dd60cd
- parent
- 16f7659b677d80ab65d40869f995baee317b83b8
- author
- Dami <dami.etoile@gmail.com>
| M | debug.go | 7 | +++- |
| A | game/ | 47 | ++++++++++++++++++++++++ |
| M | game/ | 4 | -- |
| M | game/ | 19 | ++++++++++ |
| M | hashiwokakero/ | 20 | +++-------- |
| M | hitori/ | 5 | --- |
| M | hitori/ | 58 | ++++++++---------------------- |
| M | lightsout/ | 14 | ++------ |
| M | nonogram/ | 14 | ++------ |
| M | shikaku/ | 4 | -- |
| M | shikaku/ | 12 | ++---- |
| M | sudoku/ | 1 | + |
| M | sudoku/ | 14 | ++++--- |
| M | sudoku/ | 14 | ++----- |
| M | takuzu/ | 56 | +++++++---------------------- |
| M | ui/ | 2 | +- |
| M | view.go | 3 | +- |
| M | wordsearch/ | 12 | ++----- |
@@ -9,15 +9,18 @@ func initDebugRenderer() *glamour.TermRenderer {
99 glamour.WithChromaFormatter("terminal16m"),1010 )1111 if err != nil {12- panic(err)12+ return nil1313 }1414 return renderer1515 }1616 1717 func (m model) renderDebugInfo() string {18+ if m.debugRenderer == nil {19+ return m.game.GetDebugInfo()20+ }1821 s, err := m.debugRenderer.Render(m.game.GetDebugInfo())1922 if err != nil {20- panic(err)23+ return m.game.GetDebugInfo()2124 }2225 return s2326 }@@ -0,0 +1,47 @@
1+package game2+3+import "github.com/charmbracelet/lipgloss"4+5+// GridBorderColors holds the color configuration for a grid border with6+// crosshair and solved-state highlighting.7+type GridBorderColors struct {8+ BorderFG lipgloss.AdaptiveColor9+ BackgroundBG lipgloss.AdaptiveColor10+ CrosshairBG lipgloss.AdaptiveColor11+ SolvedBorderFG lipgloss.AdaptiveColor12+ SolvedBG lipgloss.AdaptiveColor13+}14+15+// BorderChar renders a single border character with optional crosshair highlighting.16+func BorderChar(ch string, colors GridBorderColors, solved, highlight bool) string {17+ s := lipgloss.NewStyle().Foreground(colors.BorderFG).Background(colors.BackgroundBG)18+ if solved {19+ s = lipgloss.NewStyle().Foreground(colors.SolvedBorderFG).Background(colors.SolvedBG)20+ } else if highlight {21+ s = s.Background(colors.CrosshairBG)22+ }23+ return s.Render(ch)24+}25+26+// HBorderRow builds a horizontal border row (top or bottom) with per-column27+// crosshair highlighting. cellWidth is the visual width of each cell.28+func HBorderRow(w, cursorX, cellWidth int, left, right string, colors GridBorderColors, solved bool) string {29+ var parts []string30+ parts = append(parts, BorderChar(left, colors, solved, false))31+ segment := ""32+ for range cellWidth {33+ segment += "─"34+ }35+ for x := range w {36+ highlight := !solved && x == cursorX37+ s := lipgloss.NewStyle().Foreground(colors.BorderFG).Background(colors.BackgroundBG)38+ if solved {39+ s = lipgloss.NewStyle().Foreground(colors.SolvedBorderFG).Background(colors.SolvedBG)40+ } else if highlight {41+ s = s.Background(colors.CrosshairBG)42+ }43+ parts = append(parts, s.Render(segment))44+ }45+ parts = append(parts, BorderChar(right, colors, solved, false))46+ return lipgloss.JoinHorizontal(lipgloss.Top, parts...)47+}@@ -84,10 +84,6 @@ type SpawnCompleteMsg struct {
8484 // the full help display with Ctrl+H.8585 type HelpToggleMsg struct{ Show bool }8686 87-// ResetMsg is sent from the root model to games when the user wants to reset88-// the puzzle to its initial state (Ctrl+R).89-type ResetMsg struct{}90-9187 // Registry maps game type names to their import functions.9288 var Registry = map[string]func([]byte) (Gamer, error){}9389 @@ -18,12 +18,31 @@ var (
1818 Bold(true).1919 Foreground(lipgloss.AdaptiveColor{Light: "28", Dark: "78"})2020 21+ // CursorStyle highlights the cursor position with an accent background.22+ // Used by lightsout, sudoku, wordsearch.23+ CursorStyle = lipgloss.NewStyle().24+ Bold(true).25+ Foreground(lipgloss.AdaptiveColor{Light: "255", Dark: "235"}).26+ Background(lipgloss.AdaptiveColor{Light: "130", Dark: "173"})27+28+ // CursorWarmStyle highlights the cursor with a warmer background.29+ // Used by hitori, takuzu, nonogram.30+ CursorWarmStyle = lipgloss.NewStyle().31+ Bold(true).32+ Foreground(lipgloss.AdaptiveColor{Light: "255", Dark: "235"}).33+ Background(lipgloss.AdaptiveColor{Light: "130", Dark: "214"})34+2135 // CursorSolvedStyle highlights the cursor position on a solved grid.2236 // Shared across all puzzle types.2337 CursorSolvedStyle = lipgloss.NewStyle().2438 Bold(true).2539 Foreground(lipgloss.AdaptiveColor{Light: "255", Dark: "235"}).2640 Background(lipgloss.AdaptiveColor{Light: "28", Dark: "28"})41+42+ // StatusBarStyle is the shared style for the status/help bar below each puzzle grid.43+ StatusBarStyle = lipgloss.NewStyle().44+ Foreground(lipgloss.AdaptiveColor{Light: "244", Dark: "244"}).45+ MarginTop(1)2746 )2847 2948 // TitleBarView renders a title bar with the game name, mode name, and optional solved badge.@@ -18,8 +18,6 @@ var (
1818 colorSatisfiedBg = lipgloss.AdaptiveColor{Dark: "194", Light: "236"}1919 colorOverFg = lipgloss.AdaptiveColor{Dark: "160", Light: "167"}2020 colorOverBg = lipgloss.AdaptiveColor{Dark: "224", Light: "52"}21- colorCursorFg = lipgloss.AdaptiveColor{Dark: "255", Light: "235"}22- colorCursorBg = lipgloss.AdaptiveColor{Dark: "130", Light: "173"}2321 colorSelectedFg = lipgloss.AdaptiveColor{Dark: "255", Light: "235"}2422 colorSelectedBg = lipgloss.AdaptiveColor{Dark: "172", Light: "179"}2523 colorAdjacentBg = lipgloss.AdaptiveColor{Dark: "223", Light: "58"}@@ -49,11 +47,7 @@ var (
4947 Background(colorOverBg).5048 Bold(true)5149 52- islandCursorStyle = baseStyle.53- Foreground(colorCursorFg).54- Background(colorCursorBg).55- Bold(true)56-50+ islandCursorStyle = game.CursorStyle5751 islandCursorSolvedStyle = game.CursorSolvedStyle5852 5953 islandSelectedStyle = baseStyle.@@ -81,10 +75,6 @@ var (
8175 emptyStyle = baseStyle.8276 Foreground(colorEmptyDot)8377 84- statusBarStyle = lipgloss.NewStyle().85- Foreground(lipgloss.AdaptiveColor{Light: "244", Dark: "244"}).86- MarginTop(1)87-8878 gridBorderStyle = lipgloss.NewStyle().8979 Border(lipgloss.RoundedBorder()).9080 Padding(1).@@ -285,14 +275,14 @@ func bridgeVView(count int, solved bool) string {
285275 func statusBarView(selected, showFullHelp bool) string {286276 if selected {287277 if showFullHelp {288- return statusBarStyle.Render("arrows/wasd: build bridge enter/space/esc: cancel ctrl+n: menu ctrl+r: reset ctrl+h: help")278+ return game.StatusBarStyle.Render("arrows/wasd: build bridge enter/space/esc: cancel ctrl+n: menu ctrl+r: reset ctrl+h: help")289279 }290- return statusBarStyle.Render("arrows/wasd: build bridge enter/space/esc: cancel")280+ return game.StatusBarStyle.Render("arrows/wasd: build bridge enter/space/esc: cancel")291281 }292282 if showFullHelp {293- return statusBarStyle.Render("arrows/wasd: move enter/space: select island ctrl+n: menu ctrl+r: reset ctrl+h: help")283+ return game.StatusBarStyle.Render("arrows/wasd: move enter/space: select island ctrl+n: menu ctrl+r: reset ctrl+h: help")294284 }295- return statusBarStyle.Render("enter/space: select island")285+ return game.StatusBarStyle.Render("enter/space: select island")296286 }297287 298288 func infoView(p *Puzzle) string {@@ -64,11 +64,6 @@ func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {
6464 switch msg := msg.(type) {6565 case game.HelpToggleMsg:6666 m.showFullHelp = msg.Show67- case game.ResetMsg:68- m.marks = cloneMarks(m.initialMarks)69- m.solved = m.checkSolved()70- m.conflicts = computeConflicts(m.numbers, m.marks, m.size)71- m.cursor = game.Cursor{}7267 case tea.KeyMsg:7368 switch {7469 case key.Matches(msg, m.keys.ShadeCell):@@ -24,11 +24,7 @@ var (
2424 Foreground(lipgloss.AdaptiveColor{Light: "25", Dark: "75"}).2525 Background(backgroundColor)2626 27- cursorStyle = baseStyle.28- Bold(true).29- Foreground(lipgloss.AdaptiveColor{Light: "255", Dark: "235"}).30- Background(lipgloss.AdaptiveColor{Light: "130", Dark: "214"})31-27+ cursorStyle = game.CursorWarmStyle3228 cursorSolvedStyle = game.CursorSolvedStyle3329 3430 conflictStyle = baseStyle.@@ -39,10 +35,6 @@ var (
3935 solvedBG = lipgloss.AdaptiveColor{Light: "151", Dark: "22"}4036 4137 borderFG = lipgloss.AdaptiveColor{Light: "250", Dark: "240"}42-43- statusBarStyle = lipgloss.NewStyle().44- Foreground(lipgloss.AdaptiveColor{Light: "244", Dark: "244"}).45- MarginTop(1)4638 )4739 4840 const cellWidth = 3@@ -82,6 +74,14 @@ func cellView(num rune, mark cellMark, isCursor, inCursorRow, inCursorCol, solve
8274 return s.Width(cellWidth).AlignHorizontal(lipgloss.Center).Render(display)8375 }8476 77+var gridBorderColors = game.GridBorderColors{78+ BorderFG: borderFG,79+ BackgroundBG: backgroundColor,80+ CrosshairBG: crosshairBG,81+ SolvedBorderFG: lipgloss.AdaptiveColor{Light: "22", Dark: "149"},82+ SolvedBG: solvedBG,83+}84+8585 func gridView(numbers grid, marks [][]cellMark, c game.Cursor, solved bool, conflicts [][]bool) string {8686 w := 08787 if len(numbers) > 0 {@@ -93,7 +93,7 @@ func gridView(numbers grid, marks [][]cellMark, c game.Cursor, solved bool, conf
9393 var rowCells []string9494 inCursorRow := y == c.Y9595 96- rowCells = append(rowCells, borderChar("\u2502", solved, !solved && inCursorRow))96+ rowCells = append(rowCells, game.BorderChar("│", gridBorderColors, solved, !solved && inCursorRow))9797 9898 for x, num := range row {9999 isCursor := x == c.X && y == c.Y@@ -102,47 +102,19 @@ func gridView(numbers grid, marks [][]cellMark, c game.Cursor, solved bool, conf
102102 rowCells = append(rowCells, cell)103103 }104104 105- rowCells = append(rowCells, borderChar("\u2502", solved, !solved && inCursorRow))105+ rowCells = append(rowCells, game.BorderChar("│", gridBorderColors, solved, !solved && inCursorRow))106106 rows = append(rows, lipgloss.JoinHorizontal(lipgloss.Top, rowCells...))107107 }108108 109- topRow := hBorderRow(w, c.X, "\u256D", "\u256E", solved)110- botRow := hBorderRow(w, c.X, "\u2570", "\u256F", solved)109+ topRow := game.HBorderRow(w, c.X, cellWidth, "╭", "╮", gridBorderColors, solved)110+ botRow := game.HBorderRow(w, c.X, cellWidth, "╰", "╯", gridBorderColors, solved)111111 112112 return lipgloss.JoinVertical(lipgloss.Left, topRow, lipgloss.JoinVertical(lipgloss.Left, rows...), botRow)113113 }114114 115-func borderChar(ch string, solved, highlight bool) string {116- s := baseStyle.Foreground(borderFG).Background(backgroundColor)117- if solved {118- s = baseStyle.Foreground(lipgloss.AdaptiveColor{Light: "22", Dark: "149"}).Background(solvedBG)119- } else if highlight {120- s = s.Background(crosshairBG)121- }122- return s.Render(ch)123-}124-125-func hBorderRow(w, cursorX int, left, right string, solved bool) string {126- var parts []string127- parts = append(parts, borderChar(left, solved, false))128- segment := "\u2500\u2500\u2500" // ───129- for x := range w {130- highlight := !solved && x == cursorX131- s := baseStyle.Foreground(borderFG).Background(backgroundColor)132- if solved {133- s = baseStyle.Foreground(lipgloss.AdaptiveColor{Light: "22", Dark: "149"}).Background(solvedBG)134- } else if highlight {135- s = s.Background(crosshairBG)136- }137- parts = append(parts, s.Render(segment))138- }139- parts = append(parts, borderChar(right, solved, false))140- return lipgloss.JoinHorizontal(lipgloss.Top, parts...)141-}142-143115 func statusBarView(showFullHelp bool) string {144116 if showFullHelp {145- return statusBarStyle.Render("arrows/wasd: move x: shade z: circle bkspc: clear ctrl+n: menu ctrl+r: reset ctrl+h: help")117+ return game.StatusBarStyle.Render("arrows/wasd: move x: shade z: circle bkspc: clear ctrl+n: menu ctrl+r: reset ctrl+h: help")146118 }147- return statusBarStyle.Render("x: shade z: circle bkspc: clear")119+ return game.StatusBarStyle.Render("x: shade z: circle bkspc: clear")148120 }@@ -19,11 +19,7 @@ var (
1919 styleOff = baseStyle.2020 Background(colorOff)2121 22- cursorStyle = baseStyle.23- Bold(true).24- Foreground(lipgloss.AdaptiveColor{Light: "255", Dark: "235"}).25- Background(lipgloss.AdaptiveColor{Light: "130", Dark: "173"})26-22+ cursorStyle = game.CursorStyle2723 cursorSolvedStyle = game.CursorSolvedStyle2824 2925 solvedStyle = baseStyle.@@ -38,10 +34,6 @@ var (
3834 Border(lipgloss.RoundedBorder()).3935 BorderForeground(lipgloss.AdaptiveColor{Light: "22", Dark: "149"}).4036 BorderBackground(lipgloss.AdaptiveColor{Light: "151", Dark: "22"})41-42- statusBarStyle = lipgloss.NewStyle().43- Foreground(lipgloss.AdaptiveColor{Light: "244", Dark: "244"}).44- MarginTop(1)4537 )4638 4739 const (@@ -87,7 +79,7 @@ func gridView(g [][]bool, c game.Cursor, solved bool) string {
8779 8880 func statusBarView(showFullHelp bool) string {8981 if showFullHelp {90- return statusBarStyle.Render("arrows/wasd: move enter/space: toggle ctrl+n: menu ctrl+r: reset ctrl+h: help")82+ return game.StatusBarStyle.Render("arrows/wasd: move enter/space: toggle ctrl+n: menu ctrl+r: reset ctrl+h: help")9183 }92- return statusBarStyle.Render("enter/space: toggle")84+ return game.StatusBarStyle.Render("enter/space: toggle")9385 }@@ -23,11 +23,7 @@ var (
2323 Foreground(lipgloss.AdaptiveColor{Light: "250", Dark: "240"}).2424 Background(lipgloss.AdaptiveColor{Light: "254", Dark: "235"})2525 26- cursorStyle = baseStyle.27- Bold(true).28- Foreground(lipgloss.AdaptiveColor{Light: "255", Dark: "235"}).29- Background(lipgloss.AdaptiveColor{Light: "130", Dark: "214"})30-26+ cursorStyle = game.CursorWarmStyle3127 cursorSolvedStyle = game.CursorSolvedStyle3228 3329 crosshairBG = lipgloss.AdaptiveColor{Light: "254", Dark: "237"}@@ -40,10 +36,6 @@ var (
4036 hintSatisfiedStyle = baseStyle.4137 Foreground(lipgloss.AdaptiveColor{Light: "22", Dark: "149"})4238 43- statusBarStyle = lipgloss.NewStyle().44- Foreground(lipgloss.AdaptiveColor{Light: "244", Dark: "244"}).45- MarginTop(1)46-4739 separatorFG = lipgloss.AdaptiveColor{Light: "250", Dark: "240"}4840 )4941 @@ -260,9 +252,9 @@ func tileView(val rune, isCursor, inCursorRow, inCursorCol, solved bool) string
260252 261253 func statusBarView(showFullHelp bool) string {262254 if showFullHelp {263- return statusBarStyle.Render("arrows/wasd: move z: fill x: mark bkspc: clear ctrl+n: menu ctrl+r: reset ctrl+h: help")255+ return game.StatusBarStyle.Render("arrows/wasd: move z: fill x: mark bkspc: clear ctrl+n: menu ctrl+r: reset ctrl+h: help")264256 }265- return statusBarStyle.Render("z: fill x: mark bkspc: clear")257+ return game.StatusBarStyle.Render("z: fill x: mark bkspc: clear")266258 }267259 268260 func intSliceEqual(a, b []int) bool {@@ -56,10 +56,6 @@ func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {
5656 switch msg := msg.(type) {5757 case game.HelpToggleMsg:5858 m.showFullHelp = msg.Show59- case game.ResetMsg:60- m.puzzle.Rectangles = nil61- m.cursor = game.Cursor{}62- m.selectedClue = nil6359 case tea.KeyMsg:6460 if m.puzzle.IsSolved() {6561 m.cursor.Move(m.keys.CursorKeyMap, msg, m.puzzle.Width-1, m.puzzle.Height-1)@@ -52,10 +52,6 @@ var (
5252 infoSatisfied = lipgloss.AdaptiveColor{Dark: "22", Light: "149"}5353 infoText = lipgloss.AdaptiveColor{Dark: "137", Light: "137"}5454 55- statusBarStyle = lipgloss.NewStyle().56- Foreground(lipgloss.AdaptiveColor{Light: "244", Dark: "244"}).57- MarginTop(1)58-5955 gridBorderStyle = lipgloss.NewStyle().6056 Border(lipgloss.RoundedBorder()).6157 BorderForeground(borderColor)@@ -180,12 +176,12 @@ func infoView(p *Puzzle) string {
180176 func statusBarView(selected, showFullHelp bool) string {181177 if selected {182178 if showFullHelp {183- return statusBarStyle.Render("arrows: expand shift+arrows: shrink enter: confirm esc: cancel bkspc: delete ctrl+n: menu ctrl+r: reset ctrl+h: help")179+ return game.StatusBarStyle.Render("arrows: expand shift+arrows: shrink enter: confirm esc: cancel bkspc: delete ctrl+n: menu ctrl+r: reset ctrl+h: help")184180 }185- return statusBarStyle.Render("arrows: expand shift+arrows: shrink enter: confirm esc: cancel")181+ return game.StatusBarStyle.Render("arrows: expand shift+arrows: shrink enter: confirm esc: cancel")186182 }187183 if showFullHelp {188- return statusBarStyle.Render("arrows/wasd: move enter/space: select clue bkspc: delete ctrl+n: menu ctrl+r: reset ctrl+h: help")184+ return game.StatusBarStyle.Render("arrows/wasd: move enter/space: select clue bkspc: delete ctrl+n: menu ctrl+r: reset ctrl+h: help")189185 }190- return statusBarStyle.Render("enter/space: select clue bkspc: delete")186+ return game.StatusBarStyle.Render("enter/space: select clue bkspc: delete")191187 }@@ -58,6 +58,7 @@ func ImportModel(data []byte) (*Model, error) {
5858 grid: g,5959 provided: provided,6060 providedGrid: buildProvidedGrid(provided),61+ conflicts: computeConflicts(g),6162 keys: DefaultKeyMap,6263 }, nil6364 }@@ -20,6 +20,7 @@ type Model struct {
2020 grid grid2121 provided []cell2222 providedGrid [gridSize][gridSize]bool23+ conflicts [gridSize][gridSize]bool2324 keys KeyMap2425 modeTitle string2526 showFullHelp bool@@ -39,6 +40,7 @@ func New(mode SudokuMode, provided []cell) (game.Gamer, error) {
3940 grid: g,4041 provided: provided,4142 providedGrid: buildProvidedGrid(provided),43+ conflicts: computeConflicts(g),4244 keys: DefaultKeyMap,4345 modeTitle: mode.Title(),4446 }@@ -83,6 +85,7 @@ func (m Model) IsSolved() bool {
8385 // Reset implements game.Gamer.8486 func (m Model) Reset() game.Gamer {8587 m.grid = newGrid(m.provided)88+ m.conflicts = computeConflicts(m.grid)8689 return m8790 }8891 @@ -91,14 +94,14 @@ func (m *Model) updateCell(v int) {
9194 return9295 }9396 m.grid[m.cursor.Y][m.cursor.X].v = v97+ m.conflicts = computeConflicts(m.grid)9498 }9599 96100 // View implements game.Gamer.97101 func (m Model) View() string {98- conflicts := computeConflicts(m.grid)99- solved := isSolvedWith(m.grid, conflicts)102+ solved := isSolvedWith(m.grid, m.conflicts)100103 title := game.TitleBarView("Sudoku", m.modeTitle, solved)101- grid := renderGrid(m, solved, conflicts)104+ grid := renderGrid(m, solved, m.conflicts)102105 status := statusBarView(m.showFullHelp)103106 104107 return lipgloss.JoinVertical(lipgloss.Center, title, grid, status)@@ -106,10 +109,9 @@ func (m Model) View() string {
106109 107110 // GetDebugInfo implements game.Gamer.108111 func (m Model) GetDebugInfo() string {109- conflicts := computeConflicts(m.grid)110112 cursorCell := m.grid[m.cursor.Y][m.cursor.X]111113 isProvided := m.providedGrid[m.cursor.Y][m.cursor.X]112- conflict := conflicts[m.cursor.Y][m.cursor.X]114+ conflict := m.conflicts[m.cursor.Y][m.cursor.X]113115 solved := m.isSolved()114116 115117 filledCount := 0@@ -119,7 +121,7 @@ func (m Model) GetDebugInfo() string {
119121 if m.grid[y][x].v != 0 {120122 filledCount++121123 }122- if conflicts[y][x] {124+ if m.conflicts[y][x] {123125 conflictCount++124126 }125127 }@@ -4,6 +4,7 @@ import (
44 "fmt"55 "strings"66 7+ "github.com/FelineStateMachine/puzzletea/game"78 "github.com/charmbracelet/lipgloss"89 )910 @@ -23,10 +24,7 @@ var (
2324 Foreground(lipgloss.AdaptiveColor{Light: "236", Dark: "187"}).2425 Background(backgroundColor)2526 26- cursorCellStyle = lipgloss.NewStyle().27- Bold(true).28- Foreground(lipgloss.AdaptiveColor{Light: "255", Dark: "235"}).29- Background(lipgloss.AdaptiveColor{Light: "130", Dark: "173"})27+ cursorCellStyle = game.CursorStyle3028 3129 conflictCellStyle = lipgloss.NewStyle().3230 Foreground(lipgloss.AdaptiveColor{Light: "160", Dark: "167"}).@@ -50,10 +48,6 @@ var (
5048 BorderForeground(lipgloss.AdaptiveColor{Light: "22", Dark: "149"}).5149 BorderBackground(backgroundColor)5250 53- statusBarStyle = lipgloss.NewStyle().54- Foreground(lipgloss.AdaptiveColor{Light: "244", Dark: "244"}).55- MarginTop(1)56-5751 sudokuCellWidth = 25852 )5953 @@ -230,7 +224,7 @@ func computeConflicts(g grid) [gridSize][gridSize]bool {
230224 231225 func statusBarView(showFullHelp bool) string {232226 if showFullHelp {233- return statusBarStyle.Render("arrows/wasd: move 1-9: fill bkspc: clear ctrl+n: menu ctrl+r: reset ctrl+h: help")227+ return game.StatusBarStyle.Render("arrows/wasd: move 1-9: fill bkspc: clear ctrl+n: menu ctrl+r: reset ctrl+h: help")234228 }235- return statusBarStyle.Render("1-9: fill bkspc: clear")229+ return game.StatusBarStyle.Render("1-9: fill bkspc: clear")236230 }@@ -20,21 +20,13 @@ var (
2020 Foreground(lipgloss.AdaptiveColor{Light: "250", Dark: "240"}).2121 Background(backgroundColor)2222 23- cursorStyle = baseStyle.24- Bold(true).25- Foreground(lipgloss.AdaptiveColor{Light: "255", Dark: "235"}).26- Background(lipgloss.AdaptiveColor{Light: "130", Dark: "214"})27-23+ cursorStyle = game.CursorWarmStyle2824 cursorSolvedStyle = game.CursorSolvedStyle2925 3026 crosshairBG = lipgloss.AdaptiveColor{Light: "254", Dark: "237"}3127 solvedBG = lipgloss.AdaptiveColor{Light: "151", Dark: "22"}3228 3329 borderFG = lipgloss.AdaptiveColor{Light: "250", Dark: "240"}34-35- statusBarStyle = lipgloss.NewStyle().36- Foreground(lipgloss.AdaptiveColor{Light: "244", Dark: "244"}).37- MarginTop(1)3830 )3931 4032 const cellWidth = 3@@ -92,7 +84,7 @@ func gridView(g grid, provided [][]bool, c game.Cursor, solved bool) string {
9284 inCursorRow := y == c.Y9385 9486 // Left border segment for this row.95- rowCells = append(rowCells, borderChar("│", solved, !solved && inCursorRow))87+ rowCells = append(rowCells, game.BorderChar("│", gridBorderColors, solved, !solved && inCursorRow))9688 9789 for x, val := range row {9890 isCursor := x == c.X && y == c.Y@@ -102,51 +94,29 @@ func gridView(g grid, provided [][]bool, c game.Cursor, solved bool) string {
10294 }10395 10496 // Right border segment for this row.105- rowCells = append(rowCells, borderChar("│", solved, !solved && inCursorRow))97+ rowCells = append(rowCells, game.BorderChar("│", gridBorderColors, solved, !solved && inCursorRow))10698 10799 rows = append(rows, lipgloss.JoinHorizontal(lipgloss.Top, rowCells...))108100 }109101 110102 // Build top and bottom border rows with per-column crosshair highlighting.111- topRow := hBorderRow(w, c.X, "╭", "╮", solved)112- botRow := hBorderRow(w, c.X, "╰", "╯", solved)103+ topRow := game.HBorderRow(w, c.X, cellWidth, "╭", "╮", gridBorderColors, solved)104+ botRow := game.HBorderRow(w, c.X, cellWidth, "╰", "╯", gridBorderColors, solved)113105 114106 return lipgloss.JoinVertical(lipgloss.Left, topRow, lipgloss.JoinVertical(lipgloss.Left, rows...), botRow)115107 }116108 117-// borderChar renders a single border character with optional crosshair highlighting.118-func borderChar(ch string, solved, highlight bool) string {119- s := baseStyle.Foreground(borderFG).Background(backgroundColor)120- if solved {121- s = baseStyle.Foreground(lipgloss.AdaptiveColor{Light: "22", Dark: "149"}).Background(solvedBG)122- } else if highlight {123- s = s.Background(crosshairBG)124- }125- return s.Render(ch)126-}127-128-// hBorderRow builds a horizontal border row (top or bottom) with per-column crosshair.129-func hBorderRow(w, cursorX int, left, right string, solved bool) string {130- var parts []string131- parts = append(parts, borderChar(left, solved, false))132- segment := "───" // cellWidth = 3133- for x := range w {134- highlight := !solved && x == cursorX135- s := baseStyle.Foreground(borderFG).Background(backgroundColor)136- if solved {137- s = baseStyle.Foreground(lipgloss.AdaptiveColor{Light: "22", Dark: "149"}).Background(solvedBG)138- } else if highlight {139- s = s.Background(crosshairBG)140- }141- parts = append(parts, s.Render(segment))142- }143- parts = append(parts, borderChar(right, solved, false))144- return lipgloss.JoinHorizontal(lipgloss.Top, parts...)109+var gridBorderColors = game.GridBorderColors{110+ BorderFG: borderFG,111+ BackgroundBG: backgroundColor,112+ CrosshairBG: crosshairBG,113+ SolvedBorderFG: lipgloss.AdaptiveColor{Light: "22", Dark: "149"},114+ SolvedBG: solvedBG,145115 }146116 147117 func statusBarView(showFullHelp bool) string {148118 if showFullHelp {149- return statusBarStyle.Render("arrows/wasd: move z: ● x: ○ bkspc: clear ctrl+n: menu ctrl+r: reset ctrl+h: help")119+ return game.StatusBarStyle.Render("arrows/wasd: move z: ● x: ○ bkspc: clear ctrl+n: menu ctrl+r: reset ctrl+h: help")150120 }151- return statusBarStyle.Render("z: ● x: ○ bkspc: clear")121+ return game.StatusBarStyle.Render("z: ● x: ○ bkspc: clear")152122 }@@ -13,9 +13,7 @@ var (
1313 Foreground(lipgloss.AdaptiveColor{Light: "236", Dark: "252"}).1414 Background(lipgloss.AdaptiveColor{Light: "254", Dark: "236"})1515 16- cursorStyle = lipgloss.NewStyle().17- Foreground(lipgloss.AdaptiveColor{Light: "255", Dark: "235"}).18- Background(lipgloss.AdaptiveColor{Light: "130", Dark: "173"})16+ cursorStyle = game.CursorStyle1917 2018 selectionStyle = lipgloss.NewStyle().2119 Foreground(lipgloss.AdaptiveColor{Light: "235", Dark: "235"}).@@ -37,10 +35,6 @@ var (
3735 unfoundWordStyle = lipgloss.NewStyle().3836 Foreground(lipgloss.AdaptiveColor{Light: "137", Dark: "137"})3937 40- statusBarStyle = lipgloss.NewStyle().41- Foreground(lipgloss.AdaptiveColor{Light: "244", Dark: "244"}).42- MarginTop(1)43-4438 borderFG = lipgloss.AdaptiveColor{Light: "250", Dark: "240"}4539 gridBG = lipgloss.AdaptiveColor{Light: "254", Dark: "236"}4640 solvedBdrFG = lipgloss.AdaptiveColor{Light: "22", Dark: "149"}@@ -94,9 +88,9 @@ func renderView(m Model) string {
9488 9589 func statusBarView(showFullHelp bool) string {9690 if showFullHelp {97- return statusBarStyle.Render("arrows/wasd: move enter/space: select esc: cancel ctrl+n: menu ctrl+r: reset ctrl+h: help")91+ return game.StatusBarStyle.Render("arrows/wasd: move enter/space: select esc: cancel ctrl+n: menu ctrl+r: reset ctrl+h: help")9892 }99- return statusBarStyle.Render("enter/space: select esc: cancel")93+ return game.StatusBarStyle.Render("enter/space: select esc: cancel")10094 }10195 10296 func renderGrid(m Model) string {