DWM Complete suckless desktop (dwm + st + dwmblocks + picom) on FreeBSD 15 — porting guide from Linux

I recently migrated from Slackware to FreeBSD 15.0-RELEASE and ported my full suckless desktop environment. Sharing the process and the gotchas for anyone wanting to run dwm + st + dwmblocks on FreeBSD.

My dotfiles are at https://github.com/cl45h/dotfiles-freebsd

What I'm running

- dwm 6.8 with patches: vanitygaps, status2d, fancybar, swallow, cfacts, scratchpad, movestack, xresources
- st 0.9.3 with patches: HarfBuzz, alpha, scrollback, externalpipe, boxdraw, xresources
- dwmblocks with 4 blocks: volume, clock, internet, cputemp
- picom (pijulius fork) for compositing with animations
- pywal for dynamic color theming
- dunst for notifications

config.mk changes for FreeBSD

The main issue with compiling suckless tools on FreeBSD is the different paths for X11 and libraries. Here's what needs to change from the Linux defaults:

dwm config.mk:
Code:
X11INC = /usr/local/include
X11LIB = /usr/local/lib
FREETYPEINC = /usr/local/include/freetype2

INCS = -I${X11INC} -I${FREETYPEINC} `pkg-config --cflags xmu`
LIBS = -L${X11LIB} -lX11 -lX11-xcb ${XINERAMALIBS} ${FREETYPELIBS} -lxcb-res -lxcb -lXmu -lkvm

Key differences from Linux:
- -lXmu and pkg-config --cflags xmu are needed for Xresources patch support
- -lkvm is needed for the swallow patch (process lookup via kvm_getprocs() instead of /proc)
- -lxcb-res -lxcb also needed for swallow
- The CPPFLAGS need -D__BSD_VISIBLE=1 for some POSIX functions

st config.mk:
Code:
X11INC = /usr/local/include
X11LIB = /usr/local/lib
LIBS = -L$(X11LIB) -lm -lX11 -lutil -lXft \
       `$(PKG_CONFIG) --libs fontconfig` \
       `$(PKG_CONFIG) --libs freetype2` \
       `$(PKG_CONFIG) --libs harfbuzz`

Key difference: remove -lrt from LIBS. FreeBSD doesn't have librt — those functions are in libc.

Compiling

Use gmake instead of make on FreeBSD. BSD make has different syntax and will fail on suckless Makefiles:

Code:
cd ~/.local/src/dwm
gmake clean && gmake && doas gmake install

Same for st and dwmblocks.

dwmblocks scripts adapted for FreeBSD

The Linux scripts that read from /proc or /sys won't work on FreeBSD. Here's what I changed:

cputemp — CPU temperature

Linux uses sensors or reads from /sys/class/thermal. On FreeBSD, use sysctl():

Code:
#!/bin/sh
CPU_TEMP="$(sysctl -n dev.cpu.0.temperature 2>/dev/null | sed 's/C$//' | awk '{printf "%d", $1}')"
printf "🌡 %s°C" "$CPU_TEMP"

Requires coretemp kernel module loaded. Add to /etc/rc.conf:
Code:
kld_list="coretemp"

internet — network status

Linux reads from /proc/net and /sys/class/net. On FreeBSD, use ifconfig():

Code:
#!/bin/sh
# Ethernet
for iface in $(ifconfig -l); do
    case "$iface" in lo*|wlan*|tun*|pflog*) continue ;; esac
    if ifconfig "$iface" 2>/dev/null | grep -q "status: active"; then
        eth="🌐"; break
    fi
done
[ -z "$eth" ] && eth="❎"

# WiFi
if ifconfig wlan0 2>/dev/null | grep -q "status: associated"; then
    ssid=$(ifconfig wlan0 | awk '/ssid/{print $2}')
    wifi="📶 $ssid"
fi

# VPN
vpn=""
ifconfig tun0 >/dev/null 2>&1 && vpn="🔒"

printf "%s %s%s" "$wifi" "$eth" "$vpn"

volume — PulseAudio volume

This one works the same on both systems if you use PulseAudio:
Code:
pactl get-sink-volume @DEFAULT_SINK@ | awk '{print $5}'

clock

Replace setsid -f (Linux-only flag) with setsid ... & for FreeBSD compatibility.

picom (pijulius fork with animations)

The x11-wm/picom package doesn't include animations. To get the pijulius fork with animations (zoom, squeeze, slide-up, etc.), compile from source:

Code:
git clone https://github.com/pijulius/picom.git
cd picom
meson setup build
ninja -C build
sudo ninja -C build install

Dependencies: pkg install meson ninja libev uthash libconfig

.xinitrc

My ~/.xinitrc:
Code:
picom &
dwmblocks &
dunst &
xrandr --output DP-0 --left-of DP-2
exec dwm

Packages I installed for the desktop

Code:
pkg install xorg xinit xrandr xclip nsxiv xwallpaper dmenu
pkg install py311-pywal dunst noto-emoji
pkg install pulseaudio cava ranger

Gotchas summary

1. Use gmake not make
2. Remove -lrt from st (FreeBSD has it in libc)
3. Add -lXmu -lkvm -lxcb -lxcb-res to dwm for swallow + xresources
4. Add -D__BSD_VISIBLE=1 to CPPFLAGS in dwm
5. Replace /proc and /sys reads with sysctl() and ifconfig() in scripts
6. Replace setsid -f with setsid ... &
7. Replace shuf with sort -R (no shuf on base FreeBSD)
8. X11 paths are /usr/local/include and /usr/local/lib, not /usr/include

Hope this helps someone making the switch. Happy to answer questions.
 
There is a dwm port. I just run make extract on /usr/ports/x11-wm/dwm then copy /usr/ports/x11-wm/dwm/work/dwm-6.8 (or whatever version) to $HOME, then create and edit config.h. The link to your dotfiles can be useful.
I have my own https://srobb.net/dwm.html page, but the main point is that it doesn't have to be compiled from scratch. The same goes for st, which is under /usr/ports/x11/sterm. Again your dot files can be really helpful to someone just starting with either dwm or sterm.

And, dwmblocks is also a pkg and port. I've not used it, I stick with dwm's built in bar, it sounds like you have it set up to give more info than I use.
 
  • Like
Reactions: vmb
Thanks for checking it out! You're right, the port is a great starting point - I didn't know about the make extract approach, that's a clean way to get the base source with FreeBSD paths already sorted.

In my case I went the manual route because I'm running several patches on top (vanitygaps, pertag, movestack, swallow, scratchpad, and xresources for pywal integration), so the port's vanilla source wouldn't cover it. But for someone starting fresh, your approach definitely saves the hassle of fixing includes and libs paths in config.mk.

Nice page btw, bookmarked it. And yeah, x11-wm/dwmblocks is handy if you want per-block refresh intervals and clickable status
I have it showing volume, battery, clock, and a couple of custom scripts.
 
For patches, you should be able to use the files directory, that is /usr/ports/x11-wm/dwm/files and put your patches in there. Building the port should handle it. I use the move-resize patch and put it in there, then, during build, as the move-resize patch has two files, the port stops and asks file to patch, and I have to put in config.def.h, and then dwm.c. And thanks for the complement on my page, though I think the best part of it is the link to the Debian forum howto which goes over the config.h file in far more detail.
 
Thanks for the interest! I uploaded the wallpaper to my GitHub, you can grab it from the wallpapers/ directory in my dotfiles repo:
https://github.com/cl45h/dotfiles-freebsd

scottro great tip about /usr/ports/x11-wm/dwm/files, I've been compiling dwm manually with all patches applied by hand. I'll definitely give the ports approach a try.
Here's the full list of patches I'm running:

dwm:

  • vanitygaps configurable gaps between windows
  • status2d colors in the status bar via escape sequences
  • fancybar shows active window title in the bar
  • swallow terminals swallow GUI windows they spawn
  • cfacts resize individual windows in the stack
  • scratchpad floating terminal on a keybind
  • movestack move windows up/down in the stack
  • xresources load colors and config from Xresources on the fly

st:

  • HarfBuzz, alpha, scrollback, externalpipe, boxdraw, xresources

The color scheme changes dynamically with the wallpaper - I use pywal to generate a palette from the current wallpaper, then map those colors to dwm's Xresources variables
( dwm.normbgcolor, dwm.selbgcolor, etc.) and hot-reload them with Super+F5. So the bar, window borders, and terminal all match the wallpaper
automatically.

Alain De Vos
sure!
 

Attachments

  • 2026-04-20_08-38.png
    2026-04-20_08-38.png
    183.4 KB · Views: 47
I use the move-resize patch and put it in there, then, during build, as the move-resize patch has two files, the port stops and asks file to patch, and I have to put in config.def.h, and then dwm.c.
Are you fixing up filename to patch in your patch file?
This usually happens when filename to patch (including path) in the patch file doesn't match with the requirement. It should match with relative path from ${WRKSRC}. In case of x11-wm/dwm, ${WRKSRC} should be /usr/ports/x11-wm/dwm/work/dwm-6.8/ currently.

Note that patch file named extra-patch-* are NOT applied automatically and needs special care in ports Makefile.
This is because these names are reserved for conditional patches (for example, only needed when specific OPTION is enabled).
 
T-Aoki, I got the patch from suckless, they call it dwm-moveresize-<date>-diff. I rename it to patch-move-resize. The patch patches 2 files, config.def.h and dwm.c. The patch is from suckless not from FreeBSD which already has a few patches in there. I know one is for the modkey, but I'm not sure of the others, though there are options for make config. I build it with portmaster, specifying my customized config file, using
Code:
portmaster -m 'DWM_CONF=/home/scottro/dwm-6.8/config.h dwm
which skips any config questions, though it does ask file to patch, to which I answer config.def.h and then when asked a second time dwm.c, which are the two files handled by that patch.
 
though it does ask file to patch, to which I answer config.def.h and then when asked a second time dwm.c, which are the two files handled by that patch.
Being asked for filenames is unrelated to modifying two files.
Because it's impossible to identify the files to which the patch should be applied.
Please remove a/ and b/.
Code:
/usr/ports/x11-wm/dwm/files$ diff -u dwm-moveresize-20221210-7ac106c.diff patch-moveresize-20221210-7ac106c.diff
--- dwm-moveresize-20221210-7ac106c.diff        2026-04-21 20:07:05.142728000 +0900
+++ patch-moveresize-20221210-7ac106c.diff      2026-04-21 20:03:39.139941000 +0900
@@ -10,8 +10,8 @@

 diff --git a/config.def.h b/config.def.h
 index 9efa774..99049e7 100644
---- a/config.def.h
-+++ b/config.def.h
+--- config.def.h
++++ config.def.h
 @@ -79,6 +79,22 @@ static const Key keys[] = {
        { MODKEY,                       XK_m,      setlayout,      {.v = &layouts[2]} },
        { MODKEY,                       XK_space,  setlayout,      {0} },
@@ -37,8 +37,8 @@
        { MODKEY,                       XK_comma,  focusmon,       {.i = -1 } },
 diff --git a/dwm.c b/dwm.c
 index 03baf42..89ec70d 100644
---- a/dwm.c
-+++ b/dwm.c
+--- dwm.c
++++ dwm.c
 @@ -183,6 +183,8 @@ static void mappingnotify(XEvent *e);
  static void maprequest(XEvent *e);
  static void monocle(Monitor *m);
 
T-Aoki, I got the patch from suckless, they call it dwm-moveresize-<date>-diff. I rename it to patch-move-resize. The patch patches 2 files, config.def.h and dwm.c. The patch is from suckless not from FreeBSD which already has a few patches in there. I know one is for the modkey, but I'm not sure of the others, though there are options for make config. I build it with portmaster, specifying my customized config file, using
Code:
portmaster -m 'DWM_CONF=/home/scottro/dwm-6.8/config.h dwm
which skips any config questions, though it does ask file to patch, to which I answer config.def.h and then when asked a second time dwm.c, which are the two files handled by that patch.
Well, I recommend editing the moveresize patch and place it in files/ directory.
What need to be fixed would be:

Remove
Code:
diff --git a/config.def.h b/config.def.h
index 1c0b587..ff863c9 100644
and
Code:
diff --git a/dwm.c b/dwm.c
index 4465af1..89483c1 100644

Then modify
Code:
--- a/config.def.h
+++ b/config.def.h
to
Code:
--- config.def.h.orig
+++ config.def.h

and

Code:
--- a/dwm.c
+++ b/dwm.c
to
Code:
--- dwm.c.orig
+++ dwm.c
 
T-Aoki and Charlie_ I just tried doing a build with the suggested changes in the patch, and after a few mistakes, where it still asked me what file to patch, (not sure what I did wrong, I just removed it all and started over), it worked as you two said it would, no asking what file to patch. Thank you.
 
Back
Top