Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
50c6bb8
attach: add incoming attachment module skeleton
yousefakbar Jul 12, 2026
feaea53
attach: add incoming attachment object builder
yousefakbar Jul 12, 2026
998b62a
feat(attach): implement incoming rule matching and patches
yousefakbar Jul 12, 2026
b0b848d
config: add incoming attachment config shape
yousefakbar Jul 12, 2026
00d6860
feat(attach): add incoming cache extractor
yousefakbar Jul 17, 2026
a4e8a95
feat(attach): convert incoming default handlers to rules
yousefakbar Jul 17, 2026
d880e16
feat(attach): implement incoming opener execution
yousefakbar Jul 17, 2026
98138ed
feat(attach): implement incoming viewer execution
yousefakbar Jul 18, 2026
352e90d
feat(attach): implement incoming view renderer
yousefakbar Jul 18, 2026
20a777f
feat(attach): wire incoming open and view actions
yousefakbar Jul 18, 2026
82d6ee9
refactor(attach): retire legacy attachment handlers
yousefakbar Jul 18, 2026
0544693
docs: document incoming attachment rules
yousefakbar Jul 18, 2026
a50d711
fix(attach): avoid double-opening default opener
yousefakbar Jul 19, 2026
6836534
Add shorthand attachment rule config
yousefakbar Jul 20, 2026
17a153a
Document attachment shorthand config
yousefakbar Jul 20, 2026
8340e80
test(config): cover attachment shorthand config
yousefakbar Jul 20, 2026
d709c51
docs(changelog): describe attachment rule refactor
yousefakbar Jul 20, 2026
48ab5e0
docs: clarify removed attachment handlers
yousefakbar Jul 20, 2026
d3a16f1
docs(changelog): move attachment refactor notes to unreleased
yousefakbar Jul 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Rule-based received attachment configuration for cache-backed open/view behavior:
- `attachments.open`, `attachments.view`, `attachments.window`, and `attachments.cache_dir` provide the primary user-facing shorthand
- `attach.incoming.open.rules` and `attach.incoming.view.rules` remain available as the advanced rule patch API

### Changed

- Replaced deprecated `vim.loop` usage with `vim.uv` for Neovim libuv APIs.
- Received attachment open/view actions now use deterministic cache extraction and rule registries instead of top-level handler callbacks.
- The default received attachment opener now prefers `vim.ui.open()` when available and falls back to the OS opener command.

### Removed

- Removed the unused internal `notmuch.float` module. Floating attachment viewing is handled directly by the attachment viewer.
- Removed top-level received attachment `open_handler` and `view_handler` configuration callbacks in favor of `attachments.open`/`attachments.view` rules, with `attach.incoming.*` available for advanced patching.
- Removed the legacy `lua/notmuch/handlers.lua` callback implementation.

## [0.4.0] - 2026-07-12

Expand Down
106 changes: 76 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,11 @@ You can configure several global options to tailor the plugin's behavior:
| `sync.sync_mode` | Sync display mode: `"buffer"`, `"background"`, or `"terminal"` (PTY with stdin) | `buffer` |
| `queries` | Saved/pinned queries shown at top of `:Notmuch` dashboard; hidden when empty | `{}` |
| `keymaps` | Configure any (WIP) command's keymap | See `config.lua`[1] |
| `open_handler` | Callback function for opening attachments | Runs OS-aware `open`[2] |
| `view_handler` | Callback function for converting attachments to text to view in floating window | See `default_view_handler()`[2] |
| `attachments.cache_dir` | Shorthand for received attachment open/view cache directory | `stdpath("cache")/notmuch.nvim/attachments` |
| `attachments.open` | List of received attachment open rules tried before defaults | `{}` |
| `attachments.view` | List of received attachment view rules tried before defaults | `{}` |
| `attachments.window` | Shorthand for floating attachment preview window options | `{ type = "float", width = 0.8, height = 0.8, border = "rounded" }` |
| `attach.incoming.*` | Advanced received attachment rule patch API | See below |
| `render_html_body` | Render HTML email bodies inline using `w3m` (requires `w3m` installed) | `false` |
| `thread_view_mode` | Thread view mode: `"threaded"`, `"newest-first"`, or `"oldest-first"` | `"threaded"` |
| `drafts.folder` | Directory used for persistent compose/reply draft `.eml` files and JSON metadata | `stdpath("data")/notmuch.nvim/drafts` |
Expand All @@ -150,7 +153,6 @@ You can configure several global options to tailor the plugin's behavior:
| `suppress_deprecation_warning` | Suppress the warning shown when using deprecated notmuch API (< 0.32) | `false` |

[1]: https://github.com/yousefakbar/notmuch.nvim/blob/main/lua/notmuch/config.lua
[2]: https://github.com/yousefakbar/notmuch.nvim/blob/main/lua/notmuch/handlers.lua

Example configuration in plugin manager (lazy.nvim):

Expand Down Expand Up @@ -205,43 +207,87 @@ The scratch window and commands update the same draft attachment state. Set
`drafts.auto_open_attachment_window = true` if you want the scratch window to
open automatically whenever a draft opens.

### Customizing Attachment Handlers
### Customizing Received Attachment Rules

The plugin provides two handlers for working with received-message attachments:
Received-message attachments use rule registries instead of monolithic handler
callbacks. Open/view actions extract the selected MIME part to
`attach.incoming.cache_dir`, build a structured attachment object, then resolve
open or view rules. Save actions still write directly to the user-selected path.

**Open Handler**: Opens attachments externally with your system's default
application. The default handler automatically detects your OS and uses `open`
(macOS), `xdg-open` (Linux), or `start` (Windows).
For everyday customization, use the `attachments` shorthand. Rules listed in
`attachments.open` and `attachments.view` are tried before the defaults. The old
received attachment `open_handler` and `view_handler` setup callbacks have been
removed; use open/view rules instead.

**View Handler**: Converts attachments to text for display in a floating window
within Neovim. The default handler supports HTML, PDF, images, Office documents,
Markdown, archives, and plain text files. It tries multiple CLI tools for each
format and falls back gracefully if tools aren't available.
```lua
require('notmuch').setup({
attachments = {
open = {
{
name = 'pdf-zathura',
match = { ext = 'pdf' },
command = { 'zathura', '$path' },
detach = true,
fallback = 'Could not open PDF with zathura.',
},
},
view = {
{
name = 'pdf',
match = { content_type = 'application/pdf' },
commands = {
{ 'pdftotext', '-raw', '$path', '-' },
},
filetype = 'text',
fallback = 'Install pdftotext to preview PDFs.',
},
},
window = {
width = 0.9,
height = 0.9,
border = 'rounded',
},
},
})
```

To customize either handler, pass a function to `setup()`:
Advanced users can still use `attach.incoming.open.rules` and
`attach.incoming.view.rules` directly as patch tables:

- `prepend`: try rules before defaults;
- `append`: try rules after defaults;
- `replace`: replace a default rule by name;
- `disable`: disable default rules by name.

Example: replace the default PDF preview rule:

```lua
require('notmuch').setup({
-- Custom open handler
open_handler = function(attachment)
-- attachment.path contains the full file path
vim.fn.system({ 'my-custom-opener', attachment.path })
end,

-- Custom view handler
view_handler = function(attachment)
-- Must return a string to display in the floating window
local path = attachment.path
if path:match('%.pdf$') then
return vim.fn.system({ 'pdftotext', '-layout', path, '-' })
end
return vim.fn.system({ 'cat', path })
end,
attach = {
incoming = {
view = {
rules = {
replace = {
pdf = {
name = 'pdf',
match = { content_type = 'application/pdf' },
commands = {
{ 'pdftotext', '-raw', '$path', '-' },
},
filetype = 'text',
fallback = 'Install pdftotext to preview PDFs.',
},
},
},
},
},
},
})
```

The default handlers are defined in `lua/notmuch/handlers.lua` and handle many
common formats out of the box. Only override them if you need specific behavior.
The default open rule prefers `vim.ui.open()` when available and falls back to
the OS opener command. Default view rules cover HTML, PDF, images, Office
documents, Markdown, archives, text, and binary fallbacks.

### Statusline Integration

Expand Down
139 changes: 99 additions & 40 deletions doc/notmuch.txt
Original file line number Diff line number Diff line change
Expand Up @@ -343,51 +343,112 @@ option will be listed with its default value.
Default value:
`"buffer"`

*open_handler*
Callback function for opening attachments externally with system applications.
The function receives an attachment table with a 'path' field containing the
file path.
*attachments.cache_dir*
Shorthand cache directory used when opening or viewing received-message
attachments. This maps to |attach.incoming.cache_dir|.

Default behavior:
Automatically detects your OS and uses:
- `open` on macOS
- `xdg-open` on Linux
- `start` on Windows
Default value:
`stdpath("cache")/notmuch.nvim/attachments`

*attachments.open*
Shorthand list of received attachment open rules. These rules are prepended to
the default open rules, so they are tried before the built-in `system` rule.
Rule fields are the same as |attach.incoming.open.rules| entries. The old
received attachment `open_handler` setup callback has been removed; configure
open rules here instead.

Example customization: >

require('notmuch').setup({
open_handler = function(attachment)
vim.fn.system({ 'my-opener', attachment.path })
end,
attachments = {
open = {
{
name = 'pdf-zathura',
match = { ext = 'pdf' },
command = { 'zathura', '$path' },
detach = true,
fallback = 'Could not open PDF with zathura.',
},
},
},
})
<

*view_handler*
Callback function for converting attachments to text for display in a
floating window within Neovim. The function receives an attachment table
with a 'path' field and must return a string to display.

Default behavior:
Supports HTML, PDF, images, Office documents, Markdown, archives, and
plain text. Tries multiple CLI tools for each format (e.g., w3m/lynx for
HTML, pdftotext/mutool for PDF) and falls back gracefully if tools aren't
available.
*attachments.view*
Shorthand list of received attachment view rules. These rules are prepended to
the default view rules. Rule fields are the same as
|attach.incoming.view.rules| entries. The old received attachment
`view_handler` setup callback has been removed; configure view rules here
instead.

Example customization: >

require('notmuch').setup({
view_handler = function(attachment)
local path = attachment.path
if path:match('%.pdf$') then
return vim.fn.system({ 'pdftotext', '-layout', path, '-' })
end
return vim.fn.system({ 'cat', path })
end,
attachments = {
view = {
{
name = 'pdf',
match = { content_type = 'application/pdf' },
commands = { { 'pdftotext', '-raw', '$path', '-' } },
filetype = 'text',
fallback = 'Install pdftotext to preview PDFs.',
},
},
},
})
<

*attachments.window*
Shorthand floating window options for received attachment previews. This maps
to |attach.incoming.view.window|.

Default value: >
{ type = 'float', width = 0.8, height = 0.8, border = 'rounded' }
<

*attach.incoming.cache_dir*
Advanced path for the cache directory used when opening or viewing
received-message attachments. Open/view actions extract the selected MIME part
here before rule execution. Save actions still write directly to the
user-selected destination.

*attach.incoming.open.rules*
Advanced patch table for received attachment open rules. Supported patch
fields are `prepend`, `append`, `replace`, and `disable`. The default `system`
rule prefers `vim.ui.open()` when available and falls back to the OS opener
command (`open`, `xdg-open`, or `start`).

*attach.incoming.view.rules*
Advanced patch table for received attachment view rules. Default rules support
HTML, PDF, images, Office documents, Markdown, archives, text, and binary
fallback previews. Rules are patched with `prepend`, `append`, `replace`, and
`disable`.

Example replacing the PDF rule: >

require('notmuch').setup({
attach = {
incoming = {
view = {
rules = {
replace = {
pdf = {
name = 'pdf',
match = { content_type = 'application/pdf' },
commands = { { 'pdftotext', '-raw', '$path', '-' } },
filetype = 'text',
fallback = 'Install pdftotext to preview PDFs.',
},
},
},
},
},
},
})
<

Note: See lua/notmuch/handlers.lua for the complete default implementation.
*attach.incoming.view.window*
Advanced path for floating window options for received attachment previews.

*keymaps*
Keymap configuration table where you can override plugin defaults for all
Expand Down Expand Up @@ -813,16 +874,14 @@ plugin's project codebase.
- Handles received-message MIME part listing/opening/viewing/saving.
- Supports extracting URLs from messages and following GitHub patch links.

- **handlers.lua**:
- Defines default handlers for opening and viewing attachments.
- **default_open_handler()**: OS-aware external opener using open,
xdg-open, or start depending on the platform.
- **default_view_handler()**: Converts various file formats to text for
in-buffer display. Supports HTML (w3m, lynx, elinks), PDF (pdftotext,
mutool), images (chafa, catimg, viu, exiftool), Office documents
(pandoc, docx2txt), Markdown, and archives (zip, tar).
- Users can override these handlers via |open_handler| and |view_handler|
configuration options.
- **attach/incoming/**:
- Rule-based received attachment subsystem.
- Builds structured attachment objects, extracts open/view parts to cache,
resolves configurable open/view rules, runs external openers/converters,
and renders preview buffers.
- Default open behavior prefers `vim.ui.open()` with OS command fallback.
- Default view rules cover HTML, PDF, images, Office documents, Markdown,
archives, text, and binary fallback previews.

- **send.lua**:
- Implements composing, replying, persistent draft opening, and sending.
Expand Down
Loading
Loading