1# plugin
2
3Lua-based plugin system for extending Matcha. Plugins are loaded from `~/.config/matcha/plugins/` and run inside a sandboxed Lua VM (no `os`, `io`, or `debug` libraries).
4
5## How it works
6
7The `Manager` creates a Lua VM at startup, registers the `matcha` module, and loads all plugins from the user's plugins directory. Plugins can be either a single `.lua` file or a directory with an `init.lua` entry point.
8
9Plugins interact with Matcha by registering callbacks on hooks:
10
11```lua
12local matcha = require("matcha")
13
14matcha.on("email_received", function(email)
15 matcha.log("New email from: " .. email.from)
16 matcha.notify("New mail!", 3)
17end)
18```
19
20## Lua API (`matcha` module)
21
22| Function | Description |
23|----------|-------------|
24| `matcha.on(event, callback)` | Register a callback for a hook event |
25| `matcha.log(msg)` | Log a message to stderr |
26| `matcha.notify(msg [, seconds])` | Show a temporary notification in the TUI (default 2s) |
27| `matcha.set_status(area, text)` | Set a persistent status string for a view area (`"inbox"`, `"composer"`, `"email_view"`) |
28| `matcha.set_compose_field(field, value)` | Set a compose field value (`"to"`, `"cc"`, `"bcc"`, `"subject"`, `"body"`) |
29| `matcha.bind_key(key, area, description, callback)` | Register a custom keyboard shortcut for a view area (`"inbox"`, `"email_view"`, `"composer"`) |
30| `matcha.http(options)` | Make an HTTP request (see below) |
31| `matcha.prompt(placeholder, callback)` | Open a text input overlay in the composer (see below) |
32| `matcha.store_set(key, value)` | Store a string value for this plugin |
33| `matcha.store_get(key)` | Retrieve a stored string value, or `nil` |
34| `matcha.store_delete(key)` | Delete a stored key for this plugin |
35| `matcha.store_keys()` | Return a table of stored keys for this plugin |
36| `matcha.style(text, opts)` | Wrap `text` in lipgloss styling and return an ANSI-styled string (see below) |
37| `matcha.settings(spec)` | Declare configurable settings; returns a read-only proxy table for live values (see below) |
38| `matcha.get_setting(key [, plugin])` | Look up a setting value by key (defaults to current plugin) |
39
40## Hook events
41
42| Event | Callback argument | Description |
43|-------|-------------------|-------------|
44| `startup` | — | Matcha has started |
45| `shutdown` | — | Matcha is exiting |
46| `email_received` | Lua table with `uid`, `from`, `to`, `subject`, `date`, `is_read`, `account_id`, `folder` | New email arrived |
47| `email_viewed` | Same as `email_received` | User opened an email |
48| `email_send_before` | Table with `to`, `cc`, `subject`, `account_id` | About to send an email |
49| `email_send_after` | Same as `email_send_before` | Email sent successfully |
50| `folder_changed` | Folder name (string) | User switched folders |
51| `composer_updated` | Table with `body`, `body_len`, `subject`, `to`, `cc`, `bcc` | Composer content changed |
52| `email_body_render` | `(email_table, rendered, raw)` — return a string to replace the rendered body, or `nil` to keep it | About to display an email body. `rendered` is the ANSI-styled display string; `raw` is the original message source (HTML or plain text). Use for recoloring, bold/italic, removing parts, or fully replacing the displayed body with parsed output |
53
54## HTTP requests
55
56`matcha.http(options)` makes an HTTP request and returns `(response, err)`. Options is a table with:
57
58- `url` (string, required) — only `http` and `https` schemes
59- `method` (string, optional, default `"GET"`)
60- `headers` (table, optional)
61- `body` (string, optional)
62
63The response table has `status` (number), `body` (string), and `headers` (table with lowercase keys).
64
65Safety limits: 10s timeout, 1 MB response body cap.
66
67```lua
68local res, err = matcha.http({
69 url = "https://api.example.com/webhook",
70 method = "POST",
71 headers = { ["Content-Type"] = "application/json" },
72 body = '{"text":"hello"}',
73})
74if err then
75 matcha.log("error: " .. err)
76 return
77end
78matcha.log("status: " .. res.status)
79```
80
81## Persistent storage
82
83Plugins can store string key-value data between sessions. Storage is scoped per plugin and written to `~/.config/matcha/plugins/<plugin_name>/data.json`. Plugins that need structured values can encode them as strings.
84
85```lua
86local matcha = require("matcha")
87
88-- Store a value
89matcha.store_set("api_key", "sk-...")
90
91-- Retrieve a value
92local key = matcha.store_get("api_key")
93```
94
95Use `matcha.store_delete("api_key")` to remove a value. `matcha.store_keys()` returns a 1-indexed table of all keys stored by the current plugin, sorted lexicographically.
96
97## User input prompts
98
99`matcha.prompt(placeholder, callback)` opens a text input overlay in the composer. When the user presses Enter, the callback receives their input string. Pressing Esc cancels without calling the callback.
100
101Only works inside a `bind_key` callback for the `"composer"` area.
102
103```lua
104matcha.bind_key("ctrl+r", "composer", "rewrite", function(state)
105 matcha.prompt("Enter instruction:", function(input)
106 -- input is the user's text
107 matcha.log("User typed: " .. input)
108 end)
109end)
110```
111
112## Body rendering
113
114`matcha.on("email_body_render", function(email, rendered, raw) ... end)` runs
115after the email body has been converted to its final ANSI-styled form and
116before it is placed in the viewport. The callback receives:
117
118- `email`: the same table as `email_viewed`
119- `rendered`: the current display string (ANSI-styled, post-HTML→terminal)
120- `raw`: the original message body (HTML or plain text) — useful for parsing
121 the source instead of the rendered output
122
123Return a new string to replace the rendered body, or `nil` to leave it
124unchanged. Multiple registered callbacks chain in registration order; each
125subsequent callback sees the previous callback's rendered output, but always
126the same raw source.
127
128`matcha.style(text, opts)` wraps `text` in lipgloss styling. `opts` keys (all
129optional):
130
131- `color`, `bg`: string color (hex `"#rrggbb"`, named like `"red"`, or ANSI 256 number as string)
132- `bold`, `italic`, `underline`, `strikethrough`, `faint`, `blink`, `reverse`: bool
133
134```lua
135local matcha = require("matcha")
136
137matcha.on("email_body_render", function(email, rendered, raw)
138 -- highlight TODO in red bold (operates on rendered)
139 rendered = rendered:gsub("TODO", function(m)
140 return matcha.style(m, { color = "#ff0000", bold = true })
141 end)
142 -- italicize anything in *asterisks*
143 rendered = rendered:gsub("%*([^%*]+)%*", function(m)
144 return matcha.style(m, { italic = true })
145 end)
146 -- strip a tracking footer entirely
147 rendered = rendered:gsub("%-%-%-%s*Sent via Tracker.*$", "")
148 return rendered
149end)
150
151-- Parse the raw source and prepend a summary; works regardless of HTML markup.
152matcha.on("email_body_render", function(email, rendered, raw)
153 local urls = {}
154 for url in raw:gmatch("https?://[%w%-_%.~%?=&/%%#:]+") do
155 urls[#urls + 1] = url
156 end
157 local header = matcha.style("URLs: " .. #urls, { bold = true }) .. "\n\n"
158 return header .. rendered
159end)
160```
161
162Caveats:
163
164- The `rendered` string already contains ANSI escape sequences from the
165 HTML→terminal conversion. Patterns that straddle existing escapes will not
166 match — match plain text spans for predictable behavior, or operate on `raw`.
167- Returning a fully replaced string fully takes over the displayed body. To
168 build styled output from scratch, compose with `matcha.style` and join with
169 newlines.
170
171## User-configurable settings
172
173`matcha.settings(spec)` declares configurable options for a plugin. Call it
174once at the top level of the plugin file. `spec` is a table mapping a setting
175key to `{ type, default, label, description }`. Supported types:
176
177- `"boolean"` — toggled in the TUI with a checkbox-style on/off selector
178- `"number"` — edited with a numeric input
179- `"string"` — edited with a text input
180
181The function returns a read-only proxy table whose fields reflect the
182currently saved value (or the default when unset). Read fields anywhere,
183including inside hook callbacks:
184
185```lua
186local matcha = require("matcha")
187
188local cfg = matcha.settings({
189 threshold = { type = "number", default = 5, label = "Subject length threshold" },
190 enabled = { type = "boolean", default = true, label = "Enable warnings" },
191 suffix = { type = "string", default = "!", label = "Notification suffix" },
192})
193
194matcha.on("email_received", function(email)
195 if cfg.enabled and #email.subject > cfg.threshold then
196 matcha.notify("Long subject" .. cfg.suffix, 3)
197 end
198end)
199```
200
201Values are persisted in `~/.config/matcha/config.json` under
202`plugin_settings`. Edit them in **Settings → Plugins** in the TUI; booleans
203toggle with `enter`/`space`, numbers and strings open a text editor.
204
205## Available plugins
206
207The following example plugins ship in `~/.config/matcha/plugins/`:
208
209- `email_age.lua`
210- `recipient_counter.lua`
211
212## Files
213
214| File | Description |
215|------|-------------|
216| `plugin.go` | Plugin manager — Lua VM setup, plugin discovery and loading, notification/status state |
217| `hooks.go` | Hook definitions, callback registration, and hook invocation helpers |
218| `api.go` | `matcha` Lua module registration (`on`, `log`, `notify`, `set_status`, `set_compose_field`, `bind_key`, `http`, `prompt`, `style`) |
219| `http.go` | `matcha.http()` implementation — HTTP client with timeout and body size limits |
220| `prompt.go` | `matcha.prompt()` implementation — user input overlay for the composer |