XMonad Configuration Tutorial

We’re going to take you, step-by-step, through the process of configuring xmonad, setting up xmobar as a status bar, configuring trayer-srg as a tray, and making it all play nicely together.

We assume that you have read the xmonad guided tour already. It is a bit dated at this point but, because xmonad is stable, the guide should still give you a good overview of the most basic functionality.

Before we begin, here are two screenshots of the kind of desktop we will be creating together. In particular, a useful layout we’ll conjure into being—the three columns layout from XMonad.Layout.ThreeColumns with the ability to magnify stack windows via XMonad.Layout.Magnifier:

blank desktop blank desktop

So let’s get started!

Preliminaries

First you’ll want to install xmonad. You can either do this with your system’s package manager or via stack/cabal. The latter may be necessary if your distribution does not package xmonad and xmonad-contrib (or packages a version that’s very old) or you want to use the git versions instead of a tagged release. You can find instructions for how to do this in INSTALL.md.

One more word of warning: if you are on a distribution that installs every Haskell package dynamically linked—thus essentially breaking Haskell—(Arch Linux being a prominent example) then we would highly recommend installing via stack or cabal as instructed in INSTALL.md. If you still want to install xmonad via your system’s package manager, you will need to xmonad --recompile every time a Haskell dependency is updated—else xmonad may fail to start when you want to log in!

We’re going to assume xmonad version 0.17.0 and xmonad-contrib version 0.17.0 here, though most of these steps should work with older versions as well. When we get to the relevant parts, will point you to alternatives that work with at least xmonad version 0.15 and xmonad-contrib version 0.16. This will usually be accompanied by a big “IF YOU ARE ON A VERSION < 0.17.0”, so don’t worry about missing it!

Throughout the tutorial we will use, for keybindings, a syntax very akin to the GNU Emacs conventions for the same thing—so C-x means “hold down the control key and then press the x key”. One exception is that in our case M will not necessarily mean Alt (also called Meta), but “your modifier key”; this is Alt by default, although many people map it to Super instead (I will show you how to do this below).

This guide should work for any GNU/Linux distribution and even for BSD folks. Because debian-based distributions are still rather popular, we will give you the apt commands when it comes to installing software. If you use another distribution, just substitute the appropriate commands for your system.

To install xmonad, as well as some utilities, via apt you can just run

$ apt-get install xmonad libghc-xmonad-contrib-dev libghc-xmonad-dev suckless-tools

This installs xmonad itself, everything you need to configure it, and suckless-tools, which provides the application launcher dmenu. This program is used as the default application launcher on M-p.

If you are installing xmonad with stack or cabal, remember to not install xmonad and its libraries here!

For the remainder of this document, we will assume that you are running a live xmonad session in some capacity. If you have set up your ~/.xinitrc as directed in the xmonad guided tour, you should be good to go! If not, just smack an exec xmonad at the bottom of that file.

Installing Xmobar

What we need to do now—provided we want to use a bar at all—is to install xmobar. If you visit xmobar’s Installation section you will find everything from how to install it with your system’s package manager all the way to how to compile it yourself.

We will show you how we make these programs talk to each other a bit later on. For now, let’s start to explore how we can customize this window manager of ours!

Customizing XMonad

Xmonad’s configuration file is written in Haskell—but don’t worry, we won’t assume that you know the language for the purposes of this tutorial. The configuration file can either reside within $XDG_CONFIG_HOME/xmonad, ~/.xmonad, or $XMONAD_CONFIG_DIR; see man 1 xmonad for further details (the likes of $XDG_CONFIG_HOME is called a shell variable). Let’s use $XDG_CONFIG_HOME/xmonad for the purposes of this tutorial, which resolves to ~/.config/xmonad on most systems—the ~/.config directory is also the place where things will default to should $XDG_CONFIG_HOME not be set.

First, we need to create ~/.config/xmonad and, in this directory, a file called xmonad.hs. We’ll start off with importing some of the utility modules we will use. At the very top of the file, write

import XMonad

import XMonad.Util.EZConfig
import XMonad.Util.Ungrab

All of these imports are unqualified, meaning we are importing all of the functions in the respective modules. For configuration files this is what most people want. If you prefer to import things explicitly you can do so by adding the necessary function to the import statement in parentheses. For example

import XMonad.Util.EZConfig (additionalKeysP)

For the purposes of this tutorial, we will be importing everything coming from xmonad directly unqualified.

Next, a basic configuration—which is the same as the default config—is this:

main :: IO ()
main = xmonad def

In case you’re interested in what this default configuration actually looks like, you can find it under XMonad.Config. Do note that it is not advised to copy that file and use it as the basis of your configuration, as you won’t notice when a default changes!

You should be able to save the above file, with the import lines plus the other two and then press M-q to load it up. Another way to validate your xmonad.hs is to simply run xmonad --recompile in a terminal. You’ll see errors (in an xmessage popup, so make sure that is installed on your system) if it’s bad, and nothing if it’s good. It’s not the end of the world if you restart xmonad and get errors, as you will still be on your old, working, configuration and have all the time in the world to fix your errors before trying again!

Let’s add a few additional things. The default modifier key is Alt, which is also used in Emacs. Sometimes Emacs and xmonad want to use the same key for different actions. Rather than remap every common key, many people just change Mod to be the Super key—the one between Ctrl and Alt on most keyboards. We can do this by changing the above main function in the following way:

main :: IO ()
main = xmonad $ def
    { modMask = mod4Mask  -- Rebind Mod to the Super key
    }

The two dashes signify a comment to the end of the line. Notice the curly braces; these stand for a record update in Haskell (records are sometimes called “structs” in C-like languages). What it means is “take def and change its modMask field to the thing I want”. Taking a record that already has some defaults set and modifying only the fields one cares about is a pattern that is often used within xmonad, so it’s worth pausing for a bit here to really take this new syntax in.

Don’t mind the dollar sign too much; it essentially serves to split apart the xmonad function and the def { .. } record update visually. Haskell people really don’t like writing parentheses, so they sometimes use a dollar sign instead. For us this is particularly nice, because now we don’t have to awkwardly write

main :: IO ()
main = xmonad (def
    { modMask = mod4Mask  -- Rebind Mod to the Super key
    })

This will be especially handy when adding more combinators; something we will talk about later on. The dollar sign is superfluous in this example, but that will change soon enough so it’s worth introducing it here as well.

What if we wanted to add other keybindings? Say you also want to bind M-S-z to lock your screen with the screensaver, M-S-= to take a snapshot of one window, and M-] to spawn Firefox. This can be achieved with the additionalKeysP function from the XMonad.Util.EZConfig module—luckily we already have it imported! Our config file, starting with main, now looks like:

main :: IO ()
main = xmonad $ def
    { modMask = mod4Mask  -- Rebind Mod to the Super key
    }
  `additionalKeysP`
    [ ("M-S-z", spawn "xscreensaver-command -lock")
    , ("M-S-=", unGrab *> spawn "scrot -s"        )
    , ("M-]"  , spawn "firefox"                   )
    ]

That syntax look familiar?

You can find the names for special keys, like Print or the F-keys, in the XMonad.Util.EZConfig documentation.

We will cover setting up the screensaver later in this tutorial.

The unGrab before running the scrot -s command tells xmonad to release its keyboard grab before scrot -s tries to grab the keyboard itself. The little *> operator essentially just sequences two functions, i.e. f *> g says

first do f and then, discarding any result that f may have given me, do g.

Do note that you may need to install scrot if you don’t have it on your system already.

What if we wanted to augment our xmonad experience just a little more? We already have xmonad-contrib, which means endless possibilities! Say we want to add a three column layout to our layouts and also magnify focused stack windows, making it more useful on smaller screens.

We start by visiting the documentation for XMonad.Layout.ThreeColumns. The very first sentence of the Usage section tells us exactly what we want to start with:

You can use this module with the following in your ~/.xmonad/xmonad.hs:

import XMonad.Layout.ThreeColumns

Ignoring the part about where exactly our xmonad.hs is (we have opted to put it into ~/.config/xmonad/xmonad.hs, remember?) we can follow that documentation word for word. Let’s add

import XMonad.Layout.ThreeColumns

to the top of our configuration file. Most modules have a lot of accompanying text and usage examples in them—so while the type signatures may seem scary, don’t be afraid to look up the xmonad-contrib documentation on hackage!

Next we just need to tell xmonad that we want to use that particular layout. To do this, there is the layoutHook. Let’s use the default layout as a base:

myLayout = tiled ||| Mirror tiled ||| Full
  where
    tiled   = Tall nmaster delta ratio
    nmaster = 1      -- Default number of windows in the master pane
    ratio   = 1/2    -- Default proportion of screen occupied by master pane
    delta   = 3/100  -- Percent of screen to increment by when resizing panes

The so-called where-clause above simply consists of local declarations that might clutter things up where they all declared at the top-level like this

myLayout = Tall 1 (3/100) (1/2) ||| Mirror (Tall 1 (3/100) (1/2)) ||| Full

It also gives us the chance of documenting what the individual numbers mean!

Now we can add the layout according to the XMonad.Layout.ThreeColumns documentation. At this point, we would encourage you to try this yourself with just the docs guiding you. If you can’t do it, don’t worry; it’ll come with time!

We can, for example, add the additional layout like this:

myLayout = tiled ||| Mirror tiled ||| Full ||| ThreeColMid 1 (3/100) (1/2)
  where
    tiled   = Tall nmaster delta ratio
    nmaster = 1      -- Default number of windows in the master pane
    ratio   = 1/2    -- Default proportion of screen occupied by master pane
    delta   = 3/100  -- Percent of screen to increment by when resizing panes

or even, because the numbers happen to line up, like this:

myLayout = tiled ||| Mirror tiled ||| Full ||| threeCol
  where
    threeCol = ThreeColMid nmaster delta ratio
    tiled    = Tall nmaster delta ratio
    nmaster  = 1      -- Default number of windows in the master pane
    ratio    = 1/2    -- Default proportion of screen occupied by master pane
    delta    = 3/100  -- Percent of screen to increment by when resizing panes

Now we just need to tell xmonad that we want to use this modified layoutHook instead of the default. Again, try to reason this out for yourself by just looking at the documentation. Ready? Here we go:

main :: IO ()
main = xmonad $ def
    { modMask    = mod4Mask  -- Rebind Mod to the Super key
    , layoutHook = myLayout  -- Use custom layouts
    }
  `additionalKeysP`
    [ ("M-S-z", spawn "xscreensaver-command -lock")
    , ("M-S-=", unGrab *> spawn "scrot -s"        )
    , ("M-]"  , spawn "firefox"                   )
    ]

But we also wanted to add magnification, right? Luckily for us, there’s a module for that as well! It’s called XMonad.Layout.Magnifier. Again, take a look at the documentation before reading on—see if you can reason out what to do by yourself. Let’s pick the magnifiercz' modifier from the library; it magnifies a window by a given amount, but only if it’s a stack window. Add it to your three column layout thusly:

myLayout = tiled ||| Mirror tiled ||| Full ||| threeCol
  where
    threeCol = magnifiercz' 1.3 $ ThreeColMid nmaster delta ratio
    tiled    = Tall nmaster delta ratio
    nmaster  = 1      -- Default number of windows in the master pane
    ratio    = 1/2    -- Default proportion of screen occupied by master pane
    delta    = 3/100  -- Percent of screen to increment by when resizing panes

Don’t forget to import the module!

You can think of the $ here as putting everything into parentheses from the dollar to the end of the line. If you don’t like that you can also write

threeCol = magnifiercz' 1.3 (ThreeColMid nmaster delta ratio)

instead.

That’s it! Now we have a perfectly functioning three column layout with a magnified stack. If you compare this with the starting screenshots, you will see that that’s exactly the behaviour we wanted to achieve!

A last thing that’s worth knowing about are so-called “combinators”—at least we call them that, we can’t tell you what to do. These are functions that compose with the xmonad function and add a lot of hooks and other things for you (trying to achieve a specific goal), so you don’t have to do all the manual work yourself. For example, xmonad—by default—is only ICCCM compliant. Nowadays, however, a lot of programs (including many compositors) expect the window manager to also be EWMH compliant. So let’s save ourselves a lot of future trouble and add that to xmonad straight away!

This functionality is to be found in the XMonad.Hooks.EwmhDesktops module, so let’s import it:

import XMonad.Hooks.EwmhDesktops

We might also consider using the ewmhFullscreen combinator. By default, a “fullscreened” application is still bound by its window dimensions; this means that if the window occupies half of the screen before it was fullscreened, it will also do so afterwards. Some people really like this behaviour, as applications thinking they’re in fullscreen mode tend to remove a lot of clutter (looking at you, Firefox). However, because a lot of people explicitly do not want this effect (and some applications, like chromium, will misbehave and need some Hacks to make this work), we will also add the relevant function to get “proper” fullscreen behaviour here.

IF YOU ARE ON A VERSION < 0.17.0: The ewmhFullscreen function does not exist in these versions. Instead of it, you can try to add fullscreenEventHook to your handleEventHook to achieve similar functionality (how to do this is explained in the documentation of XMonad.Hooks.EwmhDesktops).

To use the two combinators, we compose them with the xmonad function in the following way:

main :: IO ()
main = xmonad $ ewmhFullscreen $ ewmh $ def
    { modMask    = mod4Mask  -- Rebind Mod to the Super key
    , layoutHook = myLayout  -- Use custom layouts
    }
  `additionalKeysP`
    [ ("M-S-z", spawn "xscreensaver-command -lock")
    , ("M-S-=", unGrab *> spawn "scrot -s"        )
    , ("M-]"  , spawn "firefox"                   )
    ]

Do mind the order of the two combinators—by a particularly awkward set of circumstances, they do not commute!

This main function is getting pretty crowded now, so let’s refactor it a little bit. A good way to do this is to split the config part into one function and the “main and all the combinators” part into another. Let’s call the config part myConfig for… obvious reasons. It would look like this:

main :: IO ()
main = xmonad $ ewmhFullscreen $ ewmh $ myConfig

myConfig = def
    { modMask    = mod4Mask  -- Rebind Mod to the Super key
    , layoutHook = myLayout  -- Use custom layouts
    }
  `additionalKeysP`
    [ ("M-S-z", spawn "xscreensaver-command -lock")
    , ("M-S-=", unGrab *> spawn "scrot -s"        )
    , ("M-]"  , spawn "firefox"                   )
    ]

Much better!

Make XMonad and Xmobar Talk to Each Other

Onto the main dish. First, we have to import the necessary modules. Add the following to your list of imports:

import XMonad.Hooks.DynamicLog
import XMonad.Hooks.StatusBar
import XMonad.Hooks.StatusBar.PP

IF YOU ARE ON A VERSION < 0.17.0: The XMonad.Hooks.StatusBar and XMonad.Hooks.StatusBar.PP modules don’t exist yet. You can find everything you need in the XMonad.Hooks.DynamicLog module, so remove these two imports.

Replace your main function above with:

main :: IO ()
main = xmonad $ ewmhFullscreen $ ewmh $ xmobarProp $ myConfig

IF YOU ARE ON A VERSION < 0.17.0: The xmobarProp function does not exist in these versions. Instead of it, use xmobar via main = xmonad . ewmh =<< xmobar myConfig and carefully read the part about pipes later on (xmobar uses pipes to make xmobar talk to xmonad).

As a quick side-note, we could have also written

main :: IO ()
main = xmonad . ewmhFullscreen . ewmh . xmobarProp $ myConfig

Notice how $ became .! The dot operator (.) in Haskell means function composition and is read from right to left. What this means in this specific case is essentially the following:

take the four functions xmonad, ewmhFullscreen, ewmh, and xmobarProp and give me the big new function xmonad . ewmhFullscreen . ewmh . xmobarProp that first executes xmobarProp, then ewmh, then ewmhFullscreen, and finally xmonad. Then give it myConfig as its argument so it can do its thing.

This should strike you as nothing more than a syntactical quirk, at least in this case. And indeed, since ($) is just function application there is a very nice relationship between (.) and ($). This may be more obvious if we write everything with parentheses and apply the (.) operator (because we do have an argument):

-- ($) version
main = xmonad $ ewmhFullscreen $ ewmh $ xmobarProp $ myConfig
-- ($) version with parentheses
main = xmonad (ewmhFullscreen (ewmh (xmobarProp (myConfig))))

-- (.) version with parentheses
main = (xmonad . ewmhFullscreen . ewmh . xmobarProp) (myConfig)
-- xmobarProp applied
main = (xmonad . ewmhFullscreen . ewmh) (xmobarProp (myConfig))
-- ewmh applied
main = (xmonad . ewmhFullscreen) (ewmh (xmobarProp (myConfig)))
-- xmonad and ewmhFullscreen applied
main = (xmonad (ewmhFullscreen (ewmh (xmobarProp (myConfig))))

It’s the same! This is special to the interplay with (.) and ($) though; if you’re on an older version of xmonad and xmonad-contrib and use xmobar instead of xmobarProp, then you have to write

main = xmonad . ewmhFullscreen . ewmh =<< xmobar myConfig

and this is not equivalent to

main = xmonad (ewmhFullscreen (ewmh =<< xmobar myConfig))

Consult a Haskell book of your choice for why this is the case.

Back to our actual goal: customizing xmonad. What the code we’ve written does is take our tweaked default configuration myConfig and add the support we need to make xmobar our status bar. Do note that you will also need to add the XMonadLog plugin to your xmobar configuration; we will do this together below, so don’t sweat it for now.

To understand why this is necessary, let’s talk a little bit about how xmonad and xmobar fit together. You can make them talk to each other in several different ways.

By default, xmobar accepts input on its stdin, which it can display at an arbitrary position on the screen. We want xmonad to send xmobar the stuff that you can see at the upper left of the starting screenshots: information about available workspaces, current layout, and open windows. Naïvely, we can achieve this by spawning a pipe and letting xmonad feed the relevant information to that pipe. The problem with that approach is that when the pipe is not being read and gets full, xmonad will freeze!

It is thus much better to switch over to property based logging, where we are writing to an X11 property and having xmobar read that; no danger when things are not being read! For this reason we have to use XMonadLog instead of StdinReader in our xmobar. There’s also an UnsafeXMonadLog available, should you want to send actions to xmobar (this is useful, for example, for XMonad.Util.ClickableWorkspaces, which is a new feature in 0.17.0).

IF YOU ARE ON A VERSION < 0.17.0: As discussed above, the xmobar function uses pipes, so you actually do want to use the StdinReader. Simply replace all occurences of XMonadLog with StdinReader below (don’t forget the template!)

Configuring Xmobar

Now, before this will work, we have to configure xmobar. Here’s a nice starting point. Be aware that, while Haskell syntax highlighting is used here to make this pretty, xmobar’s config is not a Haskell file and thus can’t execute arbitrary code—at least not by default. If you do want to configure xmobar in Haskell there is a note about that at the end of this section.

Config { overrideRedirect = False
       , font     = "xft:iosevka-9"
       , bgColor  = "#5f5f5f"
       , fgColor  = "#f8f8f2"
       , position = TopW L 90
       , commands = [ Run Weather "EGPF"
                        [ "--template", "<weather> <tempC>°C"
                        , "-L", "0"
                        , "-H", "25"
                        , "--low"   , "lightblue"
                        , "--normal", "#f8f8f2"
                        , "--high"  , "red"
                        ] 36000
                    , Run Cpu
                        [ "-L", "3"
                        , "-H", "50"
                        , "--high"  , "red"
                        , "--normal", "green"
                        ] 10
                    , Run Alsa "default" "Master"
                        [ "--template", "<volumestatus>"
                        , "--suffix"  , "True"
                        , "--"
                        , "--on", ""
                        ]
                    , Run Memory ["--template", "Mem: <usedratio>%"] 10
                    , Run Swap [] 10
                    , Run Date "%a %Y-%m-%d <fc=#8be9fd>%H:%M</fc>" "date" 10
                    , Run XMonadLog
                    ]
       , sepChar  = "%"
       , alignSep = "}{"
       , template = "%XMonadLog% }{ %alsa:default:Master% | %cpu% | %memory% * %swap% | %EGPF% | %date% "
       }

First, we set the font to use for the bar, as well as the colors. The position options are documented well on the xmobar home page or, alternatively, in the quick-start.org on GitHub. The particular option of TopW L 90 says to put the bar in the upper left of the screen, and make it consume 90% of the width of the screen (we need to leave a little bit of space for trayer-srg). If you’re up for it—and this really requires more shell-scripting than Haskell knowledge—you can also try to seamlessly embed trayer into xmobar by using trayer-padding-icon.sh and following the advice given in that thread.

In the commands list you, well, define commands. Commands are the pieces that generate the content to be displayed in your bar. These will later be combined together in the template. Here, we have defined a weather widget, a CPU widget, memory and swap widgets, a date, a volume indicator, and of course the data from xmonad via XMonadLog.

The EGPF in the weather command is a particular station. Replace both (!) occurences of it with your choice of ICAO weather stations. For a list of ICAO codes you can visit the relevant Wikipedia page. You can of course monitor more than one if you like; see xmobar’s weather monitor documentation for further details.

The template then combines everything together. The alignSep variable controls the alignment of all of the monitors. Stuff to be left-justified goes before the } character, things to be centered after it, and things to be right justified after {. We have nothing centered so there is nothing in-between them.

Save the file to ~/.xmobarrc. Now press M-q to reload xmonad; you should now see xmobar with your new configuration! Please note that, at this point, the config has to reside in ~/.xmobarrc. We will, however, discuss how to change this soon.

It is also possible to completely configure xmobar in Haskell, just like xmonad. If you want to know more about that, you can check out the xmobar.hs example in the official documentation. For a more complicated example, you can also check out jao’s xmobar.hs (he’s the current maintainer of xmobar).

Changing What XMonad Sends to Xmobar

Now that the xmobar side of the picture looks nice, what about the stuff that xmonad sends to xmobar? It would be nice to visually match these two. Sadly, this is not quite possible with our xmobarProp function; however, looking at the implementation of the function (or, indeed, the top-level documentation of the module!) should give us some ideas for how to proceed:

xmobarProp config =
  withEasySB (statusBarProp "xmobar" (pure xmobarPP)) toggleStrutsKey config

This means that xmobarProp just calls the functions withEasySB and statusBarProp with some arguments; crucially for us, notice the xmobarPP. In this context “PP” stands for “pretty-printer”—exactly what we want to modify!

Let’s copy the implementation over into our main function:

main :: IO ()
main = xmonad
     . ewmhFullscreen
     . ewmh
     . withEasySB (statusBarProp "xmobar" (pure def)) defToggleStrutsKey
     $ myConfig

IF YOU ARE ON A VERSION < 0.17.0: xmobar has a similar definition, relying on statusBar alone: xmobar = statusBar "xmobar" xmobarPP toggleStrutsKey. Sadly, the defToggleStrutsKey function is not yet exported, so you will have to define it yourself:

main :: IO ()
main = xmonad
     . ewmhFullscreen
     . ewmh
   =<< statusBar "xmobar" def toggleStrutsKey myConfig
  where
    toggleStrutsKey :: XConfig Layout -> (KeyMask, KeySym)
    toggleStrutsKey XConfig{ modMask = m } = (m, xK_b)

The defToggleStrutsKey here is just the key with which you can toggle the bar; it is bound to M-b. If you want to change this, you can also define your own:

main :: IO ()
main = xmonad
     . ewmhFullscreen
     . ewmh
     . withEasySB (statusBarProp "xmobar" (pure def)) toggleStrutsKey
     $ myConfig
  where
    toggleStrutsKey :: XConfig Layout -> (KeyMask, KeySym)
    toggleStrutsKey XConfig{ modMask = m } = (m, xK_b)

Feel free to change the binding by modifying the (m, xK_b) tuple to your liking.

If you want your xmobar configuration file to reside somewhere else than ~/.xmobarrc, you can now simply give the file to xmobar as a positional argument! For example:

main :: IO ()
main = xmonad
     . ewmhFullscreen
     . ewmh
     . withEasySB (statusBarProp "xmobar ~/.config/xmobar/xmobarrc" (pure def)) defToggleStrutsKey
     $ myConfig

Back to controlling what exactly we send to xmobar. The def pretty-printer just gives us the same result that the internal xmobarPP would have given us. Let’s try to build something on top of this. To prepare, we can first create a new function myXmobarPP with the default configuration:

myXmobarPP :: PP
myXmobarPP = def

and then plug that into our main function:

main :: IO ()
main = xmonad
     . ewmhFullscreen
     . ewmh
     . withEasySB (statusBarProp "xmobar" (pure myXmobarPP)) defToggleStrutsKey
     $ myConfig

As before, we now change things by modifying that def record until we find something that we like. First, for some functionality that we need further down we need to import one more module:

import XMonad.Util.Loggers

Now we are finally ready to make things pretty. There are a lot of options for the PP record; I’d advise you to read through all of them now, so you don’t get lost!

myXmobarPP :: PP
myXmobarPP = def
    { ppSep             = magenta " • "
    , ppTitleSanitize   = xmobarStrip
    , ppCurrent         = wrap " " "" . xmobarBorder "Top" "#8be9fd" 2
    , ppHidden          = white . wrap " " ""
    , ppHiddenNoWindows = lowWhite . wrap " " ""
    , ppUrgent          = red . wrap (yellow "!") (yellow "!")
    , ppOrder           = \[ws, l, _, wins] -> [ws, l, wins]
    , ppExtras          = [logTitles formatFocused formatUnfocused]
    }
  where
    formatFocused   = wrap (white    "[") (white    "]") . magenta . ppWindow
    formatUnfocused = wrap (lowWhite "[") (lowWhite "]") . blue    . ppWindow

    -- | Windows should have *some* title, which should not not exceed a
    -- sane length.
    ppWindow :: String -> String
    ppWindow = xmobarRaw . (\w -> if null w then "untitled" else w) . shorten 30

    blue, lowWhite, magenta, red, white, yellow :: String -> String
    magenta  = xmobarColor "#ff79c6" ""
    blue     = xmobarColor "#bd93f9" ""
    white    = xmobarColor "#f8f8f2" ""
    yellow   = xmobarColor "#f1fa8c" ""
    red      = xmobarColor "#ff5555" ""
    lowWhite = xmobarColor "#bbbbbb" ""

IF YOU ARE ON A VERSION < 0.17: Both logTitles and xmobarBorder are not available yet, so you will have to remove them. As an alternative to xmobarBorder, a common way to “mark” the currently focused workspace is by using brackets; you can try something like ppCurrent = wrap (blue "[") (blue "]") and see if you like it. Also read the bit about ppOrder further down!

That’s a lot! But don’t worry, take a deep breath and remind yourself of what you read above in the documentation of the PP record. Even if you haven’t read the documentation yet, most of the fields should be pretty self-explanatory; ppTitle formats the title of the currently focused window, ppCurrent formats the currently focused workspace, ppHidden is for the hidden workspaces that have windows on them, etc. The rest is just deciding on some pretty colours and formatting things just how we like it.

An important thing to talk about may be ppOrder. Quoting from its documentation:

By default, this function receives a list with three formatted strings, representing the workspaces, the layout, and the current window title, respectively. If you have specified any extra loggers in ppExtras, their output will also be appended to the list.

So the first three argument of the list (ws, l, and _ in our case, where the _ just means we want to ignore that argument and not give it a name) are the workspaces to show, the currently active layout, and the title of the focused window. The last element—wins—is what we gave to ppExtras; if you added more loggers to that then you would have to add more items to the list, like this:

ppOrder = \[ws, l, _, wins, more, more2] -> [ws, l, wins, more, more2]

However, many people want to show all window titles on the currently focused workspace instead. For that, one can use logTitles from XMonad.Util.Loggers (remember that module we just imported?). However, logTitles logs all titles. Naturally, we don’t want to show the focused window twice and so we suppress it here by ignoring the third argument of ppOrder and not returning it. The functions formatFocused and formatUnfocused should be relatively self explanitory—they decide how to format the focused resp. unfocused windows.

By the way, the \ ... -> syntax in there is Haskell’s way to express a lambda abstraction (or anonymous function, as some languages call it). All of the arguments of the function come directly after the \ and before the ->; in our case, this is a list with exactly four elements in it. Basically, it’s a nice way to write a function inline and not having to define it inside e.g. a where clause. The above could have also be written as

myXmobarPP :: PP
myXmobarPP = def
    { -- stuff here
    , ppOrder = myOrder
      -- more stuff here
    }
  where
    myOrder [ws, l, _, wins] = [ws, l, wins]
    -- more stuff here

If you’re unsure of the number of elements that your ppOrder will take, you can also specify the list like this:

ppOrder = \(ws : l : _ : wins : _) -> [ws, l, wins]

This says that it is a list of at least four elements (ws, l, the unnamed argument, and wins), but that afterwards everything is possible.

This config is really quite complicated. If this is too much for you, you can also really just start with the blank

myXmobarPP :: PP
myXmobarPP = def

then add something, reload xmonad, see how things change and whether you like them. If not, remove that part and try something else. If you do, try to understand how that particular piece of code works. You’ll have something approaching the above that you fully understand in no time!

So now you’ve got a status bar and xmonad. We still need a few more things: a screensaver, a tray for our apps that have tray icons, a way to set our desktop background, and the like.

For this, we will need a few pieces of software.

apt-get install trayer xscreensaver

If you want a network applet, something to set your desktop background, and a power-manager:

apt-get install nm-applet feh xfce4-power-manager

First, configure xscreensaver how you like it with the xscreensaver-demo command. Now, we will set these things up in ~/.xinitrc (we could also do most of this in xmonad’s startupHook, but ~/.xinitrc is perhaps more standard). If you want to use xmonad with a desktop environment, see Basic Desktop Environment Integration for how to do this.

Your ~/.xinitrc may wind up looking like this:

#!/bin/sh

# [... default stuff that your distro may throw in here ...] #

# Set up an icon tray
trayer --edge top --align right --SetDockType true --SetPartialStrut true \
 --expand true --width 10 --transparent true --tint 0x5f5f5f --height 18 &

# Set the default X cursor to the usual pointer
xsetroot -cursor_name left_ptr

# Set a nice background
feh --bg-fill --no-fehbg ~/.wallpapers/haskell-red-noise.png

# Fire up screensaver
xscreensaver -no-splash &

# Power Management
xfce4-power-manager &

if [ -x /usr/bin/nm-applet ] ; then
   nm-applet --sm-disable &
fi

exec xmonad

Notice the call to trayer above. The options tell it to go on the top right, with a default width of 10% of the screen (to nicely match up with xmobar, which we set to a width of 90% of the screen). We give it a color and a height.

Then we fire up the rest of the programs that interest us.

Finally, we start xmonad.

blank desktop

Mission accomplished!

Of course substitute the wallpaper for one of your own. If you like the one used above, you can find it here.

Final Touches

There may be some programs that you don’t want xmonad to tile. The classic example here is the GNU Image Manipulation Program; it pops up all sorts of new windows all the time, and they work best at defined sizes. It makes sense for xmonad to float these kinds of windows by default.

This kind of behaviour can be achieved via the manageHook, which runs when windows are created. There are several functions to help you match on a certain window in XMonad.ManageHook. For example, suppose we’d want to match on the class name of the application. With the application open, open another terminal and invoke the xprop command. Then click on the application that you would like to know the properties of. In our case you should see (among other things)

WM_CLASS(STRING) = "gimp", "Gimp"

The second string in WM_CLASS is the class name, which we can access with className from XMonad.ManageHook. The first one is usually called the instance name and is matched-on via appName from the same module.

Let’s use the class name for now. We can tell all windows with that class name to float by defining the following manageHook:

myManageHook = (className =? "Gimp" --> doFloat)

Say we also want to float all dialogs. This is easy with the isDialog function from XMonad.Hooks.ManageHelpers (which you should import) and a little modification to the myManageHook function:

myManageHook :: ManageHook
myManageHook = composeAll
    [ className =? "Gimp" --> doFloat
    , isDialog            --> doFloat
    ]

Now we just need to tell xmonad to actually use our manageHook. This is as easy as overriding the manageHook field in myConfig. You can do it like this:

myConfig = def
    { modMask    = mod4Mask      -- Rebind Mod to the Super key
    , layoutHook = myLayout      -- Use custom layouts
    , manageHook = myManageHook  -- Match on certain windows
    }
  `additionalKeysP`
    [ ("M-S-z", spawn "xscreensaver-command -lock")
    , ("M-S-=", unGrab *> spawn "scrot -s"        )
    , ("M-]"  , spawn "firefox"                   )
    ]

The Whole Thing

The full ~/.config/xmonad/xmonad.hs, in all its glory, now looks like this:

import XMonad

import XMonad.Hooks.DynamicLog
import XMonad.Hooks.ManageDocks
import XMonad.Hooks.ManageHelpers
import XMonad.Hooks.StatusBar
import XMonad.Hooks.StatusBar.PP

import XMonad.Util.EZConfig
import XMonad.Util.Loggers
import XMonad.Util.Ungrab

import XMonad.Layout.Magnifier
import XMonad.Layout.ThreeColumns

import XMonad.Hooks.EwmhDesktops


main :: IO ()
main = xmonad
     . ewmhFullscreen
     . ewmh
     . withEasySB (statusBarProp "xmobar" (pure myXmobarPP)) defToggleStrutsKey
     $ myConfig

myConfig = def
    { modMask    = mod4Mask      -- Rebind Mod to the Super key
    , layoutHook = myLayout      -- Use custom layouts
    , manageHook = myManageHook  -- Match on certain windows
    }
  `additionalKeysP`
    [ ("M-S-z", spawn "xscreensaver-command -lock")
    , ("M-S-=", unGrab *> spawn "scrot -s"        )
    , ("M-]"  , spawn "firefox"                   )
    ]

myManageHook :: ManageHook
myManageHook = composeAll
    [ className =? "Gimp" --> doFloat
    , isDialog            --> doFloat
    ]

myLayout = tiled ||| Mirror tiled ||| Full ||| threeCol
  where
    threeCol = magnifiercz' 1.3 $ ThreeColMid nmaster delta ratio
    tiled    = Tall nmaster delta ratio
    nmaster  = 1      -- Default number of windows in the master pane
    ratio    = 1/2    -- Default proportion of screen occupied by master pane
    delta    = 3/100  -- Percent of screen to increment by when resizing panes

myXmobarPP :: PP
myXmobarPP = def
    { ppSep             = magenta " • "
    , ppTitleSanitize   = xmobarStrip
    , ppCurrent         = wrap " " "" . xmobarBorder "Top" "#8be9fd" 2
    , ppHidden          = white . wrap " " ""
    , ppHiddenNoWindows = lowWhite . wrap " " ""
    , ppUrgent          = red . wrap (yellow "!") (yellow "!")
    , ppOrder           = \[ws, l, _, wins] -> [ws, l, wins]
    , ppExtras          = [logTitles formatFocused formatUnfocused]
    }
  where
    formatFocused   = wrap (white    "[") (white    "]") . magenta . ppWindow
    formatUnfocused = wrap (lowWhite "[") (lowWhite "]") . blue    . ppWindow

    -- | Windows should have *some* title, which should not not exceed a
    -- sane length.
    ppWindow :: String -> String
    ppWindow = xmobarRaw . (\w -> if null w then "untitled" else w) . shorten 30

    blue, lowWhite, magenta, red, white, yellow :: String -> String
    magenta  = xmobarColor "#ff79c6" ""
    blue     = xmobarColor "#bd93f9" ""
    white    = xmobarColor "#f8f8f2" ""
    yellow   = xmobarColor "#f1fa8c" ""
    red      = xmobarColor "#ff5555" ""
    lowWhite = xmobarColor "#bbbbbb" ""

Further Customizations

The following section mostly consists of extra bits—feel free to skip this and jump directly to Get in Touch. We’ve covered a lot of ground here and sometimes it’s really better to let things settle a bit before going further; much more so if you’re happy with how things are looking and feeling right now!

Beautifying Xmobar

A usual starting point for beautifying xmobar is either to use xpm icons, or a font like font-awesome to add icons to the rest of the text. We will show you how to do the latter. Xmobar, thankfully, makes this very easy; just put a font down under additionalFonts and wrap your Icons with <fn> tags and the respective index of the font (starting from 1). As an example, consider how we would extend our configuration above with this new functionality:

Config { overrideRedirect = False
       , font             = "xft:iosevka-9"
       , additionalFonts  = ["xft:FontAwesome-9"]
       ...
       , Run Battery
           [ ...
           , "--lows"   , "<fn=1>\62020</fn>  "
           , "--mediums", "<fn=1>\62018</fn>  "
           , "--highs"  , "<fn=1>\62016</fn>  "
             ...
           ]
       ...
       }

For an explanation of the battery commands used above, see xmobars battery documentation.

You can also specify workspaces in the same way and feed them to xmobar via the property (e.g. have "<fn=1>\xf120</fn>" as one of your workspace names).

As an example how this would look like in a real configuration, you can look at Liskin’s, slotThe’s, or TheMC47’s xmobar configuration. Do note that the last two are Haskell-based and thus may be a little hard to understand for newcomers.

Renaming Layouts

Magnifier NoMaster ThreeCol is quite a mouthful to show in your bar, right? Thankfully there is the nifty XMonad.Layout.Renamed, which makes renaming layouts easy! We will focus on the Replace constructor here, as a lot of people will find that that’s all they need. To use it we again follow the documentation (try it yourself!)—import the module and then change myLayout like this:

myLayout = tiled ||| Mirror tiled ||| Full ||| threeCol
  where
    threeCol
        = renamed [Replace "ThreeCol"]
        $ magnifiercz' 1.3
        $ ThreeColMid nmaster delta ratio
    tiled   = Tall nmaster delta ratio
    nmaster = 1      -- Default number of windows in the master pane
    ratio   = 1/2    -- Default proportion of screen occupied by master pane
    delta   = 3/100  -- Percent of screen to increment by when resizing panes

The new line renamed [Replace "ThreeCol"] tells the layout to throw its current name away and use ThreeCol instead. After reloading xmonad, you should now see this shorter name in your bar. The line breaks here are just cosmetic, by the way; if you want you can write everything in one line:

threeCol = renamed [Replace "ThreeCol"] $ magnifiercz' 1.3 $ ThreeColMid nmaster delta ratio

Get in Touch

The irc.libera.chat/#xmonad channel is very friendly and helpful. It is possible that people will not immediately answer—we do have lives as well, after all :). Eventually though, people will usually chime in if they have something helpful to say; sometimes this takes 10 minutes, other times it may well take 10 hours. If you don’t have an IRC client ready to go, the easiest way to join is via webchat—just jot down a username and you should be good to go! There is a log of the channel available, in case you do have to disconnect at some point, so don’t worry about missing any messages.

If you’re not a fan of real-time interactions, you can also post to the xmonad mailing list or the xmonad subreddit.

Trouble?

Check ~/.xsession-errors or your distribution’s equivalent first. If you’re using a distribution that does not log into a file automatically, you will have to set this up manually. For example, you could put something like

if [[ ! $DISPLAY ]]; then
  exec startx >& ~/.xsession-errors
fi

into your ~/.profile file to explicitly log everything into ~/.xsession-errors.

If you can’t figure out what’s wrong, don’t hesitate to get in touch!

Closing Thoughts

That was quite a ride! Don’t worry if you didn’t understand everything perfectly, these things take time. You can re-read parts of this guide as often as you need to and—with the risk of sounding like a broken record—if you can’t figure something out really do not be afraid to get in touch.

If you want to see a few more complicated examples of other peoples xmonad configurations, look no further! Below are (in alphabetical order) the configurations of a few of xmonad’s maintainers. Just keep in mind that these setups are very customized and perhaps a little bit hard to replicate (some may rely on features only available in personal forks or git), may or may not be documented, and most aren’t very pretty either :)