Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
58 changes: 52 additions & 6 deletions doc/notmuch.txt
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ option will be listed with its default value.
- `xdg-open` on Linux
- `start` on Windows

Example customization: >
Example customization: >lua

require('notmuch').setup({
open_handler = function(attachment)
Expand All @@ -301,6 +301,52 @@ option will be listed with its default value.
})
<

*view_handlers*
table of handlers to try for converting attachments to text for display
in a floating terminal within Neovim. Each handler contains the following keys:
- `mime` (string, mandatory): a |lua-pattern| against which the file's mime type
(obtained through `file --mime-type --brief`) is matched.
- `desc` (string, optional) an optional description, which is used as a fallback
- `try` (table): contains the actual commands that will be runned; each element
must be a table containing:
- one of `callback` or `command`.
A callback is a lua function taking a path as input
and returning:
- `success` (boolean): wether the command succeeded
- `output` (string, necessary only if success is true):
- `error` (string): an optional error message to be displayed if `success` is false
and `verbose` is true
A command is a function taking a path as its argument and returning either a
string (in which case `vim.fn.system` is used), either a table of strings (in
which case `vim.system` is used) either a table of tables of strings (the
commands will be runned one after the other through `vim.system`, the output
of the previous becoming the input of the next; if one fails, subsequent
command execution is stopped).
- `tool` (optional): used in notifications
- `verbose` (boolean): wether to notify on failure

Example customization: >lua

require('notmuch').setup({
view_handlers = {
{
mime = "^application/pdf$",
desc = "View with poppler and kitty image protocol",
try = {
{
mime = "^application/x-pie-executable$"
tool = "hexyl",
verbose = true,
command = function(p)
return { "hexyl", "--color=never", p }
end,
},
},
},
},
})
<

*view_handler*
Callback function for converting attachments to text for display in a
floating window within Neovim. The function receives an attachment table
Expand All @@ -312,7 +358,7 @@ option will be listed with its default value.
HTML, pdftotext/mutool for PDF) and falls back gracefully if tools aren't
available.

Example customization: >
Example customization: >lua

require('notmuch').setup({
view_handler = function(attachment)
Expand Down Expand Up @@ -344,7 +390,7 @@ option will be listed with its default value.
Default value:
`false`

Example: >
Example: >lua

require('notmuch').setup({
suppress_deprecation_warning = true,
Expand All @@ -367,7 +413,7 @@ option will be listed with its default value.
Default value:
`false`

Example: >
Example: >lua

require('notmuch').setup({
render_html_body = true,
Expand Down Expand Up @@ -493,7 +539,7 @@ interact with your email directly from the editor.
`{index}/{total} {sender_name} [📎{attachment_count}]`
Example: "2/5 John Doe 📎1"

Example statusline integration with lualine: >
Example statusline integration with lualine: >lua

require('lualine').setup({
sections = {
Expand All @@ -506,7 +552,7 @@ interact with your email directly from the editor.
},
})
<
Example custom keymap using buffer variables: >
Example custom keymap using buffer variables: >lua

vim.keymap.set('n', '<leader>mt', function()
local thread = vim.b.notmuch_thread
Expand Down
1 change: 1 addition & 0 deletions lua/notmuch/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ C.defaults = function()
open_handler = function(attachment)
require('notmuch.handlers').default_open_handler(attachment)
end,
view_handlers = {},
view_handler = function(attachment)
return require('notmuch.handlers').default_view_handler(attachment)
end,
Expand Down
178 changes: 91 additions & 87 deletions lua/notmuch/handlers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -30,98 +30,102 @@ H.default_view_handler = function(attachment)
local path = attachment.path -- Already expanded, careful to escape

-- Helper function to "try" commands in order until one works
local function try_commands(commands)
for _, cmd in ipairs(commands) do
-- Check if the tool exists
if vim.fn.executable(cmd.tool) == 1 then
local output
local success
local command = cmd.command(path)
if type(command) == 'table' then
local obj = vim.system(command):wait()
output = obj.stdout
success = (obj.code == 0)
else
output = vim.fn.system(command)
success = (vim.v.shell_error == 0)
end

if success then
return output
end
end
end
return nil
local try_commands = function(arg)
return require("notmuch.util").try_commands(arg, path)
end

-- Detect file type
local filetype = vim.fn.system({ 'file', '--mime-type', '-b', path }):gsub('%s+$', '')
local ext = path:match('%.([^%.]+)$') or ''

-- HTML files (most common)
if filetype:match('^text/html$') or ext:match('^html?$') then
return try_commands({
{ tool = 'w3m', command = function(p) return { 'w3m', '-T', 'text/html', '-dump', p } end },
{ tool = 'lynx', command = function(p) return { 'lynx', '-dump', '-nolist', p } end },
{ tool = 'elinks', command = function(p) return { 'elinks', '-dump', '-no-references', p } end },
}) or "HTML file (install w3m, lynx, or elinks to view)"
end

-- PDF files
if filetype:match('^application/pdf$') or ext == 'pdf' then
return try_commands({
{ tool = 'pdftotext', command = function(p) return { 'pdftotext', '-layout', p, '-' } end },
{ tool = 'mutool', command = function(p) return { 'mutool', 'draw', '-F', 'txt', p } end },
}) or "PDF file (install pdftotext or mutool to view)"
end

-- Images
if filetype:match('^image/') then
return try_commands({
{ tool = 'chafa', command = function(p) return { 'chafa', '--size', '80x40', p } end },
{ tool = 'catimg', command = function(p) return { 'catimg', '-w', '80', p } end },
{ tool = 'viu', command = function(p) return { 'viu', '-w', '80', p } end },
{ tool = 'exiftool', command = function(p) return { 'exiftool', p } end },
{ tool = 'identify', command = function(p) return { 'identify', '-verbose', p } end },
}) or "Image file (install chafa, viu, or exiftool to view)"
end

-- Office documents (docx, xlsx, pptx)
if filetype:match('officedocument') or ext:match('^(docx?|xlsx?|pptx?)$') then
return try_commands({
{ tool = 'pandoc', command = function(p) return { 'pandoc', '-t', 'plain', p } end },
{ tool = 'docx2txt', command = function(p) return { 'docx2txt', p, '-' } end },
}) or "Office document (install pandoc or docx2txt to view)"
end

-- Markdown
if filetype:match('^text/markdown$') or ext:match('^md$') then
return try_commands({
{ tool = 'pandoc', command = function(p) return { 'pandoc', '-t', 'plain', p } end },
{ tool = 'mdcat', command = function(p) return { 'mdcat', p } end },
}) or vim.fn.system({ 'cat', path })
end

-- Archives (zip, tar, tar.gz, etc.)
if filetype:match('zip') or ext == 'zip' then
return vim.fn.system({ 'unzip', '-l', path })
end
if filetype:match('tar') or ext:match('^tar%.?') then
return vim.fn.system({ 'tar', '-tvf', path })
end

-- Plain text (fallback for text/*)
if filetype:match('^text/') then
return vim.fn.system({ 'cat', path })
local filetype = vim.fn.system({ "file", "--mime-type", "-b", path }):gsub("%s+$", "")

local default = {
{
mime = "^text/html$",
desc = "HTML file (install w3m, lynx, or elinks to view)",
try = {
{ tool = "w3m", command = function(p) return { "w3m", "-T", "text/html", "-dump", p } end },
{ tool = "lynx", command = function(p) return { "lynx", "-dump", "-nolist", p } end },
{ tool = "elinks", command = function(p) return { "elinks", "-dump", "-no-references", p } end },
},
},
{
mime = "^application/pdf$",
desc = "PDF file (install pdftotext or mutool to view)",
try = {
{ tool = "pdftotext", command = function(p) return { "pdftotext", "-layout", p, "-" } end },
{ tool = "mutool", command = function(p) return { "mutool", "draw", "-F", "txt", p } end },
},
},
{
mime = "^image/",
desc = "Image file (install chafa, viu, or exiftool to view)",
try = {
{ tool = "chafa", command = function(p) return { "chafa", "--size", "80x40", p } end },
{ tool = "catimg", command = function(p) return { "catimg", "-w", "80", p } end },
{ tool = "viu", command = function(p) return { "viu", "-w", "80", p } end },
{ tool = "exiftool", command = function(p) return { "exiftool", p } end },
{ tool = "identify", command = function(p) return { "identify", "-verbose", p } end },
},
},
{
mime = "officedocument",
desc = "Office document (install pandoc or docx2txt to view)",
try = {
{ tool = "pandoc", command = function(p) return { "pandoc", "-t", "plain", p } end },
{ tool = "docx2txt", command = function(p) return { "docx2txt", p, "-" } end },
},
},
{
mime = "^text/markdown$",
try = {
{ tool = "pandoc", command = function(p) return { "pandoc", "-t", "plain", p } end },
{ tool = "mdcat", command = function(p) return { "mdcat", p } end },
{ tool = "cat", command = function(p) return { "cat", p } end },
},
},
{
mime = "zip",
try = {
{ tool = "zip", command = function(p) return { "unzip", "-l", p } end },
},
},
{
mime = "tar",
try = {
{ tool = "tar", command = function(p) return { "tar", "-tvf", p } end },
},
},
{
mime = "^text/",
try = {
{ tool = "cat", command = function(p) return { "cat", p } end },
},
},
{
mime = "",
desc = string.format(
"Unable to view binary file\nType: %s\nPath: %s",
filetype,
path
),
try = {
{ tool = "strings", command = function(p) return { "strings", p } end },
},
},
}

-- ensure user_config overrides default
local user_config = require("notmuch.config").options.view_handlers
local handlers = vim.iter({ user_config, default }):flatten():totable()

for _, tbl in ipairs(handlers) do
if filetype:match(tbl.mime) then
return try_commands(tbl.try) or tbl.desc or ""
end
end

return try_commands({
{ tool = 'strings', command = function(p) return { 'strings', p } end },
}) or string.format(
"Unable to view binary file\nType: %s\nPath: %s",
filetype,
path
)
return "No handler found for this filetype"
end

return H

-- vim: tabstop=2:shiftwidth=2:expandtab
42 changes: 42 additions & 0 deletions lua/notmuch/util.lua
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,48 @@ u.find_cursor_msg_id = function()
return nil
end

function u.try_commands(commands, path)
for _, cmd in ipairs(commands) do
local output
local success
local error

if cmd.callback then
success, output, error = cmd.callback(path)
elseif cmd.command then
local command = cmd.command(path)
if type(command) == "table" then
-- use vim.system
-- { ... }: convert to { { ... } }
if #command > 0 and type(command[1]) == "string" then
command = { command }
end
-- { { ... }, { ... } }: pipe the output of a command to the next
for _, cmd in ipairs(command) do
local obj = vim.system(cmd, { stdin = output }):wait()
output = obj.stdout
success = (obj.code == 0)
error = obj.stderr
if not success then break end
end
elseif type(command) == "string" then
-- use vim.fn.system
output = vim.fn.system(command)
success = (vim.v.shell_error == 0)
end
end

if success then
return output
else
if error and cmd.verbose then
vim.notify("failed to execute " .. (cmd.tool or "") .. ": " .. error, vim.log.levels.ERROR)
end
end
end
return nil
end

return u

-- vim: tabstop=2:shiftwidth=2:expandtab:foldmethod=indent