Autolaunch tmux with custom layout under vscode
2024-07-05 23:12 WIB - Hendrik Lie
This post is originally published as a Github Gist last year at 2023-07-22. I re-posted it with modifications here simply because I feel like documenting it here.
As a part of our company workflow, we requires a tmux setup that would automatically open specific file locations with a certain layout when vscode terminal is open.
Let’s implement them.
Issues
Our company requires us work with several project directories. For simplification, we assume that each project directories have unique directory names. Below are example arbitrary directory path under which we will have to work on:
~/App/core/
for backend repository~/App/fe/
for frontend repository~/App/deployment/
for deployment repository
We are working with our codes using vscode
, and normally
we access its built-in terminal to run various tools such as
artisan
, composer
, npm
, or even
yarn
under our project directories. Sometimes we want the
session to persist, even when vscode fails. Even better, we should also
be able to connect to the vscode terminal session from an external
terminal emulator. This is usually done because we can get wider and
cleaner view using a dedicated terminal emulator, rather than using
built-in vscode terminal.
How do we satisfy those specific use cases?
Design Overview
Since we are working with multiple directories, and we want to have named sessions we can attach to from either vscode built-in terminal or a proper terminal emulator, we have to use some form of session persistence. There are plenty of tools that allow persistent terminal sessions, such as screen and tmux. In this article, we are going to use tmux, a modern terminal multiplexer that allows persistence of terminal sessions.
Since we also have multiple directories, ideally each of those
directories have their own dedicated named sessions. For simplicity, we
will just have to use the directory’s basename for their session names.
Therefore we will have to have the following sessions:
core
, fe
, and deployment
.
We will have one default tmux configuration file that would actually show us useful information. Then for each of those sessions, we would have to craft their own config files depending on their use cases and specific requirements. Since the configuration has to work wherever we decided to attach the sessions from, the sessions would have to be made whenever we run any terminal from the first time, in a detached mode. The config file would also take care of the working directory.
At last, we want to have the persistent session as default behavior
in our vscode
terminal. We don’t want to just fired up
vscode
, open its terminal, and forgetting to set up tmux,
and then run an important program that requires persistence. Therefore,
if we fire up the vscode
terminal, we must also
automatically be attached to a tmux session.
Pseudocode
To satisfy the requirements in both of our use case and design overview, we can actually simplify our tasks in just six steps.
- On startup, check if we’re already under tmux.
- If we are not under tmux, check if there are existing session with a specific name.
- If the specified named tmux session is not present, then we make a new detached named session.
- From within the created detached name session, we send keys to it to load its custom configuration files. The config files would also take care of their working directory.
- Finally, we check if we are running under vscode.
- If we are, then we will connect to a tmux session with the same name as our working vscode directory.
A sane default tmux configuration
tmux
actually works out of the box, and can be used
right away. However, the sane configuration can be very esoteric for the
uninitiated. I have to shift through a lot of documentations, just to
get a sane default configuration. I started from Archlinux Wiki, and
then from Practical
Tmux, and I managed to compile my current default
configuration.
The gist of my current configuration is as follows:
Key | Action |
---|---|
Ctrl+a |
tmux prefix |
Ctrl+a , |
rename current window |
Ctrl+j |
detach from current tmux session |
Shift+left |
move to the left tmux window |
Shift+right |
move to the right tmux window |
Shift+down |
create a new tmux window |
Alt+left |
swap current window to the left |
Alt+right |
swap current window to the right |
Keys separated by a plus sign (‘+
’) must be pressed
together, while the one separated with a space means perform the key on
the left first, then release and press the next key in the sequence.
I also set a minimalistic but useful tmux status bar, that will show you the current session name, current session group, current tmux windows (that changes color depending on whether they are the active window or not), your username and machine name, and current machine time.
Allowing multiple clients sharing the same session
Per our requirements, we want the sessions to be accessible from both the vscode terminal AND any external terminal emulator. Even better, pair programming is actually possible by having a remote user accessing the local machine via ssh and access the same terminal session.
To better achieve that, we can spawn two sessions, with the second
session is attached and synchronized to the first main session.
Therefore, any new clients can attach to the first, be it a
vscode
terminal, a terminal emulator, or even a remote user
via ssh. To prevent cluttering of ‘zombie’ sessions, that is client
sessions that are no longer attached, we have to do clean up on the
sessions that are connected to the main session.
There are many solutions to achieve the aforementioned
specifications. First there is one from Practical Tmux
article, and there are two alternative versions in Archlinux
Wiki. The one in Archlinux Wiki is cleaner, and actually handles
cleaning up of zombie sessions better than the one in Practical Tmux
article. Both uses current datetime stamp for children sessions, and the
alternative implementation from Archlinux Wiki uses a bash function
stored in our ~/.bashrc
file uses unix timestamp to
differentiate parent session and child session.
However I am not satisfied with them for cosmetic reasons. Therefore
I modified the tmx
script implementation from both
Practical Tmux article and Archlinux Wiki one, with one major
difference: I use the substring of md5 hash of child session creation
unix timestamp as the child session name. I do that because my tmux
configuration shows both the session name and the session group name for
brevity.
You can see my tmx implementation here.
To use the script, you have to add that script in your path and make
it executable. It can easily be done by exporting your script directory
to your path. For example in my case, to add ~/.local/bin
in our $PATH
only if it is not yet appended:
test -z "$(echo $PATH|grep "$HOME/\.local/bin")" \
&& PATH="$HOME/.local/bin:$PATH"
Then to make the script executable, you can do:
chmod +x ~/.local/bin/tmx
To make the changes persistent, add them to your .bashrc
or .zshrc
.
Therefore, with tmx
script, we can create a new named
session on demand, or connect to an existing named session if it is
already present. It is useful since in the next part we will discuss
about the creation of multiple detached named sessions, each with custom
configuration files of the same name located under
~/.tmux.conf.d/sessions/
. After they are automatically
generated, we can just connect them from any terminal in our machine
just by running tmx <session_name>
. That including
automated deployment of a tmux session from vscode at later stages of
this article.
A convenience script to generate named sessions
Basically, step 1-4 of our pseudocode can be done in just a single
convenience script. Here we will make a script called set_tmux and put
it somewhere in our path. In my case, I put it under
~/.local/bin/
The content of the script is as follows (read the comments to see the workflow):
#!/bin/sh
# Sets named tmux sessions
# usage: set_tmux <session>
# Assumes ~/.tmux.conf.d/sessions/<session>.conf
# file exist
# Loads session $session, if $session is empty,
# then load a session with the same name as the
# first argument. If the first argument is empty,
# then $session name is 'main'
tmux_load_config(){
test -z "${session}" && session="$1"
test -z "${session}" && session="main"
local config_file="${HOME}/.tmux.conf.d/sessions/${session}.conf"
test -f "$config_file" && \
tmux send-keys -t "${session}" \
"tmux source-file ${config_file}" C-m
}
# If session $1 is not present, make one
configure_tmux(){
local session="$1"
if test -z "$TMUX" ; then
tmux ls 2>/dev/null | grep -q "^$session":
if test $? -gt 0 ; then
tmux new-session -d -s "$session" && \
tmux_load_config
fi
fi
}
# Loop through session names and generates named
# sessions
for x in "$@" ;
do
configure_tmux "$x"
done
The script is made with the assumption that we have more than one
working directories. For example, if we need one for each of
~/App/core
, ~/App/fe
(front-end directory),
and ~/App/deployment
, then we can just have to create three
configuration files under ~/.tmux.conf.d/sessions/
:
- core.conf
- fe.conf
- deployment.conf
We will discuss the tmux config files later in this article.
The for-loop section basically passess all arguments as session
names. Then for each of them, it would run configure_tmux()
by taking the session name as the argument. The
configure_tmux()
function would check with
tmux ls
command and piping the output to grep
to determine whether the session of that name is already run or not. If
it is not run already, only then would the function launches a new
session of that name. It would then run tmux_load_config
that basically checks if the config file for that particular session
name is present or not. If it is present, then the config file would be
loaded, otherwise it would be ignored and only default tmux
configuration is loaded.
Then in our shell configurations (either ~/.bashrc, ~/.zshrc, or both), we just have to set the following lines:
set_tmux core fe deployment
Note that you must ensure that set_tmux
is within your
path. See the previous section that discusses tmx
script on
how to add this file to your path and make it executable.
Custom tmux configuration files
In this article we assume that you put your session configuration
files under ~/.tmux.conf.d/sessions/
. For the purpose of
this article, we assume that the session name is the same as the
configuration file name. For example, tmux session core
would have a configuration file under
~/.tmux.conf.d/sessions/core.conf
.
This article also assumes that the named tmux sessions are created
with set_tmux
script called from our ~/.bashrc
or ~/.zshrc
as described in the previous section. This is
important, because set_tmux
actually checks whether
configuration file for ~/.tmux.conf.d/sessions/<session>.conf
. If it is
present, then the script will load the configuration file. Therefore the
config file will only be run once during the creation of those detached
sessions.
Since most of our desired configurations are already set in our
default tmux configuration (mine is located at ~/.config/tmux/tmux.conf
),
we would only need custom configurations for our own projects. In my
case it is usually automated creation of workspace layout. It can be of
windows, or to autorun certain commands during tmux session startup.
The simplest use case might be to automatically open two named
windows in our tmux session: git
and npm
. To
do that, we basically have to rename the first tmux window to
git
, and then we create a new named window
npm
. Here’s a simple configuration file that implements
that:
# File ~/.tmux.conf.d/sessions/fe.conf
# Rename window :0
rename-window -t :0 'git'
# Make a new named window
new-window -t :1 -n 'npm'
# Select first window
selectw -t 0
However the working directory would still be the working directory of
the spawned terminal. If the first terminal we open is the one in
vscode, it is very likely that its working directory is the project
directory, which is exactly what we want to do. However if you open
another terminal emulator, that might not be the case. Instead it is
likely that the working directory is from your home directory
($HOME
or ~/
).
We can actually circumvent that by creating new named windows with a specified working directory with the following command under the config file:
new-window -t xadf:1 -n 'git' -c ~/Documents/xadf
However when we make a new tmux session, there will always be one
default window, usually <session_name>:0
(eg.
main:0
or simply :0
). The command above would
only affect the working directory of new tmux windows. If we want to
change the current working directory of the first window, or any tmux
windows if that matters, is simply by sending key sequences to that
specific tmux window:
# Target is a named tmux session 'xadf'
# Change directory to ~/Documents/xadf, enter,
# and clear the screen
send-keys -t xadf:0 'cd ~/Documents/xadf' C-m C-l
# Clear the screen, and then display current
# branch's name and status, then enter
send-keys -t xadf:1 'clear;git status -sb' C-m
In fact we can send any arbitrary commands to a tmux window with that
method. Note that if we don’t include C-m
, the command
would just sit in the terminal as if you press those keys on the window.
It is not run yet, as we haven’t send the ENTER key to execute it. The
C-m
sequence is equivalent to pressin ENTER. Likewise, the
C-l
sequence is the same as pressing Ctrl+l
or
running clear
command.
Revisiting our fe.conf
, we can then have the following
to consistently open a detached named tmux session fe
during a terminal start if it is not present already and have it shows
the correct working directory for both windows:
# File ~/.tmux.conf.d/sessions/fe.conf
# Working directory is ~/App/fe
# Rename window :0
rename-window -t :0 'git'
# Send 'cd ~/App/fe' to window :0 and clear
send-keys -t :0 'cd ~/App/fe' C-m C-l
# Make a new named window
new-window -t :1 -n 'npm' -c ~/App/fe
# Select first window
selectw -t 0
Then we can create similar configuration files for core
and deployment
. We can determine how many tmux windows
would each of them be, and what to name (or not) each of the tmux
windows. We can also send and run arbitrary commands automatically (such
as npm run dev
, git pull
, or even
git log --oneline --graph
) in the desired tmux windows.
Automatically run tmux in vscode terminal
The last part that we need to implement are step 5-6 of our pseudocode. We want to test whether we’re running under vscode or not. Surprisingly, the solution to this is fairly trivial. You can read it from StackOverflow or AskUbuntu.
Basically we want to check whether or not $TERM_PROGRAM
environment variable is set to vscode
. If that is true,
then we are in vscode
, and we can just connect to a session
with the same name as our working directory using our tmx
script. The implementation of those statements that can be run under
bash and zsh is:
[[ "$TERM_PROGRAM" == "vscode" ]] \
&& tmx "$(basename "$PWD")" || true
The last true
command is only run if we are not in
vscode. I added it there because in some terminal prompt style, we get a
certain feedback if the last run command gives nonzero exit status. For
example in a default oh-my-zsh
installation, the prompt
indicator might turn from green to red the first time I run a terminal
from outside of vscode. That is a minor annoyance for me, especially if
that line is appended at the end of our .bashrc
and/or
.zshrc
.
The true
statement would always return exit status
0
, which guarantees a clean exit status at the beginning of
our interactive terminal prompt. That is, assuming that the
tmx
script does not terminate with a nonzero exit
status.
Conclusion
To improve our developer workflows, and addresses the issues explored in this article, we implemented the following measures:
- We implemented a sane, minimalistic, and usable default tmux
configuration in
~/.config/tmux/tmux.conf
. It can be downloaded from gitlab. - To allow a persistant terminal session that can be accessed from any terminal or ssh connection in our developer’s machine, we implemented a script to generate a new named tmux session on demand, or connect to an existing named tmux session if already present. The script can be obtained from gitlab. To use it, add the file to your path and make it executable.
- To automatically launch arbitrary amount of named tmux sessions on
terminal start up if they are not run already, and load the associated
tmux configuration files under
~/tmux.conf.d/sessions/
, we implemented aset_tmux
script. The script can be obtained from gitlab. To use it, add the file to your path and make it executable. - Developers are encouraged to create their own tmux config files and
place it under
~/.tmux.conf.d/sessions/
with the same name as their project directory and file extension.conf
, as every developer have their own specific workflows and preferences. - In
~/.bashrc
or~/.zshrc
, adds the attached snippets (Appendix) to enable our setup. Modify as you see fit.
Appendix
# Add ~/.local/bin/ to $PATH if not already
# present. It should contains tmx and set_tmux
# script for our setup
test -z "$(echo $PATH|grep "$HOME/\.local/bin")" \
&& PATH="$HOME/.local/bin:$PATH"
# Change the tmux session names according to your
# needs. Also prepare custom configurations under
# ~/.tmux.conf.d/sessions/ if desired
set_tmux core fe deployment
# Check if we are under vscode. If yes, run tmux
# and/or attach to an existing tmux session
[[ "$TERM_PROGRAM" == "vscode" ]] \
&& tmx "$(basename "$PWD")" || true