From 43df6d6b23bee7b109fb31665e3c951367626ec3 Mon Sep 17 00:00:00 2001 From: Toby Vincent Date: Wed, 18 Oct 2023 20:22:01 -0500 Subject: feat: open message part in buffer --- lua/inbox/config.lua | 4 +- lua/inbox/indexers/notmuch.lua | 210 ++++++++++++++++++++++++----------------- lua/inbox/init.lua | 43 ++++++--- lua/inbox/types.lua | 33 ++----- lua/inbox/view.lua | 92 +++++++++--------- 5 files changed, 202 insertions(+), 180 deletions(-) diff --git a/lua/inbox/config.lua b/lua/inbox/config.lua index 0a7b550..9ac9900 100644 --- a/lua/inbox/config.lua +++ b/lua/inbox/config.lua @@ -3,7 +3,7 @@ ---@field buf_options table|nil ---@field win_options table|nil ---@field columns integer[] ----@field signs table +---@field flags table ---@type inbox.Config local default_config = { @@ -28,7 +28,7 @@ local default_config = { 15, 30, }, - signs = { + flags = { unread = { text = "O", texthl = "Special", diff --git a/lua/inbox/indexers/notmuch.lua b/lua/inbox/indexers/notmuch.lua index 88add48..5b3e48c 100644 --- a/lua/inbox/indexers/notmuch.lua +++ b/lua/inbox/indexers/notmuch.lua @@ -9,21 +9,56 @@ local default_config = { } ---@class inbox.Indexer.Notmuch: inbox.Indexer, inbox.Indexer.Notmuch.Config +---@field _cache table +---@field cache? fun(id: string): inbox.Notmuch.Entry? +---@field flatten_parts? fun(part: inbox.EntryPart, parts: inbox.EntryPart[]?): inbox.EntryPart[] +---@field show_id? fun(id: string): inbox.Notmuch.Entry +---@field parse_tags? fun(tags: string[]): string[] +---@field summarize? fun(item: inbox.Notmuch.SearchResult): inbox.Summary + +---@class inbox.Notmuch.SearchResult +---@field authors string | string[] +---@field date_relative string +---@field matched integer +---@field query string[] +---@field subject string | string[] +---@field tags string[] +---@field thread integer +---@field timestamp integer +---@field total integer + +---@class inbox.Notmuch.Entry +---@field id string +---@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 = { - ---@type table> - queries = {}, - part_ids = {}, + _cache = {}, } +function M.cache(id) + if M._cache[id] == nil then + M._cache[id] = M.show_id(id) + end + + return M._cache[id] +end + function M.available() return vim.fn.executable("notmuch") == 1 end -function M.index(bufnr, callback, opts) +function M.index(maildir, callback, opts) opts = opts or {} - -- TODO: cache entries? - local json = "" local job = Job:new({ command = "notmuch", @@ -37,31 +72,21 @@ function M.index(bufnr, callback, opts) ---@type inbox.Summary[] local entries = {} + local ids = {} local signs = {} - local queries = {} for lnum, result in ipairs(results) do - table.insert(queries, result.query[1]) - table.insert(entries, M.summarize(result)) + ids[lnum] = (result.query[1]:gsub("^id:", "")) + entries[lnum] = M.summarize(result) for _, sign in pairs(M.parse_tags(result.tags)) do table.insert(signs, { sign, lnum }) end end - M.queries[bufnr] = queries - callback(bufnr, entries, signs) + callback(ids, entries, signs) end), }) - local bufname = vim.api.nvim_buf_get_name(bufnr) - local maildir, count = bufname:gsub("maildir://", "", 1) - if count == 0 then - vim.notify(("Invalid buffer name scheme: %s"):format(bufname), vim.log.levels.ERROR, { - title = "index.nvim: Failed to run indexer", - }) - return - end - local folder = Path:new(maildir):make_relative(M.database_dir) table.insert(job.args, ("folder:%s"):format(folder)) @@ -74,6 +99,7 @@ function M.index(bufnr, callback, opts) job:start() end +---@private ---@param tags string[] ---@return string[] parsed tags function M.parse_tags(tags) @@ -88,6 +114,7 @@ function M.parse_tags(tags) return signs end +---@private ---@param item inbox.Notmuch.SearchResult ---@return inbox.Summary summary function M.summarize(item) @@ -114,111 +141,118 @@ function M.summarize(item) subject, } end ----@param bufnr integer ----@param lnum integer ----@return string? -function M.get_query(bufnr, lnum) - if bufnr == 0 then - bufnr = vim.api.nvim_get_current_buf() - end - return M.queries[bufnr][lnum] -end - ----@param bufnr integer ----@param lnum integer ----@return inbox.Notmuch.ShowResult -function M.show(bufnr, lnum) - local query = M.get_query(bufnr, lnum) +---@private +---@param id string +---@return inbox.Notmuch.Entry? +function M.show_id(id) + if id == nil then + vim.notify(("Failed to find entry with id: '%s'"):format(id), vim.log.levels.ERROR) + return nil + end local job = Job:new({ command = "notmuch", - args = { "show", "--format=json", query }, + args = { "show", "--format=json", ("id:%s"):format(id) }, }) local stdout = job:sync() - local result = utils.json_decode(table.concat(stdout, "\n")) - while not vim.tbl_isempty(result) and vim.tbl_islist(result) do - result = result[1] + local entry = utils.json_decode(table.concat(stdout, "\n")) + while not vim.tbl_isempty(entry) and vim.tbl_islist(entry) do + entry = entry[1] --[[@as inbox.Notmuch.Entry]] end - return result + entry.tags = M.parse_tags(entry.tags) + entry.parts = M.flatten_parts(entry.body[1]) + + return entry end ----@param bufnr integer ----@param lnum integer ----@return inbox.Entry -function M.get_entry(bufnr, lnum) - local result = M.show(bufnr, lnum) +---@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 { - timestamp = result.timestamp, - filename = result.filename[1], - tags = M.parse_tags(result.tags), - body = result.body, - headers = result.headers, + id = entry.id, + timestamp = entry.timestamp, + filename = entry.filename[1], + tags = entry.tags, + parts = parts, + headers = entry.headers, } end ----@param part inbox.EntryPart ----@return table -function M._get_part_ids(part, part_map) +function M.flatten_parts(part, parts) + if parts == nil then + parts = {} + end + if type(part.content) == "table" then for _, p in pairs(part.content --[[@as inbox.EntryPart[] ]]) do - part_map = M._get_part_ids(p, part_map) + parts = M.flatten_parts(p, parts) end else - part_map[part["content-type"]] = part.id + table.insert(parts, part) end - return part_map + return parts end ----@param bufnr integer ----@param lnum integer ----@return table -function M.get_part_ids(bufnr, lnum) - local entry = M.show(bufnr, lnum) - if M.part_ids[bufnr] == nil then - M.part_ids[bufnr] = {} - end - if M.part_ids[bufnr][lnum] == nil then - M.part_ids[bufnr][lnum] = M._get_part_ids(entry.body[1], {}) +function M.get_part(id, content_type, callback) + local entry = M.cache(id) + + if entry == nil then + vim.notify(("Failed to get entry with id: %s"):format(id), vim.log.levels.ERROR) + return nil end - return M.part_ids[bufnr][lnum] -end ----@param bufnr integer ----@param lnum integer ----@return inbox.ContentType[] -function M.get_parts(bufnr, lnum) - local part_ids = M.get_part_ids(bufnr, lnum) - return vim.tbl_keys(part_ids) -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 + break + end + end + end ----@param bufnr integer ----@param lnum integer ----@param content_type inbox.ContentType ----@return string[]? -function M.get_part(bufnr, lnum, content_type) - local entry = M.show(bufnr, lnum) - local part_ids = M.get_part_ids(bufnr, lnum) - local part_id = part_ids[content_type] - - if part_id == nil then - vim.notify(("Failed to find part with content-type '%s'"):format(content_type), vim.log.levels.ERROR) + if part == nil then + vim.notify(("Failed to find message part for entry id: %s"):format(id), vim.log.levels.ERROR) return nil end + local stdout = {} local job = Job:new({ command = "notmuch", - args = { "show", ("--part=%s"):format(part_id), ("id:%s"):format(entry.id) }, + args = { "show", ("--part=%s"):format(part.id), ("id:%s"):format(entry.id) }, + on_stdout = vim.schedule_wrap(function(_, data) + table.insert(stdout, data) + end), + on_exit = vim.schedule_wrap(function() + callback(part["content-type"], stdout) + end), }) - return (job:sync()) + job:start() end ---@param opts inbox.Indexer.Notmuch.Config diff --git a/lua/inbox/init.lua b/lua/inbox/init.lua index 92d7405..bb59fc4 100644 --- a/lua/inbox/init.lua +++ b/lua/inbox/init.lua @@ -1,4 +1,6 @@ -local M = {} +local M = { + buffers = {}, +} function M.select() local mode = vim.api.nvim_get_mode().mode @@ -30,8 +32,7 @@ end function M.open(maildir) local view = require("inbox.view") if M.bufnr == nil then - local bufname = string.format("maildir://%s", maildir) - M.bufnr = view.initialize(bufname) + M.bufnr = view.initialize(maildir) end if not vim.api.nvim_buf_is_valid(M.bufnr) then @@ -42,20 +43,38 @@ function M.open(maildir) vim.api.nvim_set_current_buf(M.bufnr) end ----@param content_type inbox.ContentType -function M.open_entry(content_type) - if content_type == nil then - content_type = "text/plain" - end +function M.open_entry(part) + local view = require("inbox.view") local lnum = vim.api.nvim_win_get_cursor(0)[1] + local id = vim.b[0].inbox_ids[lnum] + + if id == nil then + vim.notify("Failed to get entry under cursor", vim.log.levels.ERROR) + return nil + end + + local bufnr = view.render_entry(id, part) + if bufnr ~= nil then + vim.api.nvim_set_current_buf(bufnr) + 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] - local part = indexer.get_part(0, lnum, content_type) + -- 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.print(part) + vim.ui.select(parts, {}, M.open_entry) end ---@param opts inbox.Config @@ -67,12 +86,10 @@ function M.setup(opts) config.setup(opts) indexers.setup(config.indexer_config) - for sign, value in pairs(config.signs) do + for sign, value in pairs(config.flags) do local name = utils.sign_name(sign) - vim.print(sign, value) vim.fn.sign_define(name, value) end - utils.sign_priority = vim.tbl_add_reverse_lookup(vim.tbl_keys(config.signs)) end return M diff --git a/lua/inbox/types.lua b/lua/inbox/types.lua index 7894e8d..b87991e 100644 --- a/lua/inbox/types.lua +++ b/lua/inbox/types.lua @@ -15,10 +15,10 @@ ---@class inbox.Indexer ---@field setup? fun(opts: table) ---@field available? fun(): boolean ----@field index? fun(bufnr: integer, cb: fun(entries: inbox.Summary[]), opts?: table) ----@field get_entry? fun(bufnr: integer, lnum: integer): inbox.Entry? ----@field get_parts? fun(bufnr: integer, lnum: integer): inbox.ContentType[] ----@field get_part? fun(bufnr: integer, lnum: integer, content_type: inbox.ContentType): string[] +---@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[])) ---@alias inbox.Indexer.Config ---| "notmuch" @@ -32,6 +32,7 @@ ---@field subject string ---@class inbox.Entry +---@field id string ---@field timestamp integer ---@field filename string ---@field tags string[] @@ -45,26 +46,4 @@ ---@field content-length? integer ---@field content-disposition? string ---@field content-transfer-encoding? string ----@field content? string | inbox.EntryPart[] - ----@class inbox.Notmuch.SearchResult ----@field authors string | string[] ----@field date_relative string ----@field matched integer ----@field query string[] ----@field subject string | string[] ----@field tags string[] ----@field thread integer ----@field timestamp integer ----@field total integer - ----@class inbox.Notmuch.ShowResult ----@field id string ----@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 content string diff --git a/lua/inbox/view.lua b/lua/inbox/view.lua index 60d3b96..e770417 100644 --- a/lua/inbox/view.lua +++ b/lua/inbox/view.lua @@ -19,22 +19,23 @@ function M.create_buffer(bufname, buf_options) return bufnr end ----@param bufname string +---@param maildir string ---@return integer bufnr -function M.initialize(bufname) +function M.initialize(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) -- vim.api.nvim_clear_autocmds({ buffer = bufnr, group = "Inbox" }) vim.keymap.set("n", "", require("inbox").open_entry, { buffer = bufnr }) - M.render_inbox(bufnr) + M.render_inbox(bufnr, maildir) return bufnr end @@ -45,61 +46,52 @@ function M.render_buffer_content(bufnr, lines, highlights, signs) vim.bo[bufnr].modifiable = false vim.bo[bufnr].modified = false - utils.set_highlights(bufnr, highlights) - utils.set_signs(bufnr, signs) + 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) +function M.render_inbox(bufnr, maildir) local indexer = indexers.get_indexer() - indexer.index(bufnr, M.render_inbox_async) + indexer.index(maildir, function(ids, entries, signs) + vim.b[bufnr].inbox_ids = ids + local lines, highlights = utils.render_table(entries, config.columns) + + local winid = vim.api.nvim_get_current_win() + for k, v in pairs(config.win_options) do + vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid }) + end + + M.render_buffer_content(bufnr, lines, highlights, signs) + + vim.api.nvim_set_option_value( + "winbar", + utils.render_row({ "Flags", "Date", "From", "Subject" }, { + vim.fn.getwininfo(winid)[1].textoff, + unpack(config.columns), + }), + { scope = "local", win = winid } + ) + end) end -function M.render_inbox_async(bufnr, entries, signs) - local lines, highlights = utils.render_table(entries, config.columns) - - local winid = vim.api.nvim_get_current_win() - for k, v in pairs(config.win_options) do - vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid }) - end - - M.render_buffer_content(bufnr, lines, highlights, signs) - - vim.api.nvim_set_option_value( - "winbar", - utils.render_row({ "Flags", "Date", "From", "Subject" }, { - vim.fn.getwininfo(winid)[1].textoff, - unpack(config.columns), - }), - { scope = "local", win = winid } - ) -end - ----@param bufname string ----@return integer? bufnr -function M.render_part(bufname) - local bufnr = vim.api.nvim_create_buf(true, false) - local winid = vim.api.nvim_get_current_win() - - -- vim.api.nvim_clear_autocmds({ buffer = bufnr, group = "Inbox" }) - - local buf_options = vim.tbl_extend("keep", config.buf_options, { - buftype = "acwrite", - syntax = "inbox", - filetype = "inbox", - }) +function M.render_entry(id, content_type) + local indexer = indexers.get_indexer() + local bufname = string.format("%s [%s]", id, content_type) + local bufnr = M.create_buffer(bufname, {}) - for k, v in pairs(buf_options) do - vim.api.nvim_buf_set_option(bufnr, k, v) - end - for k, v in pairs(config.win_options) do - vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid }) - end + 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) - vim.api.nvim_buf_set_name(bufnr, bufname) - M.render_inbox(bufnr) - vim.keymap.set("n", "", require("inbox").open_entry, { buffer = bufnr }) - return bufnr + vim.api.nvim_set_current_buf(bufnr) end return M -- cgit v1.2.3-70-g09d2