diff options
author | Toby Vincent <tobyv@tobyvin.dev> | 2023-10-17 21:17:04 -0500 |
---|---|---|
committer | Toby Vincent <tobyv@tobyvin.dev> | 2023-10-17 21:17:04 -0500 |
commit | 92e05edb6669d734be3bff60bb39f8a320d0f175 (patch) | |
tree | 1bc58fe8a4710f517a1e5dfd62859aab42678b3f | |
parent | a3e2977d898e60e34aaf8f681ef8a5413eaa55fd (diff) |
fix: improve signs display
-rw-r--r-- | lua/inbox/config.lua | 28 | ||||
-rw-r--r-- | lua/inbox/indexers.lua | 10 | ||||
-rw-r--r-- | lua/inbox/indexers/notmuch.lua | 201 | ||||
-rw-r--r-- | lua/inbox/init.lua | 55 | ||||
-rw-r--r-- | lua/inbox/types.lua | 76 | ||||
-rw-r--r-- | lua/inbox/utils.lua | 36 | ||||
-rw-r--r-- | lua/inbox/view.lua | 131 |
7 files changed, 395 insertions, 142 deletions
diff --git a/lua/inbox/config.lua b/lua/inbox/config.lua index db122f8..0a7b550 100644 --- a/lua/inbox/config.lua +++ b/lua/inbox/config.lua @@ -17,7 +17,7 @@ local default_config = { }, win_options = { wrap = false, - signcolumn = "auto", + signcolumn = "yes:1", cursorcolumn = false, colorcolumn = false, foldcolumn = "0", @@ -29,28 +29,32 @@ local default_config = { 30, }, signs = { - attachment = { - text = "A", - texthl = "Constant", - }, - draft = { - text = "D", + unread = { + text = "O", + texthl = "Special", + linehl = "Special", }, flagged = { text = "F", texthl = "Search", }, - passed = { - text = "P", + attachment = { + text = "A", + texthl = "Constant", }, replied = { text = "R", texthl = "Directory", }, - unread = { + signed = { text = "S", - texthl = "Special", - linehl = "Special", + texthl = "Directory", + }, + draft = { + text = "D", + }, + passed = { + text = "P", }, }, } diff --git a/lua/inbox/indexers.lua b/lua/inbox/indexers.lua index f358ce0..0c87988 100644 --- a/lua/inbox/indexers.lua +++ b/lua/inbox/indexers.lua @@ -1,13 +1,3 @@ ----@class inbox.Indexer ----@field setup? fun(opts: table) ----@field available? fun(): boolean ----@field index? fun(cb: fun(entries: inbox.Summary[]), opts: table) - ----@alias inbox.Indexer.Config ----| "notmuch" ----| string ----| table - local M = {} ---@return inbox.Indexer diff --git a/lua/inbox/indexers/notmuch.lua b/lua/inbox/indexers/notmuch.lua index c13e898..88add48 100644 --- a/lua/inbox/indexers/notmuch.lua +++ b/lua/inbox/indexers/notmuch.lua @@ -1,5 +1,6 @@ local Job = require("plenary.job") local Path = require("plenary.path") +local utils = require("inbox.utils") ---@class inbox.Indexer.Notmuch.Config local default_config = { @@ -8,14 +9,20 @@ local default_config = { } ---@class inbox.Indexer.Notmuch: inbox.Indexer, inbox.Indexer.Notmuch.Config -local M = {} +local M = { + ---@type table<integer, table<integer, string>> + queries = {}, + part_ids = {}, +} function M.available() return vim.fn.executable("notmuch") == 1 end -function M.index(callback, filters) - filters = filters or {} +function M.index(bufnr, callback, opts) + opts = opts or {} + + -- TODO: cache entries? local json = "" local job = Job:new({ @@ -24,60 +31,80 @@ function M.index(callback, filters) on_stdout = vim.schedule_wrap(function(_, stdout) json = json .. stdout end), - on_exit = vim.schedule_wrap(function(_, _, _) - local entries = {} + on_exit = vim.schedule_wrap(function() + ---@type inbox.Notmuch.SearchResult[] + local results = utils.json_decode(json) - if json and json ~= "" then - entries = vim.json.decode(json) or {} - end - - local rows = {} + ---@type inbox.Summary[] + local entries = {} local signs = {} + local queries = {} - for lnum, entry in ipairs(entries) do - local cols = M.summarize(entry) - - for _, tag in ipairs(entry.tags) do - local sign = M.map_tag_signs[tag] or tag + for lnum, result in ipairs(results) do + table.insert(queries, result.query[1]) + table.insert(entries, M.summarize(result)) + for _, sign in pairs(M.parse_tags(result.tags)) do table.insert(signs, { sign, lnum }) end - - table.insert(rows, cols) end - callback(rows, signs) + M.queries[bufnr] = queries + callback(bufnr, entries, signs) end), }) - -- TODO: handle different operators i.e. "not", "or", etc. - local filter_args = {} - for name, value in pairs(filters) do - table.insert(filter_args, ("%s:%s"):format(name, value)) + 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 filter_args_str = table.concat(filter_args, " and ") - vim.list_extend(job.args, vim.split(filter_args_str, " ")) + local folder = Path:new(maildir):make_relative(M.database_dir) + table.insert(job.args, ("folder:%s"):format(folder)) + + -- TODO: handle different operators i.e. "not", "or", etc. + for name, value in pairs(opts) do + table.insert(job.args, "and") + table.insert(job.args, ("%s:%s"):format(name, value)) + end job:start() end ----@param summary inbox.Summary ----@return table sanitized sanitized summary -function M.summarize(summary) - local date = summary.date_relative +---@param tags string[] +---@return string[] parsed tags +function M.parse_tags(tags) + local signs = {} + for _, tag in ipairs(tags) do + if M.map_tag_signs[tag] ~= nil then + table.insert(signs, M.map_tag_signs[tag]) + else + table.insert(signs, tag) + end + end + return signs +end + +---@param item inbox.Notmuch.SearchResult +---@return inbox.Summary summary +function M.summarize(item) + local date = item.date_relative local from - if type(summary.authors) == "table" then - from = summary.authors[1] --[[@as string]] + if type(item.authors) == "table" then + from = item.authors[1] --[[@as string]] else - from = summary.authors --[[@as string]] + from = item.authors --[[@as string]] end local subject - if type(summary.subject) == "table" then - subject = summary.subject[1] --[[@as string]] + if type(item.subject) == "table" then + subject = item.subject[1] --[[@as string]] else - subject = summary.subject --[[@as string]] + subject = item.subject --[[@as string]] end subject = subject:gsub("\r?\n", " ") @@ -87,6 +114,112 @@ function M.summarize(summary) 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) + + local job = Job:new({ + command = "notmuch", + args = { "show", "--format=json", query }, + }) + + 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] + end + + return result +end + +---@param bufnr integer +---@param lnum integer +---@return inbox.Entry +function M.get_entry(bufnr, lnum) + local result = M.show(bufnr, lnum) + + return { + timestamp = result.timestamp, + filename = result.filename[1], + tags = M.parse_tags(result.tags), + body = result.body, + headers = result.headers, + } +end + +---@param part inbox.EntryPart +---@return table<inbox.ContentType, integer> +function M._get_part_ids(part, part_map) + 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) + end + else + part_map[part["content-type"]] = part.id + end + + return part_map +end + +---@param bufnr integer +---@param lnum integer +---@return table<inbox.ContentType, integer> +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], {}) + 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 + +---@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) + return nil + end + + local job = Job:new({ + command = "notmuch", + args = { "show", ("--part=%s"):format(part_id), ("id:%s"):format(entry.id) }, + }) + + return (job:sync()) +end ---@param opts inbox.Indexer.Notmuch.Config function M.setup(opts) diff --git a/lua/inbox/init.lua b/lua/inbox/init.lua index b00ed84..92d7405 100644 --- a/lua/inbox/init.lua +++ b/lua/inbox/init.lua @@ -1,16 +1,63 @@ local M = {} +function M.select() + local mode = vim.api.nvim_get_mode().mode + local is_visual = mode:match("^[vV]") + + local entries = {} + if is_visual then + -- This is the best way to get the visual selection at the moment + -- https://github.com/neovim/neovim/pull/13896 + local _, start_lnum, _, _ = unpack(vim.fn.getpos("v")) + local _, end_lnum, _, _, _ = unpack(vim.fn.getcurpos()) + if start_lnum > end_lnum then + start_lnum, end_lnum = end_lnum, start_lnum + end + for i = start_lnum, end_lnum do + local entry = M.get_entry_on_line(0, i) + if entry then + table.insert(entries, entry) + end + end + else + local entry = M.get_cursor_entry() + if entry then + table.insert(entries, entry) + end + end +end + function M.open(maildir) local view = require("inbox.view") - if M.bufnr == nil then - M.bufnr = vim.api.nvim_create_buf(true, false) - view.initialize(M.bufnr, maildir) + local bufname = string.format("maildir://%s", maildir) + M.bufnr = view.initialize(bufname) + end + + if not vim.api.nvim_buf_is_valid(M.bufnr) then + --- TODO: Handle error + return end 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 + + local lnum = vim.api.nvim_win_get_cursor(0)[1] + + local indexers = require("inbox.indexers") + local indexer = indexers.get_indexer() + + local part = indexer.get_part(0, lnum, content_type) + + vim.print(part) +end + ---@param opts inbox.Config function M.setup(opts) local config = require("inbox.config") @@ -22,8 +69,10 @@ function M.setup(opts) for sign, value in pairs(config.signs) 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 1d2c549..7894e8d 100644 --- a/lua/inbox/types.lua +++ b/lua/inbox/types.lua @@ -1,4 +1,53 @@ +---@alias inbox.Headers table<inbox.Header, string> + +---@alias inbox.Header +---| "Subject" +---| "Date" +---| "To" +---| "From" +---| string + +---@alias inbox.ContentType +---| "text/plain" +---| "text/html" +---| string + +---@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[] + +---@alias inbox.Indexer.Config +---| "notmuch" +---| string +---| table + ---@class inbox.Summary +---@field tags string[] +---@field date string +---@field from string +---@field subject string + +---@class inbox.Entry +---@field timestamp integer +---@field filename string +---@field tags string[] +---@field parts string[] +---@field headers inbox.Headers + +---@class inbox.EntryPart +---@field id integer +---@field content-type inbox.ContentType +---@field content-charset? string +---@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 @@ -9,36 +58,13 @@ ---@field timestamp integer ---@field total integer ----@class inbox.Email +---@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.Part[] +---@field body inbox.EntryPart[] ---@field crypto table ---@field headers inbox.Headers - ----@alias inbox.Headers table<inbox.Header, string> - ----@alias inbox.Header ----| "Subject" ----| "Date" ----| "To" ----| "From" ----| string - ----@class inbox.Part ----@field id integer ----@field content-type inbox.ContentType ----@field content-charset? string ----@field content-length? integer ----@field content-disposition? string ----@field content-transfer-encoding? string ----@field content? string | inbox.Part[] - ----@alias inbox.ContentType ----| "text/plain" ----| "text/html" ----| string diff --git a/lua/inbox/utils.lua b/lua/inbox/utils.lua index ddc809d..3b84b83 100644 --- a/lua/inbox/utils.lua +++ b/lua/inbox/utils.lua @@ -83,4 +83,40 @@ function M.sign_name(name) return "Inbox" .. name:gsub("^%l", string.upper) end +---@param bufnr integer +---@param highlights any[][] List of highlights { group, lnum, col_start, col_end } +M.set_highlights = function(bufnr, highlights) + local ns = vim.api.nvim_create_namespace("Oil") + vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) + for _, hl in ipairs(highlights) do + vim.api.nvim_buf_add_highlight(bufnr, ns, unpack(hl)) + end +end + +---@param bufnr integer +---@param signs any[][] List of signs { name, lnum } +function M.set_signs(bufnr, signs) + local notified = {} + + for _, sign_lnum in pairs(signs) do + local sign, lnum = unpack(sign_lnum) + local name = M.sign_name(sign) + + if #vim.fn.sign_getdefined(name) > 0 then + vim.fn.sign_place(0, "inbox", name, bufnr, { lnum = lnum }) + elseif not notified[name] then + vim.notify(("Missing sign definition for sign: %s"):format(name), vim.log.levels.ERROR) + notified[name] = true + end + end +end + +function M.json_decode(json) + local results = {} + if json and json ~= "" then + results = vim.json.decode(json) or {} + end + return results +end + return M diff --git a/lua/inbox/view.lua b/lua/inbox/view.lua index 252bef3..60d3b96 100644 --- a/lua/inbox/view.lua +++ b/lua/inbox/view.lua @@ -4,87 +4,102 @@ local utils = require("inbox.utils") local M = {} ----@param bufnr integer -function M.initialize(bufnr, maildir) - if bufnr == 0 then - bufnr = vim.api.nvim_get_current_buf() - end - - if not vim.api.nvim_buf_is_valid(bufnr) then - return - end - - -- vim.api.nvim_clear_autocmds({ buffer = bufnr, group = "Inbox" }) +---@param bufname string +---@param buf_options table<string, any> | nil +---@return integer bufnr +function M.create_buffer(bufname, buf_options) + local bufnr = vim.api.nvim_create_buf(true, false) - for k, v in pairs(config.buf_options) do + for k, v in pairs(buf_options or {}) do vim.api.nvim_buf_set_option(bufnr, k, v) end - 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 - - local bufname = string.format("maildir://%s", maildir) vim.api.nvim_buf_set_name(bufnr, bufname) - M.render_buffer(bufnr) - - -- TODO: setup keymaps + return bufnr end ----@param bufnr integer -function M.render_buffer(bufnr) - if bufnr == 0 then - bufnr = vim.api.nvim_get_current_buf() - end +---@param bufname string +---@return integer bufnr +function M.initialize(bufname) + local buf_options = vim.tbl_extend("keep", config.buf_options, { + buftype = "acwrite", + syntax = "inbox", + filetype = "inbox", + }) - if not vim.api.nvim_buf_is_valid(bufnr) then - return false - end + local bufnr = M.create_buffer(bufname, buf_options) - local function render_buffer_async_callback(entries, signs) - -- TODO: sort entries? + -- vim.api.nvim_clear_autocmds({ buffer = bufnr, group = "Inbox" }) - local lines, _ = utils.render_table(entries, config.columns) + vim.keymap.set("n", "<Enter>", require("inbox").open_entry, { buffer = bufnr }) - -- TODO: setup highlights + M.render_inbox(bufnr) - 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 + return bufnr +end - for _, sign_lnum in pairs(signs) do - local sign, lnum = unpack(sign_lnum) - local name = utils.sign_name(sign) +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 #vim.fn.sign_getdefined(name) > 0 then - vim.fn.sign_place(0, "inbox", name, bufnr, { lnum = lnum }) - end - end + utils.set_highlights(bufnr, highlights) + utils.set_signs(bufnr, signs) +end - local winid = vim.api.nvim_get_current_win() +---@param bufnr integer +function M.render_inbox(bufnr) + local indexer = indexers.get_indexer() + indexer.index(bufnr, M.render_inbox_async) +end - for k, v in pairs(config.win_options) do - vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid }) - end +function M.render_inbox_async(bufnr, entries, signs) + local lines, highlights = utils.render_table(entries, config.columns) - local offset = vim.fn.getwininfo(winid)[1].textoff - local winbar_col = config.columns - table.insert(winbar_col, 1, offset) - local winbar = utils.render_row({ "Flags", "Date", "From", "Subject" }, winbar_col) - vim.api.nvim_set_option_value("winbar", winbar, { scope = "local", win = winid }) + 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 - -- TODO: cache entries + M.render_buffer_content(bufnr, lines, highlights, signs) - local indexer = indexers.get_indexer() + 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() - local bufname = vim.api.nvim_buf_get_name(bufnr) - local maildir = bufname:gsub("maildir://", "") + -- 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", + }) - indexer.index(render_buffer_async_callback, { folder = maildir }) + 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 + + vim.api.nvim_buf_set_name(bufnr, bufname) + M.render_inbox(bufnr) + vim.keymap.set("n", "<Enter>", require("inbox").open_entry, { buffer = bufnr }) + return bufnr end return M |