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 selected := m.pluginListCursor == i
160 cursor := m.contentCursor(selected)
161 style := m.contentItemStyle(selected)
162 line := fmt.Sprintf("%s (%d %s)", s.Plugin, len(s.Defs), pluralSettings(len(s.Defs)))
163 b.WriteString(style.Render(cursor+line) + "\n")
164 }
165 b.WriteString("\n")
166 b.WriteString(helpStyle.Render("↑/↓ navigate • enter open • esc back"))
167 return b.String()
168 }
169
170 defs := m.plugins.Schema(m.pluginSelected)
171 b.WriteString(accountEmailStyle.Render(m.pluginSelected) + "\n\n")
172
173 for i, def := range defs {
174 selected := m.pluginSettingCursor == i
175 cursor := m.contentCursor(selected)
176 style := m.contentItemStyle(selected)
177
178 label := def.Label
179 if label == "" {
180 label = def.Key
181 }
182 val, _ := m.plugins.GetSettingValue(m.pluginSelected, def.Key)
183 display := formatDisplayValue(def.Type, val)
184 line := fmt.Sprintf("%s: %s", label, display)
185 b.WriteString(style.Render(cursor+line) + "\n")
186 }
187
188 if m.pluginEditing {
189 b.WriteString("\n")
190 b.WriteString(m.contentFocusStyle().Render("Edit "+m.pluginEditingKey) + "\n")
191 b.WriteString(m.pluginInput.View() + "\n")
192 b.WriteString("\n")
193 b.WriteString(helpStyle.Render("enter save • esc cancel"))
194 } else {
195 if m.pluginSettingCursor < len(defs) {
196 tip := defs[m.pluginSettingCursor].Description
197 if tip != "" && !m.cfg.HideTips {
198 b.WriteString("\n")
199 b.WriteString(TipStyle.Render("Tip: " + tip))
200 }
201 }
202 b.WriteString("\n\n")
203 b.WriteString(helpStyle.Render("↑/↓ navigate • enter toggle/edit • esc back"))
204 }
205
206 return b.String()
207}
208
209func formatSettingValue(v interface{}) string {
210 switch x := v.(type) {
211 case bool:
212 if x {
213 return "true"
214 }
215 return "false"
216 case float64:
217 if x == float64(int64(x)) {
218 return strconv.FormatInt(int64(x), 10)
219 }
220 return strconv.FormatFloat(x, 'f', -1, 64)
221 case string:
222 return x
223 case nil:
224 return ""
225 default:
226 return fmt.Sprintf("%v", v)
227 }
228}
229
230func formatDisplayValue(typ plugin.SettingType, v interface{}) string {
231 if typ == plugin.SettingBool {
232 b, _ := v.(bool)
233 if b {
234 return "[x] on"
235 }
236 return "[ ] off"
237 }
238 s := formatSettingValue(v)
239 if s == "" && typ == plugin.SettingString {
240 return "(empty)"
241 }
242 return s
243}
244
245func pluralSettings(n int) string {
246 if n == 1 {
247 return "setting"
248 }
249 return "settings"
250}