bspwm - minimalistic and efficient desktop
I would like to describe the concept of my ideal desktop. The main values are simplicity - which means only necessary services and dependencies installed; efficiency - desktop actions that can be executed by keyboard shortcuts; and customisable - so I can add or remove whatever functionality I want.
The concept is very different from what I have been using my whole life. So what changed my mind? The idea came from a series called Ricing your *nix desktop, published on FreeBSD Telegram channel. That’s where I got inspired to build my own lightweight desktop, instead of relying on pre-configured ones like KDE, GNOME, etc. It helped me to overcome the knowledge gap I had and it also introduced me to the idea of tiling managers. Because I was already happy with using vim fully from keyboard, I was quite keen to see what would happen if I started controlling the whole desktop like this. And the result was fantastic!
Tech Stack Link to heading
Before we start this brief blog post about how I created my dream desktop, let’s quickly have a look at the technical stack:
- FreeBSD - operating system (13-STABLE)
- X.Org - display server
- slim - login manager
- bspwm - tiling window manager
- sxhkd - X hotkey daemon
- polybar - toolbar
Display Server and OS Link to heading
I have already explained my reasons for choosing FreeBSD over macOS in previous blog post. You can very easily follow these instructions on any GNU/Linux distribution, if you know the basics like package manager, usual file locations, etc. There’s no particular benefit of using FreeBSD over GNU/Linux with these tools, it’s just a matter of personal preference.
I chose X.Org display server instead of Wayland. Same with the OS, it’s my personal choice, since you can easily find tiling managers on Wayland too (like sway for example). I have briefly experimented with Wayland, but found no advantages in using it over X.Org server, only more problems. I know how X.Org server works, I can easily configure it and it does everything I need. There’s plenty of material to read about cons & pros of both solutions on the internet, so I won’t go into more details here.
Login Manager Link to heading
Slim is simple to install and configure login manager. I chose it over the others, because I like its FreeBSD black & red colour scheme that’s available as an extra package. It matches nicely with the colours of my ThinkPad laptop. It also doesn’t pull in any heavy dependencies on QT/KDE, GNOME, etc. Here’s how to install it:
# pkg install slim slim-freebsd-black-theme
# sysrc slim_enable=YES
Because I compiled my own version of the package without D-Bus support, I didn’t have to activate it (normally done via sysrc dbus_enable=YES). It’s because D-Bus service doesn’t provide any functionality I need and only adds unnecessary complexity and potential security weaknesses as well (like PolKit).
I also changed the current_theme’s value by switching to slim-freebsd-black-theme in the configuration file located at /usr/local/etc/slim.etc. Here’s how it looks on my laptop:

X.Org Session Link to heading
So far pretty simple and standard stuff. The truly fun part starts with defining the after-login process, which is required to start the window manager (WM) and configure X.Org server to my liking. You can find my .xinitrc below:
# merge all .Xresources
xrdb -merge ~/.Xresources
# fonts
xset +fp /home/marian/.local/share/fonts
xset +fp /usr/local/share/fonts/tamsyn-font
xset +fp /usr/local/share/fonts/powerline-fonts
xset fp rehash
# keyboard mapping
xmodmap ~/.Xmodmap
# set custom background
~/.fehbg
# cursor
xsetroot -cursor_name left_ptr
# keyboard layout
setxkbmap -layout us,cz -option grp:ctrls_toggle
setxkbmap -option "terminate:ctrl_alt_bksp"
# launch selected WM
if [ -x "$1" ]; then
exec "$1" # provided by /usr/local/share/xsessions/*.desktop file
else
exec bspwm # no WM selected, use default session
fi
I will skip several configuration commands here, like the .Xresources and .Xmodmap files, to keep the blog post reasonably “short” as I promised in the beginning. If you want to see what these strange *.desktop files contain, here’s my hand written example for bspwm.desktop file:
[Desktop Entry]
Name=bspwm
Exec=/usr/local/bin/bspwm
You might also wonder why I have support for multiple WMs there? Sometimes I’m in a retro mood and I want to enjoy the look&feel of NeXTSTEP, so I like to hit F1 on the login screen and choose WindowMaker session instead. I could also install wdm (WINGs Display Manager) to have the login manager in the same style, but I never bother, because I would have to reconfigure and restart the whole laptop.
Window Manager Link to heading
It’s time to get introduced to the core components of my desktop - bspwm and sxhkd. What sort of fresh madness are these acronyms, you ask? bspwm stands for Binary Space Partitioning tiling window manager, sxhkd stands for Simple X Hotkey Daemon.
There you go. Now, with you being properly educated, let’s install the packages with # pkg install bspwm sxhkd and move straight to their configuration.
bspwm configuration Link to heading
Here’s the content of ~/.config/bspwm/bspwmrc configuration file. The file must be executable, because bspwm is configured by bspc commands. The spacing between arguments doesn’t make a difference, it’s just there to make it prettier.
#!/bin/sh
# keyboard daemon
~/.config/sxhkd/start.sh
# polybar
~/.config/polybar/start.sh
# eDP-1 (internal display), DP-1 (external display)
bspc monitor eDP-1 -d 1 2 3 4 5 6 7 8 9 10
bspc monitor DP-1 -d 1 2 3 4 5 6 7 8 9 10
bspc config border_width 2
bspc config window_gap 5
bspc config split_ratio 0.5
bspc config borderless_monocle true
bspc config gapless_monocle true
bspc config single_monocle true
# app configurations
bspc rule -a URxvt:nnn state=pseudo_tiled center=true
bspc rule -a URxvt:htop state=floating center=true
bspc rule -a *:*:Calculator state=floating center=true
bspc rule -a URxvt:mixertui state=floating center=true
bspc rule -a URxvt:cal state=floating center=true
I use 10 desktops in the following order: web, email, RSS, music, with the last desktop assigned to a password manager. I use the rest for various apps, like vim for writing and note taking, RetroArch for gaming, etc.
I start sxhkd and polybar only as part of bspwm initialisation process, so it doesn’t run when I start WindowMaker. The content of my bspwm configuration file comes mostly from the example file located at /usr/local/share/examples/bspwm/bspwmrc.
sxhkd start script Link to heading
sxhkd daemon allows to bind various actions like desktop switching or changing size and position of the windows to global shortcuts. It can also run various apps like urxvt, musikcube, etc. directly, using memorable shortcuts. I use dmenu for launching the remaining apps that I run infrequently.
I will show here only the start up script itself, leaving the lengthy and mostly copy&paste configuration file aside:
#!/bin/sh
# kill the current instance first
pkill sxhkd
# wait until polybar dies
while pgrep -u $UID -x sxhkd >/dev/null; do sleep 1; done
# start new instance
sxhkd &
It might look pretty simple to a seasoned sysadmin, but it took me a while to get it right. The main point here is to kill all the child processes like sxhkd and polybar when bspwm is restarted.
I took most of the sxhkd configuration from the example file at /usr/local/share/examples/bspwm/sxhkdrc.
polybar Link to heading
Polybar is responsible for creating and customising status bars. bspwm itself does not provide this functionality out of the box and leaves the choice of the tool on the user. I agree with this idea of being focused only on one thing and do it well. I started with lemonbar instead, but I found it quite cumbersome and not flexible for my needs (I couldn’t figure out how to use emoji there).
Because I spent countless hours on polishing my status bar, I must expose my latest iteration of it here. The configuration file itself is stored in ~/.config/polybar/config.ini:
[bar/main]
width = 100%
height = 22
font-0="Noto Sans:size=12;2"
font-1="Wuncon Siji:size=4;2"
modules-left = bspwm
modules-right = keyboard bhyve volume battery date
wm-restack = bspwm
enable-ipc = true
[module/keyboard]
type = internal/xkeyboard
format = <label-layout>
format-prefix = ""
format-prefix-padding = 2
format-prefix-foreground = #ff5370
[module/bhyve]
type = custom/script
exec = ~/.config/polybar/bhyve.sh
interval = 5
format-prefix = ""
format-prefix-padding = 2
format-prefix-foreground = #ff5370
[module/volume]
type = custom/script
exec = ~/.config/polybar/volume.sh
interval = 5
format-prefix = ""
format-prefix-padding = 2
format-prefix-foreground = #ff5370
click-left = "urxvt -name mixertui -e mixertui"
[module/battery]
type = custom/script
exec = ~/.config/polybar/battery.sh
interval = 5
format-padding = 2
format-prefix-foreground = #ff5370
click-left = ~/.config/polybar/power-menu.sh
[module/date]
type = internal/date
interval = 30
date = %d. %m.
time = %H:%M
label = "%{F#ff5370}%{F-} %date% %{F#ff5370}%{F-} %time%"
label-padding = 2
[module/bspwm]
type = internal/bspwm
format = <label-state> <label-mode>
label-focused-foreground = #ff5370
That’s a really long file, isn’t it? Let’s dissect it piece by piece together.
Fonts Link to heading
polybar doesn’t provide any support for icons, but it does support emojis. I’m using two fonts here - Noto and Siji. Noto can be installed from packages via # pkg install noto-basic noto-emoji. Siji is not available in ports, so it must be installed manually as described on Siji home page.
bhyve monitor script Link to heading
I love using bhyve for running various VMs on my laptop. Here’s an extremely simple monitor script that tells me how many VMs I run at the moment.
#!/bin/sh
if [ -d "/dev/vmm" ]
then
INSTANCES="$(ls /dev/vmm | awk 'END{print NR}')"
else
INSTANCES=0
fi
echo "$INSTANCES"
volume monitor Link to heading
polybar volume module does not support FreeBSD audio system, so I created my own implementation of it using mixer command and some dark awk magic, which I barely understand. Yes you are right, I took it mindlessly from the internet:
#!/bin/sh
GETVOL="$(mixer vol | awk '{ print $7 }' | grep -o '[^:]*' | uniq)"
echo "$GETVOL%"
power menu Link to heading
It took me a while to persuade myself that spending 15 minutes on implementing power menu is a better idea than simply hit Meta-Enter and type doas poweroff on the terminal every time I want to switch off the laptop. I couldn’t figure out how to use Polybar’s pop-up menu, so I resigned to a quick & dirty trick with jqmenu.
#!/bin/sh
printf "Restart,doas reboot\\nShutdown,doas poweroff\\n" | jgmenu --simple --at-pointer
The important note here is that doas configuration must allow the user to run these two commands without password. I’m not crazy enough to bother with implementing an authentication dialogue window for it here.
battery monitor script Link to heading
Another polybar module I had to implement by myself, because it works only on GNU/Linux. Luckily, it’s brain dead simple to get the information from the system, provided you know where to look. Here comes a true masterpiece of my shell script programming capabilities, hang on to your hats:
#!/bin/sh
CHARGE="$(sysctl hw.acpi.battery.life | awk '{ print $2 }')%"
STATE="$(sysctl hw.acpi.battery.state | awk '{ print $2 }')"
case $STATE in
0) # full
LABEL=""
VALUE="$CHARGE"
;;
1) # discharging
LABEL=""
VALUE="$CHARGE"
;;
2) # charging
LABEL=""
VALUE="$CHARGE"
;;
5) # discharging - critical
LABEL=""
VALUE="$CHARGE"
;;
6) # charging - critical
LABEL=""
VALUE="$CHARGE"
;;
7) # no battery
LABEL=""
VALUE=""
;;
*) # error
VALUE="ERR"
;;
esac
echo "%{F#ff5370}$LABEL%{F-} $VALUE"
The end Link to heading
Congratulations for reaching the end of this monstrously long blog post. Didn’t I say I will try to keep it short? Yeah sorry, that was just a poor marketing trick to force you to read the whole thing. Anyway, I hope that it will help somebody to get inspired by it and perhaps to try something different on their desktop. See you next time!