package ui import ( "fmt" "strings" "time" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/atridad/gitea-wrapped/pkg/gitea" "github.com/atridad/gitea-wrapped/pkg/models" "github.com/atridad/gitea-wrapped/pkg/stats" ) type screenType int const ( inputScreenType screenType = iota 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 inputScreen *InputScreen reportScreen *ReportScreen loadingMsg string err error giteaClient *gitea.Client analyzer *stats.Analyzer spinnerFrame int repos []gitea.GiteaRepo username string } func NewApp() *App { return &App{ screen: inputScreenType, inputScreen: NewInputScreen(), analyzer: stats.NewAnalyzer(), } } func (a *App) Init() tea.Cmd { return a.inputScreen.Init() } 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 inputScreenType: _, cmd := a.inputScreen.Update(msg) if a.inputScreen.IsDone() { url, username, token := 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 } 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: if a.reportScreen != nil { _, cmd := a.reportScreen.Update(msg) return a, cmd } } return a, nil } func (a *App) View() string { switch a.screen { 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 } // testConnection tests API connection 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...") } } // fetchRepos fetches all 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...") } } // fetchCommits fetches commits from all repos func (a *App) fetchCommits() tea.Cmd { return func() tea.Msg { var commits []models.Commit for i, repo := range a.repos { parts := strings.Split(repo.FullName, "/") if len(parts) < 2 { continue } owner := parts[0] giteaCommits, err := a.giteaClient.GetRepoCommits(owner, repo.Name) if err != nil { continue } for _, gc := range giteaCommits { commits = append(commits, models.Commit{ SHA: gc.SHA, Message: gc.Commit.Message, Author: gc.Commit.Author.Name, Timestamp: gc.Commit.Author.Date, RepoName: repo.Name, }) } } return doneMsg{a.repos, commits, a.username} } }