cling.nvim implements a customisable and thin CLI wrapper around executable binaries in Neovim.
It can be used to quickly execute terminal commands:
- without leaving the Neovim context via multiplexing or
Ctrl+z, - without losing the text formatting of the command outputs,
- and also to interact with the output of those commands as a text-buffer.
The plugin can also be configured to wrap CLI commands that you commonly use (like jj, docker, etc.) and:
- automatically generate tab-completions in Neovim,
- implement custom keymaps for those wrapped CLI output buffers,
- automatically close the terminal buffer when the process exits (useful for interactive TUI tools).
Note
Autogenerating tab-completions in Neovim is an experimental feature.
It may not work for all available CLI tools as there is no standard way to implement subcommands and completion functions in Bash. If such as instance is encountered, please raise an Issue ticket.
- Neovim 0.10+
bash-completion: Many CLI tools' completion scripts depend on this.bash(obviously)curl(optional) for fetching remote completion scripts (Method 4).
Using lazy.nvim:
return {
"juniorsundar/cling.nvim",
config = function()
require("cling").setup({
wrappers = {
{
binary = "jj",
command = "JJ",
completion_cmd = "jj util completion bash",
},
-- {}, ...
}
})
end,
}The plugin exposes the global :Cling command, which serves as a generic entry point for executing shell commands within the plugin's environment:
:Cling: Opens an input prompt to enter a shell command interactively.:Cling with-env: Executes command with an.envfile assigned interactively.:Cling last: Executes the last executed command with.env.:Cling -- <command>: Executes the command, treating everything after--as the command string. This defaults to executing in current working directory.
When a command is executed, the output is displayed in a dedicated terminal-filetype buffer. The following default keymaps are available:
q: Closes the Cling window.<CR>(Enter): Smart file navigation. If the cursor is on a file path (common ingrep,ls, or compiler output), pressing Enter will attempt to open that file in the previous window. It supportsfile:line:colformats to jump directly to the specific location.ge: Export the terminal output to a file. ANSI escape codes are stripped and metadata (command, CWD, timestamp) is appended as comments.
The :Cling command (and all wrapper commands) respect Neovim's built-in
command modifiers for controlling split direction:
| Modifier | Result |
|---|---|
:Cling -- ls |
Bottom horizontal split (default) |
:vert Cling -- ls |
Vertical split |
:tab Cling -- ls |
New tab |
:top Cling -- ls |
Top horizontal split |
:bot Cling -- ls |
Bottom horizontal split (explicit) |
These modifiers work with wrapper commands as well:
:vert JJ log
:tab Docker psYou can export the terminal output from any Cling buffer to a file by
pressing ge in normal mode while in the output buffer. The export will:
- Prompt you for a file path (defaults to
cling-output.login CWD) - Append a metadata footer as vim modeline comments:
-- Command: echo hello
-- CWD: /home/user/project
-- Timestamp: 2026-03-06T12:00:00Z
-- vim: ft=log
Cling passes commands to the shell literally by default. %, #, <, and
> keep their normal shell meanings, so existing commands keep working as
before.
To expand a filename or cursor value, prefix the token with @:
cat @% → cat /path/to/current.lua
grep @<cWORD> src/ → grep WordUnderCursor src/
Cling consumes @ only when it is directly before a supported token. Other
uses of @ pass through unchanged. Use @@ for a literal @:
echo me@example.com → echo me@example.com
echo "type @@%" → echo "type @%"
| Marker | Expands to |
|---|---|
@% |
the current file |
@# |
the alternate file |
@#N |
the file for buffer N |
@<cword> |
the word under the cursor |
@<cWORD> |
the WORD under the cursor. Unlike <cword>, it keeps punctuation such as foo.bar. |
@<cfile> |
the file path under the cursor |
Add modifiers after a token to transform its value:
- Path:
:p(full path),:~(relative to$HOME),:.(relative to the CWD) - Anatomy:
:h(directory),:t(basename),:r(name without extension),:e(extension) - Shell-safety:
:S/:q(shell-escape the value)
Modifiers run from left to right. Given a current file of
/home/user/proj/src/cling.lua:
@% → /home/user/proj/src/cling.lua
@%:h → /home/user/proj/src
@%:t → cling.lua
@%:t:r → cling
@%:e → lua
@%:p:h:t → src
Substitution modifiers (:s///, :gs///) are not supported. A marked token
using either form passes through unchanged.
Cling resolves paths against the execution CWD selected at the prompt, not the
editor's current directory. In particular, @%:. is relative to the directory
where Cling runs the command.
Unmarked %, #, <, >, and backticks keep their shell meanings. These
commands are unchanged:
printf '%s\n' hi
make x # note
sort < a > b
echo `date`
Expansion applies to commands entered through the Cling prompt, :Cling -- ...,
and replayed history. Wrapper commands such as :JJ and :Docker do not
expand markers; their arguments stay literal.
You can define custom wrappers for your CLI tools in the setup function. Wrappers allow you to create specific Neovim user commands (e.g., :JJ, :Docker) with autocompletions that can either be derived from the CLI tool itself, or from the completion bash file.
| Option | Type | Description |
|---|---|---|
separate_history |
boolean |
Enable per-CWD command history. When true (default), commands are grouped by working directory and persisted to stdpath("data")/cling/history/. When false, uses Neovim's native input history for the command prompt. |
| Field | Type | Description |
|---|---|---|
binary |
string |
The binary (or shell command string) to execute. |
command |
string |
The Neovim user command name to register (e.g. "Lazygit"). |
help_cmd |
string |
Flag passed to the binary to crawl help output for completions. |
completion_cmd |
string |
Shell command that outputs a Bash completion script. |
completion_file |
string |
Path or URL to an existing Bash completion script. |
keymaps |
fun(buf: integer) |
Callback to define buffer-local keymaps for the output buffer. |
close_on_exit |
boolean |
If true, the terminal buffer is automatically wiped when the process exits. Defaults to false. |
cwd |
string|fun(): string |
Working directory for the command. Can be a static string or a function evaluated at invocation time. Defaults to vim.fn.getcwd(). |
no_history |
boolean |
If true, running this wrapper does not update :Cling's last command history. Defaults to true for all wrappers - set to false to opt a wrapper back into history. |
Tip
close_on_exit = true is ideal for interactive fullscreen TUI tools (e.g. lazygit, yazi) whose
terminal buffer has no useful output to read after exit. Leave it false (the default) for
output-producing commands where you want to scroll, search, or export the results afterwards.
cling.nvim provides 4 ways to generate subcommands and completions for your wrappers:
- Help Crawling (
help_cmd):- Recursively runs the binary with a help flag (e.g.,
--help) to parse subcommands and flags. - Best for: Tools that don't provide bash completion scripts but have structured help output.
- Recursively runs the binary with a help flag (e.g.,
- Completion Command (
completion_cmd):- Executes a specific command that outputs a Bash completion script, which is then parsed by the plugin.
- Best for: Modern tools that can generate their own shell completions (e.g.,
cobra-based CLIs).
- Local Completion File (
completion_file):- Points to an existing Bash completion script on your local filesystem.
- Best for: Standard system tools where the completion file is already installed (e.g.,
/usr/share/bash-completion/completions/).
- Remote Completion File (
completion_fileas URL):- Points to a URL serving a raw Bash completion script. The plugin will
curlthis file. - Best for: Tools where you want to fetch the latest completions directly from the repository without manual installation.
- Points to a URL serving a raw Bash completion script. The plugin will
To optimize performance, cling.nvim parses and caches the generated completions in stdpath("data")/cling/completions/<binary>.lua.
These cached files are loaded on subsequent startups to avoid expensive re-parsing. If you update the underlying CLI tool or want to refresh the completions, you can force a re-parse by passing the --reparse-completions flag to your wrapper command:
:<WrapperCommand> --reparse-completionsYou can also manually add completions if you want as they are all just .lua files that export a table.
This example shows how to wrap the Jujutsu VCS and to implement a custom keymap to send the outputs of jj show to a quickfix list.
It uses the completion_cmd method to generate completions dynamically.
return {
"juniorsundar/cling.nvim",
config = function()
local function strip_ansi(str)
return str:gsub("\27%[[0-9;]*m", "")
end
local function get_file_from_line(line)
local clean = strip_ansi(line)
local file = clean:match "^Modified regular file (.*):$"
if file then
return file, "Modified"
end
file = clean:match "^Added regular file (.*):$"
if file then
return file, "Added"
end
file = clean:match "^Removed regular file (.*):$"
if file then
return file, "Removed"
end
file = clean:match "^Renamed .* to (.*):$"
if file then
return file, "Renamed"
end
local _, b = clean:match "^diff %-%-git a/(.*) b/(.*)"
if b then
return b, "Git Diff"
end
return nil
end
local function populate_quickfix(buf)
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
local qf_list = {}
local current_file = nil
local current_type = nil
local last_was_gap = true
for _, raw_line in ipairs(lines) do
local line = strip_ansi(raw_line)
local file, type = get_file_from_line(line)
if file then
current_file = vim.trim(file)
current_type = type
last_was_gap = true
elseif line:match "^%s*%.%.%.%s*$" then
last_was_gap = true
elseif current_file then
local old, new = line:match "^%s*([0-9]*)%s+([0-9]*):"
if old or new then
if last_was_gap then
local lnum = tonumber(new) or tonumber(old) or 1
local text = line:sub((line:find ":" or 0) + 1)
table.insert(qf_list, {
filename = current_file,
lnum = lnum,
text = string.format("[%s] %s", current_type or "Change", vim.trim(text)),
})
last_was_gap = false
end
end
end
end
if #qf_list > 0 then
vim.fn.setqflist(qf_list, "r")
vim.notify("Quickfix populated with " .. #qf_list .. " entries", vim.log.levels.INFO)
vim.cmd "copen"
else
vim.notify("No file headers or hunks found", vim.log.levels.WARN)
end
end
require("cling").setup {
wrappers = {
{
binary = "jj",
command = "JJ",
completion_cmd = "jj util completion bash",
keymaps = function(buf)
vim.keymap.set("n", "<C-q>", function()
populate_quickfix(buf)
end, { buffer = buf, silent = true, desc = "JJ: Move diffs to quickfix" })
end,
},
},
}
end,
}For interactive fullscreen TUI tools like lazygit or yazi, the terminal buffer has no useful content once the tool exits. Setting close_on_exit = true wipes the buffer automatically so a Cling buffer doesn't persist with [Process exited 0].
lazygit is a terminal UI for git.
require("cling").setup {
wrappers = {
{
binary = "lazygit",
command = "Lazygit",
help_cmd = "--help",
close_on_exit = true,
},
},
}
vim.keymap.set("n", "<leader>GL", function()
vim.cmd "Lazygit"
vim.cmd "wincmd T"
vim.cmd "startinsert"
end, { desc = "lazygit" })yazi is a terminal file manager. Because cling runs tools inside a Neovim terminal buffer, naively wrapping yazi would cause it to open files in a nested Neovim instance rather than the parent one.
The solution is to use yazi's built-in --chooser-file flag. Instead of opening files directly, yazi writes the selected path to a temp file on exit. A small inline shell script then reads that path and uses nvim --server "$NVIM" --remote to instruct the parent Neovim instance to open it via RPC. The $NVIM socket is automatically exposed by Neovim to all its terminal children.
require("cling").setup {
wrappers = {
{
binary = [[sh -c 'f=$(mktemp); yazi --chooser-file="$f"; sel=$(cat "$f"); rm -f "$f"; [ -n "$sel" ] && nvim --server "$NVIM" --remote "$sel"']],
command = "Yazi",
close_on_exit = true,
cwd = function()
return vim.fn.expand "%:p:h"
end,
},
},
}
vim.keymap.set("n", "<leader>o", function()
vim.cmd "Yazi"
vim.cmd "startinsert"
end, { desc = "Yazi (File Explorer)" })Generating tab-completions can be achieved through following 4 methods:
wrappers = {
-- Method 1: Recursive Help Crawling
{
binary = "docker",
command = "Docker",
help_cmd = "--help",
},
-- Method 2: Completion Command
{
binary = "jj",
command = "JJ",
completion_cmd = "jj util completion bash",
},
-- Method 3: Local File
{
binary = "git",
command = "Git",
completion_file = "/usr/share/bash-completion/completions/git",
},
-- Method 4: Remote URL (requires curl)
{
binary = "eza",
command = "Eza",
completion_file = "https://raw.githubusercontent.com/eza-community/eza/main/completions/bash/eza",
},
}