api.go

  1package plugin
  2
  3import (
  4	"log"
  5
  6	"charm.land/lipgloss/v2"
  7	lua "github.com/yuin/gopher-lua"
  8)
  9
 10// registerAPI registers the "matcha" module into the Lua VM.
 11func (m *Manager) registerAPI() {
 12	L := m.state
 13
 14	mod := L.RegisterModule("matcha", map[string]lua.LGFunction{
 15		"on":                m.luaOn,
 16		"log":               m.luaLog,
 17		"notify":            m.luaNotify,
 18		"set_status":        m.luaSetStatus,
 19		"set_compose_field": m.luaSetComposeField,
 20		"bind_key":          m.luaBindKey,
 21		"http":              m.luaHTTP,
 22		"prompt":            m.luaPrompt,
 23		"style":             m.luaStyle,
 24		"settings":          m.luaSettings,
 25		"get_setting":       m.luaGetSetting,
 26	})
 27
 28	L.SetField(mod, "_VERSION", lua.LString("0.1.0"))
 29}
 30
 31// matcha.on(event, callback) — register a hook callback.
 32func (m *Manager) luaOn(L *lua.LState) int {
 33	event := L.CheckString(1)
 34	fn := L.CheckFunction(2)
 35	m.registerHook(event, fn)
 36	return 0
 37}
 38
 39// matcha.log(msg) — log a message to stderr.
 40func (m *Manager) luaLog(L *lua.LState) int {
 41	msg := L.CheckString(1)
 42	log.Printf("[plugin] %s", msg)
 43	return 0
 44}
 45
 46// matcha.set_status(area, text) — set a persistent status string for a view area.
 47// Valid areas: "inbox", "composer", "email_view".
 48func (m *Manager) luaSetStatus(L *lua.LState) int {
 49	area := L.CheckString(1)
 50	text := L.CheckString(2)
 51	m.statuses[area] = text
 52	return 0
 53}
 54
 55// matcha.notify(msg [, seconds]) — show a temporary notification in the TUI.
 56// The optional second argument sets the display duration in seconds (default 2).
 57func (m *Manager) luaNotify(L *lua.LState) int {
 58	m.pendingNotification = L.CheckString(1)
 59	m.pendingDuration = float64(L.OptNumber(2, 2))
 60	return 0
 61}
 62
 63// matcha.bind_key(key, area, description, callback) — register a custom keyboard shortcut.
 64// Valid areas: "inbox", "email_view", "composer".
 65func (m *Manager) luaBindKey(L *lua.LState) int {
 66	key := L.CheckString(1)
 67	area := L.CheckString(2)
 68	description := L.CheckString(3)
 69	fn := L.CheckFunction(4)
 70
 71	switch area {
 72	case "inbox", "email_view", "composer":
 73		m.bindings = append(m.bindings, KeyBinding{
 74			Key:         key,
 75			Area:        area,
 76			Description: description,
 77			Fn:          fn,
 78		})
 79	default:
 80		L.ArgError(2, "invalid area: must be \"inbox\", \"email_view\", or \"composer\"")
 81	}
 82	return 0
 83}
 84
 85// matcha.style(text, opts) — wrap text in lipgloss styling and return the
 86// resulting ANSI-styled string. opts is a table with optional keys:
 87//   - color, bg: string (hex "#rrggbb", ANSI 256 number as string, or named like "red")
 88//   - bold, italic, underline, strikethrough, faint, blink, reverse: bool
 89//
 90// Plugins use this from email_body_render callbacks to style matched substrings:
 91//
 92//	matcha.on("email_body_render", function(email, body)
 93//	    return (body:gsub("TODO", function(m)
 94//	        return matcha.style(m, {color = "#ff0000", bold = true})
 95//	    end))
 96//	end)
 97func (m *Manager) luaStyle(L *lua.LState) int {
 98	text := L.CheckString(1)
 99	opts := L.OptTable(2, nil)
100
101	style := lipgloss.NewStyle()
102	if opts != nil {
103		if v, ok := opts.RawGetString("color").(lua.LString); ok && v != "" {
104			style = style.Foreground(lipgloss.Color(string(v)))
105		}
106		if v, ok := opts.RawGetString("bg").(lua.LString); ok && v != "" {
107			style = style.Background(lipgloss.Color(string(v)))
108		}
109		if lua.LVAsBool(opts.RawGetString("bold")) {
110			style = style.Bold(true)
111		}
112		if lua.LVAsBool(opts.RawGetString("italic")) {
113			style = style.Italic(true)
114		}
115		if lua.LVAsBool(opts.RawGetString("underline")) {
116			style = style.Underline(true)
117		}
118		if lua.LVAsBool(opts.RawGetString("strikethrough")) {
119			style = style.Strikethrough(true)
120		}
121		if lua.LVAsBool(opts.RawGetString("faint")) {
122			style = style.Faint(true)
123		}
124		if lua.LVAsBool(opts.RawGetString("blink")) {
125			style = style.Blink(true)
126		}
127		if lua.LVAsBool(opts.RawGetString("reverse")) {
128			style = style.Reverse(true)
129		}
130	}
131
132	L.Push(lua.LString(style.Render(text)))
133	return 1
134}
135
136// matcha.settings(spec) — declare configurable settings for the current
137// plugin. spec is a table mapping setting key -> { type, default, label,
138// description }. Valid types: "boolean", "number", "string". Must be called
139// while the plugin file is being loaded (typically at the top level).
140func (m *Manager) luaSettings(L *lua.LState) int {
141	spec := L.CheckTable(1)
142	return m.declareSettings(L, spec)
143}
144
145// matcha.get_setting(key [, plugin_name]) — return the current value of a
146// setting. The optional second argument allows reading another plugin's
147// setting; defaults to the current plugin when called during load.
148func (m *Manager) luaGetSetting(L *lua.LState) int {
149	return m.getSetting(L)
150}
151
152// matcha.set_compose_field(field, value) — set a compose field value.
153// Valid fields: "to", "cc", "bcc", "subject", "body".
154func (m *Manager) luaSetComposeField(L *lua.LState) int {
155	field := L.CheckString(1)
156	value := L.CheckString(2)
157
158	switch field {
159	case "to", "cc", "bcc", "subject", "body":
160		m.pendingFields[field] = value
161	default:
162		L.ArgError(1, "invalid field: must be \"to\", \"cc\", \"bcc\", \"subject\", or \"body\"")
163	}
164	return 0
165}