package app import ( "strings" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/correx/tui-go/internal/protocol" ) // openConfig opens the config editor and requests the current config from the server. func (m *Model) openConfig() { m.overlay = OverlayConfig m.configIndex = 0 m.configEditing = false m.configEditBuf = "" m.configError = "" if len(m.configFields) == 0 { m.configLoading = true } m.client.Send(protocol.GetConfig()) } // handleConfigKey owns every key while the config overlay is open. In edit mode it captures the // value being typed; otherwise it navigates fields, stages edits, and saves. func (m Model) handleConfigKey(k keyMsg) (tea.Model, tea.Cmd) { if m.configEditing { switch k.Type { case keyEsc: m.configEditing = false m.configEditBuf = "" case keyEnter: if m.configIndex >= 0 && m.configIndex < len(m.configFields) { m.configStaged[m.configFields[m.configIndex].Key] = m.configEditBuf } m.configEditing = false m.configEditBuf = "" case keyBackspace: if n := len(m.configEditBuf); n > 0 { m.configEditBuf = m.configEditBuf[:n-1] } case keyRunes, keySpace: m.configEditBuf += string(k.Runes) } return m, nil } switch { case k.Type == keyEsc || runeIs(k, "g"): m.overlay = OverlayNone case k.Type == keyUp || runeIs(k, "k"): if m.configIndex > 0 { m.configIndex-- } case k.Type == keyDown || runeIs(k, "j"): if m.configIndex < len(m.configFields)-1 { m.configIndex++ } case k.Type == keyEnter || k.Type == keySpace: m = m.actOnConfigField() case runeIs(k, "s"): if len(m.configStaged) > 0 { m.client.Send(protocol.UpdateConfig(m.configStaged)) } } return m, nil } // actOnConfigField applies the type-appropriate action to the selected field: toggle a bool, // cycle an enum, or begin inline editing for numeric/string fields. func (m Model) actOnConfigField() Model { if m.configIndex < 0 || m.configIndex >= len(m.configFields) { return m } f := m.configFields[m.configIndex] switch f.Type { case "BOOL": if m.currentConfigValue(f) == "true" { m.configStaged[f.Key] = "false" } else { m.configStaged[f.Key] = "true" } case "ENUM": m.configStaged[f.Key] = cycleEnum(f.Options, m.currentConfigValue(f)) default: // INT, LONG, DOUBLE, STRING m.configEditing = true m.configEditBuf = m.currentConfigValue(f) } return m } // currentConfigValue returns the staged edit for a field if present, else the server value. func (m Model) currentConfigValue(f protocol.ConfigFieldDto) string { if v, ok := m.configStaged[f.Key]; ok { return v } return f.Value } // cycleEnum returns the option after cur (wrapping); falls back to the first option. func cycleEnum(options []string, cur string) string { if len(options) == 0 { return cur } for i, o := range options { if o == cur { return options[(i+1)%len(options)] } } return options[0] } func (m Model) configModal() string { t := m.theme w := m.modalWidth() var b strings.Builder b.WriteString(m.titleLine("config")) if n := len(m.configStaged); n > 0 { b.WriteString(mbg(t, " ("+itoa(n)+" unsaved)", t.P.Warn)) } b.WriteString("\n\n") if m.configLoading && len(m.configFields) == 0 { b.WriteString(mbg(t, " loading…", t.P.Faint) + "\n") b.WriteString("\n" + modalHints(t, [][2]string{{"esc", "close"}})) return t.Overlay.Width(w).Render(b.String()) } if m.configError != "" { b.WriteString(lipgloss.NewStyle().Foreground(t.P.Bad).Background(t.P.BgPanel).Render(" ▲ "+truncate(m.configError, w-8)) + "\n\n") } if len(m.configRestart) > 0 { b.WriteString(mbg(t, " saved · restart required: "+strings.Join(m.configRestart, ", "), t.P.Warn) + "\n\n") } // Windowed field list, keeping the selected row in view. bodyH := m.height*70/100 - 6 if bodyH < 4 { bodyH = 4 } off := 0 if len(m.configFields) > bodyH { off = m.configIndex - bodyH/2 if off < 0 { off = 0 } if off > len(m.configFields)-bodyH { off = len(m.configFields) - bodyH } } end := off + bodyH if end > len(m.configFields) { end = len(m.configFields) } for i := off; i < end; i++ { b.WriteString(m.configRow(i) + "\n") } b.WriteString("\n" + modalHints(t, [][2]string{ {"↑↓", "select"}, {"enter", "edit/toggle"}, {"s", "save"}, {"g/esc", "close"}, })) return t.Overlay.Width(w).Render(b.String()) } // configRow renders one field line: marker, key, current value (with edit caret / staged mark). func (m Model) configRow(i int) string { t := m.theme f := m.configFields[i] _, staged := m.configStaged[f.Key] marker := mbg(t, " ", t.P.BgPanel) keyFg := t.P.Fg if i == m.configIndex { marker = lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.BgPanel).Render("▸ ") keyFg = t.P.FgStrong } var valStr string if m.configEditing && i == m.configIndex { caret := lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.BgPanel).Render("▏") valStr = lipgloss.NewStyle().Foreground(t.P.FgStrong).Background(t.P.BgPanel).Render(m.configEditBuf) + caret } else { valFg := t.P.Accent2 if staged { valFg = t.P.Warn } val := m.currentConfigValue(f) if staged { val = "*" + val } valStr = lipgloss.NewStyle().Foreground(valFg).Background(t.P.BgPanel).Render(val) } row := marker + lipgloss.NewStyle().Foreground(keyFg).Background(t.P.BgPanel).Render(padRaw(f.Key, 42)) + " " + valStr if f.RestartRequired { row += lipgloss.NewStyle().Foreground(t.P.Faint).Background(t.P.BgPanel).Render(" (restart)") } return row }