settings.go

  1package plugin
  2
  3import (
  4	"sort"
  5
  6	lua "github.com/yuin/gopher-lua"
  7)
  8
  9// SettingType identifies the kind of value a plugin setting holds.
 10type SettingType string
 11
 12const (
 13	SettingBool   SettingType = "boolean"
 14	SettingNumber SettingType = "number"
 15	SettingString SettingType = "string"
 16)
 17
 18// SettingDef describes a single configurable plugin setting.
 19type SettingDef struct {
 20	Key         string
 21	Type        SettingType
 22	Default     interface{}
 23	Label       string
 24	Description string
 25}
 26
 27// PluginSettings holds the schema for one plugin's settings, ordered by
 28// declaration order so the TUI lists them predictably.
 29type PluginSettings struct {
 30	Plugin string
 31	Defs   []SettingDef
 32}
 33
 34// declareSettings registers the schema for the currently-loading plugin and
 35// returns a Lua table whose __index reads the live current value for a key.
 36// Plugins typically capture the returned table in a local and read fields
 37// from it at any later time, including inside hook callbacks:
 38//
 39//	local cfg = matcha.settings({
 40//	    threshold = {type = "number", default = 5},
 41//	    enabled   = {type = "boolean", default = true},
 42//	})
 43//	matcha.on("email_received", function(email)
 44//	    if cfg.enabled and #email.subject > cfg.threshold then ... end
 45//	end)
 46func (m *Manager) declareSettings(L *lua.LState, spec *lua.LTable) int { //nolint:gocritic
 47	if m.currentPlugin == "" {
 48		L.RaiseError("matcha.settings() must be called from a plugin file")
 49		return 0
 50	}
 51	plugin := m.currentPlugin
 52
 53	defs := []SettingDef{}
 54	keys := []string{}
 55	specs := map[string]SettingDef{}
 56
 57	spec.ForEach(func(k, v lua.LValue) {
 58		key, ok := k.(lua.LString)
 59		if !ok {
 60			return
 61		}
 62		entry, ok := v.(*lua.LTable)
 63		if !ok {
 64			return
 65		}
 66
 67		def := SettingDef{Key: string(key)}
 68
 69		if t, ok := entry.RawGetString("type").(lua.LString); ok {
 70			def.Type = SettingType(string(t))
 71		}
 72		if l, ok := entry.RawGetString("label").(lua.LString); ok {
 73			def.Label = string(l)
 74		}
 75		if d, ok := entry.RawGetString("description").(lua.LString); ok {
 76			def.Description = string(d)
 77		}
 78
 79		raw := entry.RawGetString("default")
 80		switch def.Type {
 81		case SettingBool:
 82			def.Default = lua.LVAsBool(raw)
 83		case SettingNumber:
 84			if n, ok := raw.(lua.LNumber); ok {
 85				def.Default = float64(n)
 86			} else {
 87				def.Default = float64(0)
 88			}
 89		case SettingString:
 90			if s, ok := raw.(lua.LString); ok {
 91				def.Default = string(s)
 92			} else {
 93				def.Default = ""
 94			}
 95		default:
 96			// Unknown type — skip.
 97			return
 98		}
 99
100		keys = append(keys, def.Key)
101		specs[def.Key] = def
102	})
103
104	sort.Strings(keys)
105	for _, k := range keys {
106		defs = append(defs, specs[k])
107	}
108
109	m.pluginSchemas[plugin] = defs
110	if _, ok := m.pluginValues[plugin]; !ok {
111		m.pluginValues[plugin] = map[string]interface{}{}
112	}
113
114	proxy := L.NewTable()
115	mt := L.NewTable()
116	L.SetField(mt, "__index", L.NewFunction(func(L *lua.LState) int {
117		key := L.CheckString(2)
118		def, ok := m.findDef(plugin, key)
119		if !ok {
120			L.Push(lua.LNil)
121			return 1
122		}
123		L.Push(toLuaValue(L, m.lookupValue(plugin, key, def)))
124		return 1
125	}))
126	L.SetField(mt, "__newindex", L.NewFunction(func(L *lua.LState) int {
127		L.RaiseError("plugin settings table is read-only; edit values in the TUI settings")
128		return 0
129	}))
130	L.SetMetatable(proxy, mt)
131	L.Push(proxy)
132	return 1
133}
134
135// getSetting returns the value of a setting for the currently-running plugin.
136// During plugin load, "currently running" means the loading plugin; outside
137// load (e.g. inside a hook callback), it falls back to the plugin that owns
138// the running closure — for now we use currentPlugin and only allow lookups
139// to the plugin that declared the schema by name.
140func (m *Manager) getSetting(L *lua.LState) int { //nolint:gocritic
141	plugin := m.currentPlugin
142	key := L.CheckString(1)
143
144	if plugin == "" {
145		// Allow optional second argument: explicit plugin name.
146		if L.GetTop() >= 2 {
147			plugin = L.CheckString(2)
148		}
149	}
150
151	def, ok := m.findDef(plugin, key)
152	if !ok {
153		L.Push(lua.LNil)
154		return 1
155	}
156
157	val := m.lookupValue(plugin, key, def)
158	L.Push(toLuaValue(L, val))
159	return 1
160}
161
162func (m *Manager) findDef(plugin, key string) (SettingDef, bool) {
163	for _, d := range m.pluginSchemas[plugin] {
164		if d.Key == key {
165			return d, true
166		}
167	}
168	return SettingDef{}, false
169}
170
171func (m *Manager) lookupValue(plugin, key string, def SettingDef) interface{} {
172	if vals, ok := m.pluginValues[plugin]; ok {
173		if v, ok := vals[key]; ok {
174			return v
175		}
176	}
177	return def.Default
178}
179
180func toLuaValue(_ *lua.LState, v interface{}) lua.LValue {
181	switch x := v.(type) {
182	case bool:
183		return lua.LBool(x)
184	case float64:
185		return lua.LNumber(x)
186	case int:
187		return lua.LNumber(x)
188	case string:
189		return lua.LString(x)
190	default:
191		return lua.LNil
192	}
193}
194
195// Schemas returns all plugin setting schemas, sorted by plugin name.
196func (m *Manager) Schemas() []PluginSettings {
197	names := make([]string, 0, len(m.pluginSchemas))
198	for name := range m.pluginSchemas {
199		names = append(names, name)
200	}
201	sort.Strings(names)
202
203	out := make([]PluginSettings, 0, len(names))
204	for _, n := range names {
205		out = append(out, PluginSettings{Plugin: n, Defs: m.pluginSchemas[n]})
206	}
207	return out
208}
209
210// Schema returns the schema for a single plugin.
211func (m *Manager) Schema(plugin string) []SettingDef {
212	return m.pluginSchemas[plugin]
213}
214
215// GetSettingValue returns the current value (or default) for a plugin setting.
216func (m *Manager) GetSettingValue(plugin, key string) (interface{}, bool) {
217	def, ok := m.findDef(plugin, key)
218	if !ok {
219		return nil, false
220	}
221	return m.lookupValue(plugin, key, def), true
222}
223
224// SetSettingValue updates a plugin setting in-memory. Coerces value to the
225// declared type. Returns false if the plugin/key is unknown.
226func (m *Manager) SetSettingValue(plugin, key string, val interface{}) bool {
227	def, ok := m.findDef(plugin, key)
228	if !ok {
229		return false
230	}
231
232	if _, ok := m.pluginValues[plugin]; !ok {
233		m.pluginValues[plugin] = map[string]interface{}{}
234	}
235	m.pluginValues[plugin][key] = coerceValue(def.Type, val)
236	return true
237}
238
239// LoadSettingValues replaces in-memory values with the given snapshot. Values
240// for unknown plugins/keys are kept as-is so freshly-disabled plugins don't
241// lose their saved settings on next launch.
242func (m *Manager) LoadSettingValues(values map[string]map[string]interface{}) {
243	if values == nil {
244		return
245	}
246	for plugin, vals := range values {
247		if _, ok := m.pluginValues[plugin]; !ok {
248			m.pluginValues[plugin] = map[string]interface{}{}
249		}
250		for k, v := range vals {
251			if def, ok := m.findDef(plugin, k); ok {
252				m.pluginValues[plugin][k] = coerceValue(def.Type, v)
253			} else {
254				m.pluginValues[plugin][k] = v
255			}
256		}
257	}
258}
259
260// AllSettingValues returns a deep copy of all plugin setting values.
261func (m *Manager) AllSettingValues() map[string]map[string]interface{} {
262	out := make(map[string]map[string]interface{}, len(m.pluginValues))
263	for p, vals := range m.pluginValues {
264		inner := make(map[string]interface{}, len(vals))
265		for k, v := range vals {
266			inner[k] = v
267		}
268		out[p] = inner
269	}
270	return out
271}
272
273func coerceValue(t SettingType, v interface{}) interface{} {
274	switch t {
275	case SettingBool:
276		switch x := v.(type) {
277		case bool:
278			return x
279		case string:
280			return x == "true"
281		case float64:
282			return x != 0
283		}
284		return false
285	case SettingNumber:
286		switch x := v.(type) {
287		case float64:
288			return x
289		case int:
290			return float64(x)
291		case bool:
292			if x {
293				return float64(1)
294			}
295			return float64(0)
296		case string:
297			return float64(0)
298		}
299		return float64(0)
300	case SettingString:
301		switch x := v.(type) {
302		case string:
303			return x
304		case float64:
305			return ""
306		case bool:
307			if x {
308				return "true"
309			}
310			return "false"
311		}
312		return ""
313	}
314	return v
315}