summaryrefslogtreecommitdiffstats
path: root/lua/inbox
diff options
context:
space:
mode:
Diffstat (limited to 'lua/inbox')
-rw-r--r--lua/inbox/config.lua59
-rw-r--r--lua/inbox/email.lua36
-rw-r--r--lua/inbox/indexers.lua48
-rw-r--r--lua/inbox/indexers/notmuch.lua67
-rw-r--r--lua/inbox/init.lua50
-rw-r--r--lua/inbox/utils.lua80
-rw-r--r--lua/inbox/view.lua98
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