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:
So let’s get started!
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 big
warning labels for the respective version bounds, 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.
In particular, it might be a good idea to set a wallpaper beforehand. Otherwise, when switching workspaces or closing windows, you might start seeing “shadows” of windows that were there before, unable to interact with them.
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!
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
-- NOTE: Only needed for versions < 0.18.0! For 0.18.0 and up, this is
-- already included in the XMonad import and will give you a warning!
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-C-s
to take a
snapshot of one window, and M-f
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-C-s", unGrab *> spawn "scrot -s" )
, ("M-f" , 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 thatf
may have given me, dog
.
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 were 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-C-s", unGrab *> spawn "scrot -s" )
, ("M-f" , 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-C-s", unGrab *> spawn "scrot -s" )
, ("M-f" , 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-C-s", unGrab *> spawn "scrot -s" )
, ("M-f" , spawn "firefox" )
]
Much better!
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
and replace your main
function above with:
main :: IO ()
main = xmonad $ ewmhFullscreen $ ewmh $ xmobarProp $ myConfig
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.
Further, the xmobarProp
function does not exist in older 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). Do note the lack of
ewmhFullscreen
, as explained above!
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
, andxmobarProp
and give me the big new functionxmonad . ewmhFullscreen . ewmh . xmobarProp
that first executesxmobarProp
, thenewmh
, thenewmhFullscreen
, and finallyxmonad
. Then give itmyConfig
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!)
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 in xmobar’s quick-start.org. 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).
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
. 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
. If you want to use XMonad with a desktop environment, see
Basic Desktop Environment Integration for how to do this. For a
version using XMonad’s built in functionality instead, see the [next
section][using-the-startupHook].
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.
Mission accomplished!
Of course substitute the wallpaper for one of your own. If you like the one used above, you can find it here.
startupHook
Instead of the .xinitrc
file, one can also use XMonad’s built in
startupHook
in order to auto start programs. The
XMonad.Util.SpawnOnce library is perfect for this use case, allowing
programs to start only once, and not with every invocation of M-q
!
This requires but a small change in myConfig
:
-- import XMonad.Util.SpawnOnce (spawnOnce)
myConfig = def
{ modMask = mod4Mask -- Rebind Mod to the Super key
, layoutHook = myLayout -- Use custom layouts
, startupHook = myStartupHook
}
`additionalKeysP`
[ ("M-S-z", spawn "xscreensaver-command -lock")
, ("M-C-s", unGrab *> spawn "scrot -s" )
, ("M-f" , spawn "firefox" )
]
myStartupHook :: X ()
myStartupHook = do
spawnOnce "trayer --edge top --align right --SetDockType true \
\--SetPartialStrut true --expand true --width 10 \
\--transparent true --tint 0x5f5f5f --height 18"
spawnOnce "feh --bg-fill --no-fehbg ~/.wallpapers/haskell-red-noise.png"
-- … and so on …
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 dialog windows. 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-C-s", unGrab *> spawn "scrot -s" )
, ("M-f" , spawn "firefox" )
]
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
-- NOTE: Importing XMonad.Util.Ungrab is only necessary for versions
-- < 0.18.0! For 0.18.0 and up, this is already included in the
-- XMonad import and will generate a warning instead!
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-C-s", unGrab *> spawn "scrot -s" )
, ("M-f" , 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" ""
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!
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 xmobar’s 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 old, Liskin’s current, slotThe’s, or TheMC47’s xmobar configuration. Do note that the last three are Haskell-based and thus may be a little hard to understand for newcomers.
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
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.
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!
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 :)