pilot.md

August 9, 2026 ยท View on GitHub


A Neovim plugin that allows you to run your project or file based on a JSON pilot file with placeholder support and customizable executors. You can edit files on the fly, and the plugin supports advanced features like checks for possible file paths, custom executors, and more.

This plugin is tested mainly on Neovim v0.12.x at the minimum. We always strive to use the latest Neovim major release version.

The source code for this plugin is available in the GitHub repository.


Features

  • Run arbitrary commands for any file or project, with full control over execution.
  • Placeholder interpolation for file paths, names, directories, and more.
  • On-the-fly configuration editing: No need to reload Neovim after changes.
  • Multiple pilot file paths: It searches your list of possible pilot file path locations.
  • Customizable file locations: Store files wherever you want.
  • Customizable executors: Run commands in new tabs, splits, vsplits, background jobs, or your own custom way.
  • Custom executors: Define your own ways to run commands, including integration with tools like tmux.
  • UI selection: If multiple commands are available, select which to run via vim.ui.select.
  • Automatic single-command execution: Optionally auto-run if only one command is available.
  • Purge/delete files: Easily remove or reset pilot files.
  • Import other files: Use "import" in your JSON to include commands from other files.
  • JSON validation and helpful errors: Clear error messages for misconfigured files.
  • Template writing: Optionally auto-generate a template when creating a new file.
  • Full Lua API: All features are accessible programmatically.

Installation

Using lazy.nvim:

return {
    "pewpewnor/pilot.nvim",
    opts = {},
}
-- or
return {
    "pewpewnor/pilot.nvim",
    config = function()
        require("pilot").setup()
    end,
}

Using packer.nvim:

use {
    "pewpewnor/pilot.nvim",
    config = function()
        require("pilot").setup()
    end
}

General Terms

  • Project pilot file: JSON file containing commands to run for the current project.
  • File type pilot file: JSON file containing commands to run for the current file type.

Default Configuration Values

{
    targets = {
        project = {
            pilot_file_path = function()
                return vim.fs.joinpath("{{pilot_data_path}}", "projects", "{{hash_sha256(cwd_path)}}.json")
            end, -- function(): string? | (function(): string?)[]
            auto_run_single_command = true, -- boolean
            default_executor = pilot.preset_executors.new_tab, -- function(command: string)
        },
    },
    write_template_to_new_pilot_file = true, -- boolean
    executors = {
        new_tab = pilot.preset_executors.new_tab,
        current_buffer = pilot.preset_executors.current_buffer,
        split = pilot.preset_executors.split,
        vsplit = pilot.preset_executors.vsplit,
        print = pilot.preset_executors.print,
        silent = pilot.preset_executors.silent,
        background_silent = pilot.preset_executors.background_silent,
        background_exit_status = pilot.preset_executors.background_exit_status,
    }, -- table<string, function(command: string, args: string[])>
    placeholders = {
        vars = {
            file_path = function()
                return vim.fn.fnameescape(vim.fn.expand("%:p"))
            end,
            file_path_relative = function()
                return vim.fn.fnameescape(vim.fn.expand("%"))
            end,
            file_name = function()
                return vim.fn.fnameescape(vim.fn.expand("%:t"))
            end,
            file_name_no_extension = function()
                return vim.fn.fnameescape(vim.fn.expand("%:t:r"))
            end,
            file_type = function()
                return vim.bo.filetype
            end,
            file_extension = function()
                return vim.fn.fnameescape(vim.fn.expand("%:e"))
            end,
            dir_path = function()
                return vim.fn.fnameescape(vim.fn.expand("%:p:h"))
            end,
            dir_name = function()
                return vim.fn.fnameescape(vim.fn.expand("%:p:h:t"))
            end,
            cwd_path = function()
                return vim.fn.fnameescape(vim.fn.getcwd())
            end,
            cwd_name = function()
                return vim.fn.fnameescape(
                    vim.fn.fnamemodify(vim.fn.getcwd(), ":t")
                )
            end,
            pilot_data_path = function()
                local pilot_data_path =
                    vim.fs.joinpath(vim.fn.stdpath("data"), "pilot")
                if vim.fn.isdirectory(pilot_data_path) == 0 then
                    vim.fn.mkdir(pilot_data_path, "p")
                end
                return vim.fn.fnameescape(pilot_data_path)
            end,
            cword = function()
                return vim.fn.expand("<cword>")
            end,
            cWORD = function()
                return vim.fn.expand("<cWORD>")
            end,
        },  -- table<string, function(): string>
        funcs = {
            hash_sha256 = function(arg)
                return vim.fn.sha256(arg)
            end,
        },  -- table<string, function(arg: string): string>
    },
    display = {
        numbered = true, -- boolean
        last_entry_new_line = false, -- boolean
    },
}

Configuration Options

targets

  • Type: table<string, RunTarget>

  • Description: A table mapping run target names to their configuration. Each run target can have its own pilot_file_path, auto_run_single_command, and default_executor.
    Built-in run targets: project, file_type. You can add custom run targets.

  • RunTarget structure:

    {
        pilot_file_path = function(): string? | (function(): string?)[], -- required
        auto_run_single_command = boolean, -- optional
        default_executor = function(command: string), -- optional
    }
    
  • Example:

    targets = {
        project = {
            pilot_file_path = "{{cwd_path}}/pilot.json",
            auto_run_single_command = true,
            default_executor = pilot.preset_executors.new_tab,
        },
        custom = {
            pilot_file_path = "{{cwd_path}}/custom.json",
            auto_run_single_command = false,
            default_executor = pilot.preset_executors.split,
        },
    }
    

write_template_to_new_pilot_file

  • Type: boolean
  • Description: If true, writes a JSON template when creating a new file (when editing a file that does not exist).

executors

  • Type: table<string, function(command: string, args: string[])>
  • Description: Table mapping executor names to executor functions.
    Used when a run file entry specifies a "executor" field.
    The executor function receives two arguments:
    • command (string): The shell command to run (with placeholders already expanded).
    • args (list of strings): The result from splitting the string that was written in the executor with whitespaces as the seperator and without the executor name (first argument) inside the list.

placeholders

  • Type: table with vars and funcs subtables
  • Description:
  • vars is a table mapping placeholder names to functions that return strings (e.g. file_name).
  • funcs is a table mapping placeholder function names to functions that accept an argument and return a string (e.g. hash_sha256).

display

  • Type: table with display / UI options
  • Description:
  • numbered whether to label each entry name with numbers when selecting an entry.
  • last_entry_new_line whether to add new line on the last entry name when selecting an entry.

Example Customization

local pilot = require("pilot")
pilot.setup({
    targets = {
        project = {
            -- customize where to find the run file path when running a project
            pilot_file_path = {
                function() return "{{cwd_path}}/pilot.json" end,
                function() return "{{cwd_path}}/.vscode/pilot.json" end,
                function()
                    if vim.fn.filereadable(vim.fn.getcwd() .. "/package-lock.json") == 1 then
                        return "{{pilot_data_path}}/npm_project.json"
                    end
                end,
            },
        },
        -- customize what happens when attempting to run a file type
        file_type = {
            auto_run_single_command = false,
            default_executor = pilot.preset_executors.split,
        },
        -- create a custom target that you can run
        universal = {
            pilot_file_path = function() return "/home/user/universal_pilot.json" end,
        },
    },
    write_template_to_new_pilot_file = false,
    -- define custom executors that can be used in any pilot file
    executors = {
        -- custom executor that executes the command in a new tmux window
        tmux_new_window = function(command, args)
            vim.fn.system("tmux new-window -d")
            vim.fn.system("tmux send-keys -t +. '" .. command .. "' Enter")
        end,
        background = pilot.preset_executors.background_exit_status,
    },
    placeholders = {
        vars = {
            -- example to add custom placeholders
            new_temp_file = function() return vim.fn.tempname() end,
            template_path = function() return pilot.utils.interpolate("{{pilot_data_path}}/templates") end,
        },
    },
    -- perhaps better display view for vanilla vim.ui.select when selecting entries
    display = {
        numbered = false,
        last_entry_new_line = true,
    },
})

vim.keymap.set("n", "<F10>", function() pilot.run("project") end)
vim.keymap.set("n", "<F12>", function() pilot.run("file_type") end)
vim.keymap.set("n", "<F11>", pilot.run_previous_task)
vim.keymap.set("n", "<Leader><F10>", function() pilot.edit_pilot_file("project") end)
vim.keymap.set("n", "<Leader><F12>", function() pilot.edit_pilot_file("file_type") end)

vim.api.nvim_create_user_command("PilotDeleteProjectPilotFile",
    function() pilot.delete_pilot_file("project") end, { nargs = 0, bar = false })
vim.api.nvim_create_user_command("PilotDeleteFileTypePilotFile",
    function() pilot.delete_pilot_file("file_type") end, { nargs = 0, bar = false })

Pilot File Format

Both project and file type pilot files use the same JSON format: an array of entries.

Each entry can be:

  • A string (the command to run)
  • An object with fields:
    • name (optional): Display name for the command.
    • cmd: String or array of strings (joined with &&).
    • executor (optional): Name of an executor that exists in executors.
    • import (optional): Path to another JSON file to import entries from.
      Imported entries are merged in place.

Example Project Pilot File

[
    {
        "name": "build & run project",
        "cmd": "make build && make run"
    },
    {
        "name": "run hovered test function name",
        "cmd": "go test -v --run {{cword}}"
    },
    {
        "cmd": ["ls {{dir_path}}", "touch 'hello world.txt'"],
        "executor": "tmux_new_window"
    },
    "echo Hello, World!"
]

Tip:
Use the mustache syntax like {{cword}} to insert a placeholder that will automatically be replaced by pilot.nvim on the fly!


Example File Type Pilot File

Let's say you want to write a file type pilot file for compiling and running C source code files.

[
    {
        "name": "clang",
        "cmd": "clang {{file_path_relative}} && ./a.out"
    },
    "gcc {{file_path}} -o {{file_name_no_extension}} ; ./{{file_name_no_extension}}"
]

Tip:
For each entry, you don't have to specify a display name if you want it to be the same as the raw command string. You can also instead use a string for defining an entry/command.

Importing/Including Existing Pilot Files:

[{ "import": "{{pilot_data_path}}/common_commands.json" }]

Placeholders

All placeholders are expanded in config paths and commands. You can escape a placeholder by using triple braces, e.g. {{{not_a_placeholder}}}.

Variables

Simple placeholders that expand to a string. Define them in placeholders.vars as functions that return a string, and use them as {{name}} in config paths or commands.

PlaceholderResolved value
{{file_path}}Current buffer's absolute file path
{{file_path_relative}}File path relative to current working directory
{{file_name}}File name (with extension)
{{file_name_no_extension}}File name without extension
{{file_type}}Filetype of current buffer (vim.bo.filetype)
{{file_extension}}File extension
{{dir_path}}Directory containing the current buffer
{{dir_name}}Name of the directory containing the current buffer
{{cwd_path}}Absolute path of the current working directory
{{cwd_name}}Name of the current working directory
{{config_path}}Absolute path to your Neovim configuration directory
{{data_path}}Absolute path to Neovim plugins data directory
{{pilot_data_path}}Absolute path to the pilot directory inside of Neovim plugins data directory
{{cword}}Word under the cursor
{{cWORD}}WORD under the cursor

Functions

Callable placeholders that accept an argument and return a string. Define them in placeholders.funcs as functions that accept a single argument and return a string, and use them like {{name(arg)}} in configs.

PlaceholderDescription / usage
{{hash_sha256(...)}}SHA256 hash of the supplied path or string (e.g. {{hash_sha256(cwd_path)}}).

Add custom vars to placeholders.vars and custom functions to placeholders.funcs.


Preset Executors

Executors are functions that run the command in a specific way.
All executors receive two arguments:

  • command (string): The shell command to run (with placeholders already expanded).
  • args (table): List of arguments (see executors).

Built-in Executors

| Executor | Description | | ----------------------------------------------- | ------------------------------------------------------------------------------- | ------------- | | pilot.preset_executors.new_tab (default) | Run the command in a new Neovim tab. Uses :tabnew | term <cmd>. | | pilot.preset_executors.current_buffer | Run the command in the current buffer (replaces buffer with terminal). | | pilot.preset_executors.split | Run the command in a new horizontal split (:split | term <cmd>). | | pilot.preset_executors.vsplit | Run the command in a new vertical split (:vsplit | term <cmd>). | | pilot.preset_executors.print | Run the command and print output to a message (blocking, uses vim.fn.system). | | pilot.preset_executors.silent | Run the command silently (blocking, no output shown). | | pilot.preset_executors.background_silent | Run the command as a background job (no output, uses vim.fn.jobstart). | | pilot.preset_executors.background_exit_status | Run as background job, print exit status on completion. |

Custom Executors

You can define your own executor functions and add them to executors.
The executor function signature is:

function(command: string, args: string[])
    -- implementation
end

Example:

executors = {
    tmux_new_window = function(command, args)
        vim.fn.system("tmux new-window -d")
        vim.fn.system("tmux send-keys -t +. '" .. command .. "' Enter")
    end,
    vsplit = require("pilot").executors.vsplit,
}

Plugin Functions

All functions are available via require("pilot").

Function NameDescription
setup(options)Configure pilot.nvim. See configuration options.
run_target(target_name)Run a command from the specified target. Prompts if multiple commands available.
run_previous_task()Re-run the last executed task.
edit_pilot_file(target_name)Open the pilot file for the specified target for editing (creates template if missing).
delete_pilot_file(target_name)Delete the pilot file for the specified target.

Tips & Recommendations

  • Use telescope-ui-select.nvim or mini.nvim's mini-pick for a better vim.ui.select() experience.
  • You can import common commands into multiple configs using the "import" key.
  • Placeholders can be escaped by using extra braces, e.g. {{{not_a_placeholder}}}.
  • To disable template writing for new files, set write_template_to_new_pilot_file = false.
  • All config files are validated on load; errors are shown in the command line.

FAQ

Q: How do I add a new executor?
A: Add a function to executors in your config and reference its key in your pilot file's "executor".

Q: How do I use placeholders in config paths?
A: All config paths support placeholders like {{cwd_path}}, {{file_type}}, etc.

Q: Can I use arrays for the cmd field?
A: Yes, arrays are joined with && to form a single shell command.

Q: What is passed to custom executors?
A: Both the expanded command string and an arg string (see preset executors).

Q: How do I contribute to this project? A: See CONTRIBUTING.md for guidelines.