diff options
author | Toby Vincent <tobyv@tobyvin.dev> | 2023-10-19 20:06:18 -0500 |
---|---|---|
committer | Toby Vincent <tobyv@tobyvin.dev> | 2023-10-19 20:06:18 -0500 |
commit | ddb7133e0df362edf6d0de7b79d56a2473637a97 (patch) | |
tree | 56bf3871095664fd64baf2c27b5c3251fbb6ced3 | |
parent | 43df6d6b23bee7b109fb31665e3c951367626ec3 (diff) |
feat: impl headers and handle multipart messages
-rw-r--r-- | lua/inbox/config.lua | 20 | ||||
-rw-r--r-- | lua/inbox/indexers/notmuch.lua | 77 | ||||
-rw-r--r-- | lua/inbox/init.lua | 100 | ||||
-rw-r--r-- | lua/inbox/types.lua | 18 | ||||
-rw-r--r-- | lua/inbox/utils.lua | 27 | ||||
-rw-r--r-- | lua/inbox/view.lua | 166 |
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 |