Neovim as a Terminal Multiplexer and Neovide as a Terminal Emulator
Motivation
I’m not a type of person who wants everything to have Neovim key bindings, but i definitely would like my terminal to have them. And not just key bindings—i want everything! I want plugins, mouse-free text selection, text and file search, colorscheme, 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 Vim emulation is a meh. The sole fact that it’s an emulation is a showstopper for me already.
:terminal
doesn’t handle nested Neovim instances.
Anecdotally, when i sorted out to achieve my goal, two persons have posted on r/neovim few pieces of the puzzle i was yet to solve. The first post has introduced unnest.nvim to me. Unfortunatelly, it doesn’t free the buffer from which you open new Neovim instances. I want to be able to open a new instance from within a terminal buffer and keep it 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 for me personally it lacked usage examples. I don’t remember the exact issues i got with flatten.nvim, but they were similar to the ones i had with nvim-unception. Probably now that i achieved my goal, i could have looked into them again, but i’ll leave it for another day.
From the second post i discovered that, unsuprisingly, 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 how to do this though. I took notes on Neovide and the lack of softwrapping in terminal buffers, and continued my research.
The Solution
I found a gem (not a Ruby one) called
neovim-remote.
First when i looked into it, the fact that the last commit was 3
years ago made me question if it works with the latest version of
Neovim and is it polished enough. Turned out ‘yes’ is the answer for
my both concerns! It adds the currently missing
--remote-tab[-wait][-silent][+{cmd}]
command line
arguments which are yet to be backported from Vim.
I used to be a Ghostty user, so to keep things simple first i will start with showing how neovim-remote can be integrated into it and then i will show how to use Neovide in case you want to.
First you need to install neovim-remote. I use macOS so i will use
Homebrew, but you should be able to install nvr with your relevant
package manager
$ brew install neovim-remote
After this you should add some 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 &!
}
Then you may integrate nvr into your Git config
[core]
editor = nvr --remote-tab-wait
[diff]
tool = nvr
[difftool "nvr"]
; REMOTE and NO_FILE are needed to 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 this 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 for opening a terminal tab.
vim.keymap.set('n', '<leader>t', function()
vim.cmd 'tab terminal'
vim.cmd.startinsert()
end)
-- The mapping for closing a tab based on the 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)
And finally, you need to make your terminal emulator to start with this command (or a bit different one in case you use something else rather than Zsh)
/bin/zsh -ic 'nvr -s +terminal +startinsert'
For example, for Ghostty you would do
initial-command = /bin/zsh -ic 'nvr -s +terminal +startinsert'
Google how to set the initial command for your terminal. If it’s impossible, try an emulator where it is.
And that’s it! Restart your terminal and you should be greated with Neovim.
Here’s a complete config with some unrelated stuff.