Files
2026-05-01 14:10:22 -06:00

311 lines
6.7 KiB
Go

package ui
import (
"fmt"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/atridad/wrapped-cli/pkg/config"
"github.com/atridad/wrapped-cli/pkg/gitea"
"github.com/atridad/wrapped-cli/pkg/models"
"github.com/atridad/wrapped-cli/pkg/stats"
)
type screenType int
const (
menuScreenType screenType = iota
inputScreenType
loadingScreenType
reportScreenType
)
type tickMsg time.Time
type progressMsg string
type errorMsg string
type doneMsg struct {
repos []gitea.GiteaRepo
commits []models.Commit
username string
}
type App struct {
screen screenType
menuScreen *MenuScreen
inputScreen *InputScreen
reportScreen *ReportScreen
loadingMsg string
err error
giteaClient *gitea.Client
analyzer *stats.Analyzer
spinnerFrame int
repos []gitea.GiteaRepo
username string
config *config.Config
}
func NewApp() *App {
cfg, err := config.LoadConfig()
if err != nil || cfg == nil {
cfg = &config.Config{Connections: []config.Connection{}}
}
screen := menuScreenType
var inputScreen *InputScreen
if len(cfg.Connections) == 0 {
screen = inputScreenType
inputScreen = NewInputScreen()
}
return &App{
screen: screen,
menuScreen: NewMenuScreen(cfg),
inputScreen: inputScreen,
config: cfg,
analyzer: stats.NewAnalyzer(),
}
}
func (a *App) Init() tea.Cmd {
if a.screen == inputScreenType && a.inputScreen != nil {
return a.inputScreen.Init()
}
return nil
}
func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.Type == tea.KeyCtrlC {
return a, tea.Quit
}
case tickMsg:
if a.screen == loadingScreenType {
a.spinnerFrame = (a.spinnerFrame + 1) % 4
return a, tea.Tick(time.Millisecond*200, func(t time.Time) tea.Msg {
return tickMsg(t)
})
}
}
switch a.screen {
case menuScreenType:
_, cmd := a.menuScreen.Update(msg)
if a.menuScreen.IsNewConnection() {
a.screen = inputScreenType
a.menuScreen.ResetSelection()
a.inputScreen = NewInputScreen()
return a, a.inputScreen.Init()
}
if selected := a.menuScreen.GetSelectedName(); selected != "" {
conn := a.config.GetConnection(selected)
if conn != nil {
a.menuScreen.ResetSelection()
a.giteaClient = gitea.NewClient(conn.URL, conn.Token)
a.username = conn.Username
a.screen = loadingScreenType
a.loadingMsg = "Connecting to Gitea..."
a.spinnerFrame = 0
return a, tea.Batch(
tea.Tick(time.Millisecond*200, func(t time.Time) tea.Msg {
return tickMsg(t)
}),
a.testConnection())
}
}
return a, cmd
case inputScreenType:
_, cmd := a.inputScreen.Update(msg)
if a.inputScreen.IsDone() {
url, username, token, connName := a.inputScreen.GetCredentials()
if url == "" {
a.inputScreen.err = fmt.Errorf("URL is required")
a.inputScreen.done = false
return a, cmd
}
if username == "" {
a.inputScreen.err = fmt.Errorf("username is required")
a.inputScreen.done = false
return a, cmd
}
if token == "" {
a.inputScreen.err = fmt.Errorf("token is required")
a.inputScreen.done = false
return a, cmd
}
if connName != "" {
a.config.AddConnection(config.Connection{
Name: connName,
URL: url,
Username: username,
Token: token,
})
a.config.Save()
}
a.giteaClient = gitea.NewClient(url, token)
a.username = username
a.screen = loadingScreenType
a.loadingMsg = "Connecting to Gitea..."
a.spinnerFrame = 0
return a, tea.Batch(cmd,
tea.Tick(time.Millisecond*200, func(t time.Time) tea.Msg {
return tickMsg(t)
}),
a.testConnection())
}
return a, cmd
case loadingScreenType:
switch msg := msg.(type) {
case progressMsg:
a.loadingMsg = string(msg)
switch string(msg) {
case "Fetching repositories...":
return a, a.fetchRepos()
case "Fetching commits...":
return a, a.fetchCommits()
}
return a, nil
case errorMsg:
a.err = fmt.Errorf("%s", msg)
a.loadingMsg = "Error"
return a, nil
case doneMsg:
a.analyzer.AddRepos(msg.repos)
a.analyzer.AddCommits(msg.commits)
userStats := a.analyzer.Generate(msg.username)
a.reportScreen = NewReportScreen(userStats)
a.screen = reportScreenType
return a, nil
}
case reportScreenType:
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.String() == "q" {
a.screen = menuScreenType
a.menuScreen = NewMenuScreen(a.config)
a.reportScreen = nil
a.analyzer = stats.NewAnalyzer()
return a, nil
}
}
if a.reportScreen != nil {
_, cmd := a.reportScreen.Update(msg)
return a, cmd
}
}
return a, nil
}
func (a *App) View() string {
switch a.screen {
case menuScreenType:
return a.menuScreen.View()
case inputScreenType:
return a.inputScreen.View()
case loadingScreenType:
return a.renderLoadingScreen()
case reportScreenType:
if a.reportScreen != nil {
return a.reportScreen.View()
}
}
return ""
}
func (a *App) renderLoadingScreen() string {
var s string
s += lipgloss.NewStyle().
Foreground(lipgloss.Color("212")).
Bold(true).
Render("Gitea Wrapped") + "\n\n"
spinners := []string{"⠋", "⠙", "⠹", "⠸"}
spinner := spinners[a.spinnerFrame%4]
s += fmt.Sprintf("%s %s\n", spinner, a.loadingMsg)
if a.err != nil {
s += "\n" + lipgloss.NewStyle().
Foreground(lipgloss.Color("1")).
Render(fmt.Sprintf("Error: %v", a.err)) + "\n"
}
return s
}
func (a *App) testConnection() tea.Cmd {
return func() tea.Msg {
if err := a.giteaClient.TestConnection(); err != nil {
return errorMsg(fmt.Sprintf("failed to connect: %v", err))
}
return progressMsg("Fetching repositories...")
}
}
func (a *App) fetchRepos() tea.Cmd {
return func() tea.Msg {
repos, err := a.giteaClient.GetUserRepos()
if err != nil {
return errorMsg(fmt.Sprintf("failed to fetch repos: %v", err))
}
a.repos = repos
return progressMsg("Fetching commits...")
}
}
func (a *App) fetchCommits() tea.Cmd {
return func() tea.Msg {
var repoRefs []gitea.RepoRef
for _, repo := range a.repos {
parts := strings.Split(repo.FullName, "/")
if len(parts) < 2 {
continue
}
repoRefs = append(repoRefs, gitea.RepoRef{
Owner: parts[0],
Name: repo.Name,
})
}
giteaCommits, err := a.giteaClient.GetReposCommitsParallel(repoRefs)
if err != nil {
return errorMsg(fmt.Sprintf("failed to fetch commits: %v", err))
}
var commits []models.Commit
for _, gc := range giteaCommits {
commits = append(commits, models.Commit{
SHA: gc.SHA,
Message: gc.Commit.Message,
Author: gc.Commit.Author.Name,
AuthorEmail: gc.Commit.Author.Email,
Timestamp: gc.Commit.Author.Date,
RepoName: gc.RepoName,
})
}
return doneMsg{a.repos, commits, a.username}
}
}