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}