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}