feat: add ui package

This commit is contained in:
2026-02-10 21:54:47 +00:00
parent b792360a2a
commit 7338c774ab
6 changed files with 281 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
package ui
import (
"tz-snipe/internal/github"
tea "github.com/charmbracelet/bubbletea"
)
type tzsMsg []string
type errMsg error
func fetchGithubCmd(username string) tea.Cmd {
return func() tea.Msg {
tzs, err := github.GetTzsForUser(username)
if err != nil {
return errMsg(err)
}
return tzsMsg(tzs)
}
}
+128
View File
@@ -0,0 +1,128 @@
package ui
import (
"fmt"
"strings"
"tz-snipe/internal/core"
"tz-snipe/internal/geodata"
"github.com/charmbracelet/bubbles/table"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type Model struct {
db geodata.TimezoneMap
username string
manualTz string
preds []core.Prediction
loadingGithubTz bool
err error // this is how error handling is to be done in bubbletea
// just pass the error to ui layer
displayTz string
table table.Model
}
// go quirk: struct trick to get lsp to highlight errors
var _ tea.Model = Model{}
// REMINDER REMINDER REMINDER
// Model, NewModel, Init, Commands, Update, View is all you need
func NewModel(db geodata.TimezoneMap, ghUser, manualTz string) Model {
m := Model{
db: db,
username: ghUser,
manualTz: manualTz,
loadingGithubTz: false,
}
switch {
case manualTz != "":
preds, err := core.GetStatsForTZs([]string{manualTz})
if err != nil {
m.err = err
} else {
m.preds = preds
m.table = newTable(m.preds)
m.displayTz = m.manualTz
}
case ghUser != "":
m.loadingGithubTz = true
}
return m
}
func (m Model) Init() tea.Cmd {
if m.loadingGithubTz {
return fetchGithubCmd(m.username)
}
return nil
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// tbf this interrupt handling should be default
// go's type assertion trick aghain
// you want it handled first and globally so that
// fall throughs work nicely
if asKey, casted := msg.(tea.KeyMsg); casted {
if asKey.Type == tea.KeyCtrlC || asKey.String() == "q" {
return m, tea.Quit
}
}
// .(type) is special go syntax
// defiendonly for interfaces and must be used in a switch
switch msg := msg.(type) {
case errMsg:
m.err = msg
return m, nil
case tzsMsg:
preds, err := core.GetStatsForTZs(msg)
if err != nil {
m.err = err
} else {
m.preds = preds
m.table = newTable(m.preds)
m.displayTz = strings.Join(msg, ", ")
m.loadingGithubTz = false
}
return m, nil
}
// anuthing else, to the table
// go quirk: non name on left side of ...
// I can't use := as then it tries to declare it
var cmd tea.Cmd
m.table, cmd = m.table.Update(msg)
return m, cmd
}
func (m Model) View() string {
if m.err != nil {
return AppStyle.Render(ErrStyle.Render("Error: " + m.err.Error()))
}
if m.loadingGithubTz {
return AppStyle.Render(SubtleStyle.Render("Fetching github commits..."))
}
if len(m.preds) == 0 {
return AppStyle.Render(SubtleStyle.Render("No valid tzs found. Are you sure it's correct?"))
}
var header string
// manual mode
if m.manualTz != "" {
header += SubtleStyle.Render(fmt.Sprintf("Tz : %s\n", m.displayTz))
} else {
header += SubtleStyle.MaxWidth(60).Render(fmt.Sprintf("User : %s\nTz %s\n", m.username, m.displayTz))
}
table := m.table.View()
view := lipgloss.JoinVertical(lipgloss.Left, header, table)
return AppStyle.Render(view)
}
+18
View File
@@ -0,0 +1,18 @@
package ui
import "github.com/charmbracelet/lipgloss"
// Go quirk: can't use := at package level, need o do this
var (
AppStyle = lipgloss.NewStyle().Margin(1, 2)
// 2. The Key Data (Green for tz, Pink for user)
KeyStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")).Bold(true)
ValStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF007C")).Bold(true)
// 3. The Subtle Text (Gray for labels/help)
SubtleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262"))
// 4. Error (Red)
ErrStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5F87"))
)
+32
View File
@@ -0,0 +1,32 @@
package ui
import (
"fmt"
"tz-snipe/internal/core"
"github.com/charmbracelet/bubbles/table"
)
func newTable(preds []core.Prediction) table.Model {
cols := []table.Column{
{Title: "Country", Width: 20},
{Title: "Probability", Width: 30},
}
var rows []table.Row
// format data & build rows
for i := 0; i < len(preds); i++ {
p := preds[i]
rows = append(rows, table.Row{p.Country, fmt.Sprintf("%.4f", p.Probability*100)})
}
table := table.New(
table.WithColumns(cols),
table.WithRows(rows),
table.WithFocused(true),
table.WithHeight(10), // scrollable after 10
)
return table
}