Switching Themes on the fly with XMonad

Edward Wibowo,

XMonad Theme

As part of my conquest to gain full control over my Linux system, I wanted to implement a way to quickly change color schemes. I wanted a system where I could change the color scheme of my entire system with ease.

There was one challenge, however: I use a window manager called XMonad, which is configured in Haskell.

This meant that I had to implement custom logic to pursue my theme-switching needs. So, here is how I did it:

Xresources

To sync a color scheme across multiple applications, there must be a central source where the assorted colors can be retrieved. In my case, I decided to use a dotfile called Xresources. Essentially, the Xresources file, located at ~/.Xresources, can be used to specify the colors of the color scheme.

Here is an example of an Xresources file:

*.foreground:  #cbccc6
*.background:  #1f2430
*.cursorColor: #ffcc66
*.color0:      #191e2a
*.color8:      #686868
*.color1:      #ed8274
*.color9:      #f28779
*.color2:      #a6cc70
*.color10:     #bae67e
*.color3:      #fad07b
*.color11:     #ffd580
*.color4:      #6dcbfa
*.color12:     #73d0ff
*.color5:      #cfbafa
*.color13:     #d4bfff
*.color6:      #90e1c6
*.color14:     #95e6cb
*.color7:      #c7c7c7
*.color15:     #ffffff

Now, the goal is to get a bunch of other applications to read the same file.

XMonad

The centerpiece of the theme-switching system is the underlying logic within the XMonad configuration.

Running $ xrdb -query allows the retrieval of the properties defined in the Xresources file. Essentially, the XMonad configuration needs to run this command and parse it to query for specific fields. This is done through the following Haskell functions (as part of the XMonad configuration):

import Data.Bifunctor (bimap)
import Data.Char as DC
import Data.List as DL
import Data.Maybe (catMaybes, fromMaybe)
import System.IO.Unsafe (unsafePerformIO)
import XMonad.Core (installSignalHandlers)
import XMonad.Util.Run (runProcessWithInput)

splitAtColon :: String -> Maybe (String, String)
splitAtColon str = splitAtTrimming str <$> DL.elemIndex ':' str
  where
    splitAtTrimming :: String -> Int -> (String, String)
    splitAtTrimming s idx = bimap trim (trim . tail) $ splitAt idx s
    trim :: String -> String
    trim = DL.dropWhileEnd DC.isSpace . DL.dropWhile DC.isSpace

getFromXres :: String -> IO String
getFromXres key = do
  installSignalHandlers
  fromMaybe "" . findValue key <$> runProcessWithInput "xrdb" ["-query"] ""
  where
    findValue :: String -> String -> Maybe String
    findValue xresKey xres =
      snd <$>
      DL.find ((== xresKey) . fst) (catMaybes $ splitAtColon <$> lines xres)

xProp :: String -> String
xProp = unsafePerformIO . getFromXres

Disclaimer: calling a process (xrdb) within the XMonad configuration isn’t the most efficient workaround. But it works.

With this code, the xProp function can be called to retrieve a property defined in the Xresources file. For example:

> xProp "*.foreground"
"#cbccc6"

To make configuration easier, I implemented the following functions to more easily retrieve certain properties:

xFont :: String
xFont =
  "xft:" ++ fst (fromMaybe (xProp "*.font", "") (splitAtColon (xProp "*.font")))

xFontSized :: String -> String
xFontSized s = xFont ++ ":size=" ++ s

xColorFg :: String
xColorFg = xProp "*.foreground"

xColorBg :: String
xColorBg = xProp "*.background"

xColor :: String -> String
xColor a = xProp $ "*.color" ++ a

With these additional functions, XMonad-specific color properties can be defined easily:

promptConfig :: XPConfig
promptConfig =
  def
    { font = xFont
    , position = CenteredAt (1 / 4) (3 / 7)
    , promptBorderWidth = 0
    , height = 30

    -- Custom colors based on Xresources colors
    , bgColor = xColorBg
    , fgColor = xColorFg
    , fgHLight = xColorBg
    , bgHLight = xColor "4"

    , historySize = 0
    }

Xmobar

There are two main ways to theme Xmobar:

  1. Through the designated configuration file.
  2. Through command-line arguments.

For a while, I tried to find a way to use the Xmobar configuration file to dynamically allocate colors; however, I found the process to be quite rigid. Xmobar does provide an option for users to write the configuration in pure Haskell, but it’s slightly more annoying to set up. So, I opted for using command-line arguments instead.

To do so, I added the following to my XMonad configuration:

barSpawner :: String -> ScreenId -> IO StatusBarConfig
barSpawner hostname screen =
  statusBarPipe
    ("xmobar" ++
     -- Set xmobar color and font through command line arguments.
     xmobarArg "B" xColorBg ++ -- The background color.
     xmobarArg "F" xColorFg ++ -- The foreground color.
     xmobarArg "f" (xFontSized "12") ++ -- Font name.
     xmobarArg "N" (xFontSized "15") ++ -- Add to the list of additional fonts.
     xmobarArg "x" [last (show screen)] ++ -- On which X screen number to start.
     " " ++ xmobarConfigPath) $
  pure (barPP screen)
  where
    xmobarArg :: String -> String -> String
    xmobarArg flag value = " -" ++ flag ++ " \"" ++ value ++ "\""

Using command-line arguments in this way allows the barSpawner function to spawn Xmobar instances using the designated theme colors.

xtheme Shell Script

To tie everything together, I wrote a shell script to fetch color themes from this repository:

#!/bin/sh

file="$HOME/.config/Xresources-theme"

theme="$(echo "$1" | sed "s/ /%20/g")" # Replace spaces with %20
link="https://raw.githubusercontent.com/mbadolato/iTerm2-Color-Schemes/master/Xresources/$theme"

content=$(curl -f "$link" | sed "/^!/d") && printf "! Xresources theme generated by xtheme.
! Theme: %s
! Link: %s
! Generated: %s

%s" "$1" "$link" "$(date +%s)" "$content" >"$file"

Running $ xtheme "nord" would download the nord color scheme from the repository and save it to the file at ~/.config/Xresources-theme. This file is then imported by adding the line #include ".config/Xresources-theme to the original Xresources file (~/.Xresources).