The Last Honest X11 Window Manager: Xmonad
In an era of dishonesty, there is one X11 window manager that continues to flourish. The final frontier of honesty. The last pillar of hope in an unsettling ecosystem where windows are no longer managed by human users… but by committees, protocols, and corporations.
For nearly twenty years, this window manager has been battle-tested. It has shined through the smoke of AUR DDoS attacks… endless attempts to force Wayland adoption by Red Hat and Canonical… and a relentless wave of dishonest “modern UX improvements” designed to undermine your ability to simply move a window one pixel to the left.
At the end of the day, that’s all an X11 window manager was ever supposed to be.
And XMonad is— as YouTux might put it— the last honest window manager.
Intro
What’s up guys, my name is Tony, and today I’m gonna give you a quick and painless guide on installing and configuring Xmonad.
Let’s jump into the installation.
Install Dependencies for Xmonad
Today, we’re going to be using NixOS for the installation and configuration, but Xmonad is available on virtually every package manager in existence, (at least all the honest ones.) If you are using a legacy distro such as Arch, Gentoo, or Debian, a link will be provided below the subscribe button for a written guide to accompany this video.
For nixos, we just need to do 3 things.
In our configuration.nix:
services = {
picom.enable = true;
displayManager = {
ly.enable = true;
};
xserver = {
enable = true;
autoRepeatDelay = 200;
autoRepeatInterval = 35;
windowManager = {
xmonad = {
enable = true;
enableContribAndExtras = true;
extraPackages = hpkgs: [
hpkgs.xmonad
hpkgs.xmonad-extras
hpkgs.xmonad-contrib
];
};
};
displayManager.sessionCommands = ''
xwallpaper --zoom ~/walls/wall1.png
'';
};
};
And then in our home.nix:
home.packages = with pkgs; [
haskell-language-server
xmobar
];
Requirements for Xmonad
If you’re on Arch, btw, here are the core dependencies:
- xorg-server
- xorg-xinit
- xmonad
- xmonad-contrib
- ghc (Glasgow Haskell Compiler)
- xmobar
- dmenu (or rofi if you prefer)
- picom (for compositing)
Extra stuff for my setup today:
- alacritty (terminal emulator)
- dmenu/rofi (application launcher)
- feh or xwallpaper (for wallpapers, we’re using xwallpaper in our nix config)
- firefox (web browser)
- ttf-jetbrains-mono-nerd (font)
- scrot or maim (for screenshots)
- picom (compositor for shadows and transparency)
So let’s install these with pacman -Sy
sudo pacman -Sy xorg-server xorg-xinit xmonad xmonad-contrib ghc xmobar dmenu alacritty xwallpaper firefox ttf-jetbrains-mono-nerd scrot picom
Alright, let’s configure xmonad and get it up and running.
Configure Xmonad
After running nixos-rebuild switch (or installing via pacman if you’re on Arch), we need to create our xmonad.hs config file.
Let’s create the directory and config:
mkdir -p ~/.config/xmonad
vim ~/.config/xmonad/xmonad.hs
Starting from Zero
Let’s start with the absolute minimum xmonad configuration. This is a complete, working config that does nothing more than launch xmonad with default settings:
import XMonad
main = xmonad def
That’s it. Three lines. This will give you a tiling window manager with default keybindings. Alt+Shift+Enter opens a terminal, Alt+p for dmenu, etc.
Let’s compile and test it:
xmonad --recompile
Now let’s build it up piece by piece.
Adding Custom Terminal and Mod Key
Most people want to use the Super key (Windows key) instead of Alt, and specify their preferred terminal:
import XMonad
main = xmonad def
{ modMask = mod4Mask -- Use Super instead of Alt
, terminal = "alacritty" -- Use alacritty as terminal
}
Adding Basic Keybindings
Let’s add some custom keybindings using EZConfig for a more readable syntax:
import XMonad
import XMonad.Util.EZConfig (additionalKeysP)
myKeys =
[ ("M-<Return>", spawn "alacritty")
, ("M-d", spawn "dmenu_run")
, ("M-q", kill)
]
main = xmonad $ def
{ modMask = mod4Mask
, terminal = "alacritty"
}
`additionalKeysP` myKeys
Adding Colors and Borders
Let’s add some visual customization:
import XMonad
import XMonad.Util.EZConfig (additionalKeysP)
myKeys =
[ ("M-<Return>", spawn "alacritty")
, ("M-d", spawn "dmenu_run")
, ("M-q", kill)
]
main = xmonad $ def
{ modMask = mod4Mask
, terminal = "alacritty"
, borderWidth = 2
, normalBorderColor = "#444b6a"
, focusedBorderColor = "#ad8ee6"
}
`additionalKeysP` myKeys
Adding Gaps and Spacing
Now let’s add some breathing room with gaps between windows:
import XMonad
import XMonad.Util.EZConfig (additionalKeysP)
import XMonad.Layout.Spacing
myLayoutHook = spacingWithEdge 3 $ layoutHook def
myKeys =
[ ("M-<Return>", spawn "alacritty")
, ("M-d", spawn "dmenu_run")
, ("M-q", kill)
]
main = xmonad $ def
{ modMask = mod4Mask
, terminal = "alacritty"
, borderWidth = 2
, normalBorderColor = "#444b6a"
, focusedBorderColor = "#ad8ee6"
, layoutHook = myLayoutHook
}
`additionalKeysP` myKeys
Adding XMobar Status Bar
Now let’s integrate xmobar to show workspaces and window information:
import XMonad
import XMonad.Util.EZConfig (additionalKeysP)
import XMonad.Layout.Spacing
import XMonad.Hooks.DynamicLog
import XMonad.Hooks.StatusBar
import XMonad.Hooks.StatusBar.PP
import XMonad.Hooks.ManageDocks
myLayoutHook = avoidStruts $ spacingWithEdge 3 $ layoutHook def
myXmobarPP :: PP
myXmobarPP = def
{ ppCurrent = xmobarColor "#0db9d7" ""
, ppHidden = xmobarColor "#a9b1d6" ""
, ppHiddenNoWindows = xmobarColor "#444b6a" ""
}
myStatusBar = statusBarProp "xmobar" (pure myXmobarPP)
myKeys =
[ ("M-<Return>", spawn "alacritty")
, ("M-d", spawn "dmenu_run")
, ("M-q", kill)
, ("M-S-r", spawn "xmonad --recompile && xmonad --restart")
]
main = xmonad $ withEasySB myStatusBar defToggleStrutsKey $ def
{ modMask = mod4Mask
, terminal = "alacritty"
, borderWidth = 2
, normalBorderColor = "#444b6a"
, focusedBorderColor = "#ad8ee6"
, layoutHook = myLayoutHook
, manageHook = manageDocks
}
`additionalKeysP` myKeys
Now we have a pretty solid foundation! But what does a full-featured config look like?
My Full Xmonad Configuration
Here’s my actual daily driver xmonad configuration with TokyoNight colors, custom layouts, workspace rules, gap controls, and all my keybindings:
import Data.Map qualified as M
import XMonad
import XMonad.Hooks.DynamicLog
import XMonad.Hooks.EwmhDesktops
import XMonad.Hooks.ManageDocks
import XMonad.Hooks.StatusBar
import XMonad.Hooks.StatusBar.PP
import XMonad.Hooks.InsertPosition
import XMonad.Layout.NoBorders
import XMonad.Layout.ResizableTile
import XMonad.Layout.Spacing
import XMonad.Layout.Spiral
import XMonad.Layout.Renamed
import XMonad.StackSet qualified as W
import XMonad.Util.EZConfig (additionalKeysP)
import XMonad.Util.Loggers
import XMonad.Util.SpawnOnce
-- TokyoNight Colors
colorBg = "#1a1b26" -- background
colorFg = "#a9b1d6" -- foreground
colorBlk = "#32344a" -- black
colorRed = "#f7768e" -- red
colorGrn = "#9ece6a" -- green
colorYlw = "#e0af68" -- yellow
colorBlu = "#7aa2f7" -- blue
colorMag = "#ad8ee6" -- magenta
colorCyn = "#0db9d7" -- cyan
colorBrBlk = "#444b6a" -- bright black
-- Appearance
myBorderWidth = 2
myNormalBorderColor = colorBrBlk
myFocusedBorderColor = colorMag
-- Gaps (matching dwm: 3px all around)
mySpacing = spacingWithEdge 3
-- Workspaces
myWorkspaces = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
-- Mod key (Super/Windows key)
myModMask = mod4Mask
-- Terminal
myTerminal = "st"
-- Layouts
myLayoutHook =
avoidStruts $
renamed [Replace "Tall"] (mySpacing tall)
||| renamed [Replace "Wide"] (mySpacing (Mirror tall))
||| renamed [Replace "Full"] (mySpacing Full)
||| renamed [Replace "Spiral"] (mySpacing (spiral (6 / 7)))
where
tall = ResizableTall 1 (3 / 100) (11 / 20) []
-- Window rules (matching dwm config)
myManageHook =
composeAll
[ className =? "Gimp" --> doFloat
, className =? "Brave-browser" --> doShift "2"
, className =? "firefox" --> doShift "3"
, className =? "Slack" --> doShift "4"
, className =? "kdenlive" --> doShift "8"
]
<+> insertPosition Below Newer
-- Key bindings (matching dwm as closely as possible)
myKeys =
-- Launch applications
[ ("M-<Return>", spawn myTerminal)
, ("M-d", spawn "rofi -show drun -theme ~/.config/rofi/config.rasi")
, ("M-r", spawn "dmenu_run")
, ("M-l", spawn "slock")
, ("C-<Print>", spawn "maim -s | xclip -selection clipboard -t image/png")
, -- Window management
("M-q", kill)
, ("M-j", windows W.focusDown)
, ("M-k", windows W.focusUp)
, ("M-<Tab>", windows W.focusDown)
, -- Master area
("M-h", sendMessage Expand)
, ("M-g", sendMessage Shrink)
, ("M-i", sendMessage (IncMasterN 1))
, ("M-p", sendMessage (IncMasterN (-1)))
, -- Layout switching
("M-t", sendMessage $ JumpToLayout "Tall")
, ("M-f", sendMessage $ JumpToLayout "Full")
, ("M-c", sendMessage $ JumpToLayout "Spiral")
, ("M-S-<Return>", sendMessage NextLayout)
, ("M-n", sendMessage NextLayout)
, -- Floating
("M-S-<Space>", withFocused toggleFloat)
, -- Gaps (z to increase, x to decrease, a to toggle)
("M-z", incWindowSpacing 3)
, ("M-x", decWindowSpacing 3)
, ("M-a", toggleWindowSpacingEnabled >> toggleScreenSpacingEnabled)
, ("M-S-a", setWindowSpacing (Border 3 3 3 3) >> setScreenSpacing (Border 3 3 3 3))
, -- Quit/Restart
("M-S-r", spawn "xmonad --recompile && xmonad --restart")
, -- Keychords for tag navigation (Mod+Space then number)
("M-<Space> 1", windows $ W.greedyView "1")
, ("M-<Space> 2", windows $ W.greedyView "2")
, ("M-<Space> 3", windows $ W.greedyView "3")
, ("M-<Space> 4", windows $ W.greedyView "4")
, ("M-<Space> 5", windows $ W.greedyView "5")
, ("M-<Space> 6", windows $ W.greedyView "6")
, ("M-<Space> 7", windows $ W.greedyView "7")
, ("M-<Space> 8", windows $ W.greedyView "8")
, ("M-<Space> 9", windows $ W.greedyView "9")
, ("M-<Space> f", spawn "firefox")
, -- Volume controls
("<XF86AudioRaiseVolume>", spawn "pactl set-sink-volume @DEFAULT_SINK@ +3%")
, ("<XF86AudioLowerVolume>", spawn "pactl set-sink-volume @DEFAULT_SINK@ -3%")
, ("<XF86AudioMute>", spawn "pactl set-sink-mute @DEFAULT_SINK@ toggle")
]
++
-- Standard TAGKEYS behavior (Mod+# to view, Mod+Shift+# to move)
[ (mask ++ "M-" ++ [key], windows $ action tag)
| (tag, key) <- zip myWorkspaces "123456789"
, (action, mask) <- [(W.greedyView, ""), (W.shift, "S-")]
]
-- Helper function for toggling float
toggleFloat w =
windows
( \s ->
if M.member w (W.floating s)
then W.sink w s
else W.float w (W.RationalRect 0.15 0.15 0.7 0.7) s
)
-- XMobar PP (Pretty Printer) configuration
myXmobarPP :: PP
myXmobarPP =
def
{ ppSep = xmobarColor colorBrBlk "" " │ "
, ppTitleSanitize = xmobarStrip
, ppCurrent = xmobarColor colorCyn ""
, ppHidden = xmobarColor colorFg ""
, ppHiddenNoWindows = xmobarColor colorBrBlk ""
, ppUrgent = xmobarColor colorRed colorYlw
, ppOrder = \[ws, l, _, wins] -> [ws, l, wins]
, ppExtras = [logTitles formatFocused formatUnfocused]
}
where
formatFocused = wrap (xmobarColor colorCyn "" "[") (xmobarColor colorCyn "" "]") . xmobarColor colorFg "" . ppWindow
formatUnfocused = wrap (xmobarColor colorBrBlk "" "[") (xmobarColor colorBrBlk "" "]") . xmobarColor colorBrBlk "" . ppWindow
ppWindow :: String -> String
ppWindow = xmobarRaw . (\w -> if null w then "untitled" else w) . shorten 30
-- Main configuration
myConfig =
def
{ modMask = myModMask
, terminal = myTerminal
, workspaces = myWorkspaces
, borderWidth = myBorderWidth
, normalBorderColor = myNormalBorderColor
, focusedBorderColor = myFocusedBorderColor
, layoutHook = myLayoutHook
, manageHook = myManageHook <+> manageDocks
, startupHook = spawnOnce "xsetroot -cursor_name left_ptr"
}
`additionalKeysP` myKeys
-- XMobar status bar configuration
myStatusBar = statusBarProp "xmobar ~/.config/xmobar/xmobarrc" (pure myXmobarPP)
main :: IO ()
main = xmonad . ewmhFullscreen . ewmh . withEasySB myStatusBar defToggleStrutsKey $ myConfig
This config includes TokyoNight colors, custom layouts (Tall, Wide, Full, Spiral), gap controls, workspace rules for automatically moving apps to specific workspaces, and tons of keybindings.
Now let’s compile and test it:
xmonad --recompile
If you’re on NixOS, you can just rebuild and then either log out and select xmonad from your display manager, or if you want to test it immediately:
nixos-rebuild switch
startx
Alright, we’re in xmonad now. As you can see, we have a clean slate with xmobar at the top. Super minimal, super honest.
Xmobar Configuration
We already have xmobar launching from our xmonad config, but let’s customize it. Let’s create an xmobar config:
mkdir -p ~/.config/xmobar
vim ~/.config/xmobar/xmobarrc
Starting with a Minimal Xmobar Config
Here’s the absolute minimal xmobar configuration:
Config {
font = "xft:monospace-10"
, bgColor = "#000000"
, fgColor = "#ffffff"
, position = Top
, commands = [ Run XMonadLog ]
, template = "%XMonadLog%"
}
This will show your workspaces and focused window. Nothing fancy, but it works.
Adding System Information
Let’s add some system monitors like CPU, memory, and the date:
Config {
font = "xft:monospace-10"
, bgColor = "#000000"
, fgColor = "#ffffff"
, position = Top
, sepChar = "%"
, alignSep = "}{"
, template = "%XMonadLog% }{ CPU: %cpu% | MEM: %memory% | %date%"
, commands =
[ Run XMonadLog
, Run Cpu ["-t", "<total>%"] 10
, Run Memory ["-t", "<usedratio>%"] 10
, Run Date "%a %b %_d %H:%M" "date" 10
]
}
Now we have workspace info on the left, and system stats on the right.
Adding Colors and Better Formatting
Let’s make it prettier with some color coding:
Config {
font = "xft:JetBrainsMono Nerd Font-12"
, bgColor = "#1a1b26"
, fgColor = "#a9b1d6"
, position = TopSize C 100 30
, sepChar = "%"
, alignSep = "}{"
, template = " %XMonadLog% }{ <fc=#7aa2f7>CPU:</fc> %cpu% | <fc=#7aa2f7>MEM:</fc> %memory% | <fc=#ad8ee6>%date%</fc> "
, commands =
[ Run XMonadLog
, Run Cpu
[ "-t", "<total>%"
, "-L", "30"
, "-H", "70"
, "-l", "#9ece6a"
, "-n", "#e0af68"
, "-h", "#f7768e"
] 10
, Run Memory
[ "-t", "<usedratio>%"
, "-L", "30"
, "-H", "70"
, "-l", "#9ece6a"
, "-n", "#e0af68"
, "-h", "#f7768e"
] 10
, Run Date "<fc=#ad8ee6>%a %b %_d %H:%M</fc>" "date" 10
]
}
Now we’ve got TokyoNight colors, with green for low usage, yellow for medium, and red for high.
My Full Xmobar Configuration
Here’s my actual daily driver xmobar config with borders, Nerd Font icons, battery monitoring, and full TokyoNight theming:
-- TokyoNight XMobar Config
Config {
-- Appearance
font = "JetBrainsMono Nerd Font Mono Bold 16"
, additionalFonts = [ "JetBrainsMono Nerd Font Mono 18" ]
, bgColor = "#1a1b26"
, fgColor = "#a9b1d6"
, position = TopSize C 100 35
, border = BottomB
, borderColor = "#444b6a"
, borderWidth = 2
-- Layout
, sepChar = "%"
, alignSep = "}{"
, template = " %XMonadLog% }{ %cpu% <fc=#444b6a>│</fc> %memory% <fc=#444b6a>│</fc> %battery% <fc=#444b6a>│</fc> %date% "
-- Plugins
, commands =
[ Run XMonadLog
, Run Cpu
[ "-t", "<fc=#7aa2f7> CPU:</fc> <total>%"
, "-L", "30"
, "-H", "70"
, "-l", "#9ece6a"
, "-n", "#e0af68"
, "-h", "#f7768e"
] 10
, Run Memory
[ "-t", "<fc=#7aa2f7> MEM:</fc> <usedratio>%"
, "-L", "30"
, "-H", "70"
, "-l", "#9ece6a"
, "-n", "#e0af68"
, "-h", "#f7768e"
] 10
, Run Battery
[ "-t", "<fc=#7aa2f7> BAT:</fc> <acstatus>"
, "-L", "20"
, "-H", "80"
, "-l", "#f7768e"
, "-n", "#e0af68"
, "-h", "#9ece6a"
, "--"
, "-o", "<left>% (<timeleft>)"
, "-O", "<fc=#e0af68>Charging</fc> <left>%"
, "-i", "<fc=#9ece6a>Charged</fc>"
] 50
, Run Date "<fc=#ad8ee6> %a %b %_d %H:%M</fc>" "date" 10
]
}
This config has Nerd Font icons, a bottom border, battery monitoring, and uses the full TokyoNight color palette with dynamic colors based on resource usage.
Now if we reload xmonad with Mod+Shift+r, we should see our customized xmobar. Sweet.
Wallpaper
Good news - we already set up the wallpaper in our configuration.nix! The line we added earlier:
displayManager.sessionCommands = ''
xwallpaper --zoom ~/walls/wall1.png
'';
This will automatically set your wallpaper on startup. But we still need to grab a wallpaper. Let’s open firefox with super+d, type firefox, and head over to wallhaven.cc to pick one out.
Let’s grab this one and put it in ~/walls/wall1.png:
mkdir -p ~/walls
# download your wallpaper and move it here
mv ~/Downloads/wallpaper.png ~/walls/wall1.png
If you want to test it without reloading X, you can run:
xwallpaper --zoom ~/walls/wall1.png
And boom, there’s our wallpaper.
Screenshot Script
For screenshots, I’m using maim with xclip to copy directly to clipboard. In the xmonad config above, I already have it bound to Control+Print:
, ("C-<Print>", spawn "maim -s | xclip -selection clipboard -t image/png")
This lets you select an area with your mouse, and it copies the screenshot directly to your clipboard. Super convenient for pasting into Discord, Slack, or wherever.
If you want to save screenshots to a file instead, you can create a script:
mkdir -p ~/.local/bin
vim ~/.local/bin/screenshot
Add this:
#!/bin/sh
# Screenshot script using maim
maim -s ~/Pictures/screenshots/$(date +%Y-%m-%d_%H-%M-%S).png
Make it executable:
chmod +x ~/.local/bin/screenshot
And you can add another keybind in your xmonad.hs if you want both options.
Customization and Tweaks
So at this point, the world is really your oyster. Xmonad is written in Haskell, so if you know Haskell, you can literally do anything you want with this window manager. That’s the beauty of it - your window manager IS your config file.
In my config, I’ve already set up a bunch of stuff:
The TokyoNight color scheme with that nice magenta focused border, 3 pixel gaps all around (matching my dwm setup), and I’m using st as my terminal because it’s fast and minimal.
For layouts, I’ve got ResizableTall, Mirror ResizableTall, Full, and Spiral. You can jump between them with:
- Super+t for tiled
- Super+f or Super+m for fullscreen
- Super+c for spiral
- Super+Shift+Enter to cycle through all layouts
For gaps, I have some really nice keybinds:
- Super+a toggles gaps on/off
- Super+z increases gap size
- Super+x decreases gap size
- Super+Shift+a resets gaps back to 3 pixels
Window rules are set up so different apps automatically go to specific workspaces:
- Browsers (Chrome, Brave) go to workspace 2
- Firefox goes to workspace 3
- Slack goes to 4
- Discord goes to 5
- Kdenlive goes to 8
Here are my essential keybinds:
| Keybind | Action |
|---|---|
| Super+Enter | Launch terminal (st) |
| Super+d | Launch rofi application launcher |
| Super+r | Launch dmenu |
| Super+l | Lock screen with slock |
| Super+q | Close focused window |
| Super+j | Focus next window |
| Super+k | Focus previous window |
| Super+Tab | Focus next window |
| Super+h | Expand master area |
| Super+g | Shrink master area |
| Super+i | Increase number of windows in master |
| Super+p | Decrease number of windows in master |
| Super+t | Switch to Tall layout |
| Super+f | Switch to Full layout |
| Super+c | Switch to Spiral layout |
| Super+Shift+Enter | Cycle to next layout |
| Super+n | Cycle to next layout |
| Super+Shift+Space | Toggle floating for focused window |
| Super+z | Increase gap size |
| Super+x | Decrease gap size |
| Super+a | Toggle gaps on/off |
| Super+Shift+a | Reset gaps to default (3px) |
| Super+Shift+r | Recompile and restart xmonad |
| Super+Space [1-9] | Keychord: Jump to workspace 1-9 |
| Super+Space f | Keychord: Launch Firefox |
| Super+[1-9] | Switch to workspace 1-9 |
| Super+Shift+[1-9] | Move window to workspace 1-9 |
| Ctrl+Print | Screenshot (select area to clipboard) |
| XF86AudioRaiseVolume | Increase volume by 3% |
| XF86AudioLowerVolume | Decrease volume by 3% |
| XF86AudioMute | Toggle mute |
I’ve put together the full xmonad config above with all my customizations, the TokyoNight colors, and all my keybinds. If you want even more customization ideas, check out the xmonad documentation - it’s honestly one of the best-documented window managers out there.
Final Thoughts
You’re now ready to use XMonad as an honest X11 window manager.
Thanks so much for checking out this tutorial. If you got value from it, and you want to find more tutorials like this, check out my youtube channel here: YouTube, or my website here: tony,btw
You can support me here: kofi