221 lines
5.6 KiB
Go
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()
|
|
}
|