This all started at an interview I had recently. I was asked about my opinion on systemd, and my answer was that I like it so far, and I find writing unit files for it much easier than writing SysVinit, Upstart, or other distro specific scripts, and I quite love the user services part about it. The interviewer mentioned that he has written a systemd service to change wallpapers as a user service, and I thought that's a brilliant idea, since almost 90% of my shell script deals with the scheduling part, and all that could be outsourced to systemd.

First, what's a user service. Basically, systemd allows you to have services defined on a per-user basis, so for your user specific needs, you don't have to clutter any global space, like /etc, everything can just live in your home. You manipulate user services with systemctl --user/journalctl --user.

Requirements

Let's specify the requirements that I want this script to do:

  • I want my wallpaper to be set to a random wallpaper from a folder when I start X
  • I want to have some kind of "next wallpaper" functionality, that rerolls the wallpaper
  • I want the wallpaper to automatically reroll at fixed intervals

So the shell script that I came up with for this problem 3 years ago (been using it since then) looks like this:

bash
#!/bin/bash
# Change wallpaper every X minutes
INTERVAL=$((30 * 60))

# Pull a random image from here
DIR=~/Dropbox/Photos/Wallpapers/

COUNTER=0
trap change_wallpaper USR1

change_wallpaper() {
  NEWPAPER=$(find "$DIR" -type f \( -name '*.jpg' -o -name '*.png' -o -name '*.jpeg' \) | shuf -n1)
  feh --bg-fill "$NEWPAPER"
  COUNTER=0
}

change_wallpaper
while true; do
  if [[ $COUNTER -ge $INTERVAL ]]; then
    change_wallpaper
  fi
  sleep 1
  COUNTER=$(( $COUNTER + 1 ))
done

We define the directory for wallpapers, initialize a counter, set the interval, then we set up a handler for the USR1 signal with trap, that executes the wallpaper changing function. This is the part that solves problem number #2, because it allows us to reset the wallpaper by sending USR1 to the script. The change_wallpaper function uses find to select all images, and shuf -n1 to filter that down to one random line, and executes feh --bg-fill to set the wallpaper.

Then I unconditionally call change_wallpaper, to solve problem number #1, when the script starts, it will change the wallpaper immediately once. The remaning while-true loop is the "scheduler" the solves number #3. It looks at the COUNTER, and calls change_wallpaper if it's greater than our specified interval, otherwise, it sleeps for a second, then increments $COUNTER. I call this script from my .xinitrc, and for making it easier to deal with problem number #2, I added a menuitem into my window managers root menu (it's a menu that pops up when I right click on the desktop), that does a pkill -USR1 wallpaper.sh. Most of the code is dealing with the scheduling logic. It's also next to impossible to notice if something goes wrong inside the script, because its standard output and error is bound to the console that X was started from (since it is started in .xinitrc).

Let's see how this would look as a systemd service, backed with a timer:

~/.config/systemd/user/wallpaper.service
shell
[Unit]
Description=Change wallpaper at regular intervals

[Service]
Type=oneshot
Environment=DISPLAY=:0.0
ExecStart=/home/user/bin/new_wallpaper.sh
~/.config/systemd/user/wallpaper.timer
shell
[Unit]
Description=Change wallpapers every 30 minutes

[Timer]
OnActiveSec=5sec
OnUnitActiveSec=30min
changer.sh
bash
#!/bin/bash

DIR=~/Dropbox/Photos/Wallpapers/
NEWPAPER=$(find "$DIR" -type f \( -name '*.jpg' -o -name '*.png' -o -name '*.jpeg' \) | shuf -n1)

if feh --bg-fill "$NEWPAPER"; then
  echo "Changed wallpaper to $NEWPAPER"
fi

It's pretty self-explanatory. The OnActiveSec takes care of changing the wallpaper 5 seconds after starting the timer, and the OnUnitActiveSec takes care of changing it afterwards every 30 minutes. These files go into ~/.config/systemd/user/ in case you are wondering (create it if you don't have it). The script became 6 lines, down from 24, however we have 3 files to manage now. We gained the ability to see errors or any kind of messages from the script (by using systemctl --user status wallpaper.service/wallpaper.timer). The script still needs to be started by .xinitrc, except now you have to call systemctl --user start wallpaper.timer. We now also have the option of suspending it, by issuing a systemctl --user stop wallpaper.timer, and we can see if it's currently running with systemctl --user status. In general, maintanence and debugging became much easier. If you want to change the wallpaper immediately, you can start the service itself, with systemctl --user start wallpaper.service (tip: if you leave out the part after the ., systemd will assume you want to interact with service).

This is complete, but We can take this another step forward. This service only makes sense if X is already running, and currently we make sure of this by launching it from .xinitrc. If you have many services that you want to start automatically after X, you can achieve a sort of pub/sub system, by using systemd targets.
Taking it a step further with targets

Create a target file, under ~/.config/systemd/user/X.target:

~/.config/systemd/user/X.target
shell
[Unit]
Description=Xorg server is running

Then change the timer:

~/.config/systemd/user/wallpaper.timer
shell
[Unit]
Description=Change wallpapers every 30 minutes

[Timer]
OnActiveSec=5sec
OnUnitActiveSec=30min

[Install]
WantedBy=X.target

Now, use systemctl --user enable wallpaper.timer. With this, you just subscribed the timer to be started when X.target triggers. So when does X.target trigger? It doesn't! It's an arbitrary target we just defined, so we have to start it manually, so put this in your .xinitrc: systemctl --user start X.target (you can replace the previous systemctl start call). Now, you can write services that depend on X, and you just have to systemctl --user enable them.

So is this better than the shell script? I'm not sure. For this particular scenario, it's more files to maintain, for sure. However, if I look at the big picture, now I have an easy way of seeing the state of all my services, and a way of seeing their logging output, that's definitely a big plus. I can definitely see myself moving other stuff that I autostart (like dropbox, rofi, dunst) into systemd user services, just so I can look at their output and status.