311 lines
6.7 KiB
Go
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}
|
|
}
|
|
}
|