Skip to main content

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 .lua file (e.g. my_plugin.lua)
  • A directory with an init.lua entry 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:

AreaWhere 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:

FieldDescription
"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:

ParameterTypeDescription
keystringKey string (e.g. "ctrl+k", "g", "ctrl+shift+a")
areastringView area: "inbox", "email_view", or "composer"
descriptionstringShort text shown in the help bar
callbackfunctionCalled 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:

FieldTypeRequiredDescription
urlstringyesRequest URL (http or https only)
methodstringnoHTTP method (default "GET")
headerstablenoRequest headers as key-value pairs
bodystringnoRequest body

Response table:

FieldTypeDescription
statusnumberHTTP status code (e.g. 200)
bodystringResponse body (capped at 1 MB)
headerstableResponse 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:

FieldTypeDescription
uidnumberUnique email ID
fromstringSender address
totableList of recipient addresses
subjectstringEmail subject line
datestringISO 8601 date string
is_readbooleanWhether the email has been read
account_idstringID of the account
folderstringFolder 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:

FieldTypeDescription
tostringRecipient(s)
ccstringCC recipient(s)
subjectstringEmail subject line
account_idstringSending 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:

FieldTypeDescription
bodystringCurrent body text
body_lennumberLength of the body in bytes
subjectstringCurrent subject line
tostringCurrent recipient(s)
ccstringCurrent CC recipient(s)
bccstringCurrent 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 as email_viewed
  • rendered: 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:

KeyTypeDescription
color, bgstringHex ("#rrggbb"), name ("red"), or ANSI 256 number string
bold, italic, underline, strikethrough, faint, blink, reverseboolToggle 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.

  1. Write your plugin as a .lua file following the API documented on this page.

  2. 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 url field points to where your plugin file is hosted. If you include the .lua file directly in the Matcha repo, you can omit url and it will default to the plugins/ directory.

  3. 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 .lua file 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:

PluginDescription
hello.luaMinimal example that logs startup/shutdown
notify_github.luaNotifies when GitHub emails arrive
send_logger.luaLogs outgoing email details
folder_announcer.luaShows a notification on folder switch
unread_counter.luaDisplays unread count in the inbox title
char_counter.luaLive character count in the composer
webhook_notify.luaPosts to a webhook when emails arrive
weather_status.luaShows current weather in the inbox status bar
ai_rewrite.luaAI-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.)
  • string
  • table
  • math
  • package (for require)

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.