Setting Up a Service to Run tmux-continuum
without a Status Line
tmux-continuum
and its companion plugin, tmux-resurrect
, are invaluable tools for managing sessions in tmux
. However, I recently encountered an issue where these plugins stopped working after I made some changes to my configuration.
Note on Nix and the Content of This Post
Please note that this post primarily addresses solutions using the Nix package manager to manage software. While I also provide more general solutions, they are not the main focus.
To provide some context, tmux-resurrect
allows you to save and restore entire tmux
sessions, while tmux-continuum
automatically saves your sessions using tmux-resurrect
and restores them on server startup. However, when I started my tmux
server recently, I didn’t get the expected recent state but instead an older one.
Installing
tmux
Plugins with Nix
Home Manager simplifies the installation oftmux
and additional plugins. Here’s an example configuration snippet:
programs.tmux = {
enable = true; # Enable the program
plugins = with pkgs.tmuxPlugins; [
# Enable a plugin with a set if you want to pass config options
{
plugin = tmux-thumbs;
# ExtraConfig will just be added to your tmux.conf file
# So, write all configurations the same way
extraConfig = ''
set -g @thumbs-key Space
'';
}
# Enable a plugin by name if you aren't passing config options to it
fuzzback
];
extraConfig = builtins.readFile ./config; # Source additional configurations from a provided config file
};
So, what went wrong? The issue was that I had turned off my status line in tmux
, and as a result, the tmux-continuum
plugin stopped working.
Problem: Running tmux-continuum
with No Status Line
tmux-continuum
vs.tmux-resurrect
While I’ve mainly discussedtmux-continuum
because it has the functionality I wanted to fix, it’s essentially a wrapper aroundtmux-resurrect
with added utility, including a lock-file mechanism for saving and automatic server restoration. Ideally, all saving should happen by callingcontinuum_save.sh
.
Unfortunately, in my case, when
continuum_save.sh
tried to callsave.sh
fromtmux-resurrect
, it wouldn’t work for some reason. I lacked the necessary troubleshooting knowledge to fix it, so I resorted to directly callingtmux-resurrect
’s save script.
Let’s first take a quick look at tmux-resurrect
itself to understand the problem domain. Essentially, this plugin consists of a .tmux
file and several helper scripts that it can invoke. The resurrect.tmux
file is, in fact, a Bash script that you include in your tmux
configuration.
$ > tree ../resurrect
# Full file path and some contents omitted
../resurrect
├── resurrect.tmux
└── scripts
├── restore.sh
├── save.sh
├── helpers.sh
├── shared.sh
└── variables.sh
Why Does the Status Line Matter?
My understanding is that the only part oftmux
that regularly refreshes and can be hooked into is the status line, which refreshes every 15 seconds. There are no other regularly executing hooks. Therefore, to maketmux
execute the script at regular intervals, the plugin injects the save script into the status line. Every 15 seconds, the status line triggers the save command, which checks if enough time has passed since the last execution and then runs and updates a global config if necessary.
With no status line, the script won’t execute on my system, so I had to explore other options. I considered various solutions from a related GitHub issue, including:
- Integrating session saving into the Bash prompt, but this wouldn’t work well with Nix because the script path can vary.
- Creating aliases for commands that terminate a
tmux
session to include a call to the save script. - Creating a
systemd
service to run in the background and auto-save on a timer, which seemed the most challenging.
Adding Auto-Saving via Bash Prompt
You can add the following code to your.bashrc
:precmd() { if [ -n "$TMUX" ]; then eval "$(tmux show-environment -s)" eval "$($HOME/path/to/continuum/scripts/continuum_save.sh)" fi } PROMPT_COMMAND=precmd
This code calls the save function every time your Bash prompt updates, ensuring the plugin’s internal checks prevent excessive saving.
Given that I use Nix, the Bash prompt method was impractical. When using Nix to install tmux-resurrect
, the script’s path is not consistent, making it unsuitable for a Bash prompt solution. To address this, I decided to create a service.
Running
save.sh
Directly
Runningsave.sh
directly poses an issue: if it’s called when notmux
session is running, it saves an empty file. To overcome this, I wrote a short Bash script:
# scripts/tmux-save.sh
#!/usr/bin/env bash
if [ "$(pgrep tmux)" ] && [ "$RES_SAVE_PATH" ]; then
$RES_SAVE_PATH quiet
fi
I used an environment variable, RES_SAVE_PATH
, which the service will pass to the script.
Systemd Services
In essence, any service consists of a .service
file residing in the directory where systemd
expects to find services. This directory can vary, but for user services, it’s often $HOME/.config/systemd/user/
.
You can create a minimal service file with just ExecStart
, ExecStop
, or SuccessAction
:
# test-service.service
[Service]
ExecStart=/usr/bin/env bash -c "echo hello world"
You can test the service with systemctl --user start test-service.service
and check its status.
$ > systemctl --user status test-service.service
○ test-service.service
Loaded: loaded (/home/bandito/.config/systemd/user/test-service.service; static)
Active: inactive (dead)
Aug 17 18:56:30 desktop systemd[1625]: Started test-service.service.
Aug 17 18:56:30 desktop env[107387]: hello world
Auto-Save Service
How should our auto-save service look?
# tmux-autosave.service
[Service]
ExecStart=bash path/to/save.sh
# Indicates that the service will run once then go inactive
Type=oneshot
[Unit]
Description=Run tmux save script every 15 minutes
OnFailure=error@%n.service
This service needs something to keep track of time and trigger it when needed: a timer.
# tmux-autosave.timer
[Install]
# Loads timers after system boots. Suggested target for most application timers.
# Other options often used are default.target or graphical.target, but those load earlier during the login shell.
WantedBy=timers.target
[Timer]
# The timer will start after 5 minutes on the first run.
OnBootSec=5min
# The timer will run every 15 minutes when the computer is active.
OnUnitActiveSec=15min
# It will activate the tmux-autosave.service unit.
Unit=tmux-autosave.service
[Unit]
Description=Run tmux save script every 15 minutes
Unfortunately, due to path issues, we can’t conveniently call the save script with a .service
file. We have to define these files directly in our Nix configuration. This leads to the choice of whether to define the service as a system or user service.
Creating a Service in Nix
There are two types of services: system and user services. A computer has only one system service, which runs as long as the system is active. In contrast, a computer can have multiple user sessions. Since auto-saving tmux
sessions is relevant only to my development environment as a user, it makes sense to keep it as a user service. Services like sound drivers or network cards are better suited for system-wide operation, as they apply to all users.
Having chosen to create a user service, we must decide whether to use a Nix module or Home Manager. Home Manager is preferable because it stores the file in a subdirectory of $HOME/.config
, making it easier to access. Nevertheless, I’ll provide examples for both options.
Home Manager:
systemd.user.services.tmux-autosave = {
Unit = {
Description = "Run tmux_resurrect save script every 15 minutes";
OnFailure = "error@%n.service";
};
Service = {
Type = "oneshot";
Environment = [
"RES_SAVE_PATH=${pkgs.tmuxPlugins.resurrect}/share/tmux-plugins/resurrect/scripts/save.sh"
];
ExecStart = "${pkgs.bash}/bin/sh ${./scripts/tmux-save}";
};
};
systemd.user.timers.tmux-autosave = {
Unit = {
Description = "Run tmux_resurrect save script every 15 minutes";
};
Timer = {
OnBootSec = "5min";
OnUnitActiveSec = "15min";
Unit = "tmux-autosave.service";
};
Install = {
WantedBy = [ "timers.target" ];
};
};
A couple of points to note: First, in the Nix language, when you specify a relative path or package in a string, Nix translates it into an absolute path. For instance, ${pkgs.bash}
becomes /nix/store/p6dlr3skfhxpyphipg2bqnj52999banh-bash-5.2-p15/bin/sh
in the actual file.
Second, I added an environment variable, RES_SAVE_PATH
, which uses Nix syntax to set the variable to the path of the tmux-resurrect
save script.
Nix Module:
systemd.user.timers.tmux-save = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "5min";
OnUnitActiveSec = "15min";
Unit = "tmux-save.service";
};
description = "Save tmux sessions";
onFailure = [ "error@%n.service" ];
};
systemd.user.services.tmux-save = {
script = ''
${pkgs.bash}/bin/sh ${./scripts/tmux-save}
'';
description = "Save tmux sessions";
environment = {
RES_SAVE_PATH = "{pkgs.tmuxPlugins.resurrect}/share/tmux-plugins/resurrect/scripts/save.sh";
};
serviceConfig = {
Type = "oneshot";
};
path = with pkgs; [ tmux toybox ];
};
The timer configuration is essentially identical, with minor syntax differences. You can find all the configuration options for services here and for timers here. One noteworthy addition is the path
section. As per the documentation:
Packages added to the service’s
PATH
environment variable. Both thebin
andsbin
subdirectories of each package are added.
Without this, the script wouldn’t have access to commands like tmux
or basename
. Adding these packages to the path is essential for proper execution. The Home Manager version doesn’t encounter the same issue because it has a different runtime environment (though the specifics are beyond my knowledge).
Viewing Service File Contents
To inspect the contents of these service files, you can use thesystemctl cat --user name_of_service.service
orname_of_timer.timer
command.
Running the Services
With the services set up, you can find the created files in ${HOME}/.config/sytemd/user
:
$ > tree ~/.config/systemd/user/
~/.config/systemd/user/
├── timers.target.wants
│ └── tmux-autosave.timer -> /nix/store/ds3qmyw4hkw72ac40g1w13vfj9ahwg44-home-manager-files/.config/systemd/user/timers.target.wants/tmux-autosave.timer
├── tmux-autosave.service -> /nix/store/ds3qmyw4hkw72ac40g1w13vfj9ahwg44-home-manager-files/.config/systemd/user/tmux-autosave.service
└── tmux-autosave.timer -> /nix/store/ds3qmyw4hkw72ac40g1w13vfj9ahwg44-home-manager-files/.config/systemd/user/tmux-autosave.timer
You might wonder about the timers.target.wants
directory. This directory and the symbolic link within it are created if you enable the timer using systemctl --user enable tmux-autosave.timer
. It’s part of the timer installation process, allowing the timers.target
unit to know which services to start.
If you had
used WantedBy=default.target
instead of WantedBy=timers.target
, the installation would have created a default.target.wants
directory with a symbolic link to the timer.
You can view the contents of the files Nix created using the cat \path\to\service
command or, for more details, systemctl cat service_name
.
$ > cat ~/.config/systemd/user/tmux-autosave.timer
# The Timer
[Install]
WantedBy=timers.target
[Timer]
OnBootSec=5min
OnUnitActiveSec=15min
Unit=tmux-autosave.service
[Unit]
Description=Run tmux_resurrect save script every 15 minutes
$ > systemctl cat --user tmux-autosave.service
# The Service
[Service]
ExecStart=/nix/store/p6dlr3skfhxpyphipg2bqnj52999banh-bash-5.2-p15/bin/sh /nix/store/dmy3nqasx8dixz5hyllzhbdri8n4n1sa-tmuxplugin-resurrect-unstable-2022-05-01/share/tmux-plugins/resurrect/scripts/save.sh quiet
Type=oneshot
[Unit]
Description=Run tmux_resurrect save script every 15 minutes
At this point, you’ve fully set up your services. Services provide a versatile way to run scripts automatically, and you can use this capability for various tasks. For example, you can create services to periodically ping the databases of your hobby projects to prevent them from going inactive when on free plans.
While Nix itself doesn’t bring anything special to services, it offers a more declarative approach. By letting Nix control the script’s contents, you can be more declarative with your services. Even though you’re still hard coding the script’s location into the service, Nix ensures that the script will always be in that location.