Neovim as a Terminal Multiplexer and Neovide as a Terminal Emulator
• Updated onMotivation
I’m not the kind of person who wants everything to have Neovim key bindings, but i definitely want my terminal to have them. And not just key bindings—i want everything. I want plugins, mouse-free text selection, text and file search, color scheme, jump list, virtual editing. I want my terminal sessions to be buffers—well, tabs, to be precise (i know, i know; but i have reasons).
What Didn’t Work
Bash’s Vi mode is meh. The fact that it’s an emulation is already a showstopper for me.
:terminal
doesn’t handle nested Neovim instances.
Anecdotally, when i set out to achieve my goal, two people posted on r/neovim with a few pieces of the puzzle i was yet to solve. The first post has introduced unnest.nvim to me. Unfortunately, it doesn’t free the buffer from which you open new Neovim instances. I want to open a new instance from a terminal buffer and still keep that buffer usable. The project’s readme mentions two other alternatives which didn’t allow my hope to die, so i started exploring them.
nvim-unception seems to work, but it lacked usage examples. I don’t recall the exact issues i had with flatten.nvim, but they were similar to the ones i had with nvim-unception.
From the second post i discovered that, unsurprisingly, someone has already solved what i was trying to tackle. The author uses Neovide as their terminal emulator, something i would probably never consider myself. The post doesn’t outline detailed instructions for doing this, though. I took notes on Neovide and the lack of soft wrapping in terminal buffers, and continued my research.
The Solution
I found a gem (not a Ruby one) called
neovim-remote.
At first glance, the last commit being 3 years old made me question
whether it still works with the latest Neovim and whether it’s
polished enough. Turned out the answer to both is ‘yes’! It adds the
currently missing
--remote-tab[-wait][-silent][+{cmd}]
command-line
arguments, which are yet to be backported from Vim.
I used Ghostty before, so first i’ll show how to integrate neovim-remote with it, then how to add Neovide on top of it.
First, install neovim-remote. I’m on macOS, so i use Homebrew, but
you can install nvr
with your package manager too
$ brew install neovim-remote
Then add the following supporting Bash to .zshrc
/.bashrc
export MANPAGER="nvr --remote-tab +Man! -"
# EMPTY_FILE and NO_FILE are needed for Git integration.
export EMPTY_FILE="$(mktemp -t empty)"
export NO_FILE="/dev/null"
# Wraps nvr and handles cases when no arguments have been passed.
v() {
if (( $# == 0 )); then nvr --remote-tab "${PWD}"
else nvr --remote-tab "$@"; fi
}
# Changes tab-local cwd on each cd call.
chpwd() {
nvr --remote-expr "execute('tcd ' . \
fnameescape('$(pwd)'))" > /dev/null &!
}
Now you can use Neovim as a man
pager

This also replicates Neovim’s behavior when no arguments are passed

Tabs get their own cwd
, so pickers show only relevant
files

Next, integrate nvr
into your Git config
[core]
editor = nvr --remote-tab-wait
[diff]
tool = nvr
[difftool "nvr"]
; EMPTY_FILE and NO_FILE replicate nvimdiff’s behavior for
; added and deleted files.
cmd = nvr -d "${REMOTE/"${NO_FILE}"/"${EMPTY_FILE}"}" \
"${LOCAL/"${NO_FILE}"/"${EMPTY_FILE}"}"
[merge]
tool = nvr
[mergetool "nvr"]
cmd = nvr -d "${REMOTE}" "${BASE}" "${LOCAL}" "${MERGED}" \
-c 'wincmd J | wincmd ='
[mergetool]
keepBackup = false

Now add the following to your Neovim config
vim.api.nvim_create_autocmd('FileType', {
-- Git waits for all the buffers it has created to be closed.
pattern = {'git*'},
callback = function() vim.bo.bufhidden = 'delete' end,
})
-- Exit Terminal mode.
vim.keymap.set('t', 'jk', '<c-\\><c-n>')
-- Map <c-n> to down arrow.
vim.keymap.set('t', '<c-n>', '<down>')
-- Map <c-p> to up arrow.
vim.keymap.set('t', '<c-p>', '<up>')
-- The mapping to open a terminal tab.
vim.keymap.set('n', '<leader>t', function()
vim.cmd 'tab terminal'
vim.cmd.startinsert()
end)
-- The mapping to close a tab based on context.
vim.keymap.set('n', '<leader>c', function()
if vim.wo.diff then vim.rpcnotify(0, 'Exit', 0) end
if vim.bo.buftype == 'terminal' then vim.cmd 'bdelete!'
else vim.cmd.tabclose() end
end)
Finally, configure your terminal emulator to start with this command (or a slightly different one in case you use something else rather than Zsh)
/bin/zsh -ic 'nvr -s +terminal +startinsert'
The above starts your terminal session in a Neovim tab in Insert mode.
For example for Ghostty, use
initial-command = /bin/zsh -ic 'nvr -s +terminal +startinsert'
Look up how to set the initial command for your emulator. If it’s not supported, try an emulator where it is.
And that’s it! Restart your terminal and you should be greated by Neovim.
This branch is a complete config (with some unrelated bits).
Going Beyond
The following lets you use Neovide as a terminal emulator. It offers niceties like cursor and UI animations, which are handy when you’re screen sharing. Also it allows opening files via context menu since it’s a native app.
Add this to your Neovide configuration
neovim-bin = "source ~/.zshrc && nvim +terminal +startinsert"
This makes your .zshrc
/.bashrc
available
to Neovide since it
doesn’t
start Neovim in an interactive shell. Neovide’s FAQ also mentions
~/.zprofile
and ~/.zlogin
for achieving
the same.
Then update your Neovim configuration with the following
local font_size_factor = 1.1
local change_font_size = function(factor)
vim.g.neovide_scale_factor = vim.g.neovide_scale_factor
* factor
end
-- These are optional, this is the full reference.
vim.g.neovide_scroll_animation_length = 0.1
vim.g.neovide_hide_mouse_when_typing = true
-- macOS-specific, see if you like it compared to neovide_fullscreen.
vim.g.neovide_macos_simple_fullscreen = true
-- Replace it with any font you like. `h12` is the font size.
vim.o.guifont = 'UbuntuMono Nerd Font:h12'
vim.keymap.set('n', '<d-=>', function()
change_font_size(font_size_factor)
end)
vim.keymap.set('n', '<d-->', function()
change_font_size(1/font_size_factor)
end)
-- Enable pasting in Neovide. These are macOS-specific. You may want
-- to replace ‘d’ which stands for the Command key (⌘) with ‘c’
-- which stands for the Control key. Neovide allows using the
-- Command key in mappings so you may want to also update your
-- existing plugins.
vim.keymap.set({'', 'i', 'c', 't'}, '<d-v>', function()
vim.api.nvim_paste(vim.fn.getreg '+', true, -1)
end)
The master branch of my configs repository is the complete final config (with some unrelated parts).
The Quirks
Sometimes the highlighting seems to be lagging behind the text: you might see it applied both where it should and where it shouldn’t.
Occasionally, the last line isn’t rendered until you start typing.
Lastly, as mentioned above, there is no soft wrapping yet, so you
may want to overcome it with
my_command | pbcopy/xclip
as suggested in
this GitHub issue comment.
The End
Feel free to drop me a line if you have any questions.
Have a nice one!