feat: add TUI feature modules for setup wizard, session browser, and settings
Add three self-contained feature modules that plug into the root App model via shared state structs and render methods. setup.go -- First-run wizard with a 5-step flow: welcome screen, API key entry (password-masked text input), default time range selector (radio-style j/k navigation over 7/30/90 day options), theme picker (radio-style over all registered themes), and a completion screen that persists choices to ~/.config/cburn/config.toml. Gracefully handles save failures by noting the settings apply for the current session only. tab_sessions.go -- Session browser with two view modes: split view (1/3 condensed list + 2/3 detail pane, scrollable with offset tracking) and full-screen detail. The detail body shows duration, prompt/API-call ratio, per-type token breakdown with cache cost attribution, per-model API call table, and subagent indicator. tab_settings.go -- Runtime settings editor with 4 configurable fields (API key, theme, default days, monthly budget). Supports inline text input editing with Enter/Esc save/cancel flow, immediate config persistence, flash "Saved!" confirmation, and error display on save failure. Theme changes apply instantly without restart.
This commit is contained in:
152
internal/tui/setup.go
Normal file
152
internal/tui/setup.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"cburn/internal/config"
|
||||
"cburn/internal/tui/theme"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// setupState tracks the first-run setup wizard state.
|
||||
type setupState struct {
|
||||
active bool
|
||||
step int // 0=welcome, 1=api key, 2=days, 3=theme, 4=done
|
||||
apiKeyIn textinput.Model
|
||||
daysChoice int // index into daysOptions
|
||||
themeChoice int // index into theme.All
|
||||
saveErr error // non-nil if config save failed
|
||||
}
|
||||
|
||||
var daysOptions = []struct {
|
||||
label string
|
||||
value int
|
||||
}{
|
||||
{"7 days", 7},
|
||||
{"30 days", 30},
|
||||
{"90 days", 90},
|
||||
}
|
||||
|
||||
func newSetupState() setupState {
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = "sk-ant-admin-... (or press Enter to skip)"
|
||||
ti.CharLimit = 256
|
||||
ti.Width = 50
|
||||
ti.EchoMode = textinput.EchoPassword
|
||||
ti.EchoCharacter = '*'
|
||||
|
||||
return setupState{
|
||||
apiKeyIn: ti,
|
||||
daysChoice: 1, // default 30 days
|
||||
}
|
||||
}
|
||||
|
||||
func (a App) renderSetup() string {
|
||||
t := theme.Active
|
||||
ss := a.setup
|
||||
|
||||
titleStyle := lipgloss.NewStyle().Foreground(t.Accent).Bold(true)
|
||||
labelStyle := lipgloss.NewStyle().Foreground(t.TextMuted)
|
||||
valueStyle := lipgloss.NewStyle().Foreground(t.TextPrimary)
|
||||
accentStyle := lipgloss.NewStyle().Foreground(t.Accent)
|
||||
greenStyle := lipgloss.NewStyle().Foreground(t.Green)
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(titleStyle.Render(" Welcome to cburn!"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render(fmt.Sprintf(" Found %s sessions in %s",
|
||||
valueStyle.Render(fmt.Sprintf("%d", len(a.sessions))),
|
||||
valueStyle.Render(a.claudeDir))))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
switch ss.step {
|
||||
case 0: // Welcome
|
||||
b.WriteString(valueStyle.Render(" Let's set up a few things."))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(accentStyle.Render(" Press Enter to continue"))
|
||||
|
||||
case 1: // API key
|
||||
b.WriteString(valueStyle.Render(" 1. Anthropic Admin API key"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(labelStyle.Render(" For real cost data from the billing API."))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(labelStyle.Render(" Get one at console.anthropic.com > Settings > Admin API keys"))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(" ")
|
||||
b.WriteString(ss.apiKeyIn.View())
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(labelStyle.Render(" Press Enter to continue (leave blank to skip)"))
|
||||
|
||||
case 2: // Default days
|
||||
b.WriteString(valueStyle.Render(" 2. Default time range"))
|
||||
b.WriteString("\n\n")
|
||||
for i, opt := range daysOptions {
|
||||
if i == ss.daysChoice {
|
||||
b.WriteString(accentStyle.Render(fmt.Sprintf(" (o) %s", opt.label)))
|
||||
} else {
|
||||
b.WriteString(labelStyle.Render(fmt.Sprintf(" ( ) %s", opt.label)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
b.WriteString(labelStyle.Render(" j/k to select, Enter to confirm"))
|
||||
|
||||
case 3: // Theme
|
||||
b.WriteString(valueStyle.Render(" 3. Color theme"))
|
||||
b.WriteString("\n\n")
|
||||
for i, th := range theme.All {
|
||||
if i == ss.themeChoice {
|
||||
b.WriteString(accentStyle.Render(fmt.Sprintf(" (o) %s", th.Name)))
|
||||
} else {
|
||||
b.WriteString(labelStyle.Render(fmt.Sprintf(" ( ) %s", th.Name)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
b.WriteString(labelStyle.Render(" j/k to select, Enter to confirm"))
|
||||
|
||||
case 4: // Done
|
||||
if ss.saveErr != nil {
|
||||
warnStyle := lipgloss.NewStyle().Foreground(t.Orange)
|
||||
b.WriteString(warnStyle.Render(fmt.Sprintf(" Could not save config: %s", ss.saveErr)))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(labelStyle.Render(" Settings will apply for this session only."))
|
||||
} else {
|
||||
b.WriteString(greenStyle.Render(" All set!"))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(labelStyle.Render(" Saved to ~/.config/cburn/config.toml"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(labelStyle.Render(" Run `cburn setup` anytime to reconfigure."))
|
||||
}
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(accentStyle.Render(" Press Enter to launch the dashboard"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (a *App) saveSetupConfig() {
|
||||
cfg, _ := config.Load()
|
||||
|
||||
apiKey := strings.TrimSpace(a.setup.apiKeyIn.Value())
|
||||
if apiKey != "" {
|
||||
cfg.AdminAPI.APIKey = apiKey
|
||||
}
|
||||
|
||||
if a.setup.daysChoice >= 0 && a.setup.daysChoice < len(daysOptions) {
|
||||
cfg.General.DefaultDays = daysOptions[a.setup.daysChoice].value
|
||||
a.days = cfg.General.DefaultDays
|
||||
}
|
||||
|
||||
if a.setup.themeChoice >= 0 && a.setup.themeChoice < len(theme.All) {
|
||||
cfg.Appearance.Theme = theme.All[a.setup.themeChoice].Name
|
||||
theme.SetActive(cfg.Appearance.Theme)
|
||||
}
|
||||
|
||||
a.setup.saveErr = config.Save(cfg)
|
||||
}
|
||||
Reference in New Issue
Block a user