DEV Community

Cover image for Tmux Toggle-able Terminals in Split Panes or Floating Windows
Peter Benjamin (they/them)
Peter Benjamin (they/them)

Posted on

Tmux Toggle-able Terminals in Split Panes or Floating Windows

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
}
Enter fullscreen mode Exit fullscreen mode

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 run split-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:

  1. Toggle the terminal with ctrl-\
  2. Move the cursor to the next pane with ctrl-b + o, or ctrl-b + <down> arrows, or even focus the pane with the mouse (if you configure set-option -g mouse on).
  3. 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
+ }
}
Enter fullscreen mode Exit fullscreen mode

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 a Z 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
popup
inside a tmux session (or ctrl-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
Enter fullscreen mode Exit fullscreen mode

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 of popup 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"
Enter fullscreen mode Exit fullscreen mode

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)