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