title | sub_title | author | theme | ||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
Wrapping your favorite CLI in neovim |
By building plugins in neovim 0.10.2 |
Chip Senkbeil |
|
A means to interact with a computer program by inputting lines of text called command-lines.
- Command-oriented: do one thing with each execution.
curl
transfers data using network protocols like HTTPgit
can retrieve & manipulate git repositoriesdocker
exposes commands to run and manage containers
- Stream-oriented: do many things over standard or network I/O.
rust-analyzer
accepts requests and responds via JSON over stdin/stdoutfirefox
can be remote controlled via marionette over TCPrg
supports outputting results to stdout as a stream of lines of JSON
- Interactive: do many things with a user interface & keyboard input.
top
displays an ever-changing list of processesbash
continuously accepts commands and execute themlazygit
provides a terminal user interface (TUI) to do git operations
Running :!
will execute the command piped (not in a terminal), and output into
a specialized space within neovim. Supplying curl with -s
will suppress curl's
output of retrieving the response.
We'll specifically use ?T
to force ANSI character response since we cannot
handle color codes in the output.
:! curl -s "https://wttr.in/?T"
Running :%!
will behave just like :!
, but place the output into the current
buffer.
:%! curl -s "https://wttr.in/?T"
Up to neovim 0.9, we needed a variety of different APIs to invoke external processes in neovim:
Method | Description | Async? |
---|---|---|
:! {cmd} |
Run {cmd} in shell connected to a pipe | No |
:%! {cmd} |
Same as :! , but inserts output into buffer |
No |
:terminal {cmd} |
Run {cmd} in non-interactive shell connected pty | Yes |
:call system({cmd}) |
Run {cmd} and get output as a string | No |
:call termopen({cmd}) |
Run {cmd} in pseudo-terminal in current buffer | Yes |
io.popen() |
Executes shell command (part of Lua stdlib) | Yes |
uv.spawn() |
Asynchronously process spawn (part of luv) | Yes |
fn.system({cmd}) |
Same as vim command system({cmd}) |
No |
fn.termopen({cmd}) |
Spawns {cmd} in a new pseudo-terminal session | Yes |
api.nvim_open_term() |
Creates a new terminal without a process | Yes |
With the introduction of vim.system()
in neovim 0.10, the act of executing
processes synchronously or asynchronously is streamlined! So now we care about:
Method | Description | Async? |
---|---|---|
vim.system() |
Run {cmd} synchronously or asynchronously | Both |
fn.termopen({cmd}) |
Spawns {cmd} in a new pseudo-terminal session | Yes |
api.nvim_open_term() |
Creates a new terminal without a process | Yes |
We're just going to focus on the first two. Rarely do you want to use
nvim_open_term()
unless you are proxying a process. A particular use case is
distant.nvim
proxying your remote shell as if it was open in neovim.
-- Runs asynchronously:
vim.system({'echo', 'hello'}, { text = true }, function(obj)
print(obj.code)
print(obj.stdout)
print(obj.stderr)
end)
-- Runs synchronously:
local obj = vim.system({'echo', 'hello'}, { text = true }):wait()
-- { code = 0, signal = 0, stdout = 'hello', stderr = '' }
In this example, temperature information for a city is pulled from
https://wttr.in
using curl
. The website wttr.in
supports a variety of
methods to output the temperature, and we'll make use of ?format=j1
to return
JSON.
So, to summarize, we'll be using:
vim.system()
to executecurl
vim.json.decode()
to parse curl's response
What do we want it to do?
- Check that
curl
exists - Ensure that
curl
is a version we expect - Verify that
https://wttr.in
(used to get temperature) is accessible
Neovim provides a simplistic framework to validate conditions for a plugin, and we can use this to both ensure that a CLI program is installed and is the right version.
A standard practice is to include a health.lua
file at the root of your
plugin that returns a check function, which you can invoke via :checkhealth MY_PLUGIN
.
local M = {}
M.check = function()
vim.health.start("foo report")
if check_setup() then
vim.health.ok("Setup is correct")
else
vim.health.error("Setup is incorrect")
end
end
return M
if vim.fn.executable("curl") == 0 then
vim.health.error("curl not found")
end
local results = vim.system({ "curl", "--version" }):wait()
local version = vim.version.parse(results.stdout)
if version.major ~= 8 then
vim.health.error("curl must be 8.x.x, but got " .. tostring(version))
end
Is https://wttr.in accessible?
local results = vim.system({ "curl", "wttr.in" }):wait()
if results.code ~= 0 then
vim.health.error("wttr.in is not accessible")
end
In this example, we leverage vim.fn.termopen()
to spawn top
within a
terminal. To make things a little fancier, we'll abstract the logic into
a neovim command that creates a floating window and embeds top
as the
running process within a terminal within the window.
So, to summarize, we'll be using:
vim.fn.termopen()
to spawntop
within a floating window
In this example, we spawn a headless instance of firefox
- meaning no
graphical interface running on our desktop - and have it start with
Marionette enabled.
Firefox ships with the Marionette server enabled, which we'll use to communicate using a WebDriver API over TCP to navigate to websites and take screenshots that we can surface within neovim as images.
So, to summarize, we'll be using:
vim.system()
to spawn Firefoxvim.uv.new_tcp()
and associated to connect & communicate with Marionettevim.base64.decode()
to decode screenshot data from firefox to save as PNGsimage.nvim
(neovim plugin) to display these screenshots within neovim
In this example, we will abstract leveraging sl
, the Sapling CLI, to both
display current commits in a buffer and support switching between commits.
So, to summarize, we'll be using:
vim.system()
to executesl
commandssl smartlog
to show a series of commitssl goto
to navigate to another commit
vim.keymap.set()
to specify buffer-local bindings to interface with our Sapling buffer
- Andrei Neculaesei (3rd on Github) for both writing
image.nvim
and directly helping me diagnose issues with its use as a means to display browser screenshots with scrolling functionality in thewebview
example.