Toggle-able Terminal in Tmux
For Vim, Neovim, Helix, or any terminal-based editor
Introduction
(Neo)Vim users are likely familiar with the integrated :terminal
. Any time
you need to compile a program or start a long running process, the
:terminal
is always near-by.
Popular plugins, like vim-floaterm
and toggleterm.nvim add some nice
ergonomics, like floating windows and key mappings for toggling the terminal.
I have been a long-time user of the integrated terminal until I started
encountering long text outputs, like URLs, that the integrated terminal
hard-wraps them mid-word, instead of soft-wrap.
This meant that what should have been a one-step task of clicking a URL
or copy-pasting text into a Vim buffer has now become a multi-step process of
fixing text by removing line returns. Doing this over-and-over, especially for
large outputs (e.g. logs) gets very tedious very quickly.
Let's explore how we can accomplish a similar experience as vim-floaterm and
toggleterm.nvim, but leveraging tmux
instead.
Getting Started
Tmux is a powerful utility. You can configure it using ~/.tmux.conf
configuration file and you can even drive it from within itself (see man tmux
for usage details). For example, tmux split-window
will split the current
tmux window into 2 vertical panes.
At a high-level, what we need to accomplish is this:
- Bind
ctrl-\
to be the keyboard shortcut for toggling the terminal in tmux. - If the current window has only 1 pane, pressing
ctrl-\
should create a new pane for the terminal. - If the current window has 2 panes, pressing
ctrl-\
should toggle the terminal pane.
First Iteration
Let's set up the key binding to either create a new split or to toggle based on
the current number of panes, like:
bind-key -n 'C-\' if-shell '[ "$(tmux list-panes | wc -l | bc)" = 1 ]' {
split-window
} {
resize-pane -Z
}
Explanation:
-
bind-key -n
: this allows us to define keybindings that do not require the tmux prefix or modifier key (ctrl-b
by default) -
if-shell '<condition>' { true } { false }
: this allows us to evaluate some condition and execute the 1st-block if true, otherwise execute the 2nd block. -
[ "$(tmux list-panes | wc -l | bc)" = 1 ]
: we list the current panes, count the lines, pipe it through a calculator. If we only have 1 pane, then we runsplit-window
to create a new pane, otherwise we leverage the zoom feature to maximize the current pane (i.e. the pane that has the cursor) to fill the entire tmux window (thus hiding the other pane).
This is already a great start. For some, this might be all that you need.
The experience looks like this:
- Toggle the terminal with
ctrl-\
- Move the cursor to the next pane with
ctrl-b
+o
, orctrl-b
+<down>
arrows, or even focus the pane with the mouse (if you configureset-option -g mouse on
). - When done with the terminal pane, focus the main pane and hit
ctrl-\
Having to manually focus tmux panes introduces a bit of friction.
Let's improve this.
Second Iteration
For an experience closer to toggleterm.nvim, where ctrl-\
toggles and
focuses the terminal, then ctrl-\
again hides the terminal and returns the
cursor to the main pane, we need to tweak the second branch (i.e. the
resize-pane
logic) to make it a little smarter, like:
bind-key -n 'C-\' if-shell '[ "$(tmux list-panes | wc -l | bc)" = 1 ]' {
- split-window
+ split-window -c '#{pane_current_path}'
} {
- resize-pane -Z
+ if-shell '[ -n "$(tmux list-panes -F ''#F'' | grep Z)" ]' {
+ select-pane -t:.-
+ } {
+ resize-pane -Z -t1
+ }
}
Explanation:
-
tmux list-panes -F '#F'
: this lists panes in a custom format. The#F
tells tmux to print the "pane flags". Zoomed panes have aZ
flag. - If there is a zoomed pane (i.e. the terminal pane is hidden), then we switch
focus (via
select-pane
) to the previous one (i.e.-t:.-
). - Otherwise, there is no zoomed pane (i.e. the terminal split pane is shown),
so we zoom into pane 1 (via
resize-pane -Z -t1
).
This is it. We have implemented the core functionality to be able to toggle
terminals with a few lines of tmux configuration.
But, there is still room for improvement. For those who prefer floating
terminal windows, this is for you.
Third Iteration
Let's toggle the terminal in a floating window. Because of the added bit of
complexity here, let's abstract the functionality into a reusable shell script
that we can extend further as needed.
Tmux has a popup
feature. In the most basic scenario, you can run tmux
inside a tmux session (or
popupctrl-b
+ :
, then type popup
and hit
<ENTER>
) and a floating window will appear with a terminal shell. However, I
was not able to find a way to toggle this popup window. So, we will combine
pop-ups with tmux sessions so we can detach and attach as the toggle mechanism.
Create a file called it tmux-toggle-term
somewhere in your $PATH
(e.g. ~/.local/bin
) with the following content:
#!/bin/bash
set -uo pipefail
FLOAT_TERM="${1:-}"
LIST_PANES="$(tmux list-panes -F '#F' )"
PANE_ZOOMED="$(echo "${LIST_PANES}" | grep Z)"
PANE_COUNT="$(echo "${LIST_PANES}" | wc -l | bc)"
if [ -n "${FLOAT_TERM}" ]; then
if [ "$(tmux display-message -p -F "#{session_name}")" = "popup" ];then
tmux detach-client
else
tmux popup -d '#{pane_current_path}' -xC -yC -w90% -h80% -E "tmux attach -t popup || tmux new -s popup"
fi
else
if [ "${PANE_COUNT}" = 1 ]; then
tmux split-window -c "#{pane_current_path}"
elif [ -n "${PANE_ZOOMED}" ]; then
tmux select-pane -t:.-
else
tmux resize-pane -Z -t1
fi
fi
Explanation:
- We expose the floating window behind a
FLOAT_TERM
flag - If
FLOAT_TERM
string is not empty, then we check the session name. If session name ofpopup
exists, then we detach, otherwise we attempt to attach. If attach fails, then we create a new session. - If
FLOAT_TERM
string is empty, then we fallback to split panes with the same logic as before.
Next, update your ~/.tmux.conf
to replace the previous config from the 1st or
2nd iterations with this:
# for splits
bind-key -n 'C-\' run-shell -b "${HOME}/path/to/tmux-toggle-term"
# or, for floats
bind-key -n 'C-\' run-shell -b "${HOME}/path/to/tmux-toggle-term float"
Tip: (Neo)Vim users who enjoy using the integrated terminal with buffer
completion in insert-mode to avoid copy/paste can still accomplish a similar
with https://github.com/wellle/tmux-complete.vim or
https://github.com/andersevenrud/cmp-tmux
Conclusion
In this post, we have seen how we can accomplish so much with so little.
I hope this inspired you to find ways to make your workflows more efficient and
productive.
Top comments (0)