Truly persistent terminals inside VS Code with tmux

There are two features I always find myself wanting when using VS Code’s integrated terminals:

Using tmux and some bash hackery I got to a terminal experience that has these two features (only in Linux) and I want to share how I went about it.

tmux set up

The first step is to install tmux. tmux is a window manager of sorts for your terminals, it allows you to (among many other things):

Most linux distributions offer prebuilt packages of tmux, see installation instructions here.

tmux keeps the state of your terminals running in the background as long as the tmux server is running, but in the case of a reboot the state of the server is not saved. With the help of tmux plugins (tmux-resurrect and tmux-continuum) tmux can automatically restore the environment after a reboot (running programs on each terminal will have to be re run).

To use tmux plugins you first need to install tmux’s plugin manager. Then install tmux-resurrect and tmux-continuum, in that order (as tmux-continuum depends on tmux-resurrect).

With the plugins installed, you can set up automatic restore by:

Command history restoration (optional)

Up until now, everything has been straightforward. However, command history can not be restored using tmux plugins as of now. Therefore, a bash script to manage bash command history globally is required. The following workaround is finnicky and very specific to my tmux workflow (which completely ignores the existence of panes and relies solely on the usage of sessions and windows inside of VS Code), so proceed with caution.

Create and save the the following script (source) in your home folder: tmux-bash-history.sh

# History control

# Avoids duplicates and commands starting with a space from being saved in the history file
export HISTCONTROL=ignoredups:erasedups:ignorespace

HISTS_DIR=$HOME/.bash_history.d
mkdir -p "${HISTS_DIR}"

# Function to get the appropriate history file based on tmux context
function getHistFile() {
    if [ -n "${TMUX_PANE}" ]; then
        echo "${HISTS_DIR}/history_tmux_$(tmux display-message -t $TMUX_PANE -p '#S:#I')"
    else
        echo "${HISTS_DIR}/history_non_tmux"
    fi
}

# Function to initialize history
function initHist() {
    HISTFILE=$(getHistFile)
    history -c
    history -r
    HISTFILE_LOADED=$HISTFILE
}

# Initialize history immediately on shell startup
initHist

# Function to update history file if necessary
function updateHistFile() {
    local CURRENTHISTFILE=$(getHistFile)
    if [[ "$CURRENTHISTFILE" != "$HISTFILE_LOADED" ]]; then
        history -w
        HISTFILE_LOADED=$CURRENTHISTFILE
    fi
}

# Ensure updateHistFile runs after each command
PROMPT_COMMAND="updateHistFile; history -a; ${PROMPT_COMMAND:-}"

Next, source the script by adding this to your .bashrc:

# Source the tmux-bash-history.sh script
if [ -f "$HOME/tmux-bash-history.sh" ]; then
  source "$HOME/tmux-bash-history.sh"
fi

This script saves the command history of each session:window pair of your tmux environment in individual files and an additional history file for commands executed outside of tmux. All of them stored in ~/.bash_history.d.

Due to the innerworkings of tmux-resurrect, this script doesn’t correctly assign the history file to all of the windows in a session. To circumvent this, a tmux hook can be set to correctly initialize the history of all windows in a terminal session at start.

Create and save the following script in ~/.tmux/scripts/: reload_history_inactive.sh

# Force tmux session to reinitialize history of inactive windows
SESSION_NAME=$(tmux display-message -p '#S')
ACTIVE_WINDOW_INDEX=$(tmux display-message -p '#I')
tmux list-windows -t "$SESSION_NAME" -F '#I' | while read -r WINDOW_INDEX; do
    if [[ "$WINDOW_INDEX" != "$ACTIVE_WINDOW_INDEX" ]]; then
        tmux send-keys -t "${SESSION_NAME}:${WINDOW_INDEX}" " clear" Enter
    fi
done

Finally, add the following to your .tmux.conf to set up the hook:

# Ensure tmux panes reinitialize history on window-linked
set-hook -g window-linked 'run-shell  "~/.tmux/scripts/reload_history_inactive.sh" ; set-hook -u window-linked'

Now indepedent command history tracking for each tmux window should work correctly from start.

VS Code set up

Inside VS Code, the behaviour I wanted was a unique tmux session named after the base folder VS Code is running from. To achieve this, you can create a costum terminal profile by adding the following (source) to your settings.json:

{
    "terminal.integrated.profiles.linux": {
        "bash": null,
        "tmux": {
            "path": "bash",
            "args": ["-c", "tmux new -ADs ${PWD##*/}"],
            "icon": "terminal-tmux",
        },
    },
    "terminal.integrated.defaultProfile.linux": "tmux",
}

So that’s it, now you will have unique tmux terminal sessions for each folder you open in VS Code and your tmux workspace and command history will be persistent even after you close VS Code or shutdown your computer.