Using Neovim as a terminal multiplexer
Oct 13, 2024
Guide assumes youâre using neovim 0.11+
One of the main features of a terminal multiplexer is that they can spawn multiple âwindowsâ. Of course, a powerful multiplexer can offer much more, with detachable sessions and so on, but I would assume the most common use case for a multiplexer is plainly itâs⊠âmultiplexingâ capability. If youâre currently using a multiplexer mainly for this reason and youâre a bit unhappy with your current setup, and if you also happen to be a neovim user, this guide was made specifically for you! Letâs explore neovimâs builtin functionalities to build a poor manâs tmux!
For as long as Iâve been using vim, Iâve been using its tab functionality. And honestly, Iâm surprised with how often itâs overlooked. Tabs in vim are slightly different from traditional tabs found in other programs: a tab can show multiple windows in any arrangement. When you launch vim with a file as an argument, you create a tab containing a single window. You can, then, manipulate the windows using commands such as :split
, to split your screen horizontally, creating another âviewâ (window) for your current file. There are plenty of window commands to do the most common operations one would expect from a⊠window manager. You can read more about them with :h CTRL-W
.
Thereâs another concept in vim that often gets mixed up with tabs: buffers. A buffer is the in-memory representation of a file. But many people, usually coming from other editors, think that vimâs tabs should behave as a âcollection of buffersâ (i.e., a list with the files that are currently open). Many plugins mimic this behavior (a popular one is bufferline). And if that fits your workflow, fantastic! But I find vanilla vim tabs the most powerful. A common use case is to create a tab for each project youâre working on, so you can have a single neovim instance1 do all the work. That sure sounds multiplexy!
However, weâll follow a different approach. I like to have a single neovim instance for each project Iâm working on, and each neovim instance has its own tabs. To me, each tab is kinda like a âmental stateâ. To explain that, Iâll use the neovim instance Iâm using to write this blog post: I have 5 tabs open, some contain posts, another one contains 2 CSS-related files (I need to tweak some stuff, but I donât wanna deal with that right now), and the last one is a terminal running vite dev
to run the blog. All of these tabs have a âmeaningâ that would be otherwise messy to manage without using tabs. Of course, the neovim community has built dozens of plugins to âmanageâ the usage of multiple files. A popular option is harpoon. Again, if youâre happy using harpoon (or any of the alternatives), fantastic! But I find these solutions to be a bit âoverengineeredâ.
The wise among you may have noticed I havenât talked about the usage of vimâs tabs just yet. Thatâs because I find the default bindings to be rather non-ergonomic. Letâs take a look at the navigation: you can use gt
to go the next tab and gT
to go to the previous tab. Most vim mappings can be repeated when preceded with a number: you can use 10ifoo
in normal mode to insert the word âfooâ 10 times2. By that logic, if you were to use 2gt
, one would expect to go 2 tabs forward, but what happens is that you jump to the second tab, regardless of how many tabs you have open (which is a little weird, but not a big deal). What I annoys me the most is that Iâm often switching tabs, so much so that pressing those 3 keys feels like too much effort. Surprisingly, Chrome comes to the rescue! Most popular browsers allow navigating between tabs using <A-[x]
, where [x]
is the tabâs number. So I borrowed that for my config, as follows:
local str = string.format
for i = 1, 9 do
vim.keymap.set("n", str("<A-%s>", i), str("%sgt", i), { desc = str("Goto tab %s", i) })
end
I have decided to adopt the Alt
key as a âtab modifierâ and also introduced some other mappings:
vim.keymap.set("n", "<A-0>", "<CMD>tablast<CR>", { desc = "Goto last tab" })
vim.keymap.set("n", "<A-]>", "<CMD>tabnext<CR>", { desc = "Goto next tab" })
vim.keymap.set("n", "<A-[>", "<CMD>tabprevious<CR>", { desc = "Goto prev tab" })
vim.keymap.set("n", "<A-->", "<CMD>tabm-<CR>", { desc = "Move tab to the left" })
vim.keymap.set("n", "<A-=>", "<CMD>tabm+<CR>", { desc = "Move tab to the right" })
vim.keymap.set("n", "<A-'>", "<CMD>tab split<CR>", { desc = "Clone window in new tab" })
The last one is particularly useful when ones wants to âtemporally maximizeâ a window, by creating a new tab.
A final note on tabs is that the default tabline
is a bit ugly. And by âa bit uglyâ I mean that it has some limitations and itâs clunky. Fortunately, like everything else in vim, itâs customizable. Writing your own tabline
is doable (I used to have mine), but itâs easier with a plugin. The one I use is tabby.nvim3. Its biggest selling point is that tab names are made unique if theyâd otherwise share the same name. Thatâs a massive help when browsing a Rust codebase with lots of mod.rs
or a SvelteKit project with lots of +page.svelte
.
Now that weâve covered (almost) everything about âmultiplexingâ, letâs talk some âterminalâ!
Iâd argue neovimâs builtin terminal also lacks some usability. The default experience can be quite confusing, in fact. It goes like this: a new user hears about using a terminal directly inside of neovim. They decide to try it for themselves and promptly run a :terminal
. The terminal shows up and, in true vim spirit, they enter âinsertâ4 mode with i
. They run some commands, but once they try to go back to normal mode, they realize that <ESC>
isnât working and they have to use a weird key combo (<C-\><C-N>
) instead. Now, Iâm no expert, but I donât find that very intuitive. Donât get me wrong, Iâm sure thereâs a good reason for that to be the default behavior, and Iâm not advocating for a change. As a workaround, what we can do instead is create a new keymap:
vim.keymap.set("t", "<Esc>", [[<C-\><C-n>]], { desc = "Exit Terminal" })
There are some other quirks with :terminal
(in spite of the great improvement with neovim 0.11). Namely, if youâre using the scrolloff
option, you might wanna disable it inside terminals. It behaves inconsistently between modes: it doesnât work inside terminal mode, and may cause undesired scrolling. You can disable it with an autocmd
:
vim.api.nvim_create_autocmd("Termopen", {
callback = function()
vim.wo[0][0].scrolloff = 0
end,
})
Itâs also worth noting that it would be pretty annoying to have to type :terminal
every time we were to spawn a terminal. Thereâs a shorthand for that, :term
, but itâs in no way short enough. Luckily, again, we can solve this issue with a keymap:
vim.keymap.set("n", "<C-w>e", "<CMD>term<CR>", { desc = "Terminal" })
And now weâre done with configuring the terminal! Now you can mix and match terminals, tabs and whatnot. Of course, you could have a âmultiplexing experienceâ without leveraging the power of tabs (some do), since vim offers many ways to manipulate windows. But to me, the job is so much easier with tabs.
Before we continue, thereâs another important limitation of neovimâs terminal that you should be aware of: using the shellâs builtin vi mode gets awkward. To the point where I prefer the default, emacs-like mode of fish. To me, this isnât much of a big deal, but I can see how this would impact someoneâs workflow.
Letâs talk about some alternatives to using the builtin terminal that also arenât based on an actual multiplexer. For the longest time, I used to have a âfloating terminalâ plugin. A common option is toggleterm.nvim. Under the hood, these use the builtin terminal, but without the default cumbersomeness. But now that you know you can have a decent experience without a plugin (and also without having too much trouble), why bother with a plugin?
On the other hand, others prefer the builtin functionality from the shell, by suspending neovim5 with <C-z>
, running the desired commands and then bringing neovim back with fg
. This is nice, but I see some shortcomings: what if you need to run a command that takes too long to finish? Or what if itâs a build related command you always want to be running? Of course, there are numerous ways to deal with these issues, but by using :term
you avoid them altogether. Although, once more: if youâre happy with <C-z>
, great!
Before wrapping up this post I need to address two other factors that heavily influence the usability of this workflow. The first one is that it would be a pain in the ass to manage a bunch of neovim (neovide) instances without a decent window manager. If youâre not using a tiling window manager, I can see how easily it would be to get lost if you had a bunch of neovims lying around. With Hyprland I donât have to worry about that: I use a workspace for each neovim instance. This setup isnât perfect (sometimes itâs hard to remember which workspace holds the instance Iâm looking for), but it gets the job done most of the time.
Another reasonable concern is⊠Quitting neovim. Seriously. Letâs say youâre quite comfortable with a bunch of tabs and terminals and so on. It would be infuriating if, once you quit neovim, all those arrangements were gone. Fortunately it doesnât have to be this way. We can use sessions. Sessions are a native feature of vim that allows you to save the state of the editor when you quit6. To make experience smoother, I use a session plugin that allows me to search sessions and save additional data (e.g., breakpoints): possession.nvim. I wonât go into detail on how to use the plugin, but it should be straightforward.
And thatâs it! Weâve built a poor manâs tmux using neovim! Thanks for reading! Check out my dotfiles to have a look at the actual implementation for this workflow.
- Neovim is even friendly to this use case, as it exposes commands such as
:tcd
to change the directory for the current tab only.â© - Thatâs a silly example, but you know the drill ;)â©
- The folks from
tabby.nvim
do a good job of explaining (in the README) how tabs can be powerful.â© - Yes, itâs the âTerminalâ mode, but from a new userâs perspective it feels like insert mode.â©
- Or any other command, really. An advantage of this approach is how agnostic it is.â©
- Obviously, you can save your state whenever you want. It just makes more sense when youâre leaving.â©