clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

chore/app-model-decomp
puzzletea / ui / table.go
table.go 3.5K
1package ui23import (4 "fmt"5 "log"67 "charm.land/bubbles/v2/table"8 "charm.land/lipgloss/v2"9 "github.com/FelineStateMachine/puzzletea/store"10 "github.com/FelineStateMachine/puzzletea/theme"11)1213// MaxTableRows is the maximum number of rows visible in the continue table.14const MaxTableRows = 201516const (17 continueTableWidth = 10418 weeklyTableWidth = 7119)2021// ContinueTableWidth is the natural width of the saved-games table.22func ContinueTableWidth() int {23 return continueTableWidth24}2526// WeeklyTableWidth is the natural width of the weekly browser table.27func WeeklyTableWidth() int {28 return weeklyTableWidth29}3031// FormatStatus converts a GameStatus enum to a human-readable display string.32func FormatStatus(s store.GameStatus) string {33 switch s {34 case store.StatusNew:35 return "New"36 case store.StatusInProgress:37 return "In Progress"38 case store.StatusCompleted:39 return "Completed"40 default:41 return string(s)42 }43}4445// InitContinueTable loads saved games from the store and builds the table46// widget. Returns the table, the loaded games, and the column definitions.47func InitContinueTable(s *store.Store, height int) (table.Model, []store.GameRecord) {48 games, err := s.ListGames()49 if err != nil {50 log.Printf("failed to list games: %v", err)51 games = nil52 }5354 columns := []table.Column{55 {Title: "Name", Width: 34},56 {Title: "Game", Width: 15},57 {Title: "Mode", Width: 15},58 {Title: "Status", Width: 12},59 {Title: "Last Updated", Width: 20},60 }6162 rows := make([]table.Row, len(games))63 for i, g := range games {64 rows[i] = table.Row{65 g.Name,66 g.GameType,67 g.Mode,68 FormatStatus(g.Status),69 g.UpdatedAt.Local().Format("Jan 02 15:04"),70 }71 }7273 visibleRows := min(len(rows), MaxTableRows)74 t := table.New(75 table.WithColumns(columns),76 table.WithRows(rows),77 table.WithFocused(true),78 table.WithWidth(continueTableWidth),79 table.WithHeight(min(max(height-2, 1), visibleRows)),80 )81 t.SetStyles(defaultTableStyles())8283 return t, games84}8586// InitWeeklyTable builds the weekly browser table from pre-rendered rows.87func InitWeeklyTable(rows []table.Row, height int) table.Model {88 columns := []table.Column{89 {Title: "Puzzle", Width: 8},90 {Title: "Game", Width: 15},91 {Title: "Mode", Width: 18},92 {Title: "Bonus XP", Width: 10},93 {Title: "Status", Width: 12},94 }9596 tableHeight := min(max(height-2, 1), MaxTableRows)97 t := table.New(98 table.WithColumns(columns),99 table.WithRows(rows),100 table.WithFocused(true),101 table.WithWidth(weeklyTableWidth),102 table.WithHeight(tableHeight),103 )104 t.SetStyles(defaultTableStyles())105 return t106}107108func defaultTableStyles() table.Styles {109 p := theme.Current()110111 st := table.DefaultStyles()112 st.Header = st.Header.113 Bold(true).114 Foreground(p.Accent).115 BorderStyle(lipgloss.NormalBorder()).116 BorderBottom(true).117 BorderForeground(p.Border)118 st.Selected = st.Selected.119 Foreground(p.AccentText).120 Background(p.Accent).121 Bold(true)122 st.Cell = st.Cell.123 Foreground(p.FG)124 return st125}126127// TablePagination returns a pagination string like "1-20 of 45" for the128// continue table, or an empty string if all rows fit on one page.129func TablePagination(t table.Model) string {130 total := len(t.Rows())131 if total <= MaxTableRows {132 return ""133 }134 cursor := t.Cursor()135 height := t.Height()136137 // Estimate the visible window based on cursor and table height.138 start := cursor - height + 1139 start = max(start, 0)140 end := start + height141 if end > total {142 end = total143 start = end - height144 }145146 return FooterHint().Render(fmt.Sprintf("%d–%d of %d", start+1, end, total))147}