diff --git a/docs/docs/Features/Plugins.md b/docs/docs/Features/Plugins.md index 0552ca7a6061899cfd2f4fd4151c4ce51b3a7cc3..25b69a41449c47f9dc94b413b9ad6fa7df13ba7c 100644 --- a/docs/docs/Features/Plugins.md +++ b/docs/docs/Features/Plugins.md @@ -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 diff --git a/main.go b/main.go index f7e3453e8938a2d61592a0944e97ddceeef280ad..57776d6a0ac60f69c365660c684a90185c52eb70 100644 --- a/main.go +++ b/main.go @@ -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 { diff --git a/plugin/README.md b/plugin/README.md index c78449395e7b9cfddebf4d0e467812f1e81f6102..1da2753ce69f87fd8353d961e98d10b1f3ee3593 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -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`) | diff --git a/plugin/api.go b/plugin/api.go index 31d36994767bff5c4f231b73d27824f9a182ef63..5a13c193be1802f613c3c0eb9490e263e2a80588 100644 --- a/plugin/api.go +++ b/plugin/api.go @@ -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 +} diff --git a/plugin/hooks.go b/plugin/hooks.go index dc843c16e015d473a92eb83de2c7abe3a671df66..2f21bd37a1b9df7abbc5f921bbb802ebb42eee81 100644 --- a/plugin/hooks.go +++ b/plugin/hooks.go @@ -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{ diff --git a/plugin/plugin.go b/plugin/plugin.go index 120f171c2bc81f096378adfd06c5182f1f4dcda1..de378ee71b0f88fc249efe60a0adc9022a570bdd 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -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] diff --git a/plugins/auto_bcc.lua b/plugins/auto_bcc.lua new file mode 100644 index 0000000000000000000000000000000000000000..d3bd441ffb7f7a9f6c4c8ad2bb4846aba2f27161 --- /dev/null +++ b/plugins/auto_bcc.lua @@ -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) diff --git a/tui/composer.go b/tui/composer.go index f9edf9163c784932050408b38fe28dde3a60f56a..401f09e4f4d8c1adffb5d7c8b3339e134a032855 100644 --- a/tui/composer.go +++ b/tui/composer.go @@ -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()