Moving Back to a Tiling WM – XMonad

2 hours ago 2

Here are my dotfiles.

When I was still using Manjaro Linux back in 2019, I got a nudge to try i3wm. It was my first experience with any window manager. And I spent nearly 5 years with it, enjoying the absolute control over my workflow. Nearing the end of 2023, when I finally decided to leave Manjaro (for good), I had a bunch of options on my hand. Fedora looked really promising at that time. But even then, I wasn’t sure I was going to be using any tiling window manager. I happily switched to Gnome in Fedora 40. I ran it along with XOrg so that I could make my capslock key act as a ctrl when held and as an escape when pressed once, using setxkbmap and xcape. But only after spending a few months there, I realized I missed that finer control at my fingertips. So, I resumed searching for a newer tiling window manager. I was also learning Haskell at that time, so picking up XMonad was natural.

XMonad Fastfetch

There are a lot of things that I like about XMonad apart from its standard tiling manager features. I enjoy writing the configuration in Haskell. Where ever possible, I try to leverage the benefits of Haskell’s strong type system. Defining keybindings with a strong type system ensures that I cannot go very wrong with it. Using stack for building my configuration allows me to port the entire configuration easily to other systems, which are my various virtual machines. I have split configuration in various modules.

src ├── Keybindings.hs ├── Layout.hs ├── Plugins │   ├── Bluetooth.hs │   ├── Pomodoro.hs │   └── Soundtrack.hs ├── Preferences.hs ├── Theme │   ├── Dmenu.hs │   ├── Font.hs │   ├── Theme.hs │   └── Xresources.hs ├── Workspaces.hs ├── xmobar.hs └── xmonad.hs 3 directories, 13 files

If you want to poke around the config according to your needs, go through Preferences.hs. It contains lots of variables which can be customized like terminal emulator, browser, scratchpads, window gap size etc. It also contains a list of applications which you would like to start automatically at boot.

Overall, the modularization has turned out to be pretty in terms of categorizing things. I tried writing a few xmobar plugins for my own needs. The guide for writing them was straightforward to begin with. I also wrote my entire xmobar configuration in Haskell itself, keeping this executable in the same project. In the end, the project itself became a one-shot way for an entire desktop environment which I can easily clone, compile and install on any system.

I will go briefly over the stack-based setup. The only thing needed is to have a build script at the root of your xmonad project. Everything else is simply a normal stack project with modules and a few executables. I have 2 executables in my project: xmonad and xmobar.

A detailed description and example build files can be found here. My build script is simple enough.

#!/bin/sh SRC_DIR=$HOME/.config/xmonad WM=xmonad unset STACK_YAML FAIL=0 cd $SRC_DIR stack install 2>.log || FAIL=1 ln -f -T $(stack exec -- which $WM) $1 2>.log || FAIL=2

XMonad Workflow

Stack is a package manager for Haskell projects and it will be used to compile the package. Install stack either via GHCup or your distribution’s package manager.

mkdir -p $HOME/.config/xmonad git clone --branch release https://github.com/weirdsmiley/xmonad $HOME/.config/xmonad/ cd $HOME/.config/xmonad ./install.sh

The installation script will install a few fonts and other tools which are default for this setup. It will also write .xinitrc and .Xresources files.

After the installation is complete, and you are logged into xmonad, pressing alt+shift+/ or alt+? will open up a dialog box containing all available keybindings.

3.1. Layouts and per-workspace layouts

XMonad provides a very easy way to describe various layouts that workspaces can follow. I found it useful to constrain only a few layouts on each workspace. I used PerWorkspace for this. This allows me to only switch between specified set of layouts. So for example, my workspace 2 is my writing workspace, in which I have 3 applications. A browser, a pdf reader and a terminal with a tmux session attached to it. This can simply be arranged as a three column layout. But sometimes certain pdfs may have smaller font size which can be tough to read in a column. If I zoom in the pdf it spills sideways, and I have to use arrow keys or h,l to move left and right.

Three column layout

To tackle this, I have another layout with added magnification on top of the three column layout. It magnifies the focused window by a certain limit. And having only these two layouts in my layout set helps me in easily cycling between layouts. I don’t have to skip through 4 different layouts which I would never use in this workspace.

Three column layout with magnification

3.2. Topbar modification

By default, XMonad adds a border to the tiled window which is in focus. I took this idea from here. This adds a title bar with formatted colors. This looks nicer that having a border surrounding the window. The focused window is colored blue while unfocused is colored black. Also, having the title names in topbar looks nice, and in a way removes the need of using XMonadLog’s application names in xmobar itself.

Window topbar

Window topbar unfocused

3.3. Type safety in keybindings

This is something which I truly adore about XMonad and writing its configuration in Haskell. I can write my keybindings in a functional manner and leverage Haskell’s type system to ensure safety. Arranging keybindings in this way, seems more fruitful than having them represented via strings.

myKeys :: XConfig Layout -> M.Map (KeyMask, KeySym) (X ()) myKeys conf@XConfig {XMonad.modMask = modm} = M.fromList $ [ ( (modm, xK_q), safeSpawn "xmonad" ["--restart"]) , ((modm, xK_f), sendMessage $ Toggle NBFULL) , ((modm, xK_l), unGrab *> safeSpawn "env" myLockscreen) ]

Each keybinding is comprised of two values of types: KeyMask and KeySym, followed by an X () action. If you don’t want to set a keymask simply pass a 0 or noModMask.

3.4. Submap keybindings and makeChords

Using submaps in xmonad-contrib, I can write a utility function to easily generate a set of keybindings with an added description.

import XMonad.Actions.Submap import qualified Data.Map as M makeChords :: a -> [((KeyMask, KeySym), String, X ())] -> [(a, X ())] makeChords majorKey subKeys = (majorKey, submap . M.fromList $ map ((k, _, a) -> (k, a)) subKeys) : [ ( majorKey , visualSubmap myVisualSubmapDef $ M.fromList $ map ((k, d, a) -> (k, (d, a))) subKeys) ] soundChords modm = makeChords (modm, xK_a) [ ( (0, xK_a), "open alsamixer" , spawn $ myNamedTerminal "alsamixer" ++ " -e alsamixer") , ( (0, xK_m), "toggle music playing" , getRunningPlayer' >>= player -> spawn $ myMusicCtrl ++ " -p "" ++ player ++ "" play-pause") ] myKeys conf@XConfig {XMonad.modMask = modm} = M.fromList $ [] ++ soundChords modm

The makeChords adds two distinct sets of keybindings, one normal set and another a visual set, which creates a dialog box when you press the main submap key. In the example above, the soundChords submap is enabled with alt+a, then you can see a dialog box containing two keybindings with their descriptions. Pressing either a or m will launch the first or the second action. The documentation also contains an example which you can read to see the actual code that will be appended to your myKeys.

My visual submap for soundChords

3.5. Xmobar configuration in Haskell

Writing the Xmobar configuration inside the same project really allows me to keep everything in one place. I create another executable alongside the xmonad executable, in my package.yaml. And then xmonad launches xmobar in the startup apps section.

executables: xmonad: main: xmonad.hs dependencies: - xmonad - xmonad-contrib - containers xmobar: main: xmobar.hs dependencies: - xmobar ghc-options: -rtsopts -threaded -with-rtsopts=-N

You may have noticed a small icon beside my layout icons on the left side of xmobar. The represent the current layout in a visual form. Try switching layouts with alt+space and see the icons change.

xmobar

myXmobarPP = def { ppLayout = case "Columns" -> "<icon=Columns.xpm/>" "MagnifiedColumns" -> "<icon=MagnifiedColumns.xpm/>" "Full" -> "<icon=Full.xpm/>" "Tall" -> "<icon=Tall.xpm/>" "ThreeCol" -> "<icon=MagnifiedColumns.xpm/>" "2-by-3 (left)" -> "<icon=TwoByThreeLeft.xpm/>" "2-by-3 (right)" -> "<icon=TwoByThreeRight.xpm/>" "2x3 LT" -> "<icon=TwoByThreeLeftWithTabs.xpm/>" "2x3 RT" -> "<icon=TwoByThreeRightWithTabs.xpm/>" "Tiled" -> "<icon=Tiled.xpm/>" _ -> "<icon=Unknown.xpm/>" } main = xmonad . withEasySB (statusBarProp "xmobar" (pure myXmobarPP)) defToggleStrutsKey $ myConfig

3.6. Scratchpads in action

I am using 4 scratchpads in total. Each scratchpad is mapped to a keybinding.

[ ((modm, xK_Return), namedScratchpadAction myScratchpads "terminal") , ((modm, xK_x), namedScratchpadAction myScratchpads "Kanboard") , ((modm, xK_z), namedScratchpadAction myScratchpads "CalibreWeb") , ((modm, xK_m), namedScratchpadAction myScratchpads "Anki") ] myScratchpads = [ NS "terminal" spawnTerm findTerm manageTerm , NS "Kanboard" spawnKanboard (className =? "Kanboard") doFullFloat , NS "Anki" spawnAnki (className =? "Anki") doFullFloat , NS "CalibreWeb" spawnCalibreWeb (className =? "CalibreWeb") doFullFloat ] ...

I realized that I don’t really open new terminals that often because I use tmux (with tmux-resurrect and tmux-continuum). So I remapped alt+enter with showing the terminal scratchpad, instead of the usual, open a new terminal.

I can open up the calibre-web instance with alt+z, and immediately resume whatever I was reading.

Calibre-Web gif

If you have any questions for me, head over to this discussion page.

Read Entire Article