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