summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorToby Vincent <tobyv@tobyvin.dev>2023-10-19 20:06:18 -0500
committerToby Vincent <tobyv@tobyvin.dev>2023-10-19 20:06:18 -0500
commitddb7133e0df362edf6d0de7b79d56a2473637a97 (patch)
tree56bf3871095664fd64baf2c27b5c3251fbb6ced3
parent43df6d6b23bee7b109fb31665e3c951367626ec3 (diff)
feat: impl headers and handle multipart messages
-rw-r--r--lua/inbox/config.lua20
-rw-r--r--lua/inbox/indexers/notmuch.lua77
-rw-r--r--lua/inbox/init.lua100
-rw-r--r--lua/inbox/types.lua18
-rw-r--r--lua/inbox/utils.lua27
-rw-r--r--lua/inbox/view.lua166
6 files changed, 287 insertions, 121 deletions
diff --git a/lua/inbox/config.lua b/lua/inbox/config.lua
index 9ac9900..4692f5c 100644
--- a/lua/inbox/config.lua
+++ b/lua/inbox/config.lua
@@ -1,20 +1,7 @@
----@class inbox.Config
----@field indexer_config inbox.Indexer.Config|nil
----@field buf_options table|nil
----@field win_options table|nil
----@field columns integer[]
----@field flags table<string, table>
-
---@type inbox.Config
local default_config = {
indexer_config = "notmuch",
- buf_options = {
- buftype = "acwrite",
- syntax = "inbox",
- filetype = "inbox",
- buflisted = false,
- bufhidden = "hide",
- },
+ buf_options = {},
win_options = {
wrap = false,
signcolumn = "yes:1",
@@ -28,6 +15,11 @@ local default_config = {
15,
30,
},
+ headers = {
+ "From",
+ "Date",
+ "Subject",
+ },
flags = {
unread = {
text = "O",
diff --git a/lua/inbox/indexers/notmuch.lua b/lua/inbox/indexers/notmuch.lua
index 5b3e48c..5e998eb 100644
--- a/lua/inbox/indexers/notmuch.lua
+++ b/lua/inbox/indexers/notmuch.lua
@@ -27,23 +27,21 @@ local default_config = {
---@field timestamp integer
---@field total integer
----@class inbox.Notmuch.Entry
----@field id string
+---@class inbox.Notmuch.Entry: inbox.Entry
---@field filename string[]
----@field timestamp integer
---@field date_relative string?
----@field tags string[]
---@field duplicate integer
---@field body inbox.EntryPart[]
---@field crypto table
----@field headers inbox.Headers
----@field parts inbox.EntryPart
---@type inbox.Indexer.Notmuch
local M = {
_cache = {},
}
+---@private
+---@param id string
+---@return inbox.Notmuch.Entry | nil
function M.cache(id)
if M._cache[id] == nil then
M._cache[id] = M.show_id(id)
@@ -59,16 +57,16 @@ end
function M.index(maildir, callback, opts)
opts = opts or {}
- local json = ""
+ local sbuf = {}
local job = Job:new({
command = "notmuch",
args = { "search", "--format=json" },
on_stdout = vim.schedule_wrap(function(_, stdout)
- json = json .. stdout
+ table.insert(sbuf, stdout)
end),
on_exit = vim.schedule_wrap(function()
---@type inbox.Notmuch.SearchResult[]
- local results = utils.json_decode(json)
+ local results = utils.json_decode(table.concat(sbuf, "\n"))
---@type inbox.Summary[]
local entries = {}
@@ -165,31 +163,15 @@ function M.show_id(id)
entry.tags = M.parse_tags(entry.tags)
entry.parts = M.flatten_parts(entry.body[1])
+ table.sort(entry.parts, function(a, b)
+ return a.id < b.id
+ end)
return entry
end
----@param id string
----@return inbox.Entry?
function M.get_entry(id)
- local entry = M.cache(id)
- if entry == nil then
- return nil
- end
-
- local parts = vim.tbl_map(function(part)
- return part["content-type"]
- end, entry.parts)
-
- ---@type inbox.Entry
- return {
- id = entry.id,
- timestamp = entry.timestamp,
- filename = entry.filename[1],
- tags = entry.tags,
- parts = parts,
- headers = entry.headers,
- }
+ return M.cache(id)
end
function M.flatten_parts(part, parts)
@@ -215,40 +197,41 @@ function M.get_part(id, content_type, callback)
if entry == nil then
vim.notify(("Failed to get entry with id: %s"):format(id), vim.log.levels.ERROR)
- return nil
+ return
end
- ---@type inbox.EntryPart?
- local part
- if content_type == nil then
- for _, p in pairs(entry.parts) do
- if part == nil or p.id < part.id then
- part = p
- end
- end
- else
- for _, p in pairs(entry.parts) do
- if p["content-type"] ~= content_type then
- part = p
+ local parts = entry.parts
+
+ local index = 1
+ if content_type ~= nil then
+ for i, p in pairs(entry.parts) do
+ if p["content-type"] == content_type then
+ index = i
break
end
end
+ else
+ index = 1
end
+ local part = parts[index]
+
+ ---@type inbox.EntryPart?
+
if part == nil then
vim.notify(("Failed to find message part for entry id: %s"):format(id), vim.log.levels.ERROR)
- return nil
+ return
end
- local stdout = {}
+ local sbuf = {}
local job = Job:new({
command = "notmuch",
args = { "show", ("--part=%s"):format(part.id), ("id:%s"):format(entry.id) },
- on_stdout = vim.schedule_wrap(function(_, data)
- table.insert(stdout, data)
+ on_stdout = vim.schedule_wrap(function(_, stdout)
+ table.insert(sbuf, stdout)
end),
on_exit = vim.schedule_wrap(function()
- callback(part["content-type"], stdout)
+ callback(entry, index, sbuf)
end),
})
diff --git a/lua/inbox/init.lua b/lua/inbox/init.lua
index bb59fc4..f3869c0 100644
--- a/lua/inbox/init.lua
+++ b/lua/inbox/init.lua
@@ -22,7 +22,7 @@ function M.select()
end
end
else
- local entry = M.get_cursor_entry()
+ local entry = M.get_id_on_cursor()
if entry then
table.insert(entries, entry)
end
@@ -31,21 +31,79 @@ end
function M.open(maildir)
local view = require("inbox.view")
- if M.bufnr == nil then
- M.bufnr = view.initialize(maildir)
+
+ if M.buffers[maildir] == nil or vim.api.nvim_buf_is_valid(M.buffers[maildir]) then
+ M.buffers[maildir] = view.initialize_inbox(maildir)
end
- if not vim.api.nvim_buf_is_valid(M.bufnr) then
- --- TODO: Handle error
- return
+ vim.api.nvim_set_current_buf(M.buffers[maildir])
+end
+
+function M.open_entry(id, content_type)
+ if id == nil then
+ id = M.get_cursor_id()
+ if id == nil then
+ return nil
+ end
+ end
+
+ if M.buffers[id] == nil or vim.api.nvim_buf_is_valid(M.buffers[id]) then
+ local utils = require("inbox.utils")
+ local view = require("inbox.view")
+
+ local maildir = utils.parse_scheme(0)
+ if maildir == nil then
+ return
+ end
+
+ local bufnr = view.initialize_entry(maildir, id, content_type)
+ M.buffers[id] = bufnr
+ end
+
+ vim.api.nvim_set_current_buf(M.buffers[id])
+end
+
+function M.select_part()
+ local entry = M.get_cursor_entry()
+
+ if entry == nil then
+ return nil
end
- vim.api.nvim_set_current_buf(M.bufnr)
+ vim.ui.select(entry.parts, {
+ prompt = "Part",
+ format_item = function(item)
+ return item["content-type"]
+ end,
+ }, function(part)
+ M.open_entry(entry.id, part)
+ end)
end
-function M.open_entry(part)
+function M.toggle_headers(bufnr)
local view = require("inbox.view")
+ if bufnr == nil or bufnr == 0 then
+ bufnr = vim.api.nvim_get_current_buf()
+ end
+
+ local id = vim.b[bufnr].inbox_id
+
+ if id == nil then
+ return
+ end
+
+ if vim.b[bufnr].show_all_headers then
+ vim.b[bufnr].show_all_headers = false
+ else
+ vim.b[bufnr].show_all_headers = true
+ end
+
+ view.render_headers(bufnr, id)
+end
+
+---@return string? id
+function M.get_cursor_id()
local lnum = vim.api.nvim_win_get_cursor(0)[1]
local id = vim.b[0].inbox_ids[lnum]
@@ -54,27 +112,21 @@ function M.open_entry(part)
return nil
end
- local bufnr = view.render_entry(id, part)
+ return id
+end
- if bufnr ~= nil then
- vim.api.nvim_set_current_buf(bufnr)
+---@return inbox.Entry? entry
+function M.get_cursor_entry()
+ local id = M.get_cursor_id()
+ if id == nil then
+ vim.notify(("Failed to get entry with id: %s"):format(id), vim.log.levels.ERROR)
+ return nil
end
-end
-function M.open_part()
local indexers = require("inbox.indexers")
local indexer = indexers.get_indexer()
- local lnum = vim.api.nvim_win_get_cursor(0)[1]
- local id = vim.b[0].inbox_ids[lnum]
- -- FIX: fails to open selected part.
- local parts = indexer.get_entry(id).parts
- if vim.tbl_isempty(parts) then
- vim.notify(("Failed to get parts for entry id: %s"):format(id), vim.log.levels.ERROR)
- return nil
- end
-
- vim.ui.select(parts, {}, M.open_entry)
+ return indexer.get_entry(id)
end
---@param opts inbox.Config
@@ -90,6 +142,8 @@ function M.setup(opts)
local name = utils.sign_name(sign)
vim.fn.sign_define(name, value)
end
+
+ M.augroup = vim.api.nvim_create_augroup("Inbox", { clear = true })
end
return M
diff --git a/lua/inbox/types.lua b/lua/inbox/types.lua
index b87991e..4a3d320 100644
--- a/lua/inbox/types.lua
+++ b/lua/inbox/types.lua
@@ -1,6 +1,12 @@
----@alias inbox.Headers table<inbox.Header, string>
+---@class inbox.Config
+---@field indexer_config inbox.Indexer.Config|nil
+---@field buf_options table|nil
+---@field win_options table|nil
+---@field columns integer[]
+---@field flags table<string, table>
+---@field headers inbox.HeaderKey[]
----@alias inbox.Header
+---@alias inbox.HeaderKey
---| "Subject"
---| "Date"
---| "To"
@@ -18,7 +24,7 @@
---@field index? fun(maildir: string, cb: fun(ids: string[], entries: inbox.Summary[], signs: (string | integer)[][]), opts?: table)
---@field get_entry? fun(id: string): inbox.Entry|nil
---@field get_parts? fun(id: string): inbox.ContentType[]
----@field get_part? fun(id: string, content_type: inbox.ContentType, callback: fun(content_type: inbox.ContentType, lines: string[]))
+---@field get_part? fun(id: string, content_type: inbox.ContentType, callback: fun(entry: inbox.Entry, part_index: integer, part_lines: string[]))
---@alias inbox.Indexer.Config
---| "notmuch"
@@ -34,10 +40,10 @@
---@class inbox.Entry
---@field id string
---@field timestamp integer
----@field filename string
+---@field filename string[]
---@field tags string[]
----@field parts string[]
----@field headers inbox.Headers
+---@field parts inbox.EntryPart[]
+---@field headers table<inbox.HeaderKey, string>
---@class inbox.EntryPart
---@field id integer
diff --git a/lua/inbox/utils.lua b/lua/inbox/utils.lua
index 3b84b83..c5954a8 100644
--- a/lua/inbox/utils.lua
+++ b/lua/inbox/utils.lua
@@ -111,6 +111,8 @@ function M.set_signs(bufnr, signs)
end
end
+---@param json string
+---@return table data
function M.json_decode(json)
local results = {}
if json and json ~= "" then
@@ -119,4 +121,29 @@ function M.json_decode(json)
return results
end
+---@param bufnr integer
+---@return string? maildir maildir name
+---@return string? id entry id
+function M.parse_scheme(bufnr)
+ local bufname = vim.api.nvim_buf_get_name(bufnr or 0)
+
+ local _, init, maildir = bufname:find("maildir://([^:]+)")
+ local _, _, id = bufname:find(":(.*)", init)
+
+ return maildir, id
+end
+
+function M.stateful_iter(table, wrap)
+ local k
+ local s_k, s_v = next(table)
+ return function()
+ local v
+ k, v = next(table, k)
+ if k == nil and wrap then
+ k, v = s_k, s_v
+ end
+ return k, v
+ end
+end
+
return M
diff --git a/lua/inbox/view.lua b/lua/inbox/view.lua
index e770417..a79bf17 100644
--- a/lua/inbox/view.lua
+++ b/lua/inbox/view.lua
@@ -4,32 +4,64 @@ local utils = require("inbox.utils")
local M = {}
----@param bufname string
+---@param path string
---@param buf_options table<string, any> | nil
---@return integer bufnr
-function M.create_buffer(bufname, buf_options)
+function M.create_buffer(path, buf_options)
local bufnr = vim.api.nvim_create_buf(true, false)
for k, v in pairs(buf_options or {}) do
vim.api.nvim_buf_set_option(bufnr, k, v)
end
+ local bufname = string.format("maildir://%s", path)
+
vim.api.nvim_buf_set_name(bufnr, bufname)
return bufnr
end
+function M.render_buffer(bufnr)
+ local maildir, id = utils.parse_scheme(bufnr)
+
+ if maildir == nil then
+ vim.notify(("Buffer is not a valid maildir buffer: %s"):format(bufnr), vim.log.levels.ERROR)
+ elseif id ~= nil and id ~= "" then
+ M.render_entry(bufnr, id)
+ else
+ M.render_inbox(bufnr, maildir)
+ end
+end
+
+---@param bufnr integer
+---@param lines string[]
+---@param start integer?
+---@param end_ integer?
+function M.set_buffer_content(bufnr, lines, start, end_)
+ if start == nil then
+ start = 0
+ end
+
+ if end_ == nil then
+ end_ = -1
+ end
+
+ vim.bo[bufnr].modifiable = true
+ vim.api.nvim_buf_set_lines(bufnr, start, end_, true, lines)
+ vim.bo[bufnr].modifiable = false
+ vim.bo[bufnr].modified = false
+end
+
---@param maildir string
---@return integer bufnr
-function M.initialize(maildir)
+function M.initialize_inbox(maildir)
local buf_options = vim.tbl_extend("keep", config.buf_options, {
buftype = "acwrite",
syntax = "inbox",
filetype = "inbox",
})
- local bufname = string.format("maildir://%s", maildir)
- local bufnr = M.create_buffer(bufname, buf_options)
+ local bufnr = M.create_buffer(maildir, buf_options)
-- vim.api.nvim_clear_autocmds({ buffer = bufnr, group = "Inbox" })
@@ -40,20 +72,6 @@ function M.initialize(maildir)
return bufnr
end
-function M.render_buffer_content(bufnr, lines, highlights, signs)
- vim.bo[bufnr].modifiable = true
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
- vim.bo[bufnr].modifiable = false
- vim.bo[bufnr].modified = false
-
- if highlights then
- utils.set_highlights(bufnr, highlights)
- end
- if signs then
- utils.set_signs(bufnr, signs)
- end
-end
-
---@param bufnr integer
function M.render_inbox(bufnr, maildir)
local indexer = indexers.get_indexer()
@@ -66,7 +84,9 @@ function M.render_inbox(bufnr, maildir)
vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid })
end
- M.render_buffer_content(bufnr, lines, highlights, signs)
+ M.set_buffer_content(bufnr, lines)
+ utils.set_highlights(bufnr, highlights)
+ utils.set_signs(bufnr, signs)
vim.api.nvim_set_option_value(
"winbar",
@@ -79,19 +99,103 @@ function M.render_inbox(bufnr, maildir)
end)
end
-function M.render_entry(id, content_type)
+---@param maildir string
+---@param id string
+---@param content_type string
+---@return integer? bufnr
+function M.initialize_entry(maildir, id, content_type)
+ local bufname = ("%s:%s"):format(maildir, id)
+ local bufnr = M.create_buffer(bufname, {
+ syntax = "mail",
+ filetype = "mail",
+ })
+
+ vim.api.nvim_clear_autocmds({ buffer = bufnr, group = require("inbox").augroup })
+
+ vim.api.nvim_create_autocmd("BufDelete", {
+ group = "Inbox",
+ buffer = bufnr,
+ callback = function()
+ require("inbox").buffers[id] = nil
+ end,
+ })
+
+ vim.keymap.set("n", "q", function()
+ vim.api.nvim_buf_delete(bufnr, {})
+ end, { buffer = bufnr })
+
+ vim.keymap.set("n", "<C-h>", require("inbox").toggle_headers, { buffer = bufnr })
+ vim.keymap.set("n", "<C-n>", function()
+ local k, v = next(vim.b[bufnr].inbox_parts, vim.b[bufnr].inbox_part_index)
+ if k == nil then
+ k, v = next(vim.b[bufnr].inbox_parts)
+ end
+ M.render_entry(bufnr, id, v["content-type"])
+ end, { buffer = bufnr })
+
+ vim.b[bufnr].inbox_id = id
+ vim.b[bufnr].header_filter = config.headers
+
+ M.render_headers(bufnr, id)
+ M.render_entry(bufnr, id, content_type)
+
+ return bufnr
+end
+
+function M.render_headers(bufnr, id)
local indexer = indexers.get_indexer()
- local bufname = string.format("%s [%s]", id, content_type)
- local bufnr = M.create_buffer(bufname, {})
-
- indexer.get_part(id, content_type, function(ct, lines)
- content_type = content_type or ct
- bufname = string.format("%s [%s]", id, content_type)
- vim.api.nvim_buf_set_name(bufnr, bufname)
- M.render_buffer_content(bufnr, lines)
- end)
+ local entry = indexer.get_entry(id)
+
+ if entry == nil then
+ return
+ end
+
+ local lines = {}
+
+ for _, name in pairs(config.headers) do
+ if entry.headers[name] ~= nil then
+ table.insert(lines, ("%s: %s"):format(name, entry.headers[name]))
+ end
+ end
+
+ if vim.b[bufnr].show_all_headers then
+ for name, value in pairs(entry.headers) do
+ if not vim.tbl_contains(config.headers, name) then
+ table.insert(lines, ("%s: %s"):format(name, value))
+ end
+ end
+ end
- vim.api.nvim_set_current_buf(bufnr)
+ table.insert(lines, "")
+
+ local cursor_pos = vim.fn.getpos(".")
+
+ if vim.b[bufnr].header_count ~= nil then
+ M.set_buffer_content(bufnr, {}, 0, vim.b[bufnr].header_count)
+ end
+
+ M.set_buffer_content(bufnr, lines, 0, 0)
+
+ vim.b[bufnr].header_count = #lines
+
+ vim.fn.setpos(".", cursor_pos)
+end
+
+function M.render_entry(bufnr, id, content_type)
+ local indexer = indexers.get_indexer()
+
+ indexer.get_part(id, content_type, function(entry, index, lines)
+ M.set_buffer_content(bufnr, lines, vim.b[bufnr].header_count)
+ vim.b[bufnr].inbox_parts = entry.parts
+ vim.b[bufnr].inbox_part_index = index
+
+ local winid = vim.api.nvim_get_current_win()
+ vim.api.nvim_set_option_value(
+ "winbar",
+ vim.b[bufnr].inbox_parts[vim.b[bufnr].inbox_part_index]["content-type"],
+ { scope = "local", win = winid }
+ )
+ end)
end
return M