feat: add agenda plugin
Import the package from my dotfiles.
This commit is contained in:
25
doc/agenda.txt
Normal file
25
doc/agenda.txt
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
*agenda.txt*
|
||||||
|
|
||||||
|
*Agenda*
|
||||||
|
|
||||||
|
Neovim integration for the `agenda` <https://git.rkcsd.com/jonas/agenda>
|
||||||
|
edit subcommand.
|
||||||
|
|
||||||
|
To open an agenda note file: >vim
|
||||||
|
|
||||||
|
Agenda last monday
|
||||||
|
|
||||||
|
If the agenda note does not exist, you can create it automatically: >vim
|
||||||
|
|
||||||
|
Agenda! last monday
|
||||||
|
|
||||||
|
The date spec can be any string understood by your `date(1)` util.
|
||||||
|
|
||||||
|
The command accepts the b, c, e, and t options of the `agenda edit`
|
||||||
|
subcommand: >vim
|
||||||
|
|
||||||
|
Agenda [-bc] [-e <extension>] [-t <name>] last monday
|
||||||
|
|
||||||
|
See `:Man agenda` for additional information.
|
||||||
|
|
||||||
|
vim:tw=78:ts=8:noet:ft=help:norl:
|
||||||
132
lua/agenda.lua
Normal file
132
lua/agenda.lua
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
local api = vim.api
|
||||||
|
local fn = vim.fn
|
||||||
|
|
||||||
|
---Wrapper for using my `agenda` script from within neovim.
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
function M.on_attach(buf)
|
||||||
|
vim.bo[buf].bufhidden = 'delete'
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return string
|
||||||
|
function M.dir()
|
||||||
|
if vim.env.AGENDA_DIR then
|
||||||
|
return vim.env.AGENDA_DIR
|
||||||
|
end
|
||||||
|
|
||||||
|
if vim.env.XDG_DATA_HOME then
|
||||||
|
return string.format('%/agenda', vim.env.XDG_DATA_HOME)
|
||||||
|
end
|
||||||
|
|
||||||
|
return string.format('%/.local/share/agenda', vim.env.HOME)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Gets the first window with an agenda note buffer in a tabpage.
|
||||||
|
---@param tabpage integer Tabpage handle, or 0 for the current tabpage.
|
||||||
|
---@param opts {dir: string?}?
|
||||||
|
---@return integer? win Window handle
|
||||||
|
function M.tabpage_get_win(tabpage, opts)
|
||||||
|
local agenda_dir = opts and opts.dir or M.dir()
|
||||||
|
|
||||||
|
for _, win in ipairs(api.nvim_tabpage_list_wins(tabpage)) do
|
||||||
|
local buf = api.nvim_win_get_buf(win)
|
||||||
|
-- Better to look at the name (file path), since that state is always there
|
||||||
|
-- (e.g. after restoring a session).
|
||||||
|
local name = api.nvim_buf_get_name(buf)
|
||||||
|
if vim.startswith(name, agenda_dir) then
|
||||||
|
return win
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class agenda.Opts
|
||||||
|
---@field backlog string? Set the -b flag.
|
||||||
|
---@field create boolean? Set the -c flag.
|
||||||
|
---@field dir string? Set this as AGENDA_DIR value.
|
||||||
|
---@field extension string? Set this as -e option value.
|
||||||
|
---@field name string? Set this as -t option value.
|
||||||
|
|
||||||
|
---Open an agenda note.
|
||||||
|
---@param date string Either the date or backlog name.
|
||||||
|
---@param opts agenda.Opts?
|
||||||
|
function M.open(date, opts)
|
||||||
|
local filepath, err = M.agenda(date, opts)
|
||||||
|
|
||||||
|
if not filepath or fn.filereadable(filepath) < 1 then
|
||||||
|
if not err then
|
||||||
|
err = string.format("agenda: file '%s' is not readable", filepath)
|
||||||
|
end
|
||||||
|
vim.notify(err, vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local win = M.tabpage_get_win(0)
|
||||||
|
if win then
|
||||||
|
vim.api.nvim_set_current_win(win)
|
||||||
|
vim.cmd.edit(filepath)
|
||||||
|
else
|
||||||
|
vim.cmd.split(filepath)
|
||||||
|
end
|
||||||
|
|
||||||
|
M.on_attach(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Query an agenda note's file path.
|
||||||
|
---@param date string Either the date or backlog name.
|
||||||
|
---@param opts agenda.Opts?
|
||||||
|
---@return string? filepath
|
||||||
|
---@return string? err
|
||||||
|
function M.agenda(date, opts)
|
||||||
|
local env = {
|
||||||
|
-- An empty environment table is invalid.
|
||||||
|
AGENDA_DIR = vim.env.AGENDA_DIR
|
||||||
|
}
|
||||||
|
local cmd = { 'agenda', '-E' }
|
||||||
|
|
||||||
|
if opts then
|
||||||
|
if opts.dir then
|
||||||
|
env.AGENDA_DIR = opts.dir
|
||||||
|
end
|
||||||
|
|
||||||
|
if opts.backlog then
|
||||||
|
cmd[#cmd + 1] = '-b'
|
||||||
|
end
|
||||||
|
|
||||||
|
if opts.create then
|
||||||
|
cmd[#cmd + 1] = '-c'
|
||||||
|
end
|
||||||
|
|
||||||
|
if opts.extension then
|
||||||
|
cmd[#cmd + 1] = '-e'
|
||||||
|
cmd[#cmd + 1] = opts.extension
|
||||||
|
end
|
||||||
|
|
||||||
|
if opts.name then
|
||||||
|
cmd[#cmd + 1] = '-t'
|
||||||
|
cmd[#cmd + 1] = opts.name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
cmd[#cmd + 1] = date
|
||||||
|
|
||||||
|
local filepath = ''
|
||||||
|
local err = ''
|
||||||
|
local pid = fn.jobstart(cmd, {
|
||||||
|
env = env,
|
||||||
|
on_stdout = function(_, data)
|
||||||
|
filepath = filepath .. data[1]
|
||||||
|
end,
|
||||||
|
on_stderr = function(_, data)
|
||||||
|
err = err .. data[1]
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
local code = unpack(fn.jobwait { pid })
|
||||||
|
if code > 0 then
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
|
||||||
|
return filepath
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
78
lua/agenda/util.lua
Normal file
78
lua/agenda/util.lua
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
local M = {}
|
||||||
|
|
||||||
|
---Parses a list of arguments **kind of** like the getopts shell builtin.
|
||||||
|
---@param args string[]
|
||||||
|
---@param optstring string
|
||||||
|
---@return function
|
||||||
|
function M.getopts(args, optstring)
|
||||||
|
local spec = {}
|
||||||
|
local last
|
||||||
|
|
||||||
|
for i = 1, #optstring do
|
||||||
|
local c = optstring:sub(i, i)
|
||||||
|
|
||||||
|
if c == ':' and last then
|
||||||
|
spec[last] = true
|
||||||
|
elseif i == #optstring then
|
||||||
|
spec[c] = false
|
||||||
|
elseif last and last ~= ':' then
|
||||||
|
spec[last] = false
|
||||||
|
end
|
||||||
|
|
||||||
|
last = c
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 1. if the option takes a value either
|
||||||
|
-- a. take the rest of the slice as its values if at least one char
|
||||||
|
-- remains in the slice
|
||||||
|
-- b. take the next whole argument as its value
|
||||||
|
-- 2. find the next option name (char)
|
||||||
|
local i = 1 -- arg index
|
||||||
|
local n = 1 -- slice index
|
||||||
|
local g = 1 -- I am not very smart
|
||||||
|
return function ()
|
||||||
|
while args[i] do
|
||||||
|
local arg = args[i]
|
||||||
|
|
||||||
|
-- TODO: Maybe write some tests instead :^)
|
||||||
|
if g > 2097152 then
|
||||||
|
error 'logic error'
|
||||||
|
end
|
||||||
|
g = g + 1
|
||||||
|
|
||||||
|
if n == 1 and not vim.startswith(arg, '-') then
|
||||||
|
-- "Normal" arguments end the iterator.
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
n = n + 1
|
||||||
|
local c = arg:sub(n, n)
|
||||||
|
if #c > n then
|
||||||
|
n = n + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if #c == 1 then
|
||||||
|
if spec[c] then
|
||||||
|
local optval = arg:sub(n + 1, #arg)
|
||||||
|
n = 1
|
||||||
|
i = i + 1
|
||||||
|
if #optval > 0 then
|
||||||
|
return i, c, optval
|
||||||
|
end
|
||||||
|
-- Remaining argument does not contain optval, consume the next argument
|
||||||
|
-- as optval.
|
||||||
|
i = i + 1
|
||||||
|
return i, c, args[i - 1]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Do not handle nil. If the opt is invalid the caller must handle it.
|
||||||
|
return i + 1, c, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Stray '-' ends the iterator.
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
121
plugin/agenda.lua
Normal file
121
plugin/agenda.lua
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
local agenda = require 'agenda'
|
||||||
|
local getopts = require 'agenda.util'.getopts
|
||||||
|
|
||||||
|
-- If neovim was started by `agenda` the `AGENDA_FILE` env var will be set
|
||||||
|
-- with the file name.
|
||||||
|
if vim.env.AGENDA_FILE == vim.api.nvim_buf_get_name(0) then
|
||||||
|
agenda.on_attach(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Callbacks in the middle of a function signature are PAIN.
|
||||||
|
local function create_user_command(name, opts)
|
||||||
|
local command = opts.command
|
||||||
|
opts.command = nil
|
||||||
|
vim.api.nvim_create_user_command(name, command, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- TODO: Handle subcommands (kinda like git-fugitive)? Could be useful if I'll
|
||||||
|
-- add a remove subcommand.
|
||||||
|
-- TODO: Handle -E option (why not?).
|
||||||
|
create_user_command('Agenda', {
|
||||||
|
desc = 'Open a note file/buffer for the given day or the current day',
|
||||||
|
nargs = '*',
|
||||||
|
bang = true,
|
||||||
|
bar = true,
|
||||||
|
command = function (command)
|
||||||
|
local opts = {
|
||||||
|
create = command.bang,
|
||||||
|
}
|
||||||
|
|
||||||
|
local i = 1
|
||||||
|
for optind, opt, optval in getopts(command.fargs, 'bce:t:') do
|
||||||
|
if opt == 'b' then
|
||||||
|
opts.backlog = true
|
||||||
|
elseif opt == 'c' then
|
||||||
|
opts.create = true
|
||||||
|
elseif opt == 'e' then
|
||||||
|
opts.extension = optval
|
||||||
|
elseif opt == 't' then
|
||||||
|
opts.name = optval
|
||||||
|
else
|
||||||
|
local err = string.format("agenda: invalid option: '%s'", opt)
|
||||||
|
vim.notify(err, vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
i = optind
|
||||||
|
end
|
||||||
|
|
||||||
|
local args = {}
|
||||||
|
while command.fargs[i] do
|
||||||
|
args[#args + 1] = command.fargs[i]
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local date
|
||||||
|
if #args > 0 then
|
||||||
|
date = table.concat(args, ' ')
|
||||||
|
else
|
||||||
|
date = opts.backlog and 'backlog' or 'today'
|
||||||
|
end
|
||||||
|
|
||||||
|
agenda.open(date, opts)
|
||||||
|
end,
|
||||||
|
-- TODO: Handle supported agenda edit options.
|
||||||
|
complete = function (arg_lead, cmd_line, cursor_pos)
|
||||||
|
local modifiers = {
|
||||||
|
next = true,
|
||||||
|
last = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
local completions = {}
|
||||||
|
|
||||||
|
local final = {}
|
||||||
|
|
||||||
|
if cmd_line:match '^Agenda!? [%w-]*$' then
|
||||||
|
completions = {
|
||||||
|
'next ',
|
||||||
|
'last ',
|
||||||
|
'yesterday',
|
||||||
|
'tomorrow',
|
||||||
|
'today',
|
||||||
|
'day',
|
||||||
|
'week',
|
||||||
|
'month',
|
||||||
|
'year',
|
||||||
|
'monday',
|
||||||
|
'tuesday',
|
||||||
|
'wednesday',
|
||||||
|
'thursday',
|
||||||
|
'friday',
|
||||||
|
'saturday',
|
||||||
|
'sunday',
|
||||||
|
}
|
||||||
|
else
|
||||||
|
local first = cmd_line:match '^Agenda (%w+)%s+%w*$'
|
||||||
|
if modifiers[first] then
|
||||||
|
completions = {
|
||||||
|
'day',
|
||||||
|
'week',
|
||||||
|
'month',
|
||||||
|
'year',
|
||||||
|
'monday',
|
||||||
|
'tuesday',
|
||||||
|
'wednesday',
|
||||||
|
'thursday',
|
||||||
|
'friday',
|
||||||
|
'saturday',
|
||||||
|
'sunday',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, can in pairs(completions) do
|
||||||
|
if vim.startswith(can, arg_lead) then
|
||||||
|
final[#final + 1] = can
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return final
|
||||||
|
end,
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user