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