A watercolor painting with a pink/blue swirly border around a rectangle split along the diagonal with the left being black and the right being white. The text reads 'Alacritty Auto Theme' in the inverse color of the background.

Fundamentally automation is being smart about being lazy, I like that. Writing scripts to do small tasks is an easy way to be lazy but there are times when you don’t want to run the script and remember to start it each time you log in. That’s where services come in handy, a lot of applications install their service to automatically do things. As an user you can do that too without needing admin rights. This article/guide goes through writing a small bash script, creating a systemd service, and running it as a regular user.

Since learning is easier in the context of a project where I’m solving a real problem, this article is focused on creating everything I need to automatically change the theme of my terminal emulator Alacritty when the system changes between light-dark theme. If you just want the project itself, the repos are on Github or Sourcehut.

Intended audience

This entry is written for an audience that might be interested in tweaking their system/wants to make small scripts but does not necessarily have a programming background so I’m aiming to explain what is happening and why do it that way. I’ll also try to point out larger concepts around programming/logic and link to those definitions.

Motivation behind this project

I like and use the Alacritty terminal emulator, but it does not automatically follow the system theme. The issue tracker discussion made it clear this feature won’t be supported. Fair enough and as an open system we can add our own customization, so that’s great. After switching to TOML and discovering partial imports, I knew I could scratch my own itch. Someone wrote a rust tool(post updated to z-bus since original publication but still relevant bits) which was helpful as a guide but I wanted something with low dependency that could be used without installing a whole build environment or running binaries that you can’t see. So I made a bash script and a systemd service and it was fun(?) to learn more about dbus.

Background

Alacritty provides the ability for an user to define themes (or override parts of a theme). All of this is achieved via the alacritty.toml configuration file (which also let your configure everything else about the application). With the switch to TOML, Alacritty allows the import of other .toml files that have themes defined, so it’s easy to keep configuration separate from the current theme. Also if any of the configuration files are updated, the terminal windows will auto-reload (if live_config_reload = true is set). This functionality makes it possible to import a theme.toml file and update the contents of the file and have Alacritty change themes live. Unfortunately, Alacritty does not automatically follow the current system theme preference so we have to create our own solution for that piece.

So how does it work? Breaking the problem down we can see there are two big pieces to this that are independent of each other; changing the theme for Alacritty programmatically and making the theme follow the system theme (light/dark mode). This is the concept around scope and requirements management, note that scope and requirements are loaded terms also used for software development and often in the same sentence but their meaning changes with context.

Changing theme programmatically

Since Alacritty is capable of doing a live-reload of it’s config file, any behavior can be changed in without relaunching the terminal. However, doing it cleanly requires creating three additional .toml files to contain all the theme information. That way the main configuration file doesn’t need to be changed (and risk messing up the whole configuration and making the terminal potential unusable) each time the theme changes. This is the concept around separation of concerns and can definitely be applied to simple scripts and config files.

Let’s look at the files: alacritty.toml⬇️

1
2
3
live_config_reload = true
import = [ "~/.config/alacritty/alacritty-auto-theme/theme.toml" ]
# Rest of Alacritty config

theme.toml⬇️

1
import = [ "~/.config/alacritty/alacritty-auto-theme/light_theme.toml" ]

The main config file alacritty.toml just points to (imports) a file called theme.toml so whatever theme name or actual theme colors are in that file are loaded (as shown above, it’ll load the light theme). But changing that file with the name of the preferred light and dark theme each time would be painful, so we’ll introduce two more files light_theme.toml and dark_theme.toml which will contain the names/color definition of the preferred theme. This way, the user only has to update those two files with their preferred theme and the script can just switch theme.toml to point to one or the other. This is the simplest use case of modularity to avoid the human and the computer from editing the contents of the same document and gain more predictability.

light_theme.toml⬇️

1
import = [ '~/.config/alacritty/themes/themes/pencil_light.toml' ]

dark_theme.toml⬇️

1
import = [ '~/.config/alacritty/themes/themes/nord.toml' ]

At this point you can manually change the light/dark theme independent of the system theme by changing what is inside the theme.toml file. However, we’d prefer that the human never actually directly touches the that file, so we can define two aliases alacritty-light or alacritty-dark to make it convenient without having to edit the file manually.

1
2
alias alacritty-light="echo \"import = [ '~/.config/alacritty/alacritty-auto-theme/light_theme.toml' ]\" > ~/.config/alacritty/alacritty-auto-theme/theme.toml"
alias alacritty-dark="echo \"import = [ '~/.config/alacritty/alacritty-auto-theme/dark_theme.toml' ]\" > ~/.config/alacritty/alacritty-auto-theme/theme.toml"

And just with that we can change our terminal theme by just calling a single command. Part one is done and successful, take the W and celebrate!

Automating theme change to follow system theme

This part is a little more involved. Since Alacritty does not provide any mechanism to determine what the current system theme preference is, we have to listen for the system announcing when the theme is changing. On Linux a lot of that communication is done over D-Bus and listening to the right message will tell us when the theme changes and then we can take action.

dbus-monitor allows us to listen to the all the messages or we can set filters to only listen to specific events. I didn’t know much about the workings of dbus so the Rust tool article linked above and several Stack Overflow threads helped me to get the syntax figured out. You can just run dbus-monitor without any filters in your terminal now to see everything talking on it. But in your script we’ll only listen for the setting change notification.

Script

AlacrittyAutoTheme.sh⬇️

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
interface="org.freedesktop.portal.Settings"
monitor_path="/org/freedesktop/portal/desktop"
monitor_member="SettingChanged"
count=0 #D-Bus fires the change event 4 times so we'll only act on it once

dbus-monitor --profile "interface='$interface',path=$monitor_path,member=$monitor_member" |
    while read line; do
	    let count++
	  if [ $count = 3 ]; then
		  theme="$(gsettings get org.gnome.desktop.interface color-scheme)"
		  if [[ "$theme" == "'prefer-dark'" ]]; then
			  #Need to set with full paths, goofy things are happening otherwise
			  echo "$(echo import = [ \'~/.config/alacritty/alacritty-auto-theme/dark_theme.toml\' ] > ~/.config/alacritty/alacritty-auto-theme/theme.toml)"
		  else
			  echo "$(echo import = [ \'~/.config/alacritty/alacritty-auto-theme/light_theme.toml\' ] > ~/.config/alacritty/alacritty-auto-theme/theme.toml)"
		  fi
		  count=0
	  fi
    done

So what’s happening here:

  • First we set up the filter (line 2-4) for settings changed then we start monitoring dbus (line 7).
  • We keep listening until we have matched our filter, now we can execute our commands. You’ll see that the first thing we do is increment a counter (line 9) and only take action the 4th time (line 10), that’s because the message goes out on dbus 4 times and I don’t know why but we only need to act once.
  • We read the current theme (line 11) so we don’t have to keep track of what it was, this is called stateless design. 1
  • We set the appropriate theme based on what the user selected (lines 12-17). Note: we could have called the aliases we defined in the previous section but the user could change the alias or it could get removed for whatever reason and we don’t want to create a dependency outside the scope of our control.
  • We reset the counter so we can start counting again the next time there’s a new event (line 19).

We can leave a terminal open all the time and keep that script running in it. That would work but we want it to auto-start every time we’re logged in and monitor in the background. That’s what a systemd service allows us to do:

Service

AlacrittyAutoTheme.service⬇️

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[Unit]
Description=Alacritty automated theme switching based on Gnome system theme
Require=dbus.service
After=dbus.service

[Service]
ExecStart=/bin/bash /home/%u/.config/alacritty/alacritty-auto-theme/AlacrittyAutoTheme.sh
Type=simple
Restart=on-failure

[Install]
WantedBy=default.target

We don’t really need to understand this beyond following the template, but here’s a good resource. So what’s happening here:

  • [Unit]: Describes what this service does and what it is dependent upon
  • [Service]: What do we want to happen? We want to run our script, so we have to say how to do that /bin/bash and where it is located ./home/%u/.config/alacritty/alacritty-auto-theme/AlacrittyAutoTheme.sh 2
  • [Install]: We want it to run only for the current user.

Install

Alright, we’re finally at the point where we can put it all these small pieces and make it all work together.

1
2
3
4
mkdir -p ~/.config/systemd/user/
cp ./AlacrittyAutoTheme.service ~/.config/systemd/user/
systemctl --user enable AlacrittyAutoTheme.service
systemctl --user start AlacrittyAutoTheme.service

We create an user space systemd service folder3 so that we don’t need admin rights on the machine to run the script as a service when we log-in. Then we copy the service to that folder and use systemctl command to talk to systemd and tell it to enable and then start our service (note --user so for user space).

The idea of arranging small tools to accomplish a big task is called composability I’m burying the lede here because all the other concepts I’ve mentioned before fall under composability but it’s too top down and theoretical until you see the whole toolchain being put together.

That’s it, we’ve scratched our own itch, created a standalone tool that could be used by others, and learned about concepts.

Conclusion

We’ve followed the UNIX philosophy fairly closely and making tools that are much more complex fundamentally follows a similar flow. I wanted to write this article as an exercise to understand the basics required to do something that most Linux users would consider rather straightforward. I still don’t know if it’s written at an appropriate level for the intended audience but I ended up having to write a LOT more than I would have imagined at the start. I want to continue making things I know more accessible to others so if this applies to you, I would love to hear your thoughts and feedback and happy to help if you have any questions.


  1. gsettings is only available on the Gnome desktop environment so we could support other systems by checking what system we’re on and calling the appropriate function to read the current state. ↩︎

  2. Systemd service does not understand relative paths like ~ (to point to home directory), but it has it’s own Specifiers like %u↩︎

  3. The -p only makes a new folder if one doesn’t exist. ↩︎