Lesson 07 — Tiling Window Managers, bspwm, and sxhkd¶
What is a Tiling Window Manager?¶
A tiling window manager (TWM) automatically arranges windows so they fill the screen without overlapping. Compare this to a floating window manager (like a traditional desktop environment) where windows overlap and you position them by dragging.
The mental model shift¶
| Floating (GNOME, KDE, Windows) | Tiling (bspwm, i3, Hyprland) |
|---|---|
| Windows float and overlap | Windows tile and fill the screen |
| You move/resize with the mouse | Keyboard controls layout |
| Mouse is primary input | Keyboard is primary input |
| One window "in focus" at a time | Every window is always visible |
The appeal: your hands never leave the keyboard. You navigate, resize, and move windows with keybinds instead of reaching for a mouse.
What is bspwm?¶
bspwm (Binary Space Partitioning Window Manager) tiles windows using a binary tree. Each split divides a region into two. Add a second window to a desktop and the screen splits in half. Add a third and one of those halves splits again. The geometry is always a partition of rectangles with no gaps or overlaps.
One window: Two windows: Three windows:
┌─────────────┐ ┌──────┬──────┐ ┌──────┬───┬──┐
│ │ │ │ │ │ │ │ │
│ A │ │ A │ B │ │ A │ B │C │
│ │ │ │ │ │ │ │ │
└─────────────┘ └──────┴──────┘ └──────┴───┴──┘
The three-tier hierarchy¶
Monitor (physical screen)
└── Desktop (virtual workspace, like workspaces in GNOME)
└── Node (a window, or an internal split node)
- A monitor shows one desktop at a time
- Each desktop has its own binary tree of windows
- You can have as many desktops as you want (commonly 9 or 10)
- Nodes are the containers in the tree — leaf nodes hold windows
The three-part architecture¶
bspwm deliberately does one thing and relies on other tools for the rest:
| Component | What it does |
|---|---|
| bspwm | Manages windows, handles X11 events, listens on a socket |
| bspc | CLI client that sends commands to bspwm via that socket |
| sxhkd | X hotkey daemon that watches for keypresses and runs commands |
This separation means bspwm has zero hardcoded keybindings. Every keybind you want is a sxhkd rule that runs a bspc command. Full control, nothing assumed.
Configuration Files¶
~/.config/bspwm/bspwmrc¶
The bspwm config is just a shell script. It runs at startup and calls bspc to configure the window manager. Make it executable (chmod +x).
The official example from the bspwm repo:
#!/bin/sh
# Start sxhkd if it isn't already running
pgrep -x sxhkd > /dev/null || sxhkd &
# Set up desktops (10 named workspaces)
bspc monitor -d I II III IV V VI VII VIII IX X
# Visual settings
bspc config border_width 2
bspc config window_gap 12
bspc config split_ratio 0.52
# In monocle layout (one big window), remove borders and gaps
bspc config borderless_monocle true
bspc config gapless_monocle true
# Window rules — tell bspwm how to handle specific apps
bspc rule -a Gimp desktop='^8' state=floating follow=on
bspc rule -a Chromium desktop='^2'
bspc rule -a mplayer2 state=floating
bspc rule -a Screenkey manage=off
Key bspc config settings to know:
| Setting | What it does | Example |
|---|---|---|
border_width |
Window border thickness in pixels | bspc config border_width 2 |
window_gap |
Gap between windows in pixels | bspc config window_gap 8 |
split_ratio |
Default split ratio (0.5 = equal) | bspc config split_ratio 0.52 |
focused_border_color |
Border color of focused window | bspc config focused_border_color '#5e81ac' |
normal_border_color |
Border color of unfocused windows | bspc config normal_border_color '#3b4252' |
borderless_monocle |
Remove border in monocle mode | bspc config borderless_monocle true |
gapless_monocle |
Remove gaps in monocle mode | bspc config gapless_monocle true |
Window rules (bspc rule -a):
# Format: bspc rule -a <WM_CLASS> [options]
bspc rule -a firefox desktop='^2' # always open on desktop 2
bspc rule -a mpv state=floating # always float
bspc rule -a Steam state=floating # float Steam
bspc rule -a Pavucontrol state=floating # float volume control
bspc rule -a "Emacs" state=tiled # force tiled
Find the WM_CLASS with: xprop | grep WM_CLASS then click the window.
~/.config/sxhkd/sxhkdrc¶
sxhkd reads this file and watches for keypresses. When a match occurs, it runs the associated command. The format is:
Rules are separated by blank lines. Indentation (tab or spaces) on the command line is required.
The full example sxhkdrc from the bspwm repo:
#
# wm independent hotkeys
#
# terminal emulator
super + Return
urxvt
# program launcher
super + @space
dmenu_run
# reload sxhkd config without restarting
super + Escape
pkill -USR1 -x sxhkd
#
# bspwm hotkeys
#
# quit / restart bspwm
super + alt + {q,r}
bspc {quit,wm -r}
# close window (graceful) / kill window (force)
super + {_,shift + }w
bspc node -{c,k}
# toggle monocle layout (one big window / back to tiling)
super + m
bspc desktop -l next
# swap current window with the biggest window on the desktop
super + g
bspc node -s biggest.window
#
# window state
#
# set window state: tiled / pseudo-tiled / floating / fullscreen
super + {t,shift + t,s,f}
bspc node -t {tiled,pseudo_tiled,floating,fullscreen}
# set node flags: marked / locked / sticky / private
super + ctrl + {m,x,y,z}
bspc node -g {marked,locked,sticky,private}
#
# focus / swap
#
# focus window in direction (vim keys)
super + {h,j,k,l}
bspc node -f {west,south,north,east}
# swap window in direction
super + shift + {h,j,k,l}
bspc node -s {west,south,north,east}
# focus next / previous window on this desktop
super + {_,shift + }c
bspc node -f {next,prev}.local.!hidden.window
# focus next / previous desktop
super + bracket{left,right}
bspc desktop -f {prev,next}.local
# focus last used node / desktop
super + {grave,Tab}
bspc {node,desktop} -f last
# focus or send window to desktop 1-9 and 0 (10)
super + {_,shift + }{1-9,0}
bspc {desktop -f,node -d} '^{1-9,10}'
#
# preselect (manual insertion)
#
# preselect a split direction for the next window
super + ctrl + {h,j,k,l}
bspc node -p {west,south,north,east}
# preselect ratio
super + ctrl + {1-9}
bspc node -o 0.{1-9}
# cancel preselection
super + ctrl + space
bspc node -p cancel
#
# resize
#
# expand window outward
super + alt + {h,j,k,l}
bspc node -z {left -20 0,bottom 0 20,top 0 -20,right 20 0}
# contract window inward
super + alt + shift + {h,j,k,l}
bspc node -z {right -20 0,top 0 20,bottom 0 -20,left 20 0}
# move a floating window
super + {Left,Down,Up,Right}
bspc node -v {-20 0,0 20,0 -20,20 0}
sxhkd Syntax in Depth¶
Brace expansion — the key time-saver¶
# This single rule...
super + {h,j,k,l}
bspc node -f {west,south,north,east}
# ...expands into four rules:
super + h → bspc node -f west
super + j → bspc node -f south
super + k → bspc node -f north
super + l → bspc node -f east
Braces in the keybinding and command expand in lockstep. This eliminates repetition.
Ranges¶
# Focus desktops 1 through 9 and 0 (which means desktop 10)
super + {1-9,0}
bspc desktop -f '^{1-9,10}'
The _ placeholder (no modifier)¶
_ means "nothing here" — the first brace item has no shift, the second adds shift.
The @ prefix (run on key release, not press)¶
@ makes the binding fire when the key is released, not pressed. Useful for launchers so they don't fire on accidental hold.
Chords (two-step sequences)¶
The semicolon means: wait for a second keypress after the first combo.
Modifier names¶
| sxhkd name | Key |
|---|---|
super |
Windows/Meta key |
alt |
Alt |
ctrl |
Ctrl |
shift |
Shift |
hyper |
Hyper (rarely used) |
XF86AudioRaiseVolume |
Multimedia keys (use xev to find names) |
Reloading sxhkd¶
This is wired to super + Escape in the example config above.
bspwm Window States¶
Windows can be in one of four states:
| State | Behaviour |
|---|---|
tiled |
Normal tiling, part of the BSP tree |
pseudo_tiled |
Tiled position but respects window size hints (some apps want specific sizes) |
floating |
Floats above the tiling layer, freely moveable |
fullscreen |
Covers everything, ignores borders and gaps |
Switch state with:
Or via sxhkd: super + {t,shift+t,s,f} from the example config.
Common bspc Commands Reference¶
# Query — useful for scripting
bspc query -N -d # list all node IDs on the current desktop
bspc query -D -m # list all desktop IDs on current monitor
bspc query -W # show current workspace state
# Navigation
bspc node -f west # focus the window to the left
bspc node -f next.local # focus the next window on this desktop
bspc desktop -f next # focus the next desktop
bspc desktop -f '^3' # focus desktop 3 (^ means "by index")
# Move windows
bspc node -s west # swap with window to the left
bspc node -d '^3' # send window to desktop 3
bspc node -m next # send window to the next monitor
# Layout
bspc desktop -l next # toggle monocle / tiling layout
bspc node -R 90 # rotate the tree 90 degrees
# Resize
bspc node -z right 50 0 # expand right edge by 50px
# Equalize
bspc node @/ -E # equalize all windows on the desktop
bspc node @/ -B # balance all splits to be equal ratio
Setting Up bspwm on NixOS¶
Option 1: NixOS system module (X11)¶
This is the simpler approach. In your configuration.nix:
# Enable X11 and bspwm
services.xserver = {
enable = true;
windowManager.bspwm.enable = true;
};
# sxhkd is pulled in as a dependency automatically
Then put your config files at:
- ~/.config/bspwm/bspwmrc (make it executable: chmod +x)
- ~/.config/sxhkd/sxhkdrc
You can also point to specific config files:
services.xserver.windowManager.bspwm = {
enable = true;
configFile = "/path/to/bspwmrc";
sxhkd.configFile = "/path/to/sxhkdrc";
};
Apply with:
Option 2: Home Manager (declarative config)¶
Home Manager can manage the bspwmrc content declaratively — no shell files to manage manually:
xsession = {
enable = true;
windowManager.bspwm = {
enable = true;
# Desktop layout: 10 desktops on one monitor
monitors = {
"eDP-1" = [ "I" "II" "III" "IV" "V" "VI" "VII" "VIII" "IX" "X" ];
};
# Equivalent to: bspc config <key> <value>
settings = {
border_width = 2;
window_gap = 12;
split_ratio = 0.52;
borderless_monocle = true;
gapless_monocle = true;
focused_border_color = "#5e81ac";
normal_border_color = "#3b4252";
};
# Window rules
rules = {
"Gimp" = { desktop = "^8"; state = "floating"; follow = true; };
"Firefox" = { desktop = "^2"; };
"Pavucontrol" = { state = "floating"; };
"mpv" = { state = "floating"; };
};
# Startup programs (run after bspwm starts)
startupPrograms = [
"picom" # compositor for transparency/shadows
"feh --bg-fill ~/wallpaper.jpg" # wallpaper
"dunst" # notification daemon
];
};
};
For sxhkd keybindings in Home Manager, use services.sxhkd:
services.sxhkd = {
enable = true;
keybindings = {
# terminal
"super + Return" = "alacritty";
# launcher
"super + @space" = "rofi -show drun";
# reload sxhkd
"super + Escape" = "pkill -USR1 -x sxhkd";
# quit / restart bspwm
"super + alt + {q,r}" = "bspc {quit,wm -r}";
# close / kill window
"super + {_,shift + }w" = "bspc node -{c,k}";
# monocle toggle
"super + m" = "bspc desktop -l next";
# window state
"super + {t,s,f}" = "bspc node -t {tiled,floating,fullscreen}";
# focus direction
"super + {h,j,k,l}" = "bspc node -f {west,south,north,east}";
# swap direction
"super + shift + {h,j,k,l}" = "bspc node -s {west,south,north,east}";
# desktop focus
"super + {1-9,0}" = "bspc desktop -f '^{1-9,10}'";
# send to desktop
"super + shift + {1-9,0}" = "bspc node -d '^{1-9,10}'";
# resize
"super + alt + {h,j,k,l}" = "bspc node -z {left -20 0,bottom 0 20,top 0 -20,right 20 0}";
};
};
Apply with:
What You Also Need¶
bspwm is minimal by design. It manages windows and nothing else. You'll need additional tools:
| Need | Common tools |
|---|---|
| Application launcher | rofi, dmenu |
| Status bar | polybar, waybar (X11: polybar), eww |
| Compositor (shadows, transparency) | picom |
| Wallpaper | feh, nitrogen |
| Notifications | dunst |
| Screen lock | i3lock, betterlockscreen |
| Screenshots | flameshot, scrot |
| Clipboard manager | xclip, copyq |
These all get started from bspwmrc (or startupPrograms in Home Manager) or as systemd user services.
Getting Your Monitor Name¶
The monitors config needs your actual monitor output name:
# List connected monitors and their names
xrandr --query | grep ' connected'
# Common names:
# eDP-1 — laptop built-in screen
# HDMI-1 — HDMI port
# DP-1 — DisplayPort
Learning Path¶
A good progression for getting comfortable with bspwm:
- Install bspwm and sxhkd, start with the example configs
- Learn to navigate:
super+hjklfor focus,super+1-9for desktops - Learn window states:
super+ffor fullscreen,super+sfor float - Add a launcher (
rofi) and a bar (polybar) - Customize keybindings to match your muscle memory
- Add window rules for apps you use regularly
- Explore preselection (
super+ctrl+hjkl) for fine-grained layout control