Files
superwork/tui/internal/tui/settings.go
T
2026-06-23 05:02:15 +08:00

221 lines
5.6 KiB
Go

package tui
import (
"fmt"
"os"
"strconv"
"strings"
"charm.land/bubbles/v2/textinput"
tea "charm.land/bubbletea/v2"
"superwork-tui/internal/auth"
"superwork-tui/internal/config"
"superwork-tui/internal/logging"
)
// settingsField is a single row in the settings panel.
type settingsField struct {
label string
input textinput.Model
readOnly bool
hint string
}
// settingsPanel holds the editable state for the settings panel.
type settingsPanel struct {
fields []settingsField
cursor int
prevState boardState
saveFn func(*config.Settings) error
}
func newSettingsPanel(s *config.Settings, prev boardState) *settingsPanel {
mkField := func(label, value string) settingsField {
ti := textinput.New()
ti.SetValue(value)
return settingsField{label: label, input: ti}
}
autoReviewVal := "false"
if s.AutoReview {
autoReviewVal = "true"
}
fields := []settingsField{
mkField("webhook_port", strconv.Itoa(s.WebhookPort)),
mkField("dev_branch", s.DevBranch),
mkField("auto_build_branch", s.AutoBuildBranch),
mkField("auto_review", autoReviewVal),
mkField("profiles_dir", s.ProfilesDir),
mkField("system_prompt_command", s.SystemPromptCommand),
mkField("worktree_post_create_script", s.WorktreePostCreateScript),
mkField("worktree_pre_remove_script", s.WorktreePreRemoveScript),
mkField("impl_tab_pre_create_script", s.ImplTabPreCreateScript),
mkField("impl_tab_post_close_script", s.ImplTabPostCloseScript),
mkField("brainstorm_prompt", s.BrainstormPrompt),
mkField("brainstorm_continue_prompt", s.BrainstormContinuePrompt),
mkField("implement_plan_prompt", s.ImplementPlanPrompt),
mkField("review_prompt", s.ReviewPrompt),
{
label: "gitea_token",
readOnly: true,
hint: resolveTokenSource(),
},
}
// Focus the first editable field.
fields[0].input.Focus()
return &settingsPanel{
fields: fields,
cursor: 0,
prevState: prev,
saveFn: config.Save,
}
}
func resolveTokenSource() string {
if os.Getenv("GITEA_TOKEN") != "" {
return "env GITEA_TOKEN ✓"
}
if _, err := auth.ResolveGiteaToken(""); err == nil {
return "tea config ✓"
}
return "未配置 (set GITEA_TOKEN or run 'tea login')"
}
// openSettings transitions to stateSettings, initialising the panel from current settings.
func (m Model) openSettings() (Model, tea.Cmd) {
s := m.settings
if s == nil {
s = config.DefaultSettings()
}
m.settingsState = newSettingsPanel(s, m.state)
m.state = stateSettings
return m, nil
}
// handleSettingsKey processes key events while in stateSettings.
func (m Model) handleSettingsKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "esc":
prev := m.settingsState.prevState
m.settingsState = nil
m.state = prev
return m, nil
case "ctrl+s":
return m.saveSettings()
case "tab", "down":
m.settingsState.moveCursor(+1)
return m, nil
case "shift+tab", "up":
m.settingsState.moveCursor(-1)
return m, nil
default:
// Forward to active text input.
p := m.settingsState
cur := p.cursor
if cur < len(p.fields) && !p.fields[cur].readOnly {
var cmd tea.Cmd
p.fields[cur].input, cmd = p.fields[cur].input.Update(msg)
return m, cmd
}
}
return m, nil
}
func (p *settingsPanel) moveCursor(delta int) {
p.fields[p.cursor].input.Blur()
n := len(p.fields)
p.cursor = ((p.cursor+delta)%n + n) % n
// Skip read-only fields when tabbing.
for p.fields[p.cursor].readOnly {
p.cursor = ((p.cursor+delta)%n + n) % n
}
p.fields[p.cursor].input.Focus()
}
func (m Model) saveSettings() (tea.Model, tea.Cmd) {
p := m.settingsState
portStr := p.fields[0].input.Value()
port, err := strconv.Atoi(portStr)
if err != nil || port < 1 || port > 65535 {
m.statusMsg = fmt.Sprintf("无效端口: %q (必须 1-65535)", portStr)
return m, nil
}
autoReview := strings.TrimSpace(p.fields[3].input.Value()) == "true"
s := &config.Settings{
WebhookPort: port,
DevBranch: p.fields[1].input.Value(),
AutoBuildBranch: p.fields[2].input.Value(),
AutoReview: autoReview,
ProfilesDir: p.fields[4].input.Value(),
SystemPromptCommand: p.fields[5].input.Value(),
WorktreePostCreateScript: p.fields[6].input.Value(),
WorktreePreRemoveScript: p.fields[7].input.Value(),
ImplTabPreCreateScript: p.fields[8].input.Value(),
ImplTabPostCloseScript: p.fields[9].input.Value(),
BrainstormPrompt: p.fields[10].input.Value(),
BrainstormContinuePrompt: p.fields[11].input.Value(),
ImplementPlanPrompt: p.fields[12].input.Value(),
ReviewPrompt: p.fields[13].input.Value(),
}
if saveErr := p.saveFn(s); saveErr != nil {
m.statusMsg = fmt.Sprintf("保存失败: %v", saveErr)
return m, nil
}
m.settings = s
m.state = p.prevState
m.settingsState = nil
m.statusMsg = "设置已保存"
logging.Default.Info("tui", "设置已保存")
return m, nil
}
// settingsView renders the settings panel.
func (m Model) settingsView() string {
if m.settingsState == nil {
return ""
}
p := m.settingsState
var sb strings.Builder
sb.WriteString(helpStyle.Render("Settings") + "\n")
sb.WriteString(strings.Repeat("─", 60) + "\n")
labelWidth := 30
for i, f := range p.fields {
label := fmt.Sprintf(" %-*s", labelWidth, f.label)
var value string
if f.readOnly {
value = helpStyle.Render(f.hint)
} else {
if i == p.cursor {
value = f.input.View()
} else {
value = "[" + f.input.Value() + "]"
}
}
sb.WriteString(label + " " + value + "\n")
}
sb.WriteString("\n")
sb.WriteString(helpStyle.Render("ctrl+s save esc cancel tab/shift+tab navigate"))
if m.statusMsg != "" {
sb.WriteString("\n" + errorStyle.Render(m.statusMsg))
}
return sb.String()
}