The classic desktop metaphor feels comfortable right up until the moment a dozen windows pile on top of each other. Dragging, resizing, and hunting for the right application with a mouse turns into a job of its own. XMonad proposes a radically different arrangement, where windows snap into place automatically, every action lives under the fingertips on the keyboard, and the entire configuration gets written in Haskell. This guide walks through deploying XMonad on Debian and Arch-based distributions, building it through Stack or Cabal, writing a first meaningful config, and shaping the manager to fit real working habits.
Why a tiling approach to window management became a serious productivity tool among engineers and developers
A mouse photographs beautifully in marketing materials and gets in the way during actual work. Every hand movement between keyboard and pointer breaks the flow of thought, and manually aligning windows eats seconds that compound into wasted hours. Tiling window managers emerged as the answer to that friction. XMonad pushed the idea further than most of its peers by being written in Haskell, a strictly typed functional language whose mathematical discipline makes crashes practically unheard of.
The architecture splits cleanly into two layers. The xmonad core ships as a minimal, stable foundation without decorative extras. Around it sits the massive xmonad-contrib library, a community-maintained collection of hundreds of additional layout algorithms, hooks, and extension modules. Together they function as what the authors themselves describe as a library for building a personal window manager.
The real strength of this approach reveals itself slowly. The first days go into learning keyboard shortcuts. The first weeks involve tweaking small values. A few months in, the configuration file has grown into a personal instrument, sharpened against the user's specific habits in a way no point-and-click settings panel could ever match.
Choosing between the distribution package manager and a source build through Stack or Cabal
Two fundamentally different roads lead to a working XMonad installation. The first runs through the system package manager and looks shorter. The second goes through Haskell-native tools like Stack or Cabal, offering more control and sidestepping the dynamic linking quirks that some distributions impose on the Haskell ecosystem.
On Debian and Ubuntu, the apt route looks refreshingly compact:
sudo apt update
sudo apt install xmonad libghc-xmonad-contrib-dev libghc-xmonad-dev suckless-tools
This pulls in xmonad itself, the contrib library with all its extensions, and the suckless-tools bundle that provides dmenu, the minimalist application launcher bound to Mod+p by default. Arch Linux traditionally ships fresh packages through pacman:
sudo pacman -S xmonad xmonad-contrib xmobar dmenu
The package-manager path carries one catch worth knowing. On distributions that link Haskell packages dynamically, Arch among the most prominent, any dependency update can leave xmonad unable to start on the next login unless the user manually runs xmonad --recompile. That fragility pushes many experienced users toward Stack or Cabal builds instead.
Stack is the recommended Haskell toolchain and arrives most reliably through GHCup, the main Haskell installer:
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
Alternatively, Stack can be installed directly from distribution repositories:
sudo apt install haskell-stack # Debian, Ubuntu
sudo dnf install stack # Fedora
sudo pacman -S stack # Arch
stack upgrade
The stack upgrade step matters because repository versions often lag behind upstream, and XMonad compilation works best on the latest release. Once Stack is ready, the config directory moves to ~/.config/xmonad, the modern location that replaces the older ~/.xmonad.
Building XMonad and xmonad-contrib from source yields a fully self-contained installation
Source builds keep the binary isolated from system updates, which protects against the dreaded login-time failure after a Haskell library bump. The workflow starts by cloning the upstream repositories into the config directory:
mkdir -p ~/.config/xmonad
cd ~/.config/xmonad
git clone https://github.com/xmonad/xmonad
git clone https://github.com/xmonad/xmonad-contrib
A stack.yaml file tells Stack which GHC version to use and where to find the local packages:
resolver: lts-22.0
packages:
- xmonad
- xmonad-contrib
The build itself requires a single command, though the first run downloads GHC and takes a while on slower connections:
stack install
Stack places the compiled binary into ~/.local/bin/xmonad, which should already sit on the PATH in most setups. A quick check confirms the version:
xmonad --version
Users who prefer Cabal over Stack follow a parallel path. The sequence updates the package index, installs the libraries into a dedicated environment, and then builds the executable:
cabal update
cabal install --package-env=$HOME/.config/xmonad --lib base xmonad xmonad-contrib
cabal install --package-env=$HOME/.config/xmonad xmonad
Both toolchains produce the same end result. The difference comes down to preference and familiarity with Haskell packaging conventions.
Launching XMonad through a display manager or a plain xinit session
The window manager needs to be told when and how to start. For users running a display manager like LightDM, GDM, or SDDM, a desktop entry under /usr/share/xsessions/xmonad.desktop usually arrives with the package. Selecting XMonad from the session dropdown at the login screen takes care of the rest.
For users who start X manually through startx, the entry point is ~/.xinitrc:
#!/bin/sh
# Set the keyboard layout
setxkbmap -layout us,fr -option grp:alt_shift_toggle &
# Set a wallpaper to avoid window shadows when switching workspaces
feh --bg-fill ~/Pictures/wallpaper.jpg &
# Start a compositor for smoother visuals
picom --daemon &
# Finally hand control to xmonad
exec xmonad
That exec matters. It replaces the shell process with xmonad, meaning no zombie parent lingers in the process tree. Setting a wallpaper before launching also solves a classic cosmetic bug where leftover window contents appear as ghost shadows on the desktop.
Writing the first meaningful xmonad.hs configuration file in Haskell
The configuration lives in ~/.config/xmonad/xmonad.hs. A minimal but usable version already reveals the elegance of the approach:
import XMonad
import XMonad.Util.EZConfig (additionalKeysP)
import XMonad.Hooks.EwmhDesktops (ewmh, ewmhFullscreen)
import XMonad.Hooks.ManageDocks (avoidStruts, docks, manageDocks)
import XMonad.Layout.Spacing (spacingRaw, Border(..))
main :: IO ()
main = xmonad
. ewmhFullscreen
. ewmh
. docks
$ myConfig
myConfig = def
{ terminal = "alacritty"
, modMask = mod4Mask -- Use the Super key as modifier
, borderWidth = 2
, normalBorderColor = "#3b4252"
, focusedBorderColor = "#88c0d0"
, workspaces = map show [1..9 :: Int]
, manageHook = manageDocks <+> manageHook def
, layoutHook = avoidStruts $ mySpacing $ layoutHook def
}
`additionalKeysP`
[ ("M-S-<Return>", spawn "alacritty")
, ("M-p", spawn "dmenu_run")
, ("M-S-q", io (return ())) -- Disable default logout
, ("M-S-l", spawn "slock") -- Lock the screen
]
mySpacing = spacingRaw False (Border 5 5 5 5) True (Border 5 5 5 5) True
Several things deserve attention in this file. The modMask = mod4Mask line rebinds the modifier key from the default Alt to the Super key, which avoids collisions with shortcuts inside most applications. The additionalKeysP function from XMonad.Util.EZConfig accepts human-readable key combinations like "M-S-<Return>" instead of the verbose traditional form. The ewmh and ewmhFullscreen wrappers make the window manager cooperate with modern applications that expect standard desktop hints, which is what lets fullscreen video in web browsers work correctly.
After saving the file, a recompile applies the changes without logging out:
xmonad --recompile
xmonad --restart
If a typo slipped in, the recompile output pinpoints the exact line and the Haskell type error. That strict feedback loop is one of the reasons experienced users trust the system so deeply.
Adding a status bar with xmobar and integrating it into the xmonad event loop
A bare tiling session shows only windows. For battery, clock, workload indicators, and workspace names, a status bar is needed. Xmobar pairs with XMonad as cleanly as a pot lid with its pot. Installation mirrors the earlier patterns:
sudo apt install xmobar
The bar reads its own configuration from ~/.config/xmobar/xmobarrc. A typical working file looks like this:
Config { font = "xft:JetBrains Mono:size=10"
, bgColor = "#2e3440"
, fgColor = "#d8dee9"
, position = TopSize L 100 24
, commands = [ Run Cpu ["-t", "CPU: <total>%"] 10
, Run Memory ["-t", "MEM: <usedratio>%"] 10
, Run Date "%a %d %b %H:%M" "date" 10
, Run StdinReader
]
, sepChar = "%"
, alignSep = "}{"
, template = "%StdinReader% }{ %cpu% | %memory% | <fc=#a3be8c>%date%</fc>"
}
Connecting xmobar to xmonad requires a small change in xmonad.hs, where the log hook pipes workspace information into the bar. Using XMonad.Hooks.StatusBar and XMonad.Hooks.StatusBar.PP handles the wiring with minimal code, and the result looks like a proper desktop environment without any of its weight.
Shaping the layout and key bindings into a personal working instrument
The default tall layout splits the screen into a master window on the left and a column of secondary windows on the right. For most tasks this works, but xmonad-contrib offers alternatives worth exploring:
- ThreeColMid places a master window in the centre flanked by two side columns, which fits ultrawide monitors beautifully
- Grid arranges everything in equal tiles, useful when monitoring many similar processes at once
- Tabbed stacks windows on top of each other with a tab bar, handy for grouping related applications
- Accordion compresses inactive windows to thin strips and expands the focused one, reminiscent of a musical squeezebox
- BinarySpacePartition lets each new window split the focused area, echoing how i3 behaves but staying within the Haskell ecosystem
Switching between layouts happens through mod+space by default. The power of XMonad lies not in having these layouts available, but in being able to combine, modify, and write new ones in Haskell. A user who needs a specific geometry for a specific workspace can express it in a few lines of type-safe code, confident that if it compiles, it will work.
Final thoughts on adopting XMonad as a daily driver on Linux
XMonad demands an upfront investment that not every user wants to make. Haskell syntax feels alien on day one, the absence of a graphical settings panel unsettles newcomers, and the learning curve can resemble a cliff face rather than a gentle slope. Yet those who climb past the first week rarely look back. The manager rewards patience with a level of control and reliability that few alternatives can match.
Stability flows directly from the functional foundation. Keyboard-driven workflows speed up every repetitive task. The contrib library provides a path to extend the system without rewriting it. A well-tuned configuration, once written, lasts for years with only minor adjustments. For engineers who spend their days inside terminals, editors, and browsers, XMonad on Linux stops feeling like a window manager and starts feeling like an extension of the hands.