Tmux Display Popup
At a Glance
Intro
I’ve known of the display-popup feature in tmux for a while now but have been happy enough with my Neovim using FTerm until recently. As I’ve made more and more use of this functionality I’ve found myself being tripped up by my muscle memory. Using the terminal inside Neovim comes with some overlapping shortcuts when doing things inside it. I’m also being tripped up by the differences between tmux and the Neovim terminal being close but just different enough.
A particularly nasty situation comes about because I do all of my development inside of tmux. This means I’m running Neovim inside of tmux, then I’ll open the Neovim terminal to create a git commit, which spawns another Neovim inside of that.
This isn’t insurmountable but it is quite annoying and I finally gave tmux
display-popup a try and it solved a lot of these muscle memory issues and
overlapping keyboard shortcut problems for me.
Once everything is setup, it looks like this:
The Files
You can just grab my dotfiles!
Or if you just want super basic versions of the files:
.tmux.conf
# Popups are neat
bind t display-popup -w 80% -h 80% -T "Shell" -E "tmux-popup"
bind -T popup C-b switch-client -T popup-prefix
bind -T popup-prefix t run "tmux display-popup -c $(tmux list-clients -F '##{client_name}' | head -n 1) -C"
bind -T popup-prefix [ copy-mode
bind -T popup-prefix Any switch-client -T popup
tmux-popup
#!/usr/bin/env bash
set -euo pipefail
session="popup"
if ! tmux has -t "$session" 2> /dev/null; then
tmux new-session -d -s "$session"
tmux set-option -s -t "$session" key-table popup
tmux set-option -s -t "$session" status off
tmux set-option -s -t "$session" prefix None
fi
exec tmux attach -t "$session" > /dev/null
Breaking It Down
That’s a lot to take in all at once! Let’s break it down a bit and look at how it all works.
bind t display-popup -w 80% -h 80% -T "Shell" -E "tmux-popup"
This creates a key bind that launches a popup that takes up 80%
width and 80% height of the terminal. It gives the popup a title of “Shell” and
launches our tmux-popup script inside of it.
Before we look at the rest of .tmux.conf file, let’s look at our tmux-popup
script as that will make the rest of the config file make sense.
This is just a variable assignment, the blog I took inspiration from had a much more complicated setup that I’ve altered.
session="popup"
This checks if a tmux session with that name exists and if it doesn’t it then executes…
if ! tmux has -t "$session" 2> /dev/null; then
This creates the new session named popup then…
tmux new-session -d -s "$session"
We configure that session to use a tmux key-table called popup so it doesn’t
use the default prefix key-table that tmux uses.
tmux set-option -s -t "$session" key-table popup
We hide the status bar if it’s turned on globally.
tmux set-option -s -t "$session" status off
We also disable prefix so you can’t use a lot of tmux’s features. This is a
personal preference as I found it got a bit fiddly otherwise.
tmux set-option -s -t "$session" prefix None
Finally we actually attach to the new popup session.
exec tmux attach -t "$session" > /dev/null
With all that out of the way, let’s take a look at our keybinds in .tmux.conf
now. This is a trick I used to make it seem like we’re still using the prefix
of Ctrl + b (the tmux default and the one I still use). This switches us to
another key-table so we can easily limit what we’re able to do here.
bind -T popup C-b switch-client -T popup-prefix
This is where a lot of the complication of this setup comes from. I wanted the same set of key presses to both open the popup and also close it. This proved a lot more difficult than expected and if you’re more of a tmux/bash guru than me you’ll probably want to change this line.
This binds tmux display-popup -C which closes any open popups, the trick part
comes from needing to specify the client to actually send that request too.
This is because if you send it to the popup session it doesn’t work, you need
to select the parent session. Since my workflow is really simple. I only ever
have one session in a server plus my popup session. So we just use tmux
list-clients to grab the first one and send the command there.
bind -T popup-prefix t run "tmux display-popup -c $(tmux list-clients -F '##{client_name}' | head -n 1) -C"
This let’s us enter copy-mode the same as normal a tmux session which will act the same as always.
bind -T popup-prefix [ copy-mode
Finally this catches any other key and sets the key-table back so we need to do Ctrl + b again.
bind -T popup-prefix Any switch-client -T popup
Running Commands from Neovim
The final piece of this puzzle was a small Lua function to run shell commands
from Neovim inside of this popup. I spent some time trying to get this to take
another table or a string in as that’s how my commands were being run before,
but I failed to do so simple and decided that this was fine for now.
function run_in_popup(cmd)
vim.system(
{ "tmux-popup", "-d" }, { },
function()
vim.system({ "tmux", "send-keys", "-t", "popup", cmd, "C-m" })
vim.system({
"tmux", "display-popup",
"-w", "80%",
"-h", "80%",
"-T", "Shell",
"-E", "tmux attach -t popup"
})
end
)
end
Outro
This is what it looks like with my full config setup.
A terminal running Neovim in tmux. A popup titled 'Shell' appears and a few commands are run including a nested Vim. Then the popup is dismissed and brought back multiple times.
I decided to write this blog post as I spent a fair bit of energy the past couple of days getting this working nicely. I thought it would be nice to give back to the community and document everything I figured out.