Detailed changes
@@ -79,6 +79,29 @@ 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 |
+
+```lua
+-- Auto-add a BCC on every new email
+matcha.on("composer_updated", function(state)
+ if state.bcc == "" then
+ matcha.set_compose_field("bcc", "archive@example.com")
+ 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).
@@ -201,6 +224,8 @@ end)
| `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) |
## Example Plugins
@@ -149,8 +149,9 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Fire composer_updated hook on key presses when the composer is active
if _, isKey := msg.(tea.KeyPressMsg); isKey {
if composer, ok := m.current.(*tui.Composer); ok && m.plugins != nil {
- m.plugins.CallComposerHook(plugin.HookComposerUpdated, composer.GetBody(), composer.GetSubject(), composer.GetTo())
+ m.plugins.CallComposerHook(plugin.HookComposerUpdated, composer.GetBody(), composer.GetSubject(), composer.GetTo(), composer.GetCc(), composer.GetBcc())
m.syncPluginStatus()
+ m.applyPluginFields(composer)
}
}
@@ -1318,6 +1319,27 @@ func (m *mainModel) syncPluginStatus() {
}
}
+func (m *mainModel) applyPluginFields(composer *tui.Composer) {
+ fields := m.plugins.TakePendingFields()
+ if fields == nil {
+ return
+ }
+ for field, value := range fields {
+ switch field {
+ case "to":
+ composer.SetTo(value)
+ case "cc":
+ composer.SetCc(value)
+ case "bcc":
+ composer.SetBcc(value)
+ case "subject":
+ composer.SetSubject(value)
+ case "body":
+ composer.SetBody(value)
+ }
+ }
+}
+
func flattenAndSort(emailsByAccount map[string][]fetcher.Email) []fetcher.Email {
var allEmails []fetcher.Email
for _, emails := range emailsByAccount {
@@ -25,6 +25,7 @@ end)
| `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"`) |
## Hook events
@@ -37,7 +38,7 @@ end)
| `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` | Composer content changed |
+| `composer_updated` | Table with `body`, `body_len`, `subject`, `to`, `cc`, `bcc` | Composer content changed |
## Available plugins
@@ -52,4 +53,4 @@ The following example plugins ship in `~/.config/matcha/plugins/`:
|------|-------------|
| `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`) |
+| `api.go` | `matcha` Lua module registration (`on`, `log`, `notify`, `set_status`, `set_compose_field`) |
@@ -11,10 +11,11 @@ func (m *Manager) registerAPI() {
L := m.state
mod := L.RegisterModule("matcha", map[string]lua.LGFunction{
- "on": m.luaOn,
- "log": m.luaLog,
- "notify": m.luaNotify,
- "set_status": m.luaSetStatus,
+ "on": m.luaOn,
+ "log": m.luaLog,
+ "notify": m.luaNotify,
+ "set_status": m.luaSetStatus,
+ "set_compose_field": m.luaSetComposeField,
})
L.SetField(mod, "_VERSION", lua.LString("0.1.0"))
@@ -51,3 +52,18 @@ func (m *Manager) luaNotify(L *lua.LState) int {
m.pendingDuration = float64(L.OptNumber(2, 2))
return 0
}
+
+// matcha.set_compose_field(field, value) — set a compose field value.
+// Valid fields: "to", "cc", "bcc", "subject", "body".
+func (m *Manager) luaSetComposeField(L *lua.LState) int {
+ field := L.CheckString(1)
+ value := L.CheckString(2)
+
+ switch field {
+ case "to", "cc", "bcc", "subject", "body":
+ m.pendingFields[field] = value
+ default:
+ L.ArgError(1, "invalid field: must be \"to\", \"cc\", \"bcc\", \"subject\", or \"body\"")
+ }
+ return 0
+}
@@ -93,7 +93,7 @@ func (m *Manager) CallFolderHook(event string, folderName string) {
}
// CallComposerHook calls a hook with composer state info.
-func (m *Manager) CallComposerHook(event string, body, subject, to string) {
+func (m *Manager) CallComposerHook(event string, body, subject, to, cc, bcc string) {
callbacks, ok := m.hooks[event]
if !ok {
return
@@ -105,6 +105,8 @@ func (m *Manager) CallComposerHook(event string, body, subject, to string) {
t.RawSetString("body", lua.LString(body))
t.RawSetString("subject", lua.LString(subject))
t.RawSetString("to", lua.LString(to))
+ t.RawSetString("cc", lua.LString(cc))
+ t.RawSetString("bcc", lua.LString(bcc))
for _, fn := range callbacks {
if err := L.CallByParam(lua.P{
@@ -19,13 +19,16 @@ type Manager struct {
// pendingNotification is set by matcha.notify() and consumed by the orchestrator.
pendingNotification string
pendingDuration float64 // seconds, 0 means default (2s)
+ // pendingFields holds compose field updates set by matcha.set_compose_field().
+ pendingFields map[string]string
}
// NewManager creates a new plugin manager with a Lua VM.
func NewManager() *Manager {
m := &Manager{
- hooks: make(map[string][]*lua.LFunction),
- statuses: make(map[string]string),
+ hooks: make(map[string][]*lua.LFunction),
+ statuses: make(map[string]string),
+ pendingFields: make(map[string]string),
}
L := lua.NewState(lua.Options{
@@ -118,6 +121,16 @@ func (m *Manager) TakePendingNotification() (PendingNotification, bool) {
return n, true
}
+// TakePendingFields returns and clears any pending compose field updates.
+func (m *Manager) TakePendingFields() map[string]string {
+ if len(m.pendingFields) == 0 {
+ return nil
+ }
+ fields := m.pendingFields
+ m.pendingFields = make(map[string]string)
+ return fields
+}
+
// StatusText returns the plugin status string for the given view area.
func (m *Manager) StatusText(area string) string {
return m.statuses[area]
@@ -0,0 +1,13 @@
+-- auto_bcc.lua
+-- Automatically adds a BCC address to every email you compose.
+-- Change the address below to your own archive/backup address.
+
+local matcha = require("matcha")
+
+local bcc_address = "archive@example.com"
+
+matcha.on("composer_updated", function(state)
+ if state.bcc == "" then
+ matcha.set_compose_field("bcc", bcc_address)
+ end
+end)
@@ -700,11 +700,41 @@ func (m *Composer) GetTo() string {
return m.toInput.Value()
}
+// SetTo updates the To field with new content.
+func (m *Composer) SetTo(to string) {
+ m.toInput.SetValue(to)
+}
+
+// GetCc returns the current Cc field value.
+func (m *Composer) GetCc() string {
+ return m.ccInput.Value()
+}
+
+// SetCc updates the Cc field with new content.
+func (m *Composer) SetCc(cc string) {
+ m.ccInput.SetValue(cc)
+}
+
+// GetBcc returns the current Bcc field value.
+func (m *Composer) GetBcc() string {
+ return m.bccInput.Value()
+}
+
+// SetBcc updates the Bcc field with new content.
+func (m *Composer) SetBcc(bcc string) {
+ m.bccInput.SetValue(bcc)
+}
+
// GetSubject returns the current Subject field value.
func (m *Composer) GetSubject() string {
return m.subjectInput.Value()
}
+// SetSubject updates the Subject field with new content.
+func (m *Composer) SetSubject(subject string) {
+ m.subjectInput.SetValue(subject)
+}
+
// GetBody returns the current Body field value.
func (m *Composer) GetBody() string {
return m.bodyInput.Value()