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} } }