Lua-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).
The 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.
Plugins interact with Matcha by registering callbacks on hooks:
local matcha = require("matcha")
matcha.on("email_received", function(email)
matcha.log("New email from: " .. email.from)
matcha.notify("New mail!", 3)
end)| Function | Description |
|---|---|
matcha.on(event, callback) |
Register a callback for a hook event |
matcha.log(msg) |
Log a message to stderr |
matcha.notify(msg [, seconds]) |
Show a temporary notification in the TUI (default 2s) |
matcha.set_status(area, text) |
Set a persistent status string for a view area ("inbox", "composer", "email_view") |
matcha.set_compose_field(field, value) |
Set a compose field value ("to", "cc", "bcc", "subject", "body") |
matcha.bind_key(key, area, description, callback) |
Register a custom keyboard shortcut for a view area ("inbox", "email_view", "composer") |
matcha.http(options) |
Make an HTTP request (see below) |
matcha.prompt(placeholder, callback) |
Open a text input overlay in the composer (see below) |
matcha.store_set(key, value) |
Store a string value for this plugin |
matcha.store_get(key) |
Retrieve a stored string value, or nil |
matcha.store_delete(key) |
Delete a stored key for this plugin |
matcha.store_keys() |
Return a table of stored keys for this plugin |
matcha.style(text, opts) |
Wrap text in lipgloss styling and return an ANSI-styled string (see below) |
matcha.settings(spec) |
Declare configurable settings; returns a read-only proxy table for live values (see below) |
matcha.get_setting(key [, plugin]) |
Look up a setting value by key (defaults to current plugin) |
| Event | Callback argument | Description |
|---|---|---|
startup |
— | Matcha has started |
shutdown |
— | Matcha is exiting |
email_received |
Lua table with uid, from, to, subject, date, is_read, account_id, folder |
New email arrived |
email_viewed |
Same as email_received |
User opened an email |
email_send_before |
Table with to, cc, subject, account_id |
About to send an email |
email_send_after |
Same as email_send_before |
Email sent successfully |
folder_changed |
Folder name (string) | User switched folders |
composer_updated |
Table with body, body_len, subject, to, cc, bcc |
Composer content changed |
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 |
matcha.http(options) makes an HTTP request and returns (response, err). Options is a table with:
url(string, required) — onlyhttpandhttpsschemesmethod(string, optional, default"GET")headers(table, optional)body(string, optional)
The response table has status (number), body (string), and headers (table with lowercase keys).
Safety limits: 10s timeout, 1 MB response body cap.
local res, err = matcha.http({
url = "https://api.example.com/webhook",
method = "POST",
headers = { ["Content-Type"] = "application/json" },
body = '{"text":"hello"}',
})
if err then
matcha.log("error: " .. err)
return
end
matcha.log("status: " .. res.status)Plugins 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.
local matcha = require("matcha")
-- Store a value
matcha.store_set("api_key", "sk-...")
-- Retrieve a value
local key = matcha.store_get("api_key")Use 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.
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.
Only works inside a bind_key callback for the "composer" area.
matcha.bind_key("ctrl+r", "composer", "rewrite", function(state)
matcha.prompt("Enter instruction:", function(input)
-- input is the user's text
matcha.log("User typed: " .. input)
end)
end)matcha.on("email_body_render", function(email, rendered, raw) ... end) runs
after the email body has been converted to its final ANSI-styled form and
before it is placed in the viewport. The callback receives:
email: the same table asemail_viewedrendered: the current display string (ANSI-styled, post-HTML→terminal)raw: the original message body (HTML or plain text) — useful for parsing the source instead of the rendered output
Return a new string to replace the rendered body, or nil to leave it
unchanged. Multiple registered callbacks chain in registration order; each
subsequent callback sees the previous callback's rendered output, but always
the same raw source.
matcha.style(text, opts) wraps text in lipgloss styling. opts keys (all
optional):
color,bg: string color (hex"#rrggbb", named like"red", or ANSI 256 number as string)bold,italic,underline,strikethrough,faint,blink,reverse: bool
local matcha = require("matcha")
matcha.on("email_body_render", function(email, rendered, raw)
-- highlight TODO in red bold (operates on rendered)
rendered = rendered:gsub("TODO", function(m)
return matcha.style(m, { color = "#ff0000", bold = true })
end)
-- italicize anything in *asterisks*
rendered = rendered:gsub("%*([^%*]+)%*", function(m)
return matcha.style(m, { italic = true })
end)
-- strip a tracking footer entirely
rendered = rendered:gsub("%-%-%-%s*Sent via Tracker.*$", "")
return rendered
end)
-- Parse the raw source and prepend a summary; works regardless of HTML markup.
matcha.on("email_body_render", function(email, rendered, raw)
local urls = {}
for url in raw:gmatch("https?://[%w%-_%.~%?=&/%%#:]+") do
urls[#urls + 1] = url
end
local header = matcha.style("URLs: " .. #urls, { bold = true }) .. "\n\n"
return header .. rendered
end)Caveats:
- The
renderedstring already contains ANSI escape sequences from the HTML→terminal conversion. Patterns that straddle existing escapes will not match — match plain text spans for predictable behavior, or operate onraw. - Returning a fully replaced string fully takes over the displayed body. To
build styled output from scratch, compose with
matcha.styleand join with newlines.
matcha.settings(spec) declares configurable options for a plugin. Call it
once at the top level of the plugin file. spec is a table mapping a setting
key to { type, default, label, description }. Supported types:
"boolean"— toggled in the TUI with a checkbox-style on/off selector"number"— edited with a numeric input"string"— edited with a text input
The function returns a read-only proxy table whose fields reflect the currently saved value (or the default when unset). Read fields anywhere, including inside hook callbacks:
local matcha = require("matcha")
local cfg = matcha.settings({
threshold = { type = "number", default = 5, label = "Subject length threshold" },
enabled = { type = "boolean", default = true, label = "Enable warnings" },
suffix = { type = "string", default = "!", label = "Notification suffix" },
})
matcha.on("email_received", function(email)
if cfg.enabled and #email.subject > cfg.threshold then
matcha.notify("Long subject" .. cfg.suffix, 3)
end
end)Values are persisted in ~/.config/matcha/config.json under
plugin_settings. Edit them in Settings → Plugins in the TUI; booleans
toggle with enter/space, numbers and strings open a text editor.
The following example plugins ship in ~/.config/matcha/plugins/:
email_age.luarecipient_counter.lua
| File | Description |
|---|---|
plugin.go |
Plugin manager — Lua VM setup, plugin discovery and loading, notification/status state |
hooks.go |
Hook definitions, callback registration, and hook invocation helpers |
api.go |
matcha Lua module registration (on, log, notify, set_status, set_compose_field, bind_key, http, prompt, style) |
http.go |
matcha.http() implementation — HTTP client with timeout and body size limits |
prompt.go |
matcha.prompt() implementation — user input overlay for the composer |