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 {
47 case key == "up" || key == kb.NavUp:
48 m.pluginListCursor = (m.pluginListCursor - 1 + len(schemas)) % len(schemas)
49 case key == "down" || key == kb.NavDown:
50 m.pluginListCursor = (m.pluginListCursor + 1) % len(schemas)
51 case key == "enter" || key == "right" || key == "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 {
68 case key == "esc" || key == "left" || key == "h" || key == kb.Cancel:
69 m.pluginSelected = ""
70 return m, nil
71 case key == "up" || key == kb.NavUp:
72 m.pluginSettingCursor = (m.pluginSettingCursor - 1 + len(defs)) % len(defs)
73 case key == "down" || key == kb.NavDown:
74 m.pluginSettingCursor = (m.pluginSettingCursor + 1) % len(defs)
75 case key == "enter" || key == "space" || key == "right" || key == "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 "enter":
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 }
118 m.pluginEditing = false
119 m.pluginInput.Blur()
120 m.persistPluginSettings()
121 return m, func() tea.Msg { return ConfigSavedMsg{} }
122 }
123
124 // Forward all other keys (typing, backspace, arrows, etc.) to textinput.
125 var cmd tea.Cmd
126 m.pluginInput, cmd = m.pluginInput.Update(msg)
127 return m, cmd
128}
129
130func (m *Settings) persistPluginSettings() {
131 if m.cfg == nil || m.plugins == nil {
132 return
133 }
134 m.cfg.PluginSettings = m.plugins.AllSettingValues()
135 _ = config.SaveConfig(m.cfg)
136}
137
138func (m *Settings) viewPlugins() string {
139 var b strings.Builder
140 b.WriteString(titleStyle.Render(t("settings.category_plugins")) + "\n\n")
141
142 if m.plugins == nil {
143 b.WriteString(accountEmailStyle.Render(" Plugin manager unavailable.\n"))
144 return b.String()
145 }
146
147 if m.pluginSelected == "" {
148 schemas := m.plugins.Schemas()
149 if len(schemas) == 0 {
150 b.WriteString(accountEmailStyle.Render(" No plugins declare configurable settings.\n"))
151 b.WriteString("\n")
152 b.WriteString(helpStyle.Render("Plugins use matcha.settings(...) to expose options."))
153 return b.String()
154 }
155
156 for i, s := range schemas {
157 cursor := " "
158 style := accountItemStyle
159 if m.pluginListCursor == i {
160 cursor = "> "
161 style = selectedAccountItemStyle
162 }
163 line := fmt.Sprintf("%s (%d %s)", s.Plugin, len(s.Defs), pluralSettings(len(s.Defs)))
164 b.WriteString(style.Render(cursor+line) + "\n")
165 }
166 b.WriteString("\n")
167 b.WriteString(helpStyle.Render("↑/↓ navigate • enter open • esc back"))
168 return b.String()
169 }
170
171 defs := m.plugins.Schema(m.pluginSelected)
172 b.WriteString(accountEmailStyle.Render(m.pluginSelected) + "\n\n")
173
174 for i, def := range defs {
175 cursor := " "
176 style := accountItemStyle
177 if m.pluginSettingCursor == i {
178 cursor = "> "
179 style = selectedAccountItemStyle
180 }
181
182 label := def.Label
183 if label == "" {
184 label = def.Key
185 }
186 val, _ := m.plugins.GetSettingValue(m.pluginSelected, def.Key)
187 display := formatDisplayValue(def.Type, val)
188 line := fmt.Sprintf("%s: %s", label, display)
189 b.WriteString(style.Render(cursor+line) + "\n")
190 }
191
192 if m.pluginEditing {
193 b.WriteString("\n")
194 b.WriteString(settingsFocusedStyle.Render("Edit "+m.pluginEditingKey) + "\n")
195 b.WriteString(m.pluginInput.View() + "\n")
196 b.WriteString("\n")
197 b.WriteString(helpStyle.Render("enter save • esc cancel"))
198 } else {
199 if m.pluginSettingCursor < len(defs) {
200 tip := defs[m.pluginSettingCursor].Description
201 if tip != "" && !m.cfg.HideTips {
202 b.WriteString("\n")
203 b.WriteString(TipStyle.Render("Tip: " + tip))
204 }
205 }
206 b.WriteString("\n\n")
207 b.WriteString(helpStyle.Render("↑/↓ navigate • enter toggle/edit • esc back"))
208 }
209
210 return b.String()
211}
212
213func formatSettingValue(v interface{}) string {
214 switch x := v.(type) {
215 case bool:
216 if x {
217 return "true"
218 }
219 return "false"
220 case float64:
221 if x == float64(int64(x)) {
222 return strconv.FormatInt(int64(x), 10)
223 }
224 return strconv.FormatFloat(x, 'f', -1, 64)
225 case string:
226 return x
227 case nil:
228 return ""
229 default:
230 return fmt.Sprintf("%v", v)
231 }
232}
233
234func formatDisplayValue(typ plugin.SettingType, v interface{}) string {
235 if typ == plugin.SettingBool {
236 b, _ := v.(bool)
237 if b {
238 return "[x] on"
239 }
240 return "[ ] off"
241 }
242 s := formatSettingValue(v)
243 if s == "" && typ == plugin.SettingString {
244 return "(empty)"
245 }
246 return s
247}
248
249func pluralSettings(n int) string {
250 if n == 1 {
251 return "setting"
252 }
253 return "settings"
254}