summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorToby Vincent <tobyv@tobyvin.dev>2023-10-17 21:17:04 -0500
committerToby Vincent <tobyv@tobyvin.dev>2023-10-17 21:17:04 -0500
commit92e05edb6669d734be3bff60bb39f8a320d0f175 (patch)
tree1bc58fe8a4710f517a1e5dfd62859aab42678b3f
parenta3e2977d898e60e34aaf8f681ef8a5413eaa55fd (diff)
fix: improve signs display
-rw-r--r--lua/inbox/config.lua28
-rw-r--r--lua/inbox/indexers.lua10
-rw-r--r--lua/inbox/indexers/notmuch.lua201
-rw-r--r--lua/inbox/init.lua55
-rw-r--r--lua/inbox/types.lua76
-rw-r--r--lua/inbox/utils.lua36
-rw-r--r--lua/inbox/view.lua131
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