plugin

package
v0.38.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 14, 2026 License: MIT Imports: 17 Imported by: 0

README

plugin

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).

How it works

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)

Lua API (matcha module)

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)

Hook events

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

HTTP requests

matcha.http(options) makes an HTTP request and returns (response, err). Options is a table with:

  • url (string, required) — only http and https schemes
  • method (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)

Persistent storage

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.

User input prompts

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)

Body rendering

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 as email_viewed
  • rendered: 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 rendered 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, or operate on raw.
  • Returning a fully replaced string fully takes over the displayed body. To build styled output from scratch, compose with matcha.style and join with newlines.

User-configurable settings

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.

Available plugins

The following example plugins ship in ~/.config/matcha/plugins/:

  • email_age.lua
  • recipient_counter.lua

Files

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

Documentation

Index

Constants

View Source
const (
	HookStartup         = "startup"
	HookShutdown        = "shutdown"
	HookEmailReceived   = "email_received"
	HookEmailSendBefore = "email_send_before"
	HookEmailSendAfter  = "email_send_after"
	HookEmailViewed     = "email_viewed"
	HookFolderChanged   = "folder_changed"
	HookComposerUpdated = "composer_updated"
	HookEmailBodyRender = "email_body_render"
)

Hook event names.

View Source
const (
	StatusInbox     = "inbox"
	StatusComposer  = "composer"
	StatusEmailView = "email_view"
)

Status area names.

Variables

This section is empty.

Functions

This section is empty.

Types

type KeyBinding added in v0.30.0

type KeyBinding struct {
	Key         string
	Area        string // "inbox", "email_view", or "composer"
	Description string
	Fn          *lua.LFunction
	Plugin      string
}

KeyBinding represents a plugin-registered keyboard shortcut.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager manages the Lua VM and loaded plugins.

Manager is not safe for concurrent use. The Lua VM itself is single- threaded, and all hook callbacks, key-binding invocations, and API calls must be dispatched from the same goroutine that owns the Manager (the orchestrator). Mutable Manager state (hooks, stores, bindings, currentPlugin, pending* fields) is therefore unprotected by design; callers that need to drive plugin events from multiple goroutines must serialize access externally.

func NewManager

func NewManager() *Manager

NewManager creates a new plugin manager with a Lua VM.

func (*Manager) AllSettingValues added in v0.38.0

func (m *Manager) AllSettingValues() map[string]map[string]interface{}

AllSettingValues returns a deep copy of all plugin setting values.

func (*Manager) Bindings added in v0.30.0

func (m *Manager) Bindings(area string) []KeyBinding

Bindings returns all plugin-registered key bindings for the given view area.

func (*Manager) CallBodyRenderHook added in v0.37.0

func (m *Manager) CallBodyRenderHook(email *lua.LTable, rendered, raw string) string

CallBodyRenderHook runs all email_body_render callbacks, threading the body string through each. Callbacks receive (email_table, rendered, raw):

  • rendered: the current display string (ANSI-styled, post-HTML→terminal)
  • raw: the original message body (HTML or plain text, same string fed to the renderer) — useful for parsing the source instead of the rendered output

A callback may return a string to replace the rendered body, or nil to leave it unchanged. Non-string returns are ignored. Multiple callbacks chain in registration order; each subsequent callback sees the previous callback's rendered output, but always the same raw source.

func (*Manager) CallComposerHook

func (m *Manager) CallComposerHook(event string, body, subject, to, cc, bcc string)

CallComposerHook calls a hook with composer state info.

func (*Manager) CallFolderHook

func (m *Manager) CallFolderHook(event string, folderName string)

CallFolderHook calls a hook with a folder name.

func (*Manager) CallHook

func (m *Manager) CallHook(event string, args ...lua.LValue)

CallHook invokes all callbacks registered for the given event.

func (*Manager) CallKeyBinding added in v0.30.0

func (m *Manager) CallKeyBinding(binding KeyBinding, args ...lua.LValue)

CallKeyBinding invokes a plugin key binding callback with the given arguments.

func (*Manager) CallSendHook

func (m *Manager) CallSendHook(event string, to, cc, subject, accountID string)

CallSendHook calls a hook with email send metadata.

func (*Manager) Close

func (m *Manager) Close()

Close shuts down the Lua VM.

func (*Manager) EmailToTable

func (m *Manager) EmailToTable(uid uint32, from string, to []string, subject string, date time.Time, isRead bool, accountID string, folder string) *lua.LTable

EmailToTable converts email fields into a Lua table.

func (*Manager) GetSettingValue added in v0.38.0

func (m *Manager) GetSettingValue(plugin, key string) (interface{}, bool)

GetSettingValue returns the current value (or default) for a plugin setting.

func (*Manager) LoadPlugins

func (m *Manager) LoadPlugins()

LoadPlugins discovers and loads plugins from ~/.config/matcha/plugins/.

func (*Manager) LoadSettingValues added in v0.38.0

func (m *Manager) LoadSettingValues(values map[string]map[string]interface{})

LoadSettingValues replaces in-memory values with the given snapshot. Values for unknown plugins/keys are kept as-is so freshly-disabled plugins don't lose their saved settings on next launch.

func (*Manager) LuaState added in v0.30.0

func (m *Manager) LuaState() *lua.LState

LuaState returns the Lua VM state for building tables.

func (*Manager) Plugins

func (m *Manager) Plugins() []string

Plugins returns the names of all loaded plugins.

func (*Manager) ResolvePrompt added in v0.31.0

func (m *Manager) ResolvePrompt(prompt *PendingPrompt, input string)

ResolvePrompt calls the stored prompt callback with the user's input.

func (*Manager) Schema added in v0.38.0

func (m *Manager) Schema(plugin string) []SettingDef

Schema returns the schema for a single plugin.

func (*Manager) Schemas added in v0.38.0

func (m *Manager) Schemas() []PluginSettings

Schemas returns all plugin setting schemas, sorted by plugin name.

func (*Manager) SetSettingValue added in v0.38.0

func (m *Manager) SetSettingValue(plugin, key string, val interface{}) bool

SetSettingValue updates a plugin setting in-memory. Coerces value to the declared type. Returns false if the plugin/key is unknown.

func (*Manager) StatusText

func (m *Manager) StatusText(area string) string

StatusText returns the plugin status string for the given view area.

func (*Manager) TakePendingFields added in v0.30.0

func (m *Manager) TakePendingFields() map[string]string

TakePendingFields returns and clears any pending compose field updates.

func (*Manager) TakePendingNotification

func (m *Manager) TakePendingNotification() (PendingNotification, bool)

TakePendingNotification returns and clears any pending notification.

func (*Manager) TakePendingPrompt added in v0.31.0

func (m *Manager) TakePendingPrompt() (*PendingPrompt, bool)

TakePendingPrompt returns and clears any pending prompt request.

type PendingNotification

type PendingNotification struct {
	Message  string
	Duration float64 // seconds, 0 means default
}

PendingNotification holds a notification message and its display duration.

type PendingPrompt added in v0.31.0

type PendingPrompt struct {
	Placeholder string
	// contains filtered or unexported fields
}

PendingPrompt holds the state for a plugin-requested user input prompt.

type PluginSettings added in v0.38.0

type PluginSettings struct {
	Plugin string
	Defs   []SettingDef
}

PluginSettings holds the schema for one plugin's settings, ordered by declaration order so the TUI lists them predictably.

type SettingDef added in v0.38.0

type SettingDef struct {
	Key         string
	Type        SettingType
	Default     interface{}
	Label       string
	Description string
}

SettingDef describes a single configurable plugin setting.

type SettingType added in v0.38.0

type SettingType string

SettingType identifies the kind of value a plugin setting holds.

const (
	SettingBool   SettingType = "boolean"
	SettingNumber SettingType = "number"
	SettingString SettingType = "string"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL