Tmux Toggle-able Terminals in Split Panes or Floating Windows
Tags: tmux, vim, neovim, helix
Published: 2023-08-31 19:50:59

---
title: Tmux Toggle-able Terminals in Split Panes or Floating Windows
published: true
description: Toggle tmux terminal in split pane or floating window without plugins.
tags: tmux, vim, neovim, helix
cover_image: https://images.unsplash.com/photo-1640552435845-d65c23b75934?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2940&q=80
# Use a ratio of 100:42 for best results.
# published_at: 2023-08-30 02:30 +0000
---

# 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](https://github.com/voldikss/vim-floaterm)
and [toggleterm.nvim](https://github.com/akinsho/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:

```tmux
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 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:

```diff
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 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:

```bash
#!/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 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:

```tmux
# 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.