README.md

  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.style(text, opts)` | Wrap `text` in lipgloss styling and return an ANSI-styled string (see below) |
 33
 34## Hook events
 35
 36| Event | Callback argument | Description |
 37|-------|-------------------|-------------|
 38| `startup` | — | Matcha has started |
 39| `shutdown` | — | Matcha is exiting |
 40| `email_received` | Lua table with `uid`, `from`, `to`, `subject`, `date`, `is_read`, `account_id`, `folder` | New email arrived |
 41| `email_viewed` | Same as `email_received` | User opened an email |
 42| `email_send_before` | Table with `to`, `cc`, `subject`, `account_id` | About to send an email |
 43| `email_send_after` | Same as `email_send_before` | Email sent successfully |
 44| `folder_changed` | Folder name (string) | User switched folders |
 45| `composer_updated` | Table with `body`, `body_len`, `subject`, `to`, `cc`, `bcc` | Composer content changed |
 46| `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 |
 47
 48## HTTP requests
 49
 50`matcha.http(options)` makes an HTTP request and returns `(response, err)`. Options is a table with:
 51
 52- `url` (string, required) — only `http` and `https` schemes
 53- `method` (string, optional, default `"GET"`)
 54- `headers` (table, optional)
 55- `body` (string, optional)
 56
 57The response table has `status` (number), `body` (string), and `headers` (table with lowercase keys).
 58
 59Safety limits: 10s timeout, 1 MB response body cap.
 60
 61```lua
 62local res, err = matcha.http({
 63    url     = "https://api.example.com/webhook",
 64    method  = "POST",
 65    headers = { ["Content-Type"] = "application/json" },
 66    body    = '{"text":"hello"}',
 67})
 68if err then
 69    matcha.log("error: " .. err)
 70    return
 71end
 72matcha.log("status: " .. res.status)
 73```
 74
 75## User input prompts
 76
 77`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.
 78
 79Only works inside a `bind_key` callback for the `"composer"` area.
 80
 81```lua
 82matcha.bind_key("ctrl+r", "composer", "rewrite", function(state)
 83    matcha.prompt("Enter instruction:", function(input)
 84        -- input is the user's text
 85        matcha.log("User typed: " .. input)
 86    end)
 87end)
 88```
 89
 90## Body rendering
 91
 92`matcha.on("email_body_render", function(email, rendered, raw) ... end)` runs
 93after the email body has been converted to its final ANSI-styled form and
 94before it is placed in the viewport. The callback receives:
 95
 96- `email`: the same table as `email_viewed`
 97- `rendered`: the current display string (ANSI-styled, post-HTML→terminal)
 98- `raw`: the original message body (HTML or plain text) — useful for parsing
 99  the source instead of the rendered output
100
101Return a new string to replace the rendered body, or `nil` to leave it
102unchanged. Multiple registered callbacks chain in registration order; each
103subsequent callback sees the previous callback's rendered output, but always
104the same raw source.
105
106`matcha.style(text, opts)` wraps `text` in lipgloss styling. `opts` keys (all
107optional):
108
109- `color`, `bg`: string color (hex `"#rrggbb"`, named like `"red"`, or ANSI 256 number as string)
110- `bold`, `italic`, `underline`, `strikethrough`, `faint`, `blink`, `reverse`: bool
111
112```lua
113local matcha = require("matcha")
114
115matcha.on("email_body_render", function(email, rendered, raw)
116    -- highlight TODO in red bold (operates on rendered)
117    rendered = rendered:gsub("TODO", function(m)
118        return matcha.style(m, { color = "#ff0000", bold = true })
119    end)
120    -- italicize anything in *asterisks*
121    rendered = rendered:gsub("%*([^%*]+)%*", function(m)
122        return matcha.style(m, { italic = true })
123    end)
124    -- strip a tracking footer entirely
125    rendered = rendered:gsub("%-%-%-%s*Sent via Tracker.*$", "")
126    return rendered
127end)
128
129-- Parse the raw source and prepend a summary; works regardless of HTML markup.
130matcha.on("email_body_render", function(email, rendered, raw)
131    local urls = {}
132    for url in raw:gmatch("https?://[%w%-_%.~%?=&/%%#:]+") do
133        urls[#urls + 1] = url
134    end
135    local header = matcha.style("URLs: " .. #urls, { bold = true }) .. "\n\n"
136    return header .. rendered
137end)
138```
139
140Caveats:
141
142- The `rendered` string already contains ANSI escape sequences from the
143  HTML→terminal conversion. Patterns that straddle existing escapes will not
144  match — match plain text spans for predictable behavior, or operate on `raw`.
145- Returning a fully replaced string fully takes over the displayed body. To
146  build styled output from scratch, compose with `matcha.style` and join with
147  newlines.
148
149## Available plugins
150
151The following example plugins ship in `~/.config/matcha/plugins/`:
152
153- `email_age.lua`
154- `recipient_counter.lua`
155
156## Files
157
158| File | Description |
159|------|-------------|
160| `plugin.go` | Plugin manager — Lua VM setup, plugin discovery and loading, notification/status state |
161| `hooks.go` | Hook definitions, callback registration, and hook invocation helpers |
162| `api.go` | `matcha` Lua module registration (`on`, `log`, `notify`, `set_status`, `set_compose_field`, `bind_key`, `http`, `prompt`, `style`) |
163| `http.go` | `matcha.http()` implementation — HTTP client with timeout and body size limits |
164| `prompt.go` | `matcha.prompt()` implementation — user input overlay for the composer |