cling.nvim implements a customisable and thin CLI wrapper around executable binaries in Neovim.
It can be used to quickly execute terminal commands:
Ctrl+z,The plugin can also be configured to wrap CLI commands that you commonly use (like jj, docker, etc.) and:
[!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.
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,
}
Cling CommandThe 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 .env file 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 in grep, ls, or compiler output), pressing Enter will attempt to open that file in the previous window. It supports file:line:col formats 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 ps
You 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:
cling-output.log in CWD)-- 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:
:p (full path), :~ (relative to $HOME), :. (relative to the CWD):h (directory), :t (basename), :r (name without extension), :e (extension):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 = trueis ideal for interactive fullscreen TUI tools (e.g.lazygit,yazi) whose terminal buffer has no useful output to read after exit. Leave itfalse(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_cmd):--help) to parse subcommands and flags.completion_cmd):cobra-based CLIs).completion_file):/usr/share/bash-completion/completions/).completion_file as URL):curl this file.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-completions
You 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,
}
close_on_exitFor 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].
lazygitlazygit 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" })
yaziyazi 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",
},
}