[Seeking] Converting a script to a Neovim plugin
-
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 -
-
Hey! I don’t know Neovim, but I just wanted to wish you good luck! That sounds like a fun project.
Hello! It looks like you're interested in this conversation, but you don't have an account yet.
Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.
With your input, this post could be even better 💗
Register Login