Plugins
Matcha supports Lua plugins for extending functionality. Plugins can react to events like receiving emails, sending messages, switching folders, and more.
Getting Started
Plugin Location
Place your plugins in ~/.config/matcha/plugins/. Matcha loads them automatically on startup.
A plugin can be either:
- A single
.luafile (e.g.my_plugin.lua) - A directory with an
init.luaentry point (e.g.my_plugin/init.lua)
~/.config/matcha/plugins/
├── hello.lua
├── notify_github.lua
└── my_plugin/
└── init.lua
Your First Plugin
Create ~/.config/matcha/plugins/hello.lua:
local matcha = require("matcha")
matcha.on("startup", function()
matcha.log("hello plugin loaded")
end)
Restart Matcha and check the log output. You should see hello plugin loaded.
API Reference
All plugin functions are accessed through the matcha module:
local matcha = require("matcha")
matcha.on(event, callback)
Register a function to be called when an event occurs.
matcha.on("email_received", function(email)
matcha.log("New email from: " .. email.from)
end)
matcha.log(message)
Write a message to Matcha's log output (stderr). Useful for debugging.
matcha.log("something happened")
matcha.set_status(area, text)
Set a persistent status string displayed in a specific part of the UI. Pass an empty string to clear it.
Available areas:
| Area | Where it appears |
|---|---|
"inbox" | Inbox title bar, next to the folder name |
"composer" | Composer help bar at the bottom |
"email_view" | Email viewer help bar at the bottom |
matcha.set_status("inbox", "5 unread") -- shows as "INBOX (5 unread)"
matcha.set_status("composer", "420 chars") -- shows in composer help bar
matcha.set_status("inbox", "") -- clears the inbox status
matcha.set_compose_field(field, value)
Set a compose field value from a plugin. Only works when the composer is active (e.g. inside a composer_updated callback). The change is applied after the hook returns.
Available fields:
| Field | Description |
|---|---|
"to" | Recipient(s) |
"cc" | CC recipient(s) |
"bcc" | BCC recipient(s) |
"subject" | Subject line |
"body" | Email body |
-- Auto-add a BCC on every new email
matcha.on("composer_updated", function(state)
if state.bcc == "" then
matcha.set_compose_field("bcc", "[email protected]")
end
end)
matcha.bind_key(key, area, description, callback)
Register a custom keyboard shortcut. The shortcut is scoped to a specific view area and shows up in the help bar. The callback receives a context table when the key is pressed.
Parameters:
| Parameter | Type | Description |
|---|---|---|
key | string | Key string (e.g. "ctrl+k", "g", "ctrl+shift+a") |
area | string | View area: "inbox", "email_view", or "composer" |
description | string | Short text shown in the help bar |
callback | function | Called when the key is pressed; receives a context table |
Context tables by area:
- inbox / email_view: Same email table as
email_viewed(uid,from,to,subject,date,is_read,account_id,folder) - composer: Same state table as
composer_updated(body,body_len,subject,to,cc,bcc)
-- Add a shortcut to show email subject in inbox
matcha.bind_key("ctrl+i", "inbox", "info", function(email)
if email then
matcha.notify("Subject: " .. email.subject, 3)
end
end)
-- Add a shortcut to insert a greeting in the composer
matcha.bind_key("ctrl+g", "composer", "greeting", function(state)
matcha.set_compose_field("body", "Hi there,\n\n" .. state.body)
end)
matcha.http(options)
Make an HTTP request. Takes a single options table and returns two values: a response table on success, or nil plus an error string on failure.
Options table:
| Field | Type | Required | Description |
|---|---|---|---|
url | string | yes | Request URL (http or https only) |
method | string | no | HTTP method (default "GET") |
headers | table | no | Request headers as key-value pairs |
body | string | no | Request body |
Response table:
| Field | Type | Description |
|---|---|---|
status | number | HTTP status code (e.g. 200) |
body | string | Response body (capped at 1 MB) |
headers | table | Response headers (lowercase keys) |
Limits: Requests time out after 10 seconds. Response bodies are capped at 1 MB. Only http:// and https:// URLs are allowed.
-- GET request
local res, err = matcha.http({ url = "https://api.example.com/status" })
if err then
matcha.log("error: " .. err)
return
end
matcha.log("status: " .. res.status)
-- POST request with headers and body
local res, err = matcha.http({
url = "https://hooks.slack.com/services/xxx",
method = "POST",
headers = { ["Content-Type"] = "application/json" },
body = '{"text":"New email received!"}',
})
matcha.prompt(placeholder, callback)
Open a text input overlay in the composer. When the user presses Enter, the callback is called with their input string. If the user presses Esc, the prompt is cancelled and the callback is not called.
This function 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)
matcha.log("User typed: " .. input)
-- Use matcha.http() + matcha.set_compose_field() to process and update the body
end)
end)
matcha.notify(message [, seconds])
Show a temporary notification in the Matcha UI. The optional second argument sets how long the notification is displayed (default 2 seconds).
matcha.notify("You have new mail!") -- shows for 2 seconds
matcha.notify("Important!", 5) -- shows for 5 seconds
matcha.notify("Quick flash", 0.5) -- shows for half a second
Events
startup
Fired once when Matcha starts, after all plugins are loaded.
matcha.on("startup", function()
matcha.log("plugin ready")
end)
shutdown
Fired when Matcha exits.
matcha.on("shutdown", function()
matcha.log("goodbye")
end)
email_received
Fired for each email when a folder's email list is fetched. Receives an email table.
matcha.on("email_received", function(email)
matcha.log(email.from .. ": " .. email.subject)
end)
Email table fields:
| Field | Type | Description |
|---|---|---|
uid | number | Unique email ID |
from | string | Sender address |
to | table | List of recipient addresses |
subject | string | Email subject line |
date | string | ISO 8601 date string |
is_read | boolean | Whether the email has been read |
account_id | string | ID of the account |
folder | string | Folder name (e.g. "INBOX") |
email_viewed
Fired when you open an email to read it. Receives the same email table as email_received.
matcha.on("email_viewed", function(email)
matcha.log("Reading: " .. email.subject)
end)
email_send_before
Fired just before an email is sent. Receives a send table.
matcha.on("email_send_before", function(email)
matcha.log("Sending to: " .. email.to)
end)
Send table fields:
| Field | Type | Description |
|---|---|---|
to | string | Recipient(s) |
cc | string | CC recipient(s) |
subject | string | Email subject line |
account_id | string | Sending account ID |
email_send_after
Fired after an email is sent successfully. No arguments.
matcha.on("email_send_after", function()
matcha.notify("Email sent!")
end)
folder_changed
Fired when you switch to a different folder. Receives the folder name as a string.
matcha.on("folder_changed", function(folder)
matcha.log("Now viewing: " .. folder)
end)
composer_updated
Fired on every keystroke while the composer is active. Receives a state table with the current composer content.
matcha.on("composer_updated", function(state)
matcha.set_status("composer", state.body_len .. " chars")
end)
State table fields:
| Field | Type | Description |
|---|---|---|
body | string | Current body text |
body_len | number | Length of the body in bytes |
subject | string | Current subject line |
to | string | Current recipient(s) |
cc | string | Current CC recipient(s) |
bcc | string | Current BCC recipient(s) |
email_body_render
Fired right before an email body is displayed in the email view. Receives (email, rendered, raw):
email: same table asemail_viewedrendered: the ANSI-styled display string (post HTML→terminal conversion)raw: the original message body (HTML or plain text) — parse this when you need the source instead of the rendered output
Return a new string to replace the rendered body, or nil to leave it unchanged. You can recolor, bold/italicize, remove parts, or fully replace the displayed body with parsed output.
matcha.on("email_body_render", function(email, rendered, raw)
-- highlight TODO red bold
rendered = rendered:gsub("TODO", function(m)
return matcha.style(m, { color = "#ff0000", bold = true })
end)
-- italicize *asterisked* spans
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)
-- Full replacement: parse raw source, prepend a URL summary.
matcha.on("email_body_render", function(email, rendered, raw)
local urls = {}
for url in raw:gmatch("https?://[%w%-_%.~%?=&/%%#:]+") do
urls[#urls + 1] = url
end
if #urls == 0 then return rendered end
local header = matcha.style("URLs: " .. #urls, { bold = true })
return header .. "\n\n" .. rendered
end)
matcha.style(text, opts) wraps text in lipgloss styling. opts is a table with optional keys:
| Key | Type | Description |
|---|---|---|
color, bg | string | Hex ("#rrggbb"), name ("red"), or ANSI 256 number string |
bold, italic, underline, strikethrough, faint, blink, reverse | bool | Toggle the corresponding attribute |
Caveat: the body string 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.
Marketplace
Matcha includes a built-in plugin marketplace with 35+ community plugins. You can browse and install plugins from the terminal or from the online marketplace.
Browse Plugins
Open the interactive TUI marketplace:
matcha marketplace
Use j/k or arrow keys to navigate, Enter to install a plugin, and q to quit. You can also access it from Matcha's main menu.
Install a Plugin
Install from the marketplace or directly by URL:
matcha install https://raw.githubusercontent.com/floatpane/matcha/master/plugins/hello.lua
Install from a local file:
matcha install path/to/my_plugin.lua
Plugins are saved to ~/.config/matcha/plugins/ and loaded on next startup.
Configure a Plugin
Open an installed plugin in your editor to change its settings:
matcha config hello # opens ~/.config/matcha/plugins/hello.lua
matcha config # opens ~/.config/matcha/config.json
Submit Your Plugin
Anyone can add their plugin to the Matcha marketplace by submitting a pull request to the matcha repository.
-
Write your plugin as a
.luafile following the API documented on this page. -
Add an entry to
plugins/registry.json:{"name": "my_plugin","title": "My Plugin","description": "A short description of what your plugin does.","file": "my_plugin.lua","url": "https://raw.githubusercontent.com/YOUR_USER/YOUR_REPO/main/my_plugin.lua"}The
urlfield points to where your plugin file is hosted. If you include the.luafile directly in the Matcha repo, you can omiturland it will default to theplugins/directory. -
Submit your pull request. Once merged, your plugin will appear in the TUI marketplace, the CLI, and the online marketplace.
Guidelines:
- Keep plugins focused — one plugin, one purpose.
- Include a comment header in your
.luafile with a description. - Test your plugin with the latest version of Matcha before submitting.
- Plugins run in a sandboxed environment — no external dependencies are available.
Example Plugins
The repository includes 35+ example plugins. Here are a few to get started:
| Plugin | Description |
|---|---|
hello.lua | Minimal example that logs startup/shutdown |
notify_github.lua | Notifies when GitHub emails arrive |
send_logger.lua | Logs outgoing email details |
folder_announcer.lua | Shows a notification on folder switch |
unread_counter.lua | Displays unread count in the inbox title |
char_counter.lua | Live character count in the composer |
webhook_notify.lua | Posts to a webhook when emails arrive |
weather_status.lua | Shows current weather in the inbox status bar |
ai_rewrite.lua | AI-powered email rewriting in the composer |
Browse the full list in the Plugin Marketplace or run matcha marketplace.
Security
Plugins run in a sandboxed Lua 5.1 environment. The following standard libraries are available:
base(print, type, tostring, pairs, ipairs, etc.)stringtablemathpackage(forrequire)
The os, io, and debug libraries are not available. Plugins cannot access the filesystem or execute system commands.
Plugins can make HTTP requests via matcha.http(), with built-in safety limits: 10-second timeout, 1 MB response cap, and only http/https schemes.