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}