Initial implementation of Gitea Wrapped TUI
- Gitea API client with repository and commit fetching - Interactive credential input screen with masked token input - Statistics analyzer for commits, languages, and activity patterns - Multi-page report screen with ASCII charts and visualizations - Integration of all components in main app coordinator - Comprehensive README with usage instructions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"github.com/atridad/gitea-wrapped/pkg/models"
|
||||
)
|
||||
|
||||
type ReportScreen struct {
|
||||
stats *models.UserStats
|
||||
page int
|
||||
maxPages int
|
||||
}
|
||||
|
||||
func NewReportScreen(stats *models.UserStats) *ReportScreen {
|
||||
return &ReportScreen{
|
||||
stats: stats,
|
||||
page: 0,
|
||||
maxPages: 3,
|
||||
}
|
||||
}
|
||||
|
||||
func (rs *ReportScreen) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rs *ReportScreen) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "q", "ctrl+c":
|
||||
return rs, tea.Quit
|
||||
case "right", "l", "n":
|
||||
if rs.page < rs.maxPages-1 {
|
||||
rs.page++
|
||||
}
|
||||
case "left", "h", "p":
|
||||
if rs.page > 0 {
|
||||
rs.page--
|
||||
}
|
||||
}
|
||||
}
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
func (rs *ReportScreen) View() string {
|
||||
header := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("212")).
|
||||
Bold(true).
|
||||
Render(fmt.Sprintf("Gitea Wrapped %d", 2026)) + "\n"
|
||||
|
||||
var content string
|
||||
switch rs.page {
|
||||
case 0:
|
||||
content = rs.renderOverviewPage()
|
||||
case 1:
|
||||
content = rs.renderLanguagesPage()
|
||||
case 2:
|
||||
content = rs.renderActivityPage()
|
||||
}
|
||||
|
||||
footer := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("8")).
|
||||
Render(fmt.Sprintf("Page %d/%d | ← → to navigate | q to quit", rs.page+1, rs.maxPages))
|
||||
|
||||
return header + "\n" + content + "\n\n" + footer
|
||||
}
|
||||
|
||||
func (rs *ReportScreen) renderOverviewPage() string {
|
||||
var s strings.Builder
|
||||
|
||||
s.WriteString(lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("14")).
|
||||
Bold(true).
|
||||
Render("Your 2026 Overview") + "\n\n")
|
||||
|
||||
s.WriteString(fmt.Sprintf("Username: %s\n", rs.stats.Username))
|
||||
s.WriteString(fmt.Sprintf("Total Commits: %d\n", rs.stats.TotalCommits))
|
||||
s.WriteString(fmt.Sprintf("Total Repositories: %d\n", rs.stats.TotalRepositories))
|
||||
s.WriteString(fmt.Sprintf("Average Commits/Day: %.2f\n", rs.stats.AverageCommitsPerDay))
|
||||
s.WriteString(fmt.Sprintf("Most Active Day: %s\n", rs.stats.MostActiveDay))
|
||||
s.WriteString(fmt.Sprintf("Most Active Month: %s\n", rs.stats.MostActiveMonth))
|
||||
|
||||
s.WriteString("\nTop Repositories:\n")
|
||||
for i, repo := range rs.stats.TopRepositories {
|
||||
if i >= 5 {
|
||||
break
|
||||
}
|
||||
s.WriteString(fmt.Sprintf(" %d. %s\n", i+1, repo.Name))
|
||||
}
|
||||
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func (rs *ReportScreen) renderLanguagesPage() string {
|
||||
var s strings.Builder
|
||||
|
||||
s.WriteString(lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("14")).
|
||||
Bold(true).
|
||||
Render("Languages You Love") + "\n\n")
|
||||
|
||||
if len(rs.stats.Languages) == 0 {
|
||||
s.WriteString("No language data available.\n")
|
||||
return s.String()
|
||||
}
|
||||
|
||||
maxLength := 30
|
||||
maxCount := 0
|
||||
for _, lang := range rs.stats.Languages {
|
||||
if lang.Count > maxCount {
|
||||
maxCount = lang.Count
|
||||
}
|
||||
}
|
||||
|
||||
for i, lang := range rs.stats.Languages {
|
||||
if i >= 10 {
|
||||
break
|
||||
}
|
||||
|
||||
barLength := int(float64(lang.Count) / float64(maxCount) * float64(maxLength))
|
||||
bar := strings.Repeat("█", barLength) + strings.Repeat("░", maxLength-barLength)
|
||||
s.WriteString(fmt.Sprintf("%-12s %s %d (%.1f%%)\n", lang.Language, bar, lang.Count, lang.Percent))
|
||||
}
|
||||
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func (rs *ReportScreen) renderActivityPage() string {
|
||||
var s strings.Builder
|
||||
|
||||
s.WriteString(lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("14")).
|
||||
Bold(true).
|
||||
Render("Your Activity Patterns") + "\n\n")
|
||||
|
||||
s.WriteString("Most Active Days of the Week:\n")
|
||||
if len(rs.stats.CommitsByWeekday) > 0 {
|
||||
maxCount := rs.stats.CommitsByWeekday[0].Count
|
||||
for i, day := range rs.stats.CommitsByWeekday {
|
||||
if i >= 7 {
|
||||
break
|
||||
}
|
||||
barLength := int(float64(day.Count) / float64(maxCount) * 20)
|
||||
bar := strings.Repeat("█", barLength) + strings.Repeat("░", 20-barLength)
|
||||
s.WriteString(fmt.Sprintf("%-12s %s %d\n", day.Day, bar, day.Count))
|
||||
}
|
||||
} else {
|
||||
s.WriteString("No weekday data available.\n")
|
||||
}
|
||||
|
||||
s.WriteString("\nMost Active Months:\n")
|
||||
if len(rs.stats.CommitsByMonth) > 0 {
|
||||
maxCount := rs.stats.CommitsByMonth[0].Count
|
||||
for i, month := range rs.stats.CommitsByMonth {
|
||||
if i >= 12 {
|
||||
break
|
||||
}
|
||||
barLength := int(float64(month.Count) / float64(maxCount) * 20)
|
||||
bar := strings.Repeat("█", barLength) + strings.Repeat("░", 20-barLength)
|
||||
monthStr := month.Date.Format("Jan 2006")
|
||||
s.WriteString(fmt.Sprintf("%-12s %s %d\n", monthStr, bar, month.Count))
|
||||
}
|
||||
} else {
|
||||
s.WriteString("No monthly data available.\n")
|
||||
}
|
||||
|
||||
return s.String()
|
||||
}
|
||||
Reference in New Issue
Block a user