do-the-needful
Task runner with multiple picker and runner backends. Pick a task, run it anywhere. Task commands can use tokens that are parsed at execution time.
About
- Tasks can be defined in setup opts, project or global config
- Tasks run via auto-detected backends: tmux, zellij, toggleterm, or a built-in neovim terminal
- Pick tasks with telescope, fzf-lua, snacks.nvim, or
vim.ui.select - Task tags make it easy to filter in any picker
- Tokens can be defined globally or scoped to a task and are parsed by an evaluated function or user input
- Per-task runner override lets you mix backends (e.g. most tasks in tmux, tests in a neovim terminal)
- Re-run the last executed task with
:NeedfulRerun - When editing a new project or global tasks, a default config will be created if one doesn't exist
Screenshots
|  |
|---|
_Actions picker_ (:Telescope do-the-needful) |
|  |
|---|
_Task selection picker_ (:Telescope do-the-needful please) |
|  |
|---|
_Prompting for input using ask function_ |
|  |
|---|
| _Spawned task will close upon completion_ |
Requirements
- Neovim >= 0.10
- plenary.nvim
- One of the following pickers (optional, falls back to
vim.ui.select): telescope.nvim, fzf-lua, snacks.nvim - One of the following runners (optional, falls back to neovim terminal): tmux, zellij, toggleterm.nvim
Usage
Commands
:Needful [tags...] " Open task picker, optionally filter by tags :NeedfulRerun " Re-run the last executed task :NeedfulRun <name> " Run a task by name (tab completion supported) :NeedfulEdit [project|global] " Edit config files
All commands support tab completion.
API
require("do-the-needful").please() -- Open task picker require("do-the-needful").please({ tags = { "build" } }) -- Filter by tags require("do-the-needful").actions() -- Open actions picker require("do-the-needful").rerun() -- Re-run last task require("do-the-needful").run_by_name("tests") -- Run task by name require("do-the-needful").edit_config("project") -- Edit project config require("do-the-needful").edit_config("global") -- Edit global config
Telescope pickers
The :Telescope extension continues to work for backward compatibility:
:Telescope do-the-needful " Actions picker :Telescope do-the-needful please " Task picker :Telescope do-the-needful project " Edit project config :Telescope do-the-needful global " Edit global config
Pickers
Pickers are auto-detected in priority order. The first available picker is
used. Set picker to use a specific backend or picker_priority to change
the order.
| Picker | Key | Requires |
|---|---|---|
| Telescope | telescope |
telescope.nvim |
| fzf-lua | fzf_lua |
fzf-lua |
| snacks | snacks |
snacks.nvim with picker |
| vim.ui.select | ui_select |
Nothing (always available) |
-- Use a specific picker require("do-the-needful").setup({ picker = "fzf_lua", }) -- Or change the auto-detection order require("do-the-needful").setup({ picker_priority = { "fzf_lua", "telescope", "snacks", "ui_select" }, })
Runners
Runners are auto-detected in priority order. The first available runner is
used. Set runner to use a specific backend or runner_priority to change
the order.
| Runner | Key | Available when |
|---|---|---|
| tmux | tmux |
$TMUX is set |
| zellij | zellij |
$ZELLIJ is set |
| toggleterm | toggleterm |
toggleterm.nvim installed |
| neovim | neovim |
Always available |
-- Use a specific runner require("do-the-needful").setup({ runner = "neovim", }) -- Or change the auto-detection order require("do-the-needful").setup({ runner_priority = { "tmux", "zellij", "toggleterm", "neovim" }, })
Features
Tasks
Tasks can be defined in 3 places:
- Setup opts
- Global config:
.tasks.jsonlocated invim.fn.stdpath("data") - Project config:
.tasks.jsonin the project directory
Window options
Runner backends read the window table from each task:
window = {
name = "name", -- window/pane name
close = false, -- close after execution
keep_current = false, -- keep focus on current window
open_relative = true, -- open after/before current window (tmux)
relative = "after", -- "after" or "before" (tmux)
}
Provider-specific options
Each runner backend supports its own options via a provider key on the task.
These are optional and complement the shared window table.
tmux
tmux = {
split = true, -- use split-window instead of new-window
direction = "horizontal", -- "horizontal" or "vertical" (split only)
size = "30%", -- pane size, e.g. "30%" or 20 (split only)
full_span = true, -- full-width/height split (split only)
reuse = true, -- reuse window with same name (new-window only)
environment = { -- environment variables passed with -e
MY_VAR = "value",
},
}
zellij
zellij = {
direction = "down", -- "up", "down", "left", "right"
floating = true, -- open as floating pane
in_place = true, -- open in place of current pane
start_suspended = true, -- start pane suspended
width = "80%", -- pane width
height = "50%", -- pane height
x = 10, -- pane x position
y = 5, -- pane y position
}
Note:
window.keep_currentno longer maps to--floatingin zellij. Usezellij = { floating = true }instead.
neovim
neovim = {
split = "vsplit", -- vim split command (default: "botright split")
size = 40, -- size prefix for split command (e.g. 40vsplit)
}
toggleterm
toggleterm = {
direction = "float", -- "horizontal", "vertical", "float", "tab"
size = 20, -- terminal size
}
JSON example:
{
"name": "tests",
"cmd": "make test",
"tmux": {
"split": true,
"direction": "vertical",
"size": "40%"
}
}
Per-task runner override
Individual tasks can specify which runner to use regardless of the global setting:
{
"name": "tests",
"cmd": "make test",
"runner": "neovim"
}
{
name = "tests",
cmd = "make test",
runner = "neovim",
}
Tag filtering
Filter tasks by tag using the :Needful command or the API:
:Needful build
require("do-the-needful").please({ tags = { "build" } })
Re-run last task
Re-run the most recently executed task without opening a picker:
:NeedfulRerun
require("do-the-needful").rerun()
Task metadata
Tasks metadata can be defined to make it easier to filter in pickers:
tags = { "eza", "home", "files" },
Global token replacement
The following task fields are parsed for tokens:
- cmd
- name
- cwd
${tokens} can be defined to be replaced in the task configuration:
global_tokens = {
["${cwd}"] = vim.fn.cwd,
["${do-the-needful}"] = "please",
["${projectname}"] = function()
return vim.fn.system("basename $(git rev-parse --show-toplevel)")
end
},
Prompting for input
Tasks can be configured to prompt for input. Token values are replaced by
global_tokens values or evaluated ask_functions:
Ask tokens are defined in each task's ask table (opt) or json object (project
and global)
ask = { -- Used to prompt for input to be passed into task
["${dir}"] = {
title = "Which directory to search", -- defaults to the name of token
type = "function", -- function or string
default = "get_cwd", --[[ defaults to "" if omitted. If ask.type is a value
other than "function", the literal value of default will be used. If
ask.type is "function", the named function in the ask_functions table will
be evaluated for the default value passed into vim.ui.input ]]
},
}
Setup
Example Lazy.nvim config
local opts = { picker = "auto", -- "auto", "telescope", "fzf_lua", "snacks", "ui_select" runner = "auto", -- "auto", "tmux", "zellij", "neovim", "toggleterm" tasks = { { name = "eza", -- name of task cmd = "eza ${dir}", -- command to run cwd = "~", -- working directory to run task tags = { "eza", "home", "files" }, -- task metadata used for searching ask = { -- Used to prompt for input to be passed into task ["${dir}"] = { title = "Which directory to search", -- defaults to the name of token type = "function", -- function or string default = "get_cwd", -- defaults to "". If ask.type is string, the literal -- value of default will be used. If ask.type is function the named -- function in the ask_functions section will be evaluated for the default }, }, window = { -- all window options are optional name = "Eza ~", -- window name close = false, -- close after execution keep_current = false, -- keep focus on current window open_relative = true, -- open after/before current window (tmux) relative = "after", -- relative direction if open_relative = true }, }, { name = "ripgrep current directory", cmd = "rg ${pattern} ${cwd}", tags = { "ripgrep", "cwd", "search", "pattern" }, ask = { ["${pattern}"] = { title = "Pattern to use", default = "error", }, }, window = { name = "Ripgrep", close = false, keep_current = true, }, }, }, edit_mode = "buffer", -- buffer, tab, split, vsplit config_file = ".tasks.json", -- name of json config file for project/global config config_order = { -- default: { project, global, opts }. Order in which -- tasks are aggregated "project", -- .task.json in project directory "global", -- .tasks.json in stdpath('data') "opts", -- tasks defined in setup opts }, tag_source = true, -- display #project, #global, or #opt after tags global_tokens = { ["${cwd}"] = vim.fn.getcwd, ["${do-the-needful}"] = "please", ["${projectname}"] = function() return vim.fn.system("basename $(git rev-parse --show-toplevel)") end, }, ask_functions = { get_cwd = function() return vim.fn.getcwd() end, current_file = function() return vim.fn.expand("%") end, }, } return { "catgoose/do-the-needful.nvim", event = "BufReadPre", keys = { { "<leader>;", [[<cmd>Needful<cr>]], "n" }, { "<leader>:", [[<cmd>NeedfulRerun<cr>]], "n" }, }, dependencies = "nvim-lua/plenary.nvim", opts = opts, }
Telescope setup
If using Telescope as your picker, load the extension in your Telescope setup:
telescope.load_extension("do-the-needful")
Telescope defaults can be set in Telescope setup:
require("telescope").setup({ ... extensions = { ["do-the-needful"] = { winblend = 10, }, } })
Telescope options can also be passed into please or actions to override the
above set defaults:
require("do-the-needful").please({ winblend = 5 }) require("do-the-needful").actions({ prompt_title = "Actions" })
Configuration
Default setup opts
{
log_level = "warn",
tasks = {},
edit_mode = "buffer",
config_file = ".tasks.json",
config_order = {
"project",
"global",
"opts",
},
tag_source = true,
picker = "auto",
runner = "auto",
picker_priority = { "telescope", "fzf_lua", "snacks", "ui_select" },
runner_priority = { "tmux", "zellij", "toggleterm", "neovim" },
global_tokens = {
["${cwd}"] = vim.fn.getcwd,
["${do-the-needful}"] = "please",
},
ask_functions = {},
}
Ask functions
Ask functions can be defined to evaluate default values for the token prompt:
ask_functions = {
["get_cwd"] = vim.fn.getcwd,
["current_file"] = function()
return vim.fn.expand("%")
end,
}
Ask tokens
The value for default can refer to a literal value or a defined ask_function.
If the value of ask.type is "function" the corresponding ask_function
defined in setup opts will be evaluated upon task selection. This value will
be used for the default value in the token prompt dialog.
In the following example the ask_function dir will be evaluated and replace
the token ${dir} in the task command.
{
"ask": {
"${dir}": {
"title": "Which directory?",
"type": "function",
"default": "dir"
}
}
}
...
ask_functions = {
dir = vim.fn.getcwd
}
...
Global tokens defaults
| Token | Description | Type | Value |
|---|---|---|---|
| ${cwd} | CWD for task | function | vim.fn.getcwd |
| ${do-the-needful} | Do the needful | string | "please" |
Editing project and global configs
Use the :NeedfulEdit command or the actions picker to choose which config
to edit:
:NeedfulEdit project :NeedfulEdit global
Project config
require("do-the-needful").edit_config("project") :Telescope do-the-needful project
Global config
require("do-the-needful").edit_config("global") :Telescope do-the-needful global
New configs
When calling the task config editing functions if the respective
.tasks.json does not exist, an example task will be created
{
"tasks": [
{
"name": "",
"cmd": "",
"tags": [""],
"window": {
"name": "",
"close": false,
"keep_current": false,
"open_relative": true,
"relative": "after"
}
}
]
}
tasks JSON schema
{
tasks: Array<{
name: string;
cmd: string;
tags: string[];
runner: "tmux" | "zellij" | "neovim" | "toggleterm"; // optional override
ask: {
"${token}": {
title: string;
type: "string" | "function";
default: string;
};
};
window: {
name: string;
close: boolean;
keep_current: boolean;
open_relative: boolean;
relative: "before" | "after";
};
tmux?: {
split: boolean;
direction: "horizontal" | "vertical";
size: string | number;
full_span: boolean;
reuse: boolean;
environment: Record<string, string>;
};
zellij?: {
direction: "up" | "down" | "left" | "right";
floating: boolean;
in_place: boolean;
start_suspended: boolean;
width: string | number;
height: string | number;
x: string | number;
y: string | number;
};
neovim?: {
split: string;
size: number;
};
toggleterm?: {
direction: "horizontal" | "vertical" | "float" | "tab";
size: number;
};
}>;
}
Extra
Neovim
My other neovim projects
Tmux
Tmux theme: