My XMonad Configuration (with Screenshots of Common XMonad Layouts)

XMonad offers unparalleled customizability, especially with the extensive xmonad-contrib library. However, as the modules in xmonad-contrib are simply listed in an alphabetical order, and there’s no voting mechanism to help differentiate the usefulness (to most users at least) of them, it took me some time to go through a few of them and find what could best benefit my workflow. Setting up xmobar and trayer for the status bar was also not that straightforward.

Here I’ll list some modules that were helpful to me (accompanied by screenshots), in the hope that some might find this article useful and save them some time. My full configuration files are posted at the end of the article.

This also serves as a note to myself as I keep exploring XMonad. I’m still a learner and I’d appreciate it if you point out mistakes in my configuration.

From i3 to XMonad

I’ve been using Thinkpad X1 Carbon with Arch Linux for a while and my experience has been great. The only two features I miss from MacOS are the built-in Dictionary and the seamless HiDPI support, but I can get by without them just fine. The pleasure of being able to harness the full power of Arch Linux together with a proper window manager far outweighs the inconvenience. The topic of X1 Carbon vs. Macbook is probably best left for another article though.

I started with i3, as it is undoubtedly the most popular WM out there, and perhaps the most beginner-friendly. However, some of i3’s inflexibility constantly gnawed at me. Eventually I decided that I’m comfortable enough with WMs to begin exploring something more customizable.

In comparison to i3, the mental model adopted by XMonad is (unexpectedly) much more intuitive in several aspects, out of the box:

Useful modules from xmonad-contrib

The above are only the beginning, as xmonad-contrib offers many ready-to-use modules which massively enhance the already great defaults.

Layouts

Layout algorithms are the fundamentals of any window manager. There are tons of layouts in xmonad-contrib, but save for a summary page without screenshots on the Wiki, there doesn’t seem to be much easily accessible information around. I’ve tried out each layout in there. IMO while most of them suit very specific needs and might not be very useful for most users’ daily workflow, a few of them could become indispensable. I’ll list such layouts below, complete with screenshots.

Tabbed (XMonad.Layout.Tabbed)

Tabbed Screenshot

This layout adds tabs to the default Fullscreen layout. It’s in a sense similar to i3’s default fullscreen layout.

This is the essential layout for multi-monitor setups, where each application automatically occupies the whole screen.

TwoPane (XMonad.Layout.TwoPane)

TwoPane Screenshot 1

TwoPane Screenshot 2

This is a frequent use case I had in i3: Divide a window into two panes and cycle between applications within an individual pane. For example, I might have a tech talk playing in one pane, while alternatively programming with a code editor or taking notes with org-mode in the other pane. Another example is keeping Anki/an article open in one pane and cycling between different dictionary apps in the other. The TwoPane layout achieves this by fixing the application in the main pane while allowing you to cycle through other applications in the secondary pane.

By default the split is vertical. However, just like the case in Tall layout, by simply mirroring the layout you can also make the split horizontal, as shown in the screenshot.

Mirror TwoPane Screenshot

There’s also a DragPane layout that allows you to additional resize the split ratio by mouse, and offers more configuration options. However it didn’t seem to work on my system as the pane borders constantly blink.

ResizableTall (XMonad.Layout.ResizableTile)

ResizableTall Screenshot

The default Tall layout only allows for adjusting the ratio of the main split, i.e. all the secondary panes will have the same size. However, there might be a use case where you want to have one relatively large secondary pane (e.g. Emacs) and a relatively small secondary pane (e.g. terminal). ResizableTall extends Tall by allowing for the layout to be extended just fine.

The screenshot shows both the ratio of the main split and that between the secondary panes adjusted.

emptyBSP (XMonad.Layout.BinarySpacePartition)

EmptyBSP Screenshot

This layout will automatically split your focused window in two to make space for the newly created window.

Spiral, Dwindle (XMonad.Layout.Dwindle)

Spiral Screenshot

These two layouts imitate awesomeWM and produce increasingly smaller windows in fixed locations.

I find the above listed layouts able to satisfy almost all of my daily needs for now. However, you can create much more complicated custom layouts by using modules such as Xmonad.Layout.Combo or Xmonad.Layout.LayoutCombinators.

XMonad.Actions.PhysicalScreens

This is an essential module for multi-monitor setups. When multiple monitors are connected, the screen ids get assigned quite arbitrarily by default. However, we’d normally want the screens numbered in a left-to-right order according to their physical locations. This module provides the getScreen and viewScreen functions that help us do just that.

Example usage:

--
-- mod-{w,e,r}, Switch to physical/Xinerama screens 1, 2, or 3
-- mod-shift-{w,e,r}, Move client to screen 1, 2, or 3
--
[((modm .|. mask, key), f sc)
    | (key, sc) <- zip [xK_w, xK_e, xK_r] [0..]
    , (f, mask) <- [(viewScreen def, 0), (sendToScreen def, shiftMask)]]

XMonad.Layout.Spacing

This achieves the same thing as that by i3gaps. XMonad argues that the correct terminology for this should be “spacing” instead of “gaps”, since “gaps” should refer to the gap between a window and the edges, not between panes within a window.

This makes the layout a bit less crowded.

myLayout = avoidStruts $
  ...
  ||| tiled
  ||| twopane
  ...
  where
     tiled = spacing 3 $ ResizableTall nmaster delta ratio []
     twopane = spacing 3 $ TwoPane delta ratio

XMonad.Hooks.EwmhDesktops

You need to add an ewmh hook if you want to correctly use rofi to locate and switch to a running application by its name.

Just import the module and then add ewmh as such:

main = do
    ...
    xmonad $ ewmh def
        {
        ...
        }

XMonad.Layout.NoBorders

It would be silly to have a border around the window if the window always occupies the whole screen. Use noBorders to avoid that in such layouts (e.g. tabbed, Full).

myLayout = avoidStruts $
  noBorders (tabbed shrinkText myTabConfig)
  ...

historyHook (XMonad.Actions.GroupNavigation)

historyHook keeps track of your window history and allows for actions such as going back to the most recent window.

Just append >> historyHook to the end of your logHook, e.g.

        , logHook = dynamicLogWithPP myPP {
                                          ppOutput = hPutStrLn xmproc
                                          }
                        >> historyHook

xmobar and trayer

Normally one would want to have a status bar and an application/applet tray. The most popular choices for those seems to be xmobar and trayer.

The configuration options for xmobar is stored in .xmobarrc. They are relatively well-documented in the official README.

Note that one would need to manually leave some space to the side of the xmobar so that the trayer can be displayed:

In .xmobarrc:

Config {
       ...
       , position = TopW L 85
       ...
       }

I spawn xmobar in my xmonad.hs file:

main = do
    xmproc <- spawnPipe "xmobar"
    xmonad $ ewmh def
        {
        ...
        , logHook = dynamicLogWithPP myPP {
                                          ppOutput = hPutStrLn xmproc
                                          }
                        >> historyHook
        ...
        }

and spawn trayer in my startup script:

trayer --edge top --align right --SetDockType true --SetPartialStrut true \
       --expand true --width 15 --transparent true --alpha 0 --tint 0x283339 --height 20\
       --monitor 1 &

Note that by setting --transparent true, --alpha 0 --tint 0x283339, I was able to ensure that it has the same background color as what I set in .xmobarrc.

Since xmobar and trayer are completely separate processes, if one of them crashes you can just relaunch it individually without impacting the other one’s normal functioning.

Adding an entry in /usr/share/xsessions for startup applications

Finally, when logging in, one might want to launch some startup applications prior to launching xmonad itself, just as one would do in .i3/config with exec.

You can simply write a bash script run-xmonad which includes all the commands you want to run. For example:

#!/bin/bash

setxkbmap -option "ctrl:nocaps"
xset r rate 200 50 &

trayer --edge top --align right --SetDockType true --SetPartialStrut true \
       --expand true --width 15 --transparent true --alpha 0 --tint 0x283339 --height 20\
       --monitor 1 &
nm-applet &
xfce4-power-manager &
pactl load-module module-bluetooth-discover &
blueman-applet &

# User apps
dropbox start &
thunderbird &
goldendict &
pulseaudio &
pa-applet &

xmonad

Note that there is a file /usr/share/xsessions/xmonad.desktop already, which allows you to launch xmonad after logging into an xsession. You can simply create a copy and change the line

Name=Xmonad
Exec=xmonad

to

Name=Xmonad (with startup apps)
Exec=/home/jx/Dropbox/scripts/run-xmonad

You should then be able to choose this new entry from your dm at your next login.

My full configuration files

~/.xmonad/xmonad.hs

import XMonad hiding ((|||))
import qualified XMonad.StackSet as W
import qualified Data.Map        as M

-- Useful for rofi
import XMonad.Hooks.EwmhDesktops
import XMonad.Hooks.DynamicLog
import XMonad.Hooks.ManageDocks
import XMonad.Util.Run(spawnPipe)
import XMonad.Util.EZConfig(additionalKeys, additionalKeysP, additionalMouseBindings)
import System.IO
import System.Exit
-- Last window
import XMonad.Actions.GroupNavigation
-- Last workspace. Seems to conflict with the last window hook though so just
-- disabled it.
-- import XMonad.Actions.CycleWS
-- import XMonad.Hooks.WorkspaceHistory (workspaceHistoryHook)
import XMonad.Layout.Tabbed
import XMonad.Hooks.InsertPosition
import XMonad.Layout.SimpleDecoration (shrinkText)
-- Imitate dynamicLogXinerama layout
import XMonad.Util.WorkspaceCompare
import XMonad.Hooks.ManageHelpers
-- Order screens by physical location
import XMonad.Actions.PhysicalScreens
import Data.Default
-- For getSortByXineramaPhysicalRule
import XMonad.Layout.LayoutCombinators
-- smartBorders and noBorders
import XMonad.Layout.NoBorders
-- spacing between tiles
import XMonad.Layout.Spacing
-- Insert new tabs to the right: https://stackoverflow.com/questions/50666868/how-to-modify-order-of-tabbed-windows-in-xmonad?rq=1
-- import XMonad.Hooks.InsertPosition

--- Layouts
-- Resizable tile layout
import XMonad.Layout.ResizableTile
-- Simple two pane layout.
import XMonad.Layout.TwoPane
import XMonad.Layout.BinarySpacePartition
import XMonad.Layout.Dwindle

myTabConfig = def { activeColor = "#556064"
                  , inactiveColor = "#2F3D44"
                  , urgentColor = "#FDF6E3"
                  , activeBorderColor = "#454948"
                  , inactiveBorderColor = "#454948"
                  , urgentBorderColor = "#268BD2"
                  , activeTextColor = "#80FFF9"
                  , inactiveTextColor = "#1ABC9C"
                  , urgentTextColor = "#1ABC9C"
                  , fontName = "xft:Noto Sans CJK:size=10:antialias=true"
                  }

myLayout = avoidStruts $
  noBorders (tabbed shrinkText myTabConfig)
  ||| tiled
  ||| Mirror tiled
  -- ||| noBorders Full
  ||| twopane
  ||| Mirror twopane
  ||| emptyBSP
  ||| Spiral L XMonad.Layout.Dwindle.CW (3/2) (11/10) -- L means the non-main windows are put to the left.

  where
     -- The last parameter is fraction to multiply the slave window heights
     -- with. Useless here.
     tiled = spacing 3 $ ResizableTall nmaster delta ratio []
     -- In this layout the second pane will only show the focused window.
     twopane = spacing 3 $ TwoPane delta ratio
     -- The default number of windows in the master pane
     nmaster = 1
     -- Default proportion of screen occupied by master pane
     ratio   = 1/2
     -- Percent of screen to increment by when resizing panes
     delta   = 3/100

myPP = def { ppCurrent = xmobarColor "#1ABC9C" "" . wrap "[" "]"
           , ppTitle = xmobarColor "#1ABC9C" "" . shorten 60
           , ppVisible = wrap "(" ")"
           , ppUrgent  = xmobarColor "red" "yellow"
           , ppSort = getSortByXineramaPhysicalRule def
           }

myManageHook = composeAll [ isFullscreen --> doFullFloat

                          ]

myKeys conf@(XConfig {XMonad.modMask = modm}) = M.fromList $

    -- launch a terminal
    [ ((modm .|. shiftMask, xK_Return), spawn $ XMonad.terminal conf)

    -- close focused window
    , ((modm .|. shiftMask, xK_q     ), kill)

     -- Rotate through the available layout algorithms
    , ((modm,               xK_space ), sendMessage NextLayout)

    --  Reset the layouts on the current workspace to default
    , ((modm .|. shiftMask, xK_space ), setLayout $ XMonad.layoutHook conf)
    , ((modm .|. shiftMask, xK_h), sendMessage $ JumpToLayout "Mirror Tall")
    , ((modm .|. shiftMask, xK_v), sendMessage $ JumpToLayout "Tall")
    , ((modm .|. shiftMask, xK_f), sendMessage $ JumpToLayout "Full")
    , ((modm .|. shiftMask, xK_t), sendMessage $ JumpToLayout "Tabbed Simplest")

    -- Resize viewed windows to the correct size
    , ((modm,               xK_n     ), refresh)

    -- Move focus to the next window
    , ((modm,               xK_Tab   ), windows W.focusDown)

    -- Move focus to the next window
    , ((modm,               xK_j     ), windows W.focusDown)

    -- Move focus to the previous window
    , ((modm,               xK_k     ), windows W.focusUp  )

    -- Move focus to the master window
    , ((modm,               xK_m     ), windows W.focusMaster  )

    -- Swap the focused window and the master window
    , ((modm,               xK_Return), windows W.swapMaster)

    -- Swap the focused window with the next window
    , ((modm .|. shiftMask, xK_j     ), windows W.swapDown  )

    -- Swap the focused window with the previous window
    , ((modm .|. shiftMask, xK_k     ), windows W.swapUp    )

    -- Shrink the master area
    , ((modm,               xK_h     ), sendMessage Shrink)

    -- Expand the master area
    , ((modm,               xK_l     ), sendMessage Expand)

    -- Shrink and expand ratio between the secondary panes, for the ResizableTall layout
    , ((modm .|. shiftMask,               xK_h), sendMessage MirrorShrink)
    , ((modm .|. shiftMask,               xK_l), sendMessage MirrorExpand)

    -- Push window back into tiling
    , ((modm,               xK_t     ), withFocused $ windows . W.sink)

    -- Increment the number of windows in the master area
    , ((modm              , xK_comma ), sendMessage (IncMasterN 1))

    -- Deincrement the number of windows in the master area
    , ((modm              , xK_period), sendMessage (IncMasterN (-1)))

    -- Toggle the status bar gap
    -- Use this binding with avoidStruts from Hooks.ManageDocks.
    -- See also the statusBar function from Hooks.DynamicLog.
    , ((modm              , xK_b     ), sendMessage ToggleStruts)
    , ((controlMask, xK_Print), spawn "sleep 0.2; scrot -s")
    , ((0, xK_Print), spawn "scrot") -- 0 means no extra modifier key needs to be pressed in this case.
    , ((modm, xK_F3), spawn "pcmanfm")
    , ((modm.|. shiftMask, xK_F3), spawn "gksu pcmanfm")
    ]

    ++
      [((m .|. modm, k), windows $ f i)
      | (i, k) <- zip (XMonad.workspaces conf) [xK_1 .. xK_9]
      , (f, m) <- [(W.greedyView, 0), (W.shift, shiftMask)]]
    ++
      [((m .|. modm, key), f sc)
      | (key, sc) <- zip [xK_a, xK_s, xK_d] [0..]
      -- Order screen by physical order instead of arbitrary numberings.
      , (f, m) <- [(viewScreen def, 0), (sendToScreen def, shiftMask)]]

main = do
    xmproc <- spawnPipe "xmobar"
    xmonad $ ewmh def
        { modMask = mod4Mask
        , keys = myKeys
        , manageHook = manageDocks <+> myManageHook
        , layoutHook = myLayout
        , handleEventHook = handleEventHook def <+> docksEventHook
        , logHook = dynamicLogWithPP myPP {
                                          ppOutput = hPutStrLn xmproc
                                          }
                        >> historyHook
        , terminal = "lxterminal"
        -- This is the color of the borders of the windows themselves.
        , normalBorderColor  = "#2f3d44"
        , focusedBorderColor = "#1ABC9C"
        }
        `additionalKeysP`
        [
          ("M1-<Space>", spawn "rofi -combi-modi window,run,drun -show combi -modi combi")
          , ("C-M1-<Space>", spawn "rofi -show run")
          -- Restart xmonad. This is the same keybinding as from i3
          , ("M-S-c", spawn "xmonad --recompile; xmonad --restart")
          , ("M-S-q", kill)
          , ("M-'", windows W.swapMaster)
          , ("M1-<Tab>", nextMatch History (return True))
          , ("M-<Return>", spawn "lxterminal")
          -- Make it really hard to mispress...
          , ("M-M1-S-e", io (exitWith ExitSuccess))
          , ("M-M1-S-l", spawn "i3lock")
          , ("M-M1-S-s", spawn "i3lock && systemctl suspend")
          , ("M-M1-S-h", spawn "i3lock && systemctl hibernate")
        ] `additionalMouseBindings`
        [ ((mod4Mask, button4), (\w -> windows W.focusUp))
        , ((mod4Mask, button5), (\w -> windows W.focusDown))
        ]

~/.xmobarrc

Config {
       font = "xft:Noto Sans:size=9:antialias=true,Noto Sans CJK SC:size=9:antialias=true"
       , bgColor = "#283339"
       , fgColor = "#F9fAF9"
       , position = TopW L 85
       , commands = [
                    Run Battery [ "--template" , "B: <acstatus>"
                                , "--L" , "15"
                                , "--H" , "75"
                                , "--low"      , "darkred"
                                , "--normal"   , "darkorange"
                                , "--high"     , "#1ABC9C"
                                , "--" -- battery specific options
                                       -- discharging status
                                       , "-o"	, "<left>% (<timeleft>)"
                                       -- AC "on" status
                                       , "-O"	, "<fc=#dAA520>Charging</fc>"
                                       -- charged status
                                       , "-i"	, "<fc=#1ABC9C>Charged</fc>"
                                ] 50
                    -- , Run Cpu [ "--template" , "C: <total>%", "-L","0","-H","50","--normal","#1ABC9C","--high","darkred"] 10
                    -- , Run Memory ["-t","M: <usedratio>%"] 10
                    , Run DiskU [("/", "D: <free>")] ["-L", "20", "-H", "60"] 10
                    -- , Run Swap [] 10
                    , Run Date "%a %d.%m. %H:%M" "date" 10
                    , Run StdinReader
                    ]
       , sepChar = "%"
       , alignSep = "}{"
       , template = "%StdinReader% }{ %battery% | %disku% | %date%"
       }