1package app23import (4 "fmt"5 "strings"67 "github.com/FelineStateMachine/puzzletea/registry"8 "github.com/FelineStateMachine/puzzletea/theme"9 "github.com/FelineStateMachine/puzzletea/ui"1011 tea "charm.land/bubbletea/v2"12 "charm.land/lipgloss/v2"13)1415func (m model) View() tea.View {16 v := tea.NewView(m.viewContent())17 v.AltScreen = true18 if m.state == gameView || m.state == exportView {19 v.MouseMode = tea.MouseModeCellMotion20 v.KeyboardEnhancements.ReportEventTypes = true21 }22 return v23}2425func (m model) viewContent() string {26 if m.state == gameView {27 return m.renderGameView()28 }2930 screen := m.activeScreen()31 if screen == nil {32 return fmt.Sprintf("unknown state: %d", m.state)33 }34 return screen.View(m.notice)35}3637func (m model) gameSelectViewContent() string {38 metrics := categoryPickerSize(m.width, m.height)3940 listView := lipgloss.NewStyle().41 Width(metrics.listWidth).42 Height(metrics.listHeight).43 Border(lipgloss.NormalBorder(), false, true, false, false).44 BorderForeground(theme.Current().Border).45 Render(m.nav.gameSelectList.View())4647 detailBox := lipgloss.NewStyle().48 Width(metrics.detailWidth).49 Height(metrics.detailHeight).50 Padding(2, 3).51 Render(m.nav.categoryDetail.View())5253 body := lipgloss.JoinHorizontal(lipgloss.Top, listView, strings.Repeat(" ", categoryGapWidth), detailBox)54 if metrics.stacked {55 body = lipgloss.JoinVertical(lipgloss.Left, listView, "", detailBox)56 }57 body = lipgloss.NewStyle().58 Width(metrics.bodyWidth).59 Height(metrics.bodyHeight).60 Render(body)6162 panel := ui.Panel(63 "Select Category",64 m.appendNotice(body),65 "↑/↓ navigate • / filter • enter select • esc back",66 )67 return ui.CenterView(m.width, m.height, panel)68}6970func (m model) weeklyViewContent() string {71 title := "Weekly Gauntlet — " + m.weeklyPanelTitle()72 if len(m.weekly.rows) == 0 {73 body := "No completed puzzles for this week yet."74 if m.isCurrentWeeklySelection() {75 body = "No weekly puzzles are available."76 }77 return ui.Panel(78 title,79 body,80 "←/→ week • esc back",81 )82 }8384 footer := "←/→ week • enter open • esc back"85 if !m.isCurrentWeeklySelection() {86 footer = "←/→ week • enter review • esc back"87 }88 if pg := ui.TablePagination(m.weekly.table); pg != "" {89 footer = pg + " " + footer90 }9192 description := m.weekly.table.View()93 if !m.isCurrentWeeklySelection() {94 description = lipgloss.JoinVertical(95 lipgloss.Left,96 ui.DimItemStyle().Render("Review only: completed puzzles from this week."),97 "",98 description,99 )100 }101102 return ui.Panel(title, description, footer)103}104105func categoryPickerListHeight() int {106 const (107 maxVisibleItems = 8108 filterChrome = 2109 )110111 return maxVisibleItems + filterChrome112}113114func renderCategoryDetailContent(entry registry.Entry, width int) string {115 p := theme.Current()116 desc := lipgloss.NewStyle().117 Foreground(p.FG).118 Width(width).119 Render(entry.Definition.Description)120121 meta := ui.DimItemStyle().Render(fmt.Sprintf("%d modes available", len(entry.Modes)))122 content := lipgloss.JoinVertical(lipgloss.Left,123 ui.PanelTitle().Render(entry.Definition.Name),124 meta,125 "",126 desc,127 "",128 ui.SelectedItemStyle().Render("Modes"),129 renderModeList(entry, width),130 )131 return content132}133134func renderModeList(entry registry.Entry, width int) string {135 if len(entry.Modes) == 0 {136 return ui.DimItemStyle().Render("No modes available.")137 }138139 displayTitles := modeDisplayTitles(entry)140 modeNames := make([]string, 0, len(displayTitles))141 for _, title := range displayTitles {142 modeNames = append(modeNames, "• "+title)143 }144 if len(modeNames) == 0 {145 return ui.DimItemStyle().Render("No modes available.")146 }147148 if width < 24 || len(modeNames) == 1 {149 lines := make([]string, 0, len(modeNames))150 for _, name := range modeNames {151 lines = append(lines, lipgloss.NewStyle().Width(width).Render(name))152 }153 return strings.Join(lines, "\n")154 }155156 rows := (len(modeNames) + 1) / 2157 leftWidth := max(width/2-1, 1)158 rightWidth := max(width-leftWidth-2, 1)159160 left := make([]string, 0, rows)161 right := make([]string, 0, rows)162 for i, name := range modeNames {163 line := lipgloss.NewStyle().Width(leftWidth).Render(name)164 if i < rows {165 left = append(left, line)166 continue167 }168 line = lipgloss.NewStyle().Width(rightWidth).Render(name)169 right = append(right, line)170 }171 for len(right) < len(left) {172 right = append(right, strings.Repeat(" ", rightWidth))173 }174175 lines := make([]string, 0, rows)176 for i := range left {177 lines = append(lines, lipgloss.JoinHorizontal(lipgloss.Top, left[i], " ", right[i]))178 }179 return strings.Join(lines, "\n")180}181182func (m model) themeSelectViewContent() string {183 p := theme.Current()184185 themeName := theme.DefaultThemeName186 if item, ok := m.theme.list.SelectedItem().(ui.MenuItem); ok {187 themeName = item.ItemTitle188 }189190 const panelChrome = 8191 innerH := m.height - panelChrome192 innerH = max(innerH, 10)193194 listView := m.theme.list.View()195196 previewBorder := lipgloss.NewStyle().197 Border(lipgloss.RoundedBorder()).198 BorderForeground(p.Border).199 Padding(1, 2)200201 preview := theme.PreviewPanel(themeName, innerH-4)202 previewBox := previewBorder.Render(preview)203204 body := lipgloss.JoinHorizontal(lipgloss.Top, listView, " ", previewBox)205206 panel := ui.Panel(207 "Select Theme",208 m.appendNotice(body),209 "↑/↓ navigate • / filter • enter select • esc back",210 )211 return ui.CenterView(m.width, m.height, panel)212}