1package tui
2
3import (
4 "fmt"
5 "strconv"
6 "strings"
7
8 tea "charm.land/bubbletea/v2"
9 "github.com/floatpane/matcha/config"
10 "github.com/floatpane/matcha/plugin"
11)
12
13// updatePlugins handles input for the plugins settings category. The view has
14// three states:
15//
16// 1. Plugin list (m.pluginSelected == ""): pick a plugin to configure.
17// 2. Plugin settings list (m.pluginSelected != "", m.pluginEditing == false):
18// navigate keys; enter/space toggles booleans, enter on number/string
19// opens an editor.
20// 3. Editing input (m.pluginEditing == true): textinput for number/string;
21// enter commits, esc cancels.
22func (m *Settings) updatePlugins(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
23 if m.plugins == nil {
24 return m, nil
25 }
26
27 if m.pluginEditing {
28 return m.updatePluginEditor(msg)
29 }
30
31 if m.pluginSelected == "" {
32 return m.updatePluginList(msg)
33 }
34
35 return m.updatePluginSettings(msg)
36}
37
38func (m *Settings) updatePluginList(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
39 schemas := m.plugins.Schemas()
40 if len(schemas) == 0 {
41 return m, nil
42 }
43
44 kb := config.Keybinds.Global
45 key := msg.String()
46 switch key {
47 case "up", kb.NavUp:
48 m.pluginListCursor = (m.pluginListCursor - 1 + len(schemas)) % len(schemas)
49 case keyDown, kb.NavDown:
50 m.pluginListCursor = (m.pluginListCursor + 1) % len(schemas)
51 case keyEnter, keyRight, "l":
52 m.pluginSelected = schemas[m.pluginListCursor].Plugin
53 m.pluginSettingCursor = 0
54 }
55 return m, nil
56}
57
58func (m *Settings) updatePluginSettings(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
59 defs := m.plugins.Schema(m.pluginSelected)
60 if len(defs) == 0 {
61 m.pluginSelected = ""
62 return m, nil
63 }
64
65 kb := config.Keybinds.Global
66 key := msg.String()
67 switch key {
68 case "esc", "left", "h", kb.Cancel:
69 m.pluginSelected = ""
70 return m, nil
71 case "up", kb.NavUp:
72 m.pluginSettingCursor = (m.pluginSettingCursor - 1 + len(defs)) % len(defs)
73 case keyDown, kb.NavDown:
74 m.pluginSettingCursor = (m.pluginSettingCursor + 1) % len(defs)
75 case keyEnter, "space", keyRight, "l":
76 def := defs[m.pluginSettingCursor]
77 switch def.Type {
78 case plugin.SettingBool:
79 cur, _ := m.plugins.GetSettingValue(m.pluginSelected, def.Key)
80 b, _ := cur.(bool)
81 m.plugins.SetSettingValue(m.pluginSelected, def.Key, !b)
82 m.persistPluginSettings()
83 return m, func() tea.Msg { return ConfigSavedMsg{} }
84 case plugin.SettingNumber, plugin.SettingString:
85 m.beginPluginEdit(def)
86 }
87 }
88 return m, nil
89}
90
91func (m *Settings) beginPluginEdit(def plugin.SettingDef) {
92 m.pluginEditing = true
93 m.pluginEditingKey = def.Key
94 m.pluginEditingType = def.Type
95 cur, _ := m.plugins.GetSettingValue(m.pluginSelected, def.Key)
96 m.pluginInput.SetValue(formatSettingValue(cur))
97 m.pluginInput.Focus()
98}
99
100func (m *Settings) updatePluginEditor(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
101 switch msg.String() {
102 case "esc":
103 m.pluginEditing = false
104 m.pluginInput.Blur()
105 return m, nil
106 case keyEnter:
107 raw := m.pluginInput.Value()
108 switch m.pluginEditingType {
109 case plugin.SettingNumber:
110 n, err := strconv.ParseFloat(strings.TrimSpace(raw), 64)
111 if err != nil {
112 return m, nil
113 }
114 m.plugins.SetSettingValue(m.pluginSelected, m.pluginEditingKey, n)
115 case plugin.SettingString:
116 m.plugins.SetSettingValue(m.pluginSelected, m.pluginEditingKey, raw)
117 case plugin.SettingBool:
118 // Bool settings are toggled directly, not via text input
119 }
120 m.pluginEditing = false
121 m.pluginInput.Blur()
122 m.persistPluginSettings()
123 return m, func() tea.Msg { return ConfigSavedMsg{} }
124 }
125
126 // Forward all other keys (typing, backspace, arrows, etc.) to textinput.
127 var cmd tea.Cmd
128 m.pluginInput, cmd = m.pluginInput.Update(msg)
129 return m, cmd
130}
131
132func (m *Settings) persistPluginSettings() {
133 if m.cfg == nil || m.plugins == nil {
134 return
135 }
136 m.cfg.PluginSettings = m.plugins.AllSettingValues()
137 _ = config.SaveConfig(m.cfg)
138}
139
140func (m *Settings) viewPlugins() string {
141 var b strings.Builder
142 b.WriteString(titleStyle.Render(t("settings.category_plugins")) + "\n\n")
143
144 if m.plugins == nil {
145 b.WriteString(accountEmailStyle.Render(" Plugin manager unavailable.\n"))
146 return b.String()
147 }
148
149 if m.pluginSelected == "" {
150 schemas := m.plugins.Schemas()
151 if len(schemas) == 0 {
152 b.WriteString(accountEmailStyle.Render(" No plugins declare configurable settings.\n"))
153 b.WriteString("\n")
154 b.WriteString(helpStyle.Render("Plugins use matcha.settings(...) to expose options."))
155 return b.String()
156 }
157
158 for i, s := range schemas {
159 cursor := " "
160 style := accountItemStyle
161 if m.pluginListCursor == i {
162 cursor = "> "
163 style = selectedAccountItemStyle
164 }
165 line := fmt.Sprintf("%s (%d %s)", s.Plugin, len(s.Defs), pluralSettings(len(s.Defs)))
166 b.WriteString(style.Render(cursor+line) + "\n")
167 }
168 b.WriteString("\n")
169 b.WriteString(helpStyle.Render("↑/↓ navigate • enter open • esc back"))
170 return b.String()
171 }
172
173 defs := m.plugins.Schema(m.pluginSelected)
174 b.WriteString(accountEmailStyle.Render(m.pluginSelected) + "\n\n")
175
176 for i, def := range defs {
177 cursor := " "
178 style := accountItemStyle
179 if m.pluginSettingCursor == i {
180 cursor = "> "
181 style = selectedAccountItemStyle
182 }
183
184 label := def.Label
185 if label == "" {
186 label = def.Key
187 }
188 val, _ := m.plugins.GetSettingValue(m.pluginSelected, def.Key)
189 display := formatDisplayValue(def.Type, val)
190 line := fmt.Sprintf("%s: %s", label, display)
191 b.WriteString(style.Render(cursor+line) + "\n")
192 }
193
194 if m.pluginEditing {
195 b.WriteString("\n")
196 b.WriteString(settingsFocusedStyle.Render("Edit "+m.pluginEditingKey) + "\n")
197 b.WriteString(m.pluginInput.View() + "\n")
198 b.WriteString("\n")
199 b.WriteString(helpStyle.Render("enter save • esc cancel"))
200 } else {
201 if m.pluginSettingCursor < len(defs) {
202 tip := defs[m.pluginSettingCursor].Description
203 if tip != "" && !m.cfg.HideTips {
204 b.WriteString("\n")
205 b.WriteString(TipStyle.Render("Tip: " + tip))
206 }
207 }
208 b.WriteString("\n\n")
209 b.WriteString(helpStyle.Render("↑/↓ navigate • enter toggle/edit • esc back"))
210 }
211
212 return b.String()
213}
214
215func formatSettingValue(v interface{}) string {
216 switch x := v.(type) {
217 case bool:
218 if x {
219 return "true"
220 }
221 return "false"
222 case float64:
223 if x == float64(int64(x)) {
224 return strconv.FormatInt(int64(x), 10)
225 }
226 return strconv.FormatFloat(x, 'f', -1, 64)
227 case string:
228 return x
229 case nil:
230 return ""
231 default:
232 return fmt.Sprintf("%v", v)
233 }
234}
235
236func formatDisplayValue(typ plugin.SettingType, v interface{}) string {
237 if typ == plugin.SettingBool {
238 b, _ := v.(bool)
239 if b {
240 return "[x] on"
241 }
242 return "[ ] off"
243 }
244 s := formatSettingValue(v)
245 if s == "" && typ == plugin.SettingString {
246 return "(empty)"
247 }
248 return s
249}
250
251func pluralSettings(n int) string {
252 if n == 1 {
253 return "setting"
254 }
255 return "settings"
256}