- Restore initialGrid in takuzu/lightsout ImportModel to prevent nil slice panic when pressing Ctrl+R on saved/completed puzzles - Persist modeTitle in hashiwokakero/sudoku save data so title bar renders correctly after loading a saved game - Switch all puzzle Update handlers from tea.KeyMsg to tea.KeyPressMsg so key release events no longer trigger duplicate side effects - Cap game view width to terminal width before centering to prevent JoinVertical(Center) status bar inflation from causing line wrapping - Remove phantom empty line from JoinVertical when debug is off, fixing nonogram mouse Y offset being off by one row - Account for MaxWidth capping in nonogram gridOrigin so horizontal hit-testing stays accurate on narrow terminals - Add ListHeight helper and disable pagination to prevent list rendering glitches on resize
- commit
- eb8440f4cd777f76eea1935b3a143213fd7d5b38
- parent
- 8f8936d995b708db2f35995cce89861a276c23c8
- author
- Dami <dami.etoile@gmail.com>
| M | game/ | 2 | +- |
| M | hashiwokakero/ | 19 | ++++++++------ |
| M | hashiwokakero/ | 6 | ++-- |
| M | hitori/ | 2 | +- |
| M | lightsout/ | 42 | ++++++++++++++++++++---------- |
| M | lightsout/ | 2 | +- |
| M | nonogram/ | 5 | ++++ |
| M | shikaku/ | 6 | ++-- |
| M | sudoku/ | 11 | +++++--- |
| M | sudoku/ | 2 | +- |
| M | takuzu/ | 22 | +++++++++++---- |
| M | takuzu/ | 2 | +- |
| M | ui/ | 16 | +++++++++++ |
| M | update.go | 14 | +++++----- |
| M | view.go | 15 | +++++------ |
| M | wordsearch/ | 4 | +- |
@@ -12,7 +12,7 @@ type Cursor struct {
1212 1313 // Move updates the cursor position based on the key message and bounds.1414 // Returns true if the cursor moved.15-func (c *Cursor) Move(keys CursorKeyMap, msg tea.KeyMsg, maxX, maxY int) bool {15+func (c *Cursor) Move(keys CursorKeyMap, msg tea.KeyPressMsg, maxX, maxY int) bool {1616 switch {1717 case key.Matches(msg, keys.Up):1818 if c.Y > 0 {@@ -6,10 +6,11 @@ import (
66 )77 88 type Save struct {9- Width int `json:"width"`10- Height int `json:"height"`11- Islands []Island `json:"islands"`12- Bridges []Bridge `json:"bridges"`9+ Width int `json:"width"`10+ Height int `json:"height"`11+ Islands []Island `json:"islands"`12+ Bridges []Bridge `json:"bridges"`13+ ModeTitle string `json:"mode_title,omitempty"`1314 }1415 1516 func ImportModel(data []byte) (*Model, error) {@@ -31,16 +32,18 @@ func ImportModel(data []byte) (*Model, error) {
3132 Bridges: save.Bridges,3233 },3334 cursorIsland: cursorIsland,35+ modeTitle: save.ModeTitle,3436 keys: DefaultKeyMap,3537 }, nil3638 }3739 3840 func (m Model) GetSave() ([]byte, error) {3941 save := Save{40- Width: m.puzzle.Width,41- Height: m.puzzle.Height,42- Islands: m.puzzle.Islands,43- Bridges: m.puzzle.Bridges,42+ Width: m.puzzle.Width,43+ Height: m.puzzle.Height,44+ Islands: m.puzzle.Islands,45+ Bridges: m.puzzle.Bridges,46+ ModeTitle: m.modeTitle,4447 }4548 jsonData, err := json.Marshal(save)4649 if err != nil {@@ -42,7 +42,7 @@ func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {
4242 switch msg := msg.(type) {4343 case game.HelpToggleMsg:4444 m.showFullHelp = msg.Show45- case tea.KeyMsg:45+ case tea.KeyPressMsg:4646 if m.selectedIsland != nil {4747 m = m.handleBridgeMode(msg)4848 } else {@@ -52,7 +52,7 @@ func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {
5252 return m, nil5353 }5454 55-func (m Model) handleNavMode(msg tea.KeyMsg) Model {55+func (m Model) handleNavMode(msg tea.KeyPressMsg) Model {5656 switch {5757 case key.Matches(msg, m.keys.Up):5858 m = m.moveCursor(0, -1)@@ -69,7 +69,7 @@ func (m Model) handleNavMode(msg tea.KeyMsg) Model {
6969 return m7070 }7171 72-func (m Model) handleBridgeMode(msg tea.KeyMsg) Model {72+func (m Model) handleBridgeMode(msg tea.KeyPressMsg) Model {7373 switch {7474 case key.Matches(msg, m.keys.Up):7575 m = m.cycleBridge(0, -1)@@ -64,7 +64,7 @@ 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 tea.KeyMsg:67+ case tea.KeyPressMsg:6868 switch {6969 case key.Matches(msg, m.keys.ShadeCell):7070 if !m.solved {@@ -8,18 +8,20 @@ import (
88 )99 1010 type Save struct {11- Grid [][]bool `json:"grid"`12- CursorX int `json:"cx"`13- CursorY int `json:"cy"`14- ModeTitle string `json:"mode_title"`11+ Grid [][]bool `json:"grid"`12+ InitialGrid [][]bool `json:"initial_grid,omitempty"`13+ CursorX int `json:"cx"`14+ CursorY int `json:"cy"`15+ ModeTitle string `json:"mode_title"`1516 }1617 1718 func (m Model) GetSave() ([]byte, error) {1819 save := Save{19- Grid: m.grid,20- CursorX: m.cursor.X,21- CursorY: m.cursor.Y,22- ModeTitle: m.modeTitle,20+ Grid: m.grid,21+ InitialGrid: m.initialGrid,22+ CursorX: m.cursor.X,23+ CursorY: m.cursor.Y,24+ ModeTitle: m.modeTitle,2325 }2426 jsonData, err := json.Marshal(save)2527 if err != nil {@@ -41,12 +43,24 @@ func ImportModel(data []byte) (*Model, error) {
4143 h := len(s.Grid)4244 w := len(s.Grid[0])4345 46+ // Fall back to a copy of the current grid for saves that predate47+ // the initialGrid field.48+ initial := s.InitialGrid49+ if len(initial) == 0 {50+ initial = make([][]bool, h)51+ for y := range h {52+ initial[y] = make([]bool, w)53+ copy(initial[y], s.Grid[y])54+ }55+ }56+4457 return &Model{45- grid: s.Grid,46- width: w,47- height: h,48- cursor: game.Cursor{X: s.CursorX, Y: s.CursorY},49- modeTitle: s.ModeTitle,50- keys: DefaultKeyMap,58+ grid: s.Grid,59+ initialGrid: initial,60+ width: w,61+ height: h,62+ cursor: game.Cursor{X: s.CursorX, Y: s.CursorY},63+ modeTitle: s.ModeTitle,64+ keys: DefaultKeyMap,5165 }, nil5266 }@@ -58,7 +58,7 @@ func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {
5858 switch msg := msg.(type) {5959 case game.HelpToggleMsg:6060 m.showFullHelp = msg.Show61- case tea.KeyMsg:61+ case tea.KeyPressMsg:6262 switch {6363 case key.Matches(msg, m.keys.Toggle):6464 if !m.IsSolved() {@@ -88,6 +88,11 @@ func (m *Model) gridOrigin() (x, y int) {
8888 viewWidth := lipgloss.Width(gameView)8989 viewHeight := lipgloss.Height(gameView)9090 91+ // The root view caps the game view to terminal width before centering.92+ if viewWidth > m.termWidth {93+ viewWidth = m.termWidth94+ }95+9196 // Centering offset applied by the root's lipgloss.Place().9297 centerX := max((m.termWidth-viewWidth)/2, 0)9398 centerY := max((m.termHeight-viewHeight)/2, 0)@@ -56,7 +56,7 @@ 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 tea.KeyMsg:59+ case tea.KeyPressMsg:6060 if m.puzzle.IsSolved() {6161 m.cursor.Move(m.keys.CursorKeyMap, msg, m.puzzle.Width-1, m.puzzle.Height-1)6262 } else if m.selectedClue != nil {@@ -68,7 +68,7 @@ func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {
6868 return m, nil6969 }7070 71-func (m Model) handleNavMode(msg tea.KeyMsg) Model {71+func (m Model) handleNavMode(msg tea.KeyPressMsg) Model {7272 switch {7373 case key.Matches(msg, m.keys.Select):7474 clue := m.puzzle.FindClueAt(m.cursor.X, m.cursor.Y)@@ -95,7 +95,7 @@ func (m Model) handleNavMode(msg tea.KeyMsg) Model {
9595 return m9696 }9797 98-func (m Model) handleExpansionMode(msg tea.KeyMsg) Model {98+func (m Model) handleExpansionMode(msg tea.KeyPressMsg) Model {9999 clue := m.puzzle.FindClueByID(*m.selectedClue)100100 if clue == nil {101101 m.selectedClue = nil@@ -13,8 +13,9 @@ type exportCell struct {
1313 }1414 1515 type Save struct {16- Grid string `json:"grid"`17- Provided []exportCell `json:"provided"`16+ Grid string `json:"grid"`17+ Provided []exportCell `json:"provided"`18+ ModeTitle string `json:"mode_title,omitempty"`1819 }1920 2021 func gridToString(g grid) string {@@ -59,6 +60,7 @@ func ImportModel(data []byte) (*Model, error) {
5960 provided: provided,6061 providedGrid: buildProvidedGrid(provided),6162 conflicts: computeConflicts(g),63+ modeTitle: save.ModeTitle,6264 keys: DefaultKeyMap,6365 }, nil6466 }@@ -69,8 +71,9 @@ func (m Model) GetSave() ([]byte, error) {
6971 provided[i] = exportCell{X: c.x, Y: c.y, V: c.v}7072 }7173 save := Save{72- Grid: gridToString(m.grid),73- Provided: provided,74+ Grid: gridToString(m.grid),75+ Provided: provided,76+ ModeTitle: m.modeTitle,7477 }7578 jsonData, err := json.Marshal(save)7679 if err != nil {@@ -57,7 +57,7 @@ func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {
5757 switch msg := msg.(type) {5858 case game.HelpToggleMsg:5959 m.showFullHelp = msg.Show60- case tea.KeyMsg:60+ case tea.KeyPressMsg:6161 switch {6262 case key.Matches(msg, m.keys.FillValue):6363 val, _ := strconv.Atoi(msg.String())@@ -43,13 +43,23 @@ func ImportModel(data []byte) (*Model, error) {
4343 g := newGrid(state(s.State))4444 provided := deserializeProvided(s.Provided, s.Size)4545 46+ initial := g.clone()47+ for y := range s.Size {48+ for x := range s.Size {49+ if !provided[y][x] {50+ initial[y][x] = emptyCell51+ }52+ }53+ }54+4655 m := &Model{47- size: s.Size,48- grid: g,49- provided: provided,50- cursor: game.Cursor{X: 0, Y: 0},51- keys: DefaultKeyMap,52- modeTitle: s.ModeTitle,56+ size: s.Size,57+ grid: g,58+ initialGrid: initial,59+ provided: provided,60+ cursor: game.Cursor{X: 0, Y: 0},61+ keys: DefaultKeyMap,62+ modeTitle: s.ModeTitle,5363 }5464 m.solved = m.checkSolved()5565 return m, nil@@ -48,7 +48,7 @@ func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {
4848 switch msg := msg.(type) {4949 case game.HelpToggleMsg:5050 m.showFullHelp = msg.Show51- case tea.KeyMsg:51+ case tea.KeyPressMsg:5252 switch {5353 case key.Matches(msg, m.keys.PlaceZero):5454 if !m.provided[m.cursor.Y][m.cursor.X] && !m.solved {@@ -30,5 +30,21 @@ func InitList(items []list.Item, title string) list.Model {
3030 l.DisableQuitKeybindings()3131 l.SetFilteringEnabled(false)3232 l.SetShowHelp(false)33+ l.SetShowStatusBar(false)34+ l.SetShowPagination(false)3335 return l3436 }37+38+// ListHeight returns the minimum height needed to display all items in a39+// single page, avoiding pagination. The bubbles list component has a40+// circular dependency between pagination visibility and PerPage calculation41+// that causes rendering glitches when pages change, so we size the list to42+// prevent pagination entirely when the terminal is tall enough.43+func ListHeight(l list.Model) int {44+ n := len(l.Items())45+ // title bar (title line + bottom padding) = 2 lines46+ const titleHeight = 247+ // Default delegate: height=2, spacing=1 → 3 lines per item.48+ const itemSlot = 349+ return titleHeight + itemSlot*n50+}@@ -25,18 +25,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
2525 case tea.WindowSizeMsg:2626 m.width = msg.Width2727 m.height = msg.Height28- menuW, menuH := min(m.width, 64), min(m.height, 24)29- m.mainMenuList.SetSize(menuW, menuH)30- m.gameSelectList.SetSize(menuW, menuH)28+ menuW := min(m.width, 64)29+ m.mainMenuList.SetSize(menuW, min(m.height, ui.ListHeight(m.mainMenuList)))30+ m.gameSelectList.SetSize(menuW, min(m.height, ui.ListHeight(m.gameSelectList)))3131 if m.state == modeSelectView {32- m.modeSelectList.SetSize(menuW, menuH)32+ m.modeSelectList.SetSize(menuW, min(m.height, ui.ListHeight(m.modeSelectList)))3333 }3434 if m.state == continueView {3535 m.continueTable.SetWidth(m.width)3636 m.continueTable.SetHeight(m.height)3737 }3838 if m.state == helpSelectView {39- m.helpSelectList.SetSize(menuW, menuH)39+ m.helpSelectList.SetSize(menuW, min(m.height, ui.ListHeight(m.helpSelectList)))4040 }4141 if m.state == helpDetailView {4242 m.helpViewport.SetWidth(m.width)@@ -152,7 +152,7 @@ func (m model) handleMainMenuEnter() (tea.Model, tea.Cmd) {
152152 m.state = continueView153153 case "Guides":154154 m.helpSelectList = ui.InitList(GameCategories, "How to Play")155- m.helpSelectList.SetSize(min(m.width, 64), min(m.height, 24))155+ m.helpSelectList.SetSize(min(m.width, 64), min(m.height, ui.ListHeight(m.helpSelectList)))156156 m.state = helpSelectView157157 case "Quit":158158 return m, tea.Quit@@ -216,7 +216,7 @@ func (m model) handleGameSelectEnter() (tea.Model, tea.Cmd) {
216216 }217217 m.selectedCategory = cat218218 m.modeSelectList = ui.InitList(cat.Modes, cat.Name+" - Select Mode")219- m.modeSelectList.SetSize(m.gameSelectList.Width(), m.gameSelectList.Height())219+ m.modeSelectList.SetSize(min(m.width, 64), min(m.height, ui.ListHeight(m.modeSelectList)))220220 m.state = modeSelectView221221 return m, nil222222 }@@ -41,16 +41,15 @@ func (m model) View() tea.View {
4141 if m.game == nil {4242 content = ""4343 } else {44- var debugInfo string44+ gameView := lipgloss.NewStyle().MaxWidth(m.width).Render(m.game.View())45+ centered := gameView4546 if m.debug {46- debugInfo = ui.DebugStyle.Render(m.debuginfo)47+ debugInfo := lipgloss.NewStyle().MaxWidth(m.width).Render(48+ ui.DebugStyle.Render(m.debuginfo),49+ )50+ centered = lipgloss.JoinVertical(lipgloss.Center, gameView, debugInfo)4751 }48- content = ui.CenterView(m.width, m.height,49- lipgloss.JoinVertical(lipgloss.Center,50- m.game.View(),51- debugInfo,52- ),53- )52+ content = ui.CenterView(m.width, m.height, centered)5453 }5554 case helpSelectView:5655 content = ui.CenterView(m.width, m.height, m.helpSelectList.View())@@ -71,13 +71,13 @@ func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {
7171 switch msg := msg.(type) {7272 case game.HelpToggleMsg:7373 m.showFullHelp = msg.Show74- case tea.KeyMsg:74+ case tea.KeyPressMsg:7575 return m.handleKeyPress(msg)7676 }7777 return m, nil7878 }7979 80-func (m Model) handleKeyPress(msg tea.KeyMsg) (game.Gamer, tea.Cmd) {80+func (m Model) handleKeyPress(msg tea.KeyPressMsg) (game.Gamer, tea.Cmd) {8181 switch {8282 case key.Matches(msg, m.keys.Select):8383 m.handleSelect()