Added bento :)
This commit is contained in:
+243
-173
@@ -8,17 +8,19 @@ import (
|
||||
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"
|
||||
"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 (
|
||||
inputScreenType screenType = iota
|
||||
loadingScreenType
|
||||
reportScreenType
|
||||
menuScreenType screenType = iota
|
||||
inputScreenType
|
||||
loadingScreenType
|
||||
reportScreenType
|
||||
)
|
||||
|
||||
type tickMsg time.Time
|
||||
@@ -26,215 +28,283 @@ type progressMsg string
|
||||
type errorMsg string
|
||||
|
||||
type doneMsg struct {
|
||||
repos []gitea.GiteaRepo
|
||||
commits []models.Commit
|
||||
username string
|
||||
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
|
||||
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 {
|
||||
return &App{
|
||||
screen: inputScreenType,
|
||||
inputScreen: NewInputScreen(),
|
||||
analyzer: stats.NewAnalyzer(),
|
||||
}
|
||||
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 {
|
||||
return a.inputScreen.Init()
|
||||
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 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)
|
||||
switch a.screen {
|
||||
case menuScreenType:
|
||||
_, cmd := a.menuScreen.Update(msg)
|
||||
|
||||
if a.inputScreen.IsDone() {
|
||||
url, username, token := a.inputScreen.GetCredentials()
|
||||
if a.menuScreen.IsNewConnection() {
|
||||
a.screen = inputScreenType
|
||||
a.menuScreen.ResetSelection()
|
||||
a.inputScreen = NewInputScreen()
|
||||
return a, a.inputScreen.Init()
|
||||
}
|
||||
|
||||
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 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
|
||||
|
||||
a.giteaClient = gitea.NewClient(url, token)
|
||||
a.username = 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, tea.Batch(cmd,
|
||||
tea.Tick(time.Millisecond*200, func(t time.Time) tea.Msg {
|
||||
return tickMsg(t)
|
||||
}),
|
||||
a.testConnection())
|
||||
}
|
||||
return a, cmd
|
||||
|
||||
return a, cmd
|
||||
case inputScreenType:
|
||||
_, cmd := a.inputScreen.Update(msg)
|
||||
|
||||
case loadingScreenType:
|
||||
switch msg := msg.(type) {
|
||||
case progressMsg:
|
||||
a.loadingMsg = string(msg)
|
||||
if a.inputScreen.IsDone() {
|
||||
url, username, token, connName := a.inputScreen.GetCredentials()
|
||||
|
||||
switch string(msg) {
|
||||
case "Fetching repositories...":
|
||||
return a, a.fetchRepos()
|
||||
case "Fetching commits...":
|
||||
return a, a.fetchCommits()
|
||||
}
|
||||
return a, nil
|
||||
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
|
||||
}
|
||||
|
||||
case errorMsg:
|
||||
a.err = fmt.Errorf("%s", msg)
|
||||
a.loadingMsg = "Error"
|
||||
return a, nil
|
||||
if connName != "" {
|
||||
a.config.AddConnection(config.Connection{
|
||||
Name: connName,
|
||||
URL: url,
|
||||
Username: username,
|
||||
Token: token,
|
||||
})
|
||||
a.config.Save()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
a.giteaClient = gitea.NewClient(url, token)
|
||||
a.username = username
|
||||
a.screen = loadingScreenType
|
||||
a.loadingMsg = "Connecting to Gitea..."
|
||||
a.spinnerFrame = 0
|
||||
|
||||
case reportScreenType:
|
||||
if a.reportScreen != nil {
|
||||
_, cmd := a.reportScreen.Update(msg)
|
||||
return a, cmd
|
||||
}
|
||||
}
|
||||
return a, tea.Batch(cmd,
|
||||
tea.Tick(time.Millisecond*200, func(t time.Time) tea.Msg {
|
||||
return tickMsg(t)
|
||||
}),
|
||||
a.testConnection())
|
||||
}
|
||||
|
||||
return a, nil
|
||||
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 inputScreenType:
|
||||
return a.inputScreen.View()
|
||||
case loadingScreenType:
|
||||
return a.renderLoadingScreen()
|
||||
case reportScreenType:
|
||||
if a.reportScreen != nil {
|
||||
return a.reportScreen.View()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
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"
|
||||
var s string
|
||||
s += lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("212")).
|
||||
Bold(true).
|
||||
Render("Gitea Wrapped") + "\n\n"
|
||||
|
||||
spinners := []string{"⠋", "⠙", "⠹", "⠸"}
|
||||
spinner := spinners[a.spinnerFrame%4]
|
||||
spinners := []string{"⠋", "⠙", "⠹", "⠸"}
|
||||
spinner := spinners[a.spinnerFrame%4]
|
||||
|
||||
s += fmt.Sprintf("%s %s\n", spinner, a.loadingMsg)
|
||||
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"
|
||||
if a.err != nil {
|
||||
s += "\n" + lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("1")).
|
||||
Render(fmt.Sprintf("Error: %v", a.err)) + "\n"
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
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...")
|
||||
}
|
||||
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))
|
||||
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...")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
for i, repo := range a.repos {
|
||||
giteaCommits, err := a.giteaClient.GetReposCommitsParallel(repoRefs)
|
||||
if err != nil {
|
||||
return errorMsg(fmt.Sprintf("failed to fetch commits: %v", err))
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
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}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/atridad/wrapped-cli/pkg/models"
|
||||
"github.com/atridad/wrapped-cli/pkg/reports"
|
||||
)
|
||||
|
||||
func ExportReport(stats *models.UserStats) (string, error) {
|
||||
return reports.SaveReport(stats)
|
||||
}
|
||||
+28
-6
@@ -14,17 +14,20 @@ const (
|
||||
urlInput inputScreenState = iota
|
||||
usernameInput
|
||||
tokenInput
|
||||
connNameInput
|
||||
)
|
||||
|
||||
type InputScreen struct {
|
||||
urlField textinput.Model
|
||||
usernameField textinput.Model
|
||||
tokenField textinput.Model
|
||||
connNameField textinput.Model
|
||||
focusedField inputScreenState
|
||||
err error
|
||||
url string
|
||||
username string
|
||||
token string
|
||||
connName string
|
||||
done bool
|
||||
}
|
||||
|
||||
@@ -40,10 +43,14 @@ func NewInputScreen() *InputScreen {
|
||||
tokenField.Placeholder = "your_access_token"
|
||||
tokenField.EchoMode = textinput.EchoPassword
|
||||
|
||||
connNameField := textinput.New()
|
||||
connNameField.Placeholder = "(optional) Connection name"
|
||||
|
||||
return &InputScreen{
|
||||
urlField: urlField,
|
||||
usernameField: usernameField,
|
||||
tokenField: tokenField,
|
||||
connNameField: connNameField,
|
||||
focusedField: urlInput,
|
||||
}
|
||||
}
|
||||
@@ -72,6 +79,11 @@ func (is *InputScreen) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return is, nil
|
||||
case tokenInput:
|
||||
is.token = is.tokenField.Value()
|
||||
is.focusedField = connNameInput
|
||||
is.connNameField.Focus()
|
||||
return is, nil
|
||||
case connNameInput:
|
||||
is.connName = is.connNameField.Value()
|
||||
is.done = true
|
||||
return is, nil
|
||||
}
|
||||
@@ -83,17 +95,20 @@ func (is *InputScreen) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
is.username = is.usernameField.Value()
|
||||
case tokenInput:
|
||||
is.token = is.tokenField.Value()
|
||||
case connNameInput:
|
||||
is.connName = is.connNameField.Value()
|
||||
}
|
||||
|
||||
if msg.Type == tea.KeyTab {
|
||||
is.focusedField = (is.focusedField + 1) % 3
|
||||
is.focusedField = (is.focusedField + 1) % 4
|
||||
} else {
|
||||
is.focusedField = (is.focusedField - 1 + 3) % 3
|
||||
is.focusedField = (is.focusedField - 1 + 4) % 4
|
||||
}
|
||||
|
||||
is.urlField.Blur()
|
||||
is.usernameField.Blur()
|
||||
is.tokenField.Blur()
|
||||
is.connNameField.Blur()
|
||||
|
||||
switch is.focusedField {
|
||||
case urlInput:
|
||||
@@ -102,6 +117,8 @@ func (is *InputScreen) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
is.usernameField.Focus()
|
||||
case tokenInput:
|
||||
is.tokenField.Focus()
|
||||
case connNameInput:
|
||||
is.connNameField.Focus()
|
||||
}
|
||||
return is, nil
|
||||
}
|
||||
@@ -119,6 +136,8 @@ func (is *InputScreen) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
is.usernameField, cmd = is.usernameField.Update(msg)
|
||||
case tokenInput:
|
||||
is.tokenField, cmd = is.tokenField.Update(msg)
|
||||
case connNameInput:
|
||||
is.connNameField, cmd = is.connNameField.Update(msg)
|
||||
}
|
||||
|
||||
return is, cmd
|
||||
@@ -132,7 +151,7 @@ func (is *InputScreen) View() string {
|
||||
Bold(true).
|
||||
Render("Gitea Wrapped") + "\n\n"
|
||||
|
||||
s += "Enter your Gitea credentials to get started:\n\n"
|
||||
s += "Enter your Gitea credentials:\n\n"
|
||||
|
||||
s += "Gitea URL\n"
|
||||
s += is.urlField.View() + "\n\n"
|
||||
@@ -143,6 +162,9 @@ func (is *InputScreen) View() string {
|
||||
s += "Access Token\n"
|
||||
s += is.tokenField.View() + "\n\n"
|
||||
|
||||
s += "Connection Name (optional - to save for later)\n"
|
||||
s += is.connNameField.View() + "\n\n"
|
||||
|
||||
if is.err != nil {
|
||||
s += lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("1")).
|
||||
@@ -151,7 +173,7 @@ func (is *InputScreen) View() string {
|
||||
|
||||
s += lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("8")).
|
||||
Render("(press Tab to navigate, Enter to continue, Ctrl+C to quit)") + "\n"
|
||||
Render("(Tab to navigate, Enter to continue, Ctrl+C to quit)") + "\n"
|
||||
|
||||
return s
|
||||
}
|
||||
@@ -160,8 +182,8 @@ func (is *InputScreen) IsDone() bool {
|
||||
return is.done
|
||||
}
|
||||
|
||||
func (is *InputScreen) GetCredentials() (string, string, string) {
|
||||
return is.url, is.username, is.token
|
||||
func (is *InputScreen) GetCredentials() (url, username, token, connName string) {
|
||||
return is.url, is.username, is.token, is.connName
|
||||
}
|
||||
|
||||
type errMsg error
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"github.com/atridad/wrapped-cli/pkg/config"
|
||||
)
|
||||
|
||||
type MenuScreen struct {
|
||||
connections []config.Connection
|
||||
selected int
|
||||
newSelected bool
|
||||
confirmed bool
|
||||
}
|
||||
|
||||
func NewMenuScreen(cfg *config.Config) *MenuScreen {
|
||||
return &MenuScreen{
|
||||
connections: cfg.Connections,
|
||||
selected: 0,
|
||||
confirmed: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MenuScreen) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MenuScreen) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "up", "k":
|
||||
if m.selected > 0 {
|
||||
m.selected--
|
||||
}
|
||||
case "down", "j":
|
||||
menuItems := len(m.connections) + 1
|
||||
if m.selected < menuItems-1 {
|
||||
m.selected++
|
||||
}
|
||||
case "enter":
|
||||
if m.selected == len(m.connections) {
|
||||
m.newSelected = true
|
||||
} else if m.selected < len(m.connections) {
|
||||
m.confirmed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *MenuScreen) View() string {
|
||||
var s string
|
||||
s += lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("212")).
|
||||
Bold(true).
|
||||
Render("Gitea Wrapped") + "\n\n"
|
||||
|
||||
if len(m.connections) == 0 {
|
||||
s += "No saved connections yet.\n\n"
|
||||
|
||||
newPrefix := " "
|
||||
if m.selected == 0 {
|
||||
newPrefix = "→ "
|
||||
}
|
||||
if m.selected == 1 {
|
||||
}
|
||||
|
||||
s += fmt.Sprintf("%s+ New Connection\n", newPrefix)
|
||||
|
||||
s += "\n" + lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("8")).
|
||||
Render("↑/↓ navigate | Enter to select | Ctrl+C to quit")
|
||||
} else {
|
||||
s += "Select a connection:\n\n"
|
||||
|
||||
for i, conn := range m.connections {
|
||||
prefix := " "
|
||||
if i == m.selected {
|
||||
prefix = "→ "
|
||||
}
|
||||
s += fmt.Sprintf("%s%s (%s)\n", prefix, conn.Name, conn.URL)
|
||||
}
|
||||
|
||||
newPrefix := " "
|
||||
if m.selected == len(m.connections) {
|
||||
newPrefix = "→ "
|
||||
}
|
||||
if m.selected == len(m.connections)+1 {
|
||||
}
|
||||
|
||||
s += fmt.Sprintf("%s+ New Connection\n", newPrefix)
|
||||
|
||||
s += "\n" + lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("8")).
|
||||
Render("↑/↓ navigate | Enter to select | Ctrl+C to quit")
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (m *MenuScreen) IsNewConnection() bool {
|
||||
return m.newSelected
|
||||
}
|
||||
|
||||
func (m *MenuScreen) GetSelectedName() string {
|
||||
if m.confirmed && m.selected < len(m.connections) {
|
||||
return m.connections[m.selected].Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *MenuScreen) ResetSelection() {
|
||||
m.newSelected = false
|
||||
m.confirmed = false
|
||||
m.selected = 0
|
||||
}
|
||||
+18
-8
@@ -7,20 +7,23 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"github.com/atridad/gitea-wrapped/pkg/models"
|
||||
"github.com/atridad/wrapped-cli/pkg/models"
|
||||
)
|
||||
|
||||
type ReportScreen struct {
|
||||
stats *models.UserStats
|
||||
page int
|
||||
maxPages int
|
||||
stats *models.UserStats
|
||||
page int
|
||||
maxPages int
|
||||
exportFile string
|
||||
}
|
||||
|
||||
func NewReportScreen(stats *models.UserStats) *ReportScreen {
|
||||
exportFile, _ := ExportReport(stats)
|
||||
return &ReportScreen{
|
||||
stats: stats,
|
||||
page: 0,
|
||||
maxPages: 3,
|
||||
stats: stats,
|
||||
page: 0,
|
||||
maxPages: 3,
|
||||
exportFile: exportFile,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +70,14 @@ func (rs *ReportScreen) View() string {
|
||||
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
|
||||
report := header + "\n" + content + "\n\n" + footer
|
||||
if rs.exportFile != "" {
|
||||
report += "\n" + lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("10")).
|
||||
Render(fmt.Sprintf("✓ Report saved: %s", rs.exportFile))
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
||||
|
||||
func (rs *ReportScreen) renderOverviewPage() string {
|
||||
|
||||
Reference in New Issue
Block a user