package main import ( "encoding/json" "fmt" "net/http" "os" "strings" "time" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // Styles var ( titleStyle = lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("#FAFAFA")). Background(lipgloss.Color("#7D56F4")). Padding(0, 1) itemStyle = lipgloss.NewStyle(). PaddingLeft(2) scoreStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#FFD700")) // Gold color errorStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#FF5555")). Bold(true) ) type Game struct { ID int64 `json:"id"` Name string `json:"name"` PlatformID int64 `json:"platform_id"` Score int32 `json:"score"` ReleaseYear string `json:"release_year"` Finished string `json:"finished,omitempty"` } type GamesResponse struct { Body []Game `json:"body"` } type Model struct { spinner spinner.Model loading bool err error games []Game quitting bool } func InitialModel() Model { s := spinner.New() s.Spinner = spinner.Dot s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) return Model{spinner: s, loading: true} } type loadedMsg []Game type errMsg error func fetchGames() tea.Msg { client := &http.Client{Timeout: 5 * time.Second} resp, err := client.Get("http://localhost:8080/games") if err != nil { return errMsg(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return errMsg(fmt.Errorf("status code: %d", resp.StatusCode)) } var data GamesResponse if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { return errMsg(err) } return loadedMsg(data.Body) } func (m Model) Init() tea.Cmd { return tea.Batch(m.spinner.Tick, fetchGames) } func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: if msg.String() == "q" || msg.String() == "ctrl+c" { m.quitting = true return m, tea.Quit } case spinner.TickMsg: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd case loadedMsg: m.loading = false m.games = msg return m, nil case errMsg: m.loading = false m.err = msg return m, nil } return m, nil } func (m Model) View() string { if m.quitting { return "Bye!\n" } s := titleStyle.Render("My Game Collection") + "\n\n" if m.loading { s += fmt.Sprintf("%s Loading games...", m.spinner.View()) } else if m.err != nil { s += errorStyle.Render(fmt.Sprintf("Error: %v", m.err)) s += "\nMake sure the API is running on localhost:8080" } else { if len(m.games) == 0 { s += "No games found." } else { for _, g := range m.games { score := scoreStyle.Render(fmt.Sprintf("★ %d", g.Score)) line := fmt.Sprintf("• %s (%s) - %s", g.Name, g.ReleaseYear, score) if g.Finished != "" { line += " [✓ Finished]" } s += itemStyle.Render(line) + "\n" } } } s += strings.Repeat("\n", 2) + lipgloss.NewStyle().Foreground(lipgloss.Color("#555")).Render("Press 'q' to quit") return s } func main() { p := tea.NewProgram(InitialModel()) if _, err := p.Run(); err != nil { fmt.Printf("Alas, there's been an error: %v", err) os.Exit(1) } }