Run any shell / terminal application — a file manager, fuzzy finder, git or database TUI, a system
monitor — inside a Neovim floating window or bottom dock, and act on what it returns (e.g. open the
selected file). Ships ~55 ready-made launchers for popular tools, exposed through a single
:LvimShell <name> command.
Requires Neovim >= 0.10 and lvim-utils.
lvim-shell is a small per-call API (require("lvim-shell").float / .split) plus the bundled addons
registry. The one line you run at startup is require("lvim-shell.addons").command(), which registers
the :LvimShell <name> command over the bundled launchers (see below). setup() is optional — call
require("lvim-shell").setup({ … }) if you want to fold persistent config defaults into the module (see
Default configuration); a per-call config still overrides them.
Install and manage it from the LVIM package manager — open the Plugins tab and install / update / pin it:
:LvimInstaller pluginslvim-installer installs plugins through Neovim's built-in vim.pack, so no external plugin manager is needed.
vim.pack.add({
{ src = "https://github.com/lvim-tech/lvim-utils" },
{ src = "https://github.com/lvim-tech/lvim-shell" },
})
require("lvim-shell.addons").command()lvim-shell launches a command as a Neovim terminal job, hosted in a lvim-utils frame (the shared
chassis — border, title, navigable footer, sector navigation, theme). It opens as a centered float or
as a dock — area (the cmdline/msgarea zone, editor + statusline above) or bottom (a bottom float
dock) — themed to your colorscheme, and closes when the program exits. The program communicates its
result back to Neovim in one of two ways:
- Result files (default). lvim-shell allocates fresh per-session temp files and exports their
paths to the job as
$LVIM_SHELL_FILE,$LVIM_SHELL_QFand$LVIM_SHELL_QUERY. A file manager / fuzzy finder writes the chosen path(s) there; on exit lvim-shell reads them and opens each with the method you picked from inside the terminal (edit / split / vsplit / tabedit) or routes them to the quickfix list. The files are private to this Neovim, auto-removed, and deleted after each read — never a lingering, shared/tmppath. (See Returning results.) - RPC (file-free). Neovim also exports
$NVIM(its RPC socket), so an RPC-capable command can act on the parent Neovim directly, with no files.
Programs that only run (a git client, a monitor) return nothing — lvim-shell simply reloads any buffers they changed on disk when they close.
lvim-shell is a consumer of the shared lvim-utils dock-stack manager, which enforces one visible
window per layout. Opening lvim-shell in a layout (float / area / bottom) that already holds a
picker or terminal parks that occupant (keeping its state) and shows the shell in its place — the two
never overlap. The parked occupant stays on that layout's stack, so <Leader>n / <Leader>p cycle back to
it, and closing the shell reveals it again. Without lvim-utils (the standalone fallback) the shell simply
opens its frame directly.
The dock keys every entry by (id, layout), so the SAME shell opened in a different layout is a
separate live entry — a shell can be docked in float, bottom and area at once (one entry
in each stack, each with its own terminal window). Re-opening the shell in a layout it already occupies just
re-shows that entry (never a duplicate in that stack, and it does not move the shell in the others).
Because lvim-shell launches full-screen TUI apps (vifm, lazygit, htop) that own every key — Esc,
q, <Leader>, all of it — its terminal gets zero dock keymaps: no <Leader> owner is installed on
it. The app keeps all its keys and closes with its own; its exit closes lvim-shell.
Register the command, then launch any bundled tool by name:
:LvimShell yazi
:LvimShell lazygit ~/project
:LvimShell lazygit area " dock in the msgarea zone (editor + statusline above)
:LvimShell yazi bottom ~/project " a bottom dock, in ~/project:LvimShell <name> [float|area|bottom] [dir] — <name> completes over the addons on $PATH;
the optional layout overrides where it opens (float default, area — the msgarea/cmdline dock, or
bottom — a bottom dock; both keep the editor and statusline visible above); the optional
dir is the working directory (default: cwd). Both extra args are order-independent. The geometry
(float width/height, dock height) is not a lvim-shell key — it comes from the shared
lvim-utils config.dock.geometry, edited live via :LvimUtils
(the control-center Utils tab). Inside the float:
| Key | Action |
|---|---|
<C-e> |
open the selection in the current window |
<C-x> |
open in a horizontal split |
<C-v> |
open in a vertical split |
<C-t> |
open in a new tab |
<C-q> |
send the results to the quickfix list |
<C-Space> |
close the shell |
<C-x><C-x> |
force-close + kill the process |
<C-j> |
jump to the footer action bar (then h/l select, <CR> fire) |
<C-k> |
frame sector up (footer / terminal) |
The shell is hosted in a lvim-utils frame, so it also gets that chassis' navigable footer action bar
(the same actions as buttons), <C-j>/<C-k> sector navigation (responsive with chevrons), and the shared
border / title / theme. q / <Esc> close it from the footer or editor.
(For tools that don't return files, only close / force-close apply — the rest are disabled so the program keeps its own keys.)
Optionally add a :Lvim<Name> shortcut per installed addon (:LvimYazi, :LvimLazygit, …):
require("lvim-shell.addons").commands()Or launch straight from Lua:
require("lvim-shell.addons").run("lazygit", "~/project")Every entry below is a name you pass to :LvimShell <name>. Only the tools actually installed on
$PATH are offered in completion. Categories marked ↩ returns files open the chosen path(s) in
Neovim; the rest just run in the float.
| name | program |
|---|---|
yazi |
File manager (Rust) |
ranger |
File manager (Python) |
nnn |
File manager (fast, C) |
lf |
File manager (Go) |
vifm |
File manager (vi-like) |
xplr |
File explorer (hackable) |
broot |
Tree navigator |
| name | program |
|---|---|
fzf |
Fuzzy finder |
fzf_preview |
Fuzzy finder + preview |
skim |
Fuzzy finder (Rust, sk) |
television |
Fuzzy finder (tv) |
Interactive live grep whose (multi-)selected matches land in the quickfix list. They write
file:line:col:text rows to $LVIM_SHELL_QF and default their open method to qf, so on exit lvim-shell
fills the quickfix list (no in-terminal <C-q> needed; <Tab> multi-selects in fzf, and you can still
switch a pick to edit/split/… per row). Each needs its whole toolchain on $PATH — offered in completion
only when present.
| name | program | needs |
|---|---|---|
live_grep |
Live grep (rg + fzf) → quickfix | fzf + rg |
grep_qf |
Live grep (grep + fzf) → quickfix | fzf + grep |
:LvimShell live_grep " float; type to search, <Tab> to multi-select, <CR> → quickfix
:LvimShell live_grep bottom ~/p " a bottom dock, searching ~/pThese are POSIX-shell pipelines (rg/grep/sed). To roll your own, register an addon whose cmd writes
file:line:col:text to $LVIM_SHELL_QF and whose config.edit_cmd is "qf":
require("lvim-shell.addons").registry.my_grep = {
bin = "fzf",
needs = { "rg" }, -- every extra binary that must also be on PATH
desc = "My live grep → quickfix",
returns_files = true,
config = { edit_cmd = "qf" }, -- default the open method to the quickfix list
cmd = ": | fzf --disabled --multi --delimiter :"
.. " --bind 'start:reload:rg --column --line-number --no-heading --smart-case -- {q} || true'"
.. " --bind 'change:reload:rg --column --line-number --no-heading --smart-case -- {q} || true'"
.. ' > "$LVIM_SHELL_QF"',
}| name | program |
|---|---|
lazygit |
Git TUI (lazygit) |
gitui |
Git TUI (Rust) |
tig |
Git history browser |
gitu |
Git TUI (magit-like) |
serie |
Git commit graph |
| name | program |
|---|---|
lazysql |
SQL client TUI |
harlequin |
SQL IDE (Python) |
gobang |
SQL client (Rust) |
rainfrog |
Postgres TUI |
| name | program |
|---|---|
lazydocker |
Docker TUI (lazydocker) |
k9s |
Kubernetes TUI |
dry |
Docker manager |
ctop |
Container metrics |
oxker |
Docker TUI (Rust) |
kdash |
Kubernetes dashboard |
| name | program |
|---|---|
htop |
Process viewer |
btop |
Resource monitor (btop) |
bottom |
Resource monitor (btm) |
glances |
System overview |
bpytop |
Resource monitor (Python) |
gotop |
Activity monitor (Go) |
nvtop |
GPU monitor |
gtop |
System dashboard (Node) |
| name | program |
|---|---|
ncdu |
Disk usage analyzer |
gdu |
Disk usage (Go) |
dua |
Disk usage (interactive) |
diskonaut |
Disk space navigator |
| name | program |
|---|---|
atac |
API client (Rust) |
slumber |
API client (Rust) |
posting |
API client (Python) |
| name | program |
|---|---|
neomutt |
Email client |
aerc |
Email client (aerc) |
newsboat |
RSS/Atom reader |
weechat |
Chat client (IRC/…) |
irssi |
IRC client |
| name | program |
|---|---|
ncmpcpp |
MPD client |
cmus |
Music player (cmus) |
spotify |
Spotify (spt) |
musikcube |
Music player |
| name | program |
|---|---|
taskwarrior |
Taskwarrior TUI |
dooit |
TODO manager |
termscp |
File transfer (SCP/SFTP) |
gpg_tui |
GnuPG key manager |
bandwhich |
Network usage by process |
impala |
Wi-Fi manager |
bluetui |
Bluetooth manager |
Inspect the live set from Lua with require("lvim-shell.addons").registry (all entries) or
.available() (only those on $PATH).
setup() deep-merges per-addon overrides into the registry — tweak only the addons you name (the
rest keep their defaults), or add new ones. Call it before command() / commands():
require("lvim-shell.addons").setup({
-- pass a direct-colour TERM so neomutt renders real colours
neomutt = { config = { env = { TERM = "kitty-direct" } } },
-- open lazygit in a bottom dock instead of a float
lazygit = { mode = "split" },
-- replace a command outright
yazi = { cmd = 'yazi --chooser-file="$LVIM_SHELL_FILE" --cwd-file="$LVIM_SHELL_QUERY"' },
})
require("lvim-shell.addons").command()Each override is a partial spec (cmd, mode, suffix, config, bin, desc, returns_files) merged
over the default. config is a per-call lvim-shell config (so config.env, config.ui.float, config.mappings
all apply to that addon only).
To run something not in the registry, call the API directly. float/split take the command, the key
replayed after a method key (usually <CR>; a file manager may use l), and an optional per-call
config table (merged over the defaults):
local shell = require("lvim-shell")
-- a file-returning tool → write the pick to $LVIM_SHELL_FILE
shell.float('yazi --chooser-file="$LVIM_SHELL_FILE" .', "<CR>")
-- a plain TUI in a bottom dock
shell.split(
"lazygit",
"<CR>",
{ mappings = { edit = false, split = false, vsplit = false, tabedit = false, qf = false } }
)To make it a permanent addon, add it to the registry instead:
require("lvim-shell.addons").registry.mytool = {
bin = "mytool",
desc = "My tool",
returns_files = true,
cmd = 'mytool --out "$LVIM_SHELL_FILE"',
}Pass overrides as the third argument to float/split (or via an addon's config field), or make them
persistent with the optional setup() — it deep-merges into the module's default config in place (via
the shared lvim-utils.utils.merge, clean array-replace), so every later float/split picks them up
while a per-call config still wins:
require("lvim-shell").setup({
ui = { float = { title_pos = "left", blend = 10 } },
mappings = { close = "<C-c>" }, -- rebind the close key everywhere
})Note the geometry (float width/height, dock height) is not here — it comes from the shared lvim-utils
config.dock.geometry, edited live via :LvimUtils (control-center Utils tab):
local base_config = {
ui = {
float = {
title = "LvimShell", -- frame title on the top border (false/nil hides it)
title_pos = "center", -- "left" | "center" | "right"
border = nil, -- nil → the shared lvim-utils border; or any nvim border spec
float_hl = "LvimShellNormal", -- the terminal window's Normal (palette-themed)
blend = 0, -- winblend for the terminal window
},
},
-- Dock integration, namespaced under `dock` (matching lvim-dependencies' `config.dock.*`).
dock = {
-- true = full dock-STACK consumer (managed: cyclable <Leader>n/p/x/m, :LvimDock,
-- one-visible-per-layout, no overlap); false = geometry-only (central dock.slot size/
-- backdrop, opens standalone, NOT in the stack).
dock_stack = true,
-- Per-plugin per-layout ANCHORED geometry overrides, deep-merged per field OVER the global
-- lvim-utils config.dock.geometry.<layout>; empty {} = inherit the global unchanged. Each
-- layout may carry: height, height_auto, backdrop = { enabled, mode, dim = { amount },
-- darken = { amount } }, auto_hide, keep_focus. FLOAT ALSO: width, width_auto. area/bottom
-- are ALWAYS full-width — NO width/width_auto (ignored if set).
force = { float = {}, area = {}, bottom = {} },
},
edit_cmd = "edit",
on_close = {},
on_open = {},
footer = true, -- the navigable footer action bar (false to hide it)
footer_bar = { -- GROUPS of footer action ids (a divider between groups); display order only
{ "edit", "split", "vsplit", "tabedit", "qf" },
{ "help", "close" },
},
footer_separator = "●", -- glyph dividing the footer button groups
mappings = {
-- The keymap CHEATSHEET — terminal-NORMAL (`<C-\><C-n>` first: in terminal-insert the hosted
-- program owns every key). Built from THIS table, so a rebind shows up in it; also a footer chip.
help = "g?",
split = "<C-x>",
vsplit = "<C-v>",
tabedit = "<C-t>",
edit = "<C-e>",
close = "<C-Space>",
force_close = "<C-x><C-x>", -- force close + kill (false to disable)
footer = "<C-j>", -- jump to the footer action bar (frame sector-down)
nav_up = "<C-k>", -- leave upward (frame sector-up → the editor, in a split)
qf = "<C-q>",
},
-- nil → per-session tempfiles (private to this Neovim, auto-removed); their paths are exported to the
-- command as $LVIM_SHELL_FILE / $LVIM_SHELL_QF / $LVIM_SHELL_QUERY. Set { list, qf, query } to pin them.
files = nil,
env = nil,
cwd = nil, -- directory the command runs in (nil → cwd); relative result paths resolve from it
}dock.dock_stack(defaulttrue) — open through the managed dock stack (cyclable with<Leader>n/<Leader>p, closable with<Leader>x, listed in the<Leader>mdock menu; one visible consumer per layout, no overlap). Set itfalseto open standalone: the frame still gets its central geometry fromdock.slot(and anydock.forcebelow), but it does not join the stack — no parking, no cycling.dock.force— per-layout anchored geometry overrides, deep-merged per field over the globallvim-utilsconfig.dock.geometry.<layout>(an empty{}inherits the global unchanged). Each layout may setheight,height_auto,backdrop,auto_hide,keep_focus;floatalso takeswidth/width_auto.areaandbottomare always full-width — theirwidth/width_autoare ignored.dock.forceapplies in both modes (evendock.dock_stack = falseforces its own size / backdrop).
require("lvim-shell").setup({
dock = {
dock_stack = true,
force = {
area = { height = 0.4 }, -- a taller area dock than the global default
float = { width = 0.6, height = 0.7 }, -- a specific float size for the shell
},
},
})A launched command returns paths in either of two ways:
-
Files (default) — lvim-shell allocates fresh per-session temp files (via
vim.fn.tempname(): private to this Neovim, auto-removed on exit, deleted after each read — never a lingering, shared/tmppath) and exports their paths to the job:$LVIM_SHELL_FILE— write chosen paths, one per line;$LVIM_SHELL_QF— write grep-stylefile:line:col:textfor a quickfix list;$LVIM_SHELL_QUERY— write an optional quickfix title.
On the terminal's exit lvim-shell opens each path with the pending method (or fills the quickfix list).
-
RPC (file-free) — Neovim exports
$NVIM(its RPC socket) to the job, so an RPC-capable command can act on the parent directly, e.g.nvim --server "$NVIM" --remote-expr "v:lua.require'lvim-shell'.close()".
Windows. The result-file paths are platform-agnostic (vim.fn.tempname()), and on Windows their
backslashes are normalised to forward slashes so they splice cleanly into the shell command (cmd/pwsh
accept them, as do rg/fzf). The $LVIM_SHELL_QF reader also tolerates a leading drive letter, so a
C:\path\file.lua:12:3:text (or C:/…) row is parsed as one filename, not split on the C:. POSIX
path:line:col:text rows parse exactly as before. (The bundled grep→quickfix pipelines above are
POSIX-shell; on Windows supply your own equivalent cmd.)
The terminal window uses LvimShellNormal, self-themed from the lvim-utils palette and re-applied on every
colorscheme change (transparent-aware; falls back to NormalFloat without lvim-utils). Point float_hl at any
group to override. The border, title and footer come from the lvim-utils frame chassis — they follow its
shared groups, so the shell matches every other lvim-tech panel automatically.
