1package plugin
2
3import (
4 "log"
5 "os"
6 "path/filepath"
7 "strings"
8
9 lua "github.com/yuin/gopher-lua"
10)
11
12// Manager manages the Lua VM and loaded plugins.
13type Manager struct {
14 state *lua.LState
15 hooks map[string][]*lua.LFunction
16 plugins []string
17 // statuses holds persistent status strings per view area, shown in the UI.
18 statuses map[string]string
19 // pendingNotification is set by matcha.notify() and consumed by the orchestrator.
20 pendingNotification string
21 pendingDuration float64 // seconds, 0 means default (2s)
22}
23
24// NewManager creates a new plugin manager with a Lua VM.
25func NewManager() *Manager {
26 m := &Manager{
27 hooks: make(map[string][]*lua.LFunction),
28 statuses: make(map[string]string),
29 }
30
31 L := lua.NewState(lua.Options{
32 SkipOpenLibs: true,
33 })
34
35 // Open only safe standard libraries (no os, io, debug)
36 for _, lib := range []struct {
37 name string
38 fn lua.LGFunction
39 }{
40 {lua.LoadLibName, lua.OpenPackage},
41 {lua.BaseLibName, lua.OpenBase},
42 {lua.TabLibName, lua.OpenTable},
43 {lua.StringLibName, lua.OpenString},
44 {lua.MathLibName, lua.OpenMath},
45 } {
46 L.Push(L.NewFunction(lib.fn))
47 L.Push(lua.LString(lib.name))
48 L.Call(1, 0)
49 }
50
51 m.state = L
52 m.registerAPI()
53
54 return m
55}
56
57// LoadPlugins discovers and loads plugins from ~/.config/matcha/plugins/.
58func (m *Manager) LoadPlugins() {
59 home, err := os.UserHomeDir()
60 if err != nil {
61 return
62 }
63
64 pluginsDir := filepath.Join(home, ".config", "matcha", "plugins")
65 entries, err := os.ReadDir(pluginsDir)
66 if err != nil {
67 return
68 }
69
70 for _, entry := range entries {
71 path := filepath.Join(pluginsDir, entry.Name())
72
73 if entry.IsDir() {
74 // Directory plugin: look for init.lua
75 initPath := filepath.Join(path, "init.lua")
76 if _, err := os.Stat(initPath); err == nil {
77 m.loadPlugin(entry.Name(), initPath)
78 }
79 } else if strings.HasSuffix(entry.Name(), ".lua") {
80 // Single-file plugin
81 name := strings.TrimSuffix(entry.Name(), ".lua")
82 m.loadPlugin(name, path)
83 }
84 }
85}
86
87func (m *Manager) loadPlugin(name, path string) {
88 if err := m.state.DoFile(path); err != nil {
89 log.Printf("plugin %q: load error: %v", name, err)
90 return
91 }
92 m.plugins = append(m.plugins, name)
93 log.Printf("plugin %q: loaded", name)
94}
95
96// Plugins returns the names of all loaded plugins.
97func (m *Manager) Plugins() []string {
98 return m.plugins
99}
100
101// PendingNotification holds a notification message and its display duration.
102type PendingNotification struct {
103 Message string
104 Duration float64 // seconds, 0 means default
105}
106
107// TakePendingNotification returns and clears any pending notification.
108func (m *Manager) TakePendingNotification() (PendingNotification, bool) {
109 if m.pendingNotification == "" {
110 return PendingNotification{}, false
111 }
112 n := PendingNotification{
113 Message: m.pendingNotification,
114 Duration: m.pendingDuration,
115 }
116 m.pendingNotification = ""
117 m.pendingDuration = 0
118 return n, true
119}
120
121// StatusText returns the plugin status string for the given view area.
122func (m *Manager) StatusText(area string) string {
123 return m.statuses[area]
124}
125
126// Close shuts down the Lua VM.
127func (m *Manager) Close() {
128 if m.state != nil {
129 m.state.Close()
130 }
131}