plugin.go

  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}