https://github.com/juniorsundar/cling.nvim
Thin wrapper around your command-line
https://github.com/juniorsundar/cling.nvim
cli lua neovim neovim-plugin
Last synced: about 2 months ago
JSON representation
Thin wrapper around your command-line
- Host: GitHub
- URL: https://github.com/juniorsundar/cling.nvim
- Owner: juniorsundar
- License: mit
- Created: 2026-01-02T09:35:04.000Z (6 months ago)
- Default Branch: main
- Last Pushed: 2026-04-23T18:58:30.000Z (about 2 months ago)
- Last Synced: 2026-04-23T20:27:32.325Z (about 2 months ago)
- Topics: cli, lua, neovim, neovim-plugin
- Language: Lua
- Homepage:
- Size: 273 KB
- Stars: 22
- Watchers: 0
- Forks: 2
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# cling.nvim



`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.
## Requirements
- Neovim 0.10+
- `bash-completion`: Many CLI tools' completion scripts depend on this.
- `bash` *(obviously)*
- `curl` (optional) for fetching remote completion scripts (Method 4).
## Installation
Using [lazy.nvim](https://github.com/folke/lazy.nvim):
```lua
return {
"juniorsundar/cling.nvim",
config = function()
require("cling").setup({
wrappers = {
{
binary = "jj",
command = "JJ",
completion_cmd = "jj util completion bash",
},
-- {}, ...
}
})
end,
}
```
## Usage
### The `Cling` Command
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 `.env` file assigned interactively.
* **`:Cling last`**: Executes the last executed command with `.env`.
* **`:Cling -- `**: Executes the command, treating everything after `--` as the command string. This defaults to executing in current working directory.
### Output Buffer Keymaps
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.
* **`` (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.
### Split Modes
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:
```vim
:vert JJ log
:tab Docker ps
```
### Exporting Output
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:
* Prompt you for a file path (defaults to `cling-output.log` in 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
```
## Configuration
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.
### Setup Options
| 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. |
### Wrapper Fields
| 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.
### Completion Generation
`cling.nvim` provides **4 ways** to generate subcommands and completions for your wrappers:
1. **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.
2. **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).
3. **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/`).
4. **Remote Completion File (`completion_file` as URL)**:
* Points to a URL serving a raw Bash completion script. The plugin will `curl` this file.
* *Best for:* Tools where you want to fetch the latest completions directly from the repository without manual installation.
### Caching & Updates
To optimize performance, `cling.nvim` parses and caches the generated completions in `stdpath("data")/cling/completions/.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:
```vim
: --reparse-completions
```
You can also manually add completions if you want as they are all just `.lua` files that export a table.
## Examples
### Wrapping Jujutsu (jj) with custom keymaps
This example shows how to wrap the [Jujutsu](https://github.com/martinvonz/jj) 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.
```lua
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", "", function()
populate_quickfix(buf)
end, { buffer = buf, silent = true, desc = "JJ: Move diffs to quickfix" })
end,
},
},
}
end,
}
```
### Wrapping TUI tools with `close_on_exit`
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]`.
#### Example: `lazygit`
[lazygit](https://github.com/jesseduffield/lazygit) is a terminal UI for git.
```lua
require("cling").setup {
wrappers = {
{
binary = "lazygit",
command = "Lazygit",
help_cmd = "--help",
close_on_exit = true,
},
},
}
vim.keymap.set("n", "GL", function()
vim.cmd "Lazygit"
vim.cmd "wincmd T"
vim.cmd "startinsert"
end, { desc = "lazygit" })
```
#### Example: `yazi`
[yazi](https://github.com/sxyazi/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.
```lua
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", "o", function()
vim.cmd "Yazi"
vim.cmd "startinsert"
end, { desc = "Yazi (File Explorer)" })
```
### Different methods of generating tab-completion
Generating tab-completions can be achieved through following 4 methods:
```lua
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",
},
}
```