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