diff options
Diffstat (limited to 'lua')
-rw-r--r-- | lua/inbox/config.lua | 59 | ||||
-rw-r--r-- | lua/inbox/email.lua | 36 | ||||
-rw-r--r-- | lua/inbox/indexers.lua | 48 | ||||
-rw-r--r-- | lua/inbox/indexers/notmuch.lua | 67 | ||||
-rw-r--r-- | lua/inbox/init.lua | 50 | ||||
-rw-r--r-- | lua/inbox/utils.lua | 80 | ||||
-rw-r--r-- | lua/inbox/view.lua | 98 |
7 files changed, 316 insertions, 122 deletions
diff --git a/lua/inbox/config.lua b/lua/inbox/config.lua index 988b2fe..1ba2566 100644 --- a/lua/inbox/config.lua +++ b/lua/inbox/config.lua @@ -1,46 +1,33 @@ ---@class inbox.Config ----@field indexer inbox.Config.Indexer|nil ----@field indexer_config table|nil - ----@alias inbox.Config.Indexer ----| "notmuch" ----| string ----| inbox.Indexer +---@field indexer_config inbox.Indexer.Config|nil +---@field buf_options table|nil +---@field win_options table|nil ---@type inbox.Config local default_config = { - indexer = "notmuch", - indexer_opts = {}, + indexer_config = "notmuch", + buf_options = { + buftype = "acwrite", + syntax = "inbox", + filetype = "inbox", + buflisted = false, + bufhidden = "hide", + }, + win_options = { + wrap = false, + signcolumn = "yes:3", -- can signs be used for flags?? + number = false, + relativenumber = false, + cursorcolumn = false, + foldcolumn = "0", + spell = false, + list = false, + }, } ---@class inbox.Config local M = {} ----@param indexer inbox.Config.Indexer ----@return inbox.Indexer? -function M.get_indexer(indexer) - if not indexer then - vim.notify("No indexer set", vim.log.levels.ERROR) - return nil - end - - if type(indexer) == "string" then - local ok - ok, indexer = pcall(require, string.format("inbox.indexers.%s", indexer)) - if not ok then - vim.notify(string.format("Indexer not found: '%s'", indexer), vim.log.levels.ERROR) - end - end - - -- TODO: Validate indexer spec - - if indexer.available() then - return indexer - else - vim.notify(string.format("Indexer not available: '%s'", indexer), vim.log.levels.ERROR) - end -end - ---@param opts inbox.Config M.setup = function(opts) local config = vim.tbl_deep_extend("keep", opts or {}, default_config) @@ -48,10 +35,6 @@ M.setup = function(opts) for k, v in pairs(config) do M[k] = v end - - M.indexer = M.get_indexer(M.indexer) - - M.indexer.setup(M.indexer_config) end return M diff --git a/lua/inbox/email.lua b/lua/inbox/email.lua index 1d2c549..c680b16 100644 --- a/lua/inbox/email.lua +++ b/lua/inbox/email.lua @@ -42,3 +42,39 @@ ---| "text/plain" ---| "text/html" ---| string + +local M = {} + +M.summary_widths = { + 10, + 30, +} + +---@param summary inbox.Summary +---@return table sanitized sanitized summary +function M.sanitize_summary(summary) + local date = summary.date_relative:gsub("^([A-Z][a-z][a-z])%w%w+", "%1.") + + local from + if type(summary.authors) == "table" then + from = summary.authors[1] --[[@as string]] + else + from = summary.authors --[[@as string]] + end + + local subject + if type(summary.subject) == "table" then + subject = summary.subject[1] --[[@as string]] + else + subject = summary.subject --[[@as string]] + end + subject = subject:gsub("\r?\n", " ") + + return { + date, + from, + subject, + } +end + +return M diff --git a/lua/inbox/indexers.lua b/lua/inbox/indexers.lua new file mode 100644 index 0000000..f358ce0 --- /dev/null +++ b/lua/inbox/indexers.lua @@ -0,0 +1,48 @@ +---@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 +function M.get_indexer() + if not M.indexer then + local config = require("inbox.config") + M.setup(config.indexer_config) + end + + return M.indexer +end + +---@param opts inbox.Indexer.Config +function M.setup(opts) + if not opts then + vim.notify("No indexer set", vim.log.levels.ERROR) + return nil + end + + if type(opts) == "string" then + opts = { name = opts } + end + + local ok, indexer = pcall(require, string.format("inbox.indexers.%s", opts.name)) + if not ok then + vim.notify(string.format("Indexer not found: '%s'", indexer), vim.log.levels.ERROR) + end + + -- TODO: Validate indexer spec + if indexer.available() then + M.indexer = indexer + M.indexer.setup(opts) + else + vim.notify(string.format("Indexer not available: '%s'", indexer), vim.log.levels.ERROR) + end +end + +return M diff --git a/lua/inbox/indexers/notmuch.lua b/lua/inbox/indexers/notmuch.lua index 8ef2a90..171fabf 100644 --- a/lua/inbox/indexers/notmuch.lua +++ b/lua/inbox/indexers/notmuch.lua @@ -3,40 +3,9 @@ local Path = require("plenary.path") ---@class inbox.Indexer.Notmuch.Config local default_config = { - database_dir = Path:new(vim.env.XDG_DATA_DIR, "mail"), + database_dir = Path:new(vim.env.XDG_DATA_HOME, "mail").filename, } -local Query = {} - -function Query:new() - local obj = { - command = "notmuch", - args = { - "show", - "--format=json", - }, - } - - setmetatable(obj, Query) - return obj -end - -function Query:account(account) - self.args:insert("and") - self.args:insert(("path:%s/**"):format(account)) - return self -end - -function Query:folder(folder) - self.args:insert("and") - self.args:insert(("folder:/.*\\/%s/"):format(folder)) - return self -end - -function Query:build() - return Job:new(self) -end - ---@class inbox.Indexer.Notmuch: inbox.Indexer, inbox.Indexer.Notmuch.Config local M = {} @@ -44,25 +13,37 @@ function M.available() return vim.fn.executable("notmuch") == 1 end -function M.index(account, folder) +function M.index(callback, filters) + filters = filters or {} + + local json = "" local job = Job:new({ command = "notmuch", args = { "search", "--format=json" }, + on_stdout = vim.schedule_wrap(function(_, stdout) + json = json .. stdout + end), + on_exit = vim.schedule_wrap(function(_, _, _) + if json == "" then + json = "[]" + end + + local entries = vim.json.decode(json) + callback(entries) + end), }) - if account then - table.insert(job.args, "and") - table.insert(job.args, ("path:%s/**"):format(account)) - end - - if folder then - table.insert(job.args, "and") - table.insert(job.args, ("folder:/.*\\/%s/"):format(folder)) + -- 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)) end + local filter_args_str = table.concat(filter_args, " and ") + vim.list_extend(job.args, vim.split(filter_args_str, " ")) - local json = job:sync() + vim.print(job.args) - return vim.json.decode(table.concat(json, "\n")) or {} + job:start() end ---@param opts inbox.Indexer.Notmuch.Config diff --git a/lua/inbox/init.lua b/lua/inbox/init.lua index 0c3dc27..5841481 100644 --- a/lua/inbox/init.lua +++ b/lua/inbox/init.lua @@ -1,55 +1,23 @@ ----@class inbox.Indexer ----@field setup? fun(opts: table) ----@field available? fun(): boolean ----@field index? fun(account?:string, maildir?:string): inbox.Summary[] - local M = {} ----@param email inbox.Summary ----@return string summary -function M.summerize(email) - local tags = "" - for _, tag in pairs(email.tags) do - tags = tags .. string.sub(tag, 1, 1) - end - - local from = email.authors - if type(email.authors) == "table" then - from = email.authors[1] --[[@cast from string]] - end - - local subject = email.subject - if type(email.subject) == "table" then - subject = email.subject[1] --[[@cast subject string]] - end - subject = subject:gsub("\r?\n", " ") - - local summary = { - email.date_relative, - from, - tags, - subject, - } - - return table.concat(summary, "\t") -end - -function M.list_mail(...) - local config = require("inbox.config") - local results = config.indexer.index(...) - local lines = {} +function M.open(maildir) + local view = require("inbox.view") - for _, item in pairs(results) do - table.insert(lines, M.summerize(item)) + if M.bufnr == nil then + M.bufnr = vim.api.nvim_create_buf(true, false) + view.initialize(M.bufnr, maildir) end - return lines + vim.api.nvim_set_current_buf(M.bufnr) end ---@param opts inbox.Config function M.setup(opts) local config = require("inbox.config") + local indexers = require("inbox.indexers") + config.setup(opts) + indexers.setup(config.indexer_config) end return M diff --git a/lua/inbox/utils.lua b/lua/inbox/utils.lua new file mode 100644 index 0000000..93b4d47 --- /dev/null +++ b/lua/inbox/utils.lua @@ -0,0 +1,80 @@ +local M = {} + +---@class inbox.TextChunk +---@field [1] string text +---@field [2] string hl + +---@param text string +---@param length nil|integer +---@return string +function M.rpad(text, length) + if not length then + return text + end + + if text:len() <= length then + return text .. string.rep(" ", length - text:len()) + else + return string.sub(text, 1, length - 3) .. "..." + end +end + +---@param cols (string | inbox.TextChunk)[] +---@param col_widths integer[] +---@return string +---@return any[][] List of highlights {group, col_start, col_end} +function M.render_row(cols, col_widths) + local pieces = {} + local highlights = {} + + local col = 0 + for i, chunk in ipairs(cols) do + local text, hl + if type(chunk) == "table" then + text, hl = unpack(chunk) --[[@as string]] + else + text = chunk --[[@as string]] + end + text = M.rpad(text, col_widths[i]) + table.insert(pieces, text) + local col_end = col + text:len() + 1 + if hl then + table.insert(highlights, { hl, col, col_end }) + end + col = col_end + end + + return table.concat(pieces, " "), highlights +end + +---@param rows (string | inbox.TextChunk)[][] +---@param col_widths integer[] +---@return string[] +---@return any[][] List of highlights {group, lnum, col_start, col_end} +function M.render_table(rows, col_widths) + local lines = {} + local highlights = {} + + for lnum, cols in ipairs(rows) do + local line, line_hls = M.render_row(cols, col_widths) + table.insert(lines, line) + + for _, hl in pairs(line_hls) do + local group, col_start, col_end = unpack(hl) + table.insert(highlights, { group, lnum, col_start, col_end }) + end + end + + return lines, highlights +end + +---@param winid integer +---@param cols (string | inbox.TextChunk)[] +---@param col_widths integer[] +function M.render_winbar(winid, cols, col_widths) + local winbar = M.render_row(cols, col_widths) + local offset = vim.fn.getwininfo(winid)[1].textoff + 1 + vim.api.nvim_set_option_value("winbar", string.rep(" ", offset) .. winbar, { scope = "local", win = winid }) +end + +return M diff --git a/lua/inbox/view.lua b/lua/inbox/view.lua new file mode 100644 index 0000000..cfe256c --- /dev/null +++ b/lua/inbox/view.lua @@ -0,0 +1,98 @@ +local config = require("inbox.config") +local email = require("inbox.email") +local indexers = require("inbox.indexers") +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" }) + + for k, v in pairs(config.buf_options) 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 +end + +---@param bufnr integer +function M.render_buffer(bufnr) + if bufnr == 0 then + bufnr = vim.api.nvim_get_current_buf() + end + + if not vim.api.nvim_buf_is_valid(bufnr) then + return false + end + + local function render_buffer_async_callback(entries) + -- TODO: sort entries? + + utils.render_table({ {} }, email.summary_widths) + + local summaries = {} + + -- local signs + for _, entry in ipairs(entries) do + local cols = email.sanitize_summary(entry) + + -- TODO: format signs + -- local _ = email.tags + -- table.insert(signs, email.tags) + + table.insert(summaries, cols) + end + + -- TODO: configurable table columns and column widths + + local lines, _ = utils.render_table(summaries, email.summary_widths) + + -- TODO: setup highlights + -- TODO: setup signs (tags) + + 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 + + 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 winbar = utils.render_row({ "Date", "From", "Subject" }, email.summary_widths) + local offset = vim.fn.getwininfo(winid)[1].textoff + 1 + vim.api.nvim_set_option_value("winbar", string.rep(" ", offset) .. winbar, { scope = "local", win = winid }) + end + + -- TODO: cache entries + + local indexer = indexers.get_indexer() + + local bufname = vim.api.nvim_buf_get_name(bufnr) + local maildir = bufname:gsub("maildir://", "") + + indexer.index(render_buffer_async_callback, { folder = maildir }) +end + +return M |