settings_plugins.go

  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}