Seeked skill
If you know how to build a Neovim plugin and allow for custom user keybinding, we can be friend 
The project
Few months ago, I decided to build something that would allow one to stay in Neovim while interacting with Taskwarrior. I didn’t want to reinvent Taskwarrior so I went with a simple script limited to my needs. After sharing it online, some people showed interest in this tool. As a result, I’ve added few features, but the remaining issue is to convert this script as a real Neovim plugin.
The features
-
Create/edit/update a task: Type #TW some text and use the default keybinding <leader>ta to create a task with some text as description, and add a task annotation in the form of “+line filepath” so you can easily access this task’s line from Taskwarrior.
The script will recognize the #TW pattern and ask for a project name, start and due date, and tags for this task. By default, the due date is set to start+1h to fit my specific needs, but you can change that by editing line 90.
All these fields are optional. The task will be added to Taskwarrior, and the task UUID will be appended to the line which will be commented.
-
Delete a task: Using the default <leader>td keybinding will delete the current line if it has a valid task UUID and remove the task in Taskwarrior. In the background, it will also add task’ annotations to all tasks below the current line.
-
Undo actions: Revert the last delete action with the default <leader>tu keybinding.
-
Retrieve task info summary: With the default <leader>ti keybinding, can call a notification window that will show you a summary of the task info.
-
Mark task as completed: Can be done using <leader>tc keybinding.
The missing steps
I’ve never built a real Neovim plugin. and this project was designed to be one more script to my collection. Hence, there are two issues remaining to be solved to convert this idea to a plugin:
- Create a correct git structure to allow for pulling by Neovim plugin managers.
- Allow for users to customize the keybindings.
The current script
local M = {}
-- Default
M.keybindings.keybindings = {
create_or_update_task = "<leader>ta",
task_delete = "<leader>td",
task_undo = "<leader>tu",
task_info = "<leader>ti",
}
-- Function to allow users to define their own keybindings
function M.setup(custom_keybindings)
-- Merge custom keybindings with the default ones
if custom_keybindings then
for action, key in pairs(custom_keybindings) do
if M.keybindings[action] then
M.keybindings[action] = key
end
end
end
-- Rebind the keys based on the defined keybindings
vim.keymap.set("n", M.keybindings.create_or_update_task, function()
M.create_or_update_task()
end)
vim.keymap.set("n", M.keybindings.task_delete, function()
M.task_delete()
end)
vim.keymap.set("n", M.keybindings.task_undo, function()
M.task_undo()
end)
vim.keymap.set("n", M.keybindings.task_info, function()
M.task_info()
end)
end
-- Annotation update function
function M.annotation_update(line_nb)
if line_nb == 0 then
line_nb = vim.fn.line(".")
end
local total_lines = vim.api.nvim_buf_line_count(0)
for line = line_nb, total_lines do
local current_line = vim.fn.getline(line)
local task_id = string.match(current_line, "UUID: ([%w-]+)")
local annot_line_cmd = string.format("task %s export | jq '.[].annotations.[-1].description'", task_id)
local annot = vim.fn.system(annot_line_cmd)
local annot_line = string.match(annot, "+(%d+)")
annot_line = tonumber(annot_line)
if annot ~= "" and annot_line ~= line then
local file_path = vim.fn.expand("%:p")
local annotation = string.format("+%s %s", line, vim.fn.shellescape(file_path))
local annotate_cmd = string.format('task %s annotate "%s"', task_id, annotation)
vim.fn.system(annotate_cmd)
vim.notify("Annotation(s) updated")
elseif task_id and annot == "" then
vim.notify("Can't find UUID on line " .. line)
end
end
end
-- Create or update task
function M.create_or_update_task()
local current_line = vim.fn.getline(".")
local file_path = vim.fn.expand("%:p") -- Get full path of current file
local line_number = vim.fn.line(".") -- Get current line number
-- Ask for parameters
local task_tag = ""
local start = vim.fn.input("Start date (MMDDYYHH:MM): ")
local due = vim.fn.input("Due date (default: start+1h): ")
local project = vim.fn.input("Project name: ")
local has_note = false
local additional_tags_input = vim.fn.input("Tags (separated by spaces): ")
local additional_tags = {}
-- Keywords to look for
local keywords = { "#TW" }
for _, keyword in ipairs(keywords) do
local kw_start_index, kw_end_index = string.find(current_line, keyword)
-- Check line validity
if not kw_start_index then
vim.notify("No valid keyword found")
else
local id_keyword = ":: UUID:"
local task_id = string.match(current_line, "UUID: ([%w-]+)")
local id_start_index = string.find(current_line, id_keyword)
local task_cmd
if task_id then
local task_description = string.sub(current_line, kw_end_index + 2, id_start_index - 2)
task_cmd = string.format('task %s mod %s "%s"', task_id, task_tag, task_description)
else
local task_description = string.sub(current_line, kw_end_index + 1)
task_cmd = string.format('task add %s "%s"', task_tag, task_description)
end
-- Add additional tags if available
for tag in additional_tags_input:gmatch("%S+") do
table.insert(additional_tags, "+" .. tag)
if string.match(tag, "note") then
has_note = true
end
end
if #additional_tags > 0 then
task_cmd = task_cmd .. " " .. table.concat(additional_tags, " ")
end
-- Add project if available
if #project > 0 then
task_cmd = task_cmd .. " project:" .. project
elseif project == " " then
task_cmd = task_cmd .. " project:"
end
-- Add start date if available
if #start > 0 then
task_cmd = task_cmd .. " start:" .. start
end
-- Add due date if available and tag is not note
if #due > 0 and not has_note then
task_cmd = task_cmd .. " due:" .. due
elseif has_note then
task_cmd = task_cmd .. " due:"
elseif due == " " then
task_cmd = task_cmd .. " due:"
else
task_cmd = task_cmd .. " due:start+1h"
end
-- Execute the task add command
local output = vim.fn.system(task_cmd)
-- Task update notification
local task_id = string.match(current_line, "UUID: ([%w-]+)")
if task_id then
vim.notify("Task updated")
end
-- Add annotation to new task
local new_task_id = string.match(output, "Created task (%d+)%.")
if new_task_id then
local tasks_number_cmd = "task count status=pending"
local tasks_number = vim.fn.system(tasks_number_cmd)
tasks_number = tasks_number:gsub("%s+$", "")
local new_task_id_cmd = string.format("task %s export | jq '.[].uuid' | sed 's/\"//g'", tasks_number)
new_task_id = vim.fn.system(new_task_id_cmd)
new_task_id = new_task_id:gsub("%s+$", "")
-- Annotate task with filename and line number
local annotation = string.format("+%s %s", line_number, vim.fn.shellescape(file_path))
local annotate_cmd = string.format('task %s annotate "%s"', new_task_id, annotation)
vim.fn.system(annotate_cmd)
vim.notify("Task created")
-- Add UUID to line
local line_id = current_line .. " :: UUID: " .. new_task_id
vim.fn.setline(".", line_id)
-- Comment the line
vim.api.nvim_command("normal! gcc")
elseif not task_id then
vim.notify("Failed to extract task ID")
end
-- Update annotation on line change
M.annotation_update(0)
end
end
end
-- Task delete function
function M.task_delete()
local current_line = vim.fn.getline(".")
local task_id = string.match(current_line, "UUID: ([%w-]+)")
local status_cmd = string.format("task %s export | jq '.[].status' | sed 's/\"//g'", task_id)
if task_id then
local delete_cmd = string.format("task rc.confirmation=off del %s", task_id)
vim.fn.system(delete_cmd)
vim.notify("Task " .. task_id .. " deleted")
vim.api.nvim_command("normal! dd")
M.annotation_update(0)
else
vim.notify("Can't find UUID task")
end
end
function M.task_complete()
local current_line = vim.fn.getline(".")
local task_id = string.match(current_line, "UUID: ([%w-]+)")
if task_id then
local complete_cmd = string.format("task %s mod status:completed", task_id)
vim.fn.system(complete_cmd)
vim.notify("Task " .. task_id .. " completed")
M.annotation_update(0)
else
vim.notify("Can't find UUID task")
end
end
-- Undo function
function M.task_undo()
local undo_cmd = string.format("task rc.confirmation=off undo")
local undo_output = vim.fn.system(undo_cmd)
vim.cmd("undo")
vim.notify("Undo output: ", undo_output)
end
-- Task info function
function M.task_info()
local current_line = vim.fn.getline(".")
local task_id = string.match(current_line, "UUID: ([%w-]+)")
local info_cmd = string.format("task %s info | head -n 12", task_id)
local info = vim.fn.system(info_cmd)
vim.notify(info)
end
return M