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.
- do-the-needful
- 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
![]() |
|---|
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 |
- 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
: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 filesAll commands support tab completion.
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 configThe :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 configPickers 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 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" },
})Tasks can be defined in 3 places:
- Setup opts
- Global config:
.tasks.jsonlocated invim.fn.stdpath("data") - Project config:
.tasks.jsonin the project directory
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)
}Each runner backend supports its own options via a provider key on the task.
These are optional and complement the shared window table.
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 = {
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 = {
split = "vsplit", -- vim split command (default: "botright split")
size = 40, -- size prefix for split command (e.g. 40vsplit)
}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%"
}
}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",
}Filter tasks by tag using the :Needful command or the API:
:Needful buildrequire("do-the-needful").please({ tags = { "build" } })Re-run the most recently executed task without opening a picker:
:NeedfulRerunrequire("do-the-needful").rerun()Tasks metadata can be defined to make it easier to filter in pickers:
tags = { "eza", "home", "files" },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
},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 ]]
},
}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,
}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" }){
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 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,
}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
}
...| Token | Description | Type | Value |
|---|---|---|---|
| ${cwd} | CWD for task | function | vim.fn.getcwd |
| ${do-the-needful} | Do the needful | string | "please" |
Use the :NeedfulEdit command or the actions picker to choose which config
to edit:
:NeedfulEdit project
:NeedfulEdit globalrequire("do-the-needful").edit_config("project")
:Telescope do-the-needful projectrequire("do-the-needful").edit_config("global")
:Telescope do-the-needful globalWhen 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: 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;
};
}>;
}My other neovim projects
Tmux theme:


