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.
153 lines
4.4 KiB
Go
153 lines
4.4 KiB
Go
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)
|
|
}
|