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