A Neovim plugin that allows you to seamlessly integrate external TUIs (for example, an application like scooter) that can be launched from Neovim and then re-use the original Neovim instance for editing.
There are a number of tools that have support for configuring external editor commands, but they don't all need to have dedicated Neovim plugins. Tools like yazi and lazygit have enough community support (and additional feature requirements) that make it reasonable to have a dedicated plugin to support their integration, but we don't always need/want that level of integration from our tools. Often just being able to configure the external editor (and adding a command to launch the tool) is more than enough. That's where nvim-external-tui comes in.
- Simple API for registering external TUI tools
- Automatic command creation with visual selection support
- Terminal window management (floating by default)
- Bidirectional communication between Neovim and external tools
- Support for pre-launch and post-callback hooks
- Automatic callback function generation
Using lazy.nvim
{
'gfontenot/nvim-external-tui',
dependencies = {
'folke/snacks.nvim', -- Optional: provides enhanced terminal management
},
config = function()
-- Your tool configurations here
end
}Using packer.nvim
use {
'gfontenot/nvim-external-tui',
requires = {
'folke/snacks.nvim', -- Optional: provides enhanced terminal management
},
config = function()
-- Your tool configurations here
end
}The plugin works out of the box with no configuration required. If you want to
explicitly set the terminal provider, you can use the setup function:
require('external-tui').setup({
terminal_provider = 'builtin', -- 'snacks' | 'builtin' | nil (auto-detect)
})| Option | Type | Default | Description |
|---|---|---|---|
terminal_provider |
string or table |
nil |
Terminal backend: 'snacks', 'builtin', nil for auto-detection, or a table with provider-specific config |
When set to nil (the default), the plugin will use snacks.nvim if available,
otherwise it falls back to the builtin floating terminal.
You can pass provider-specific configuration by using a table format:
-- Snacks with custom window config
require('external-tui').setup({
terminal_provider = {
snacks = {
win = { style = 'float', position = 'bottom' }
}
}
})
-- Builtin with custom dimensions
require('external-tui').setup({
terminal_provider = {
builtin = {
width = 0.9,
height = 0.9,
border = 'single',
style = 'minimal',
}
}
})The presence of the snacks or builtin key determines which provider to
use, and its value is merged with the default configuration. Unspecified
fields fall back to their default values.
For Snacks configuration options, see the
Snacks terminal documentation.
The default Snacks config is { win = { style = 'float' } }.
The builtin provider supports a limited set of options:
| Option | Type | Default | Description |
|---|---|---|---|
width |
number |
0.8 |
Window width as percentage of editor (0.0-1.0) |
height |
number |
0.8 |
Window height as percentage of editor (0.0-1.0) |
border |
string |
'rounded' |
Border style (see :help nvim_open_win) |
style |
string |
'minimal' |
Window style (see :help nvim_open_win) |
For more advanced terminal configuration, consider using Snacks.
Assume you have a tool named neatui that does neat things in a tui, and
allows you to launch an editor to perform manual tasks. It has the following
API:
❯ neatui --help
Usage: neatui [OPTIONS]
Options:
--editor <EDITOR> Command to use when launching external editor
--prefill-text <SEARCH_TEXT> Text to prefill a field that will be used to do neat things
-h, --help Print help
-V, --version Print version
To integrate this tool into Neovim yourself, you'd need to maintain a number of custom configuration pieces:
- A user command that is able to launch a terminal for this command (including support for ranges and arguments to prefill text)
- The code required to present that terminal (including state tracking in order to be able to dismiss the terminal when finished)
- The editor command to call back into Neovim using
--remote-send
This isn't an overwhelming amount of configuration, but if you start to add multiple tools that need this kind of configuration it can get out of hand quickly. However, with nvim-external-tui, integration looks like this:
local external_tui = require('external-tui')
local config = external_tui.add({
user_cmd = 'Neatui', -- Creates :Neatui command
cmd = 'neatui', -- External command to run
text_flag = '--prefill-text', -- Flag to pass selected/input text to the command
editor_command = '--editor', -- Flag for configuring the external editor
})This creates a :Neatui command that:
- Launches a floating window running the
neatuiapplication - Accepts visual selection:
:'<,'>Scooter - Accepts arguments:
:Scooter search_term - Opens without arguments:
:Scooter - Re-uses the original Neovim instance when performing editor actions
If your command doesn't support overriding the editor command via the cli and
instead requires it to be specified in an external config, you can still use
this plugin and it can still help you configure the bidirectional support. The
add() function returns a table with the editor command that needs to be
configured in your external tool. This table can be used to print out the
commands you need to add to your external config in order to get the
integration working:
local config = external_tui.add({ ... })
print(config.editor_command)
-- Output: nvim --server $NVIM --remote-send '<cmd>lua EditLineFromNeatui("%file", %line)<CR>'
print(config.callback_name)
-- Output: EditLineFromNeatuinvim-external-tui also supports optional pre/post launch hooks that you can use to perform actions automatically:
external_tui.add({
user_cmd = 'Neatui',
cmd = 'neatui',
text_flag = '--prefill-text',
editor_flag = '--editor',
-- Called before launching the TUI
pre_launch = function(text)
print("Launching with text:", text)
vim.cmd('write') -- Save current buffer
end,
-- Called after opening the file
post_callback = function(file_path, line)
vim.cmd('normal! zz') -- Center the line on screen
end,
})Register a new external TUI tool integration.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
user_cmd |
string |
Yes | - | Neovim command name (e.g., 'Scooter' creates :Scooter) |
cmd |
string |
Yes | - | External command to execute (e.g., 'scooter') |
text_flag |
string |
No | nil |
Flag for passing selected text (e.g., '--search-text') |
editor_flag |
string |
No | nil |
Flag for passing editor command (e.g., '--editor') |
file_format |
string |
No | '%file' |
Template variable for file path in tool's config |
line_format |
string |
No | '%line' |
Template variable for line number in tool's config |
pre_launch |
function |
No | nil |
Hook called before launching TUI: function(search_text) |
post_callback |
function |
No | nil |
Hook called after opening file: function(file_path, line) |
Table with:
editor_command: String to configure in external toolcallback_name: Name of the generated callback function
The editor command uses template variables that the external tool should replace:
%file- Full path to the selected file%line- Line number to jump to
These defaults can be overridden by passing the file_format and line_format options.
Example Config: scooter
local external_tui = require('external-tui')
external_tui.add({
user_cmd = 'Scooter',
cmd = 'scooter',
text_flag = '--search-text',
editor_flag = '--editor-command',
})For versions of Scooter before 0.8.4, you would need to omit the editor_flag
option in the plugin config, and instead set the command in the Scooter config
directly:
# ~/.config/scooter/config.toml
[editor_open]
command = "nvim --server $NVIM --remote-send '<cmd>lua EditLineFromScooter(\"%file\", %line)<CR>'"- Neovim >= 0.9.0
- snacks.nvim (optional) - If installed, snacks.nvim will be used for terminal management. Otherwise, a builtin floating terminal is used.
This plugin is heavily inspired by the Neovim integration for scooter, as evidenced by the heavy use of that tool in the examples. The original code used for this plugin is a modified version of the sample code in that project, generalized for arbitrary tool usage and wrapped up in a plugin format.