11
This commit is contained in:
commit
e6f1776d4f
264 files changed
+54215
No files matched your search
@@ -0,0 +1,220 @@
|
||||
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()
|
||||
}
|
||||
Reference in new issue
Block a user