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 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}