summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorToby Vincent <tobyv@tobyvin.dev>2023-10-18 20:22:01 -0500
committerToby Vincent <tobyv@tobyvin.dev>2023-10-18 20:22:01 -0500
commit43df6d6b23bee7b109fb31665e3c951367626ec3 (patch)
tree05311197f0058319e1420afbf2ef9b1f9eca6899
parent92e05edb6669d734be3bff60bb39f8a320d0f175 (diff)
feat: open message part in buffer
-rw-r--r--lua/inbox/config.lua4
-rw-r--r--lua/inbox/indexers/notmuch.lua210
-rw-r--r--lua/inbox/init.lua43
-rw-r--r--lua/inbox/types.lua33
-rw-r--r--lua/inbox/view.lua92
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<string, table>
+---@field flags table<string, 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<string, inbox.Notmuch.Entry>
+---@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<integer, table<integer, string>>
- 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<inbox.ContentType, integer>
-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<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], {})
+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", "<Enter>", 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", "<Enter>", require("inbox").open_entry, { buffer = bufnr })
- return bufnr
+ vim.api.nvim_set_current_buf(bufnr)
end
return M