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		"store_set":          m.luaStoreSet,
 24		"store_get":          m.luaStoreGet,
 25		"store_delete":       m.luaStoreDelete,
 26		"store_keys":         m.luaStoreKeys,
 27		"style":              m.luaStyle,
 28		"settings":           m.luaSettings,
 29		"get_setting":        m.luaGetSetting,
 30		"mark_read":          m.luaMarkRead,
 31		"mark_unread":        m.luaMarkUnread,
 32		"suppress_auto_read": m.luaSuppressAutoRead,
 33	})
 34
 35	L.SetField(mod, "_VERSION", lua.LString("0.1.0"))
 36}
 37
 38// matcha.on(event, callback) — register a hook callback.
 39func (m *Manager) luaOn(L *lua.LState) int { //nolint:gocritic
 40	event := L.CheckString(1)
 41	fn := L.CheckFunction(2)
 42	m.registerHook(event, fn)
 43	return 0
 44}
 45
 46// matcha.log(msg) — log a message to stderr.
 47func (m *Manager) luaLog(L *lua.LState) int { //nolint:gocritic
 48	msg := L.CheckString(1)
 49	log.Printf("[plugin] %s", msg)
 50	return 0
 51}
 52
 53// matcha.set_status(area, text) — set a persistent status string for a view area.
 54// Valid areas: "inbox", "composer", "email_view".
 55func (m *Manager) luaSetStatus(L *lua.LState) int { //nolint:gocritic
 56	area := L.CheckString(1)
 57	text := L.CheckString(2)
 58	m.statuses[area] = text
 59	return 0
 60}
 61
 62// matcha.notify(msg [, seconds]) — show a temporary notification in the TUI.
 63// The optional second argument sets the display duration in seconds (default 2).
 64func (m *Manager) luaNotify(L *lua.LState) int { //nolint:gocritic
 65	m.pendingNotification = L.CheckString(1)
 66	m.pendingDuration = float64(L.OptNumber(2, 2))
 67	return 0
 68}
 69
 70// matcha.bind_key(key, area, description, callback) — register a custom keyboard shortcut.
 71// Valid areas: "inbox", "email_view", "composer".
 72func (m *Manager) luaBindKey(L *lua.LState) int { //nolint:gocritic
 73	key := L.CheckString(1)
 74	area := L.CheckString(2)
 75	description := L.CheckString(3)
 76	fn := L.CheckFunction(4)
 77
 78	switch area {
 79	case "inbox", "email_view", "composer":
 80		m.bindings = append(m.bindings, KeyBinding{
 81			Key:         key,
 82			Area:        area,
 83			Description: description,
 84			Fn:          fn,
 85			Plugin:      m.currentPlugin,
 86		})
 87	default:
 88		L.ArgError(2, "invalid area: must be \"inbox\", \"email_view\", or \"composer\"")
 89	}
 90	return 0
 91}
 92
 93// matcha.style(text, opts) — wrap text in lipgloss styling and return the
 94// resulting ANSI-styled string. opts is a table with optional keys:
 95//   - color, bg: string (hex "#rrggbb", ANSI 256 number as string, or named like "red")
 96//   - bold, italic, underline, strikethrough, faint, blink, reverse: bool
 97//
 98// Plugins use this from email_body_render callbacks to style matched substrings:
 99//
100//	matcha.on("email_body_render", function(email, body)
101//	    return (body:gsub("TODO", function(m)
102//	        return matcha.style(m, {color = "#ff0000", bold = true})
103//	    end))
104//	end)
105func (m *Manager) luaStyle(L *lua.LState) int { //nolint:gocritic
106	text := L.CheckString(1)
107	opts := L.OptTable(2, nil)
108
109	style := lipgloss.NewStyle()
110	if opts != nil {
111		if v, ok := opts.RawGetString("color").(lua.LString); ok && v != "" {
112			style = style.Foreground(lipgloss.Color(string(v)))
113		}
114		if v, ok := opts.RawGetString("bg").(lua.LString); ok && v != "" {
115			style = style.Background(lipgloss.Color(string(v)))
116		}
117		if lua.LVAsBool(opts.RawGetString("bold")) {
118			style = style.Bold(true)
119		}
120		if lua.LVAsBool(opts.RawGetString("italic")) {
121			style = style.Italic(true)
122		}
123		if lua.LVAsBool(opts.RawGetString("underline")) {
124			style = style.Underline(true)
125		}
126		if lua.LVAsBool(opts.RawGetString("strikethrough")) {
127			style = style.Strikethrough(true)
128		}
129		if lua.LVAsBool(opts.RawGetString("faint")) {
130			style = style.Faint(true)
131		}
132		if lua.LVAsBool(opts.RawGetString("blink")) {
133			style = style.Blink(true)
134		}
135		if lua.LVAsBool(opts.RawGetString("reverse")) {
136			style = style.Reverse(true)
137		}
138	}
139
140	L.Push(lua.LString(style.Render(text)))
141	return 1
142}
143
144// matcha.settings(spec) — declare configurable settings for the current
145// plugin. spec is a table mapping setting key -> { type, default, label,
146// description }. Valid types: "boolean", "number", "string". Must be called
147// while the plugin file is being loaded (typically at the top level).
148func (m *Manager) luaSettings(L *lua.LState) int { //nolint:gocritic
149	spec := L.CheckTable(1)
150	return m.declareSettings(L, spec)
151}
152
153// matcha.get_setting(key [, plugin_name]) — return the current value of a
154// setting. The optional second argument allows reading another plugin's
155// setting; defaults to the current plugin when called during load.
156func (m *Manager) luaGetSetting(L *lua.LState) int { //nolint:gocritic
157	return m.getSetting(L)
158}
159
160// matcha.mark_read(uid, account_id, folder) — queue a mark-as-read op for the given email.
161// The orchestrator dispatches the IMAP/backend call after the hook or keybinding returns.
162func (m *Manager) luaMarkRead(L *lua.LState) int { //nolint:gocritic
163	uid := uint32(L.CheckInt(1))
164	accountID := L.CheckString(2)
165	folder := L.CheckString(3)
166	m.pendingFlagOps = append(m.pendingFlagOps, FlagOp{UID: uid, AccountID: accountID, Folder: folder, Read: true})
167	return 0
168}
169
170// matcha.mark_unread(uid, account_id, folder) — queue a mark-as-unread op for the given email.
171func (m *Manager) luaMarkUnread(L *lua.LState) int { //nolint:gocritic
172	uid := uint32(L.CheckInt(1))
173	accountID := L.CheckString(2)
174	folder := L.CheckString(3)
175	m.pendingFlagOps = append(m.pendingFlagOps, FlagOp{UID: uid, AccountID: accountID, Folder: folder, Read: false})
176	return 0
177}
178
179// matcha.suppress_auto_read() — prevent the currently viewed email from being
180// automatically marked as read. Must be called inside an email_viewed callback.
181func (m *Manager) luaSuppressAutoRead(L *lua.LState) int { //nolint:gocritic
182	m.suppressAutoRead = true
183	return 0
184}
185
186// matcha.set_compose_field(field, value) — set a compose field value.
187// Valid fields: "to", "cc", "bcc", "subject", "body".
188func (m *Manager) luaSetComposeField(L *lua.LState) int { //nolint:gocritic
189	field := L.CheckString(1)
190	value := L.CheckString(2)
191
192	switch field {
193	case "to", "cc", "bcc", "subject", "body":
194		m.pendingFields[field] = value
195	default:
196		L.ArgError(1, "invalid field: must be \"to\", \"cc\", \"bcc\", \"subject\", or \"body\"")
197	}
198	return 0
199}