Skip to content

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:

Keyboard input → sxhkd → bspc (send socket messages) → bspwm (window manager)
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:

# Optional comment
<keybinding>
    <command>

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)

# super+w closes, super+shift+w kills
super + {_,shift + }w
    bspc node -{c,k}

_ means "nothing here" — the first brace item has no shift, the second adds shift.

The @ prefix (run on key release, not press)

super + @space
    dmenu_run

@ 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)

# Press super+o, then press e/w/m
super + o ; {e,w,m}
    {emacsclient -c,firefox,thunderbird}

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

# Reload config without restarting
pkill -USR1 -x 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:

bspc node -t tiled
bspc node -t floating
bspc node -t fullscreen

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:

sudo nixos-rebuild switch --flake .#yourhostname

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:

home-manager switch --flake .#yourusername


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:

  1. Install bspwm and sxhkd, start with the example configs
  2. Learn to navigate: super+hjkl for focus, super+1-9 for desktops
  3. Learn window states: super+f for fullscreen, super+s for float
  4. Add a launcher (rofi) and a bar (polybar)
  5. Customize keybindings to match your muscle memory
  6. Add window rules for apps you use regularly
  7. Explore preselection (super+ctrl+hjkl) for fine-grained layout control

Sources