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:
2026-05-01 09:43:09 -06:00
commit fec14022cd
12 changed files with 1152 additions and 0 deletions
+167
View File
@@ -0,0 +1,167 @@
package ui
import (
"fmt"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type inputScreenState int
const (
urlInput inputScreenState = iota
usernameInput
tokenInput
)
type InputScreen struct {
urlField textinput.Model
usernameField textinput.Model
tokenField textinput.Model
focusedField inputScreenState
err error
url string
username string
token string
done bool
}
func NewInputScreen() *InputScreen {
urlField := textinput.New()
urlField.Placeholder = "https://gitea.example.com"
urlField.Focus()
usernameField := textinput.New()
usernameField.Placeholder = "your_username"
tokenField := textinput.New()
tokenField.Placeholder = "your_access_token"
tokenField.EchoMode = textinput.EchoPassword
return &InputScreen{
urlField: urlField,
usernameField: usernameField,
tokenField: tokenField,
focusedField: urlInput,
}
}
func (is *InputScreen) Init() tea.Cmd {
return textinput.Blink
}
func (is *InputScreen) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.Type {
case tea.KeyCtrlC:
return is, tea.Quit
case tea.KeyEnter:
switch is.focusedField {
case urlInput:
is.url = is.urlField.Value()
is.focusedField = usernameInput
is.usernameField.Focus()
return is, nil
case usernameInput:
is.username = is.usernameField.Value()
is.focusedField = tokenInput
is.tokenField.Focus()
return is, nil
case tokenInput:
is.token = is.tokenField.Value()
is.done = true
return is, nil
}
case tea.KeyTab, tea.KeyShiftTab:
switch is.focusedField {
case urlInput:
is.url = is.urlField.Value()
case usernameInput:
is.username = is.usernameField.Value()
case tokenInput:
is.token = is.tokenField.Value()
}
if msg.Type == tea.KeyTab {
is.focusedField = (is.focusedField + 1) % 3
} else {
is.focusedField = (is.focusedField - 1 + 3) % 3
}
is.urlField.Blur()
is.usernameField.Blur()
is.tokenField.Blur()
switch is.focusedField {
case urlInput:
is.urlField.Focus()
case usernameInput:
is.usernameField.Focus()
case tokenInput:
is.tokenField.Focus()
}
return is, nil
}
case errMsg:
is.err = msg
return is, nil
}
var cmd tea.Cmd
switch is.focusedField {
case urlInput:
is.urlField, cmd = is.urlField.Update(msg)
case usernameInput:
is.usernameField, cmd = is.usernameField.Update(msg)
case tokenInput:
is.tokenField, cmd = is.tokenField.Update(msg)
}
return is, cmd
}
func (is *InputScreen) View() string {
var s string
s += lipgloss.NewStyle().
Foreground(lipgloss.Color("212")).
Bold(true).
Render("Gitea Wrapped") + "\n\n"
s += "Enter your Gitea credentials to get started:\n\n"
s += "Gitea URL\n"
s += is.urlField.View() + "\n\n"
s += "Username\n"
s += is.usernameField.View() + "\n\n"
s += "Access Token\n"
s += is.tokenField.View() + "\n\n"
if is.err != nil {
s += lipgloss.NewStyle().
Foreground(lipgloss.Color("1")).
Render(fmt.Sprintf("Error: %v", is.err)) + "\n"
}
s += lipgloss.NewStyle().
Foreground(lipgloss.Color("8")).
Render("(press Tab to navigate, Enter to continue, Ctrl+C to quit)") + "\n"
return s
}
func (is *InputScreen) IsDone() bool {
return is.done
}
func (is *InputScreen) GetCredentials() (string, string, string) {
return is.url, is.username, is.token
}
type errMsg error