Switching Themes on the fly with XMonad
Edward Wibowo,
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:
- Through the designated configuration file.
- 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
).