Using Polybar with dwm

January 14, 2022

While I love dwm’s many features and the option to change the source code to your needs, I was not as fond of the bar. It was simply lacking the customization options i was looking for, or would make simple features too complex to have. To solve this, I made Polybar work with dwm’s tags.

I have tried many window managers while using GNU/Linux. I liked all of them at first, but quickly found them to be limiting. After I became unhappy with Qtile, I was ready to switch again. Having heard of it often, I decided to try suckless’s dwm.

I quickly fell in love with it. Anything I wanted to do had already been done by someone else, I simply had to install the patch. Any errors I had were understandable and could be solved by simply looking at the relevant code.

The only thing I wanted to change, which I could not find information on, was using a different bar. I have to admit that I used to frequent r/unixporn and got into GNU/Linux for the looks. It was however through this, that I discovered all the benefits it brought. I still love a good looking system though and could not be charmed by dwm’s default look.

To solve this I decided to install polybar and spin up an instance. What this gives you however, is not satisfactory. The default bar will still be there, toggling the bar will obviously not be an option and most importantly: your tag information will be missing.

Step One: Replacing the default bar

You will first have to get dwm’s status bar out of the way to make space for polybar. Simply turning it off in config.h is not an option, as this will also remove the spacing dwm leaves for the bar. In some window managers this might mean game-over, but dwm allows us to simply edit the source code.

You will only need to remove all references to barwin in dwm.c. Look for calls like this one:

XMoveResizeWindow(dpy, m->barwin, m->wx, m->by, m->ww, bh);

Be sure to also remove all unnecessary code like in updatebars(void), which is solely responsible for drawing the contents of the bar window.

Recompiling dwm should now result in a bar-free experience, while still preserving the space.

Step Two: Getting dwm to share its tag state

dwm does by default not provide enough information for any module to render some kind of tag status. You might try polybar’s xworkspaces module, but it will not work, as dwm does not share enough information. Even if it would however, xworkspaces is meant to display only one selected tag. This is meant for manual tilers like i3wm, but is not satisfactory for dwm because of its dynamic nature. Selecting multiple tags at once will only show one tag as active. To solve these problems we will simply make dwm share the current configuration and the state all tags are in.

For this we will store the information for each monitor in the X-server properties of the root window. This is where dwm would set such values anyway and the xprop utility makes it easy to read those values later.


void setdwmdesktops(char *tagState, Monitor *m){
    char *atom = malloc(sizeof(char) * 40);
    sprintf(atom, "");
    char *num = malloc(sizeof(char) * 12);
    sprintf(num, "%d", m->num);
    strcat(atom, "DWM_TAG_STATE_");
    strcat(atom, num);
    strcat(atom, "\0");
    XChangeProperty(dpy, root, XInternAtom(dpy, atom, False), XA_STRING, 8, PropModeReplace, tagState, TAGSLENGTH);

To set the values for each monitor I use this function. It takes an input string and sets it as the state for the specified monitor.

For more information on this see the Xlib Programming Manual.

The string I set for the value is generated by the following code.


char *tagState = malloc(TAGSLENGTH * sizeof(char));
unsigned int i, occ = 0, urg = 0;
Client *c;

for (c = m->clients; c; c = c->next) {
	occ |= c->tags;
	if (c->isurgent)
		urg |= c->tags;
for(i = 0; i < TAGSLENGTH; i++) {
    tagState[i] = 'e';
    if (occ & (1 << i)) {tagState[i] = 'o';}
    if (urg & (1 << i)) {tagState[i] = 'u';}
    if (m->tagset[m->seltags] & (1 << i)) {tagState[i] = 'a';}

The state of each tag is represented by a single char. I fittingly hooked dwm’s original drawbar function to execute this, after stripping it from it’s previous content.

With this, dwm’s state is now automatically kept updated.

Step Three: Polybar module

To display this information in polybar I use a custom script as a module.

#! /bin/bash

# Get the colors from xressources (optional)
back=$(xgetres "*.background")
fore=$(xgetres "*.foreground")
prim=$(xgetres "*.color2")
pr_a=$(xgetres "*.color10")
seco=$(xgetres "*.color4")
se_a=$(xgetres "*.color12")

# Listen for updates of the tagstate
xprop -spy -root "DWM_TAG_STATE_$MONITOR" 2>/dev/null | {
    # Read the tag state
    while true; do
        IFS=$'\t' read -ra tags_string <<< "$(xprop -notype -root "DWM_TAG_STATE_$MONITOR")"
            [[ $tags_string =~ \"(.*)\" ]]
            # Print the state for each tag to polybar
            # Formatting Tags are used here according to polybar's wiki
            for i in {1..9}
                case ${tags:$i:1} in
                        # the tag is viewed on the focused monitor
                        echo "%{F$prim}%{B$back}  %{F-}%{B-}"
                        # : the tag is not empty
                        echo "%{F$prim}%{B$back}  %{F-}%{B-}"
                        # ! the tag contains an urgent window
                        echo "%{F$seco}%{B$back}  %{F-}%{B-}"
                        # . the tag is empty
                        echo "%{F$fore}%{B$back}  %{F-}%{B-}"
        } | tr -d "\n"

    read -r || break

As you can see, with custom modules polybar simply displays the last line echoed by the script. Reading the value for each tag is easy using xprop, and we can format the output to our needs.

Finally, to piece all of this together, we need to start polybar for each of our monitors.

To do this we use another script, which reads in all connected monitors and launches an instance of polybar for each of them.

#! /bin/bash
for m in $(xrandr --query | grep " connected" | cut -d" " -f1); do
    CMD="env MONITOR=${i} ~/.config/polybar/polybar-scripts/"
    env TAG_CMD="$CMD" MONITOR="$m" polybar --reload example &

What is notable is that we piece together the command needed to execute the tag-script and pass it on as an environment variable. We do this because we need to specify which monitor’s state the script should display. Simply passing on the number of the monitor will unfortunately not work, as polybar cannot concatenate config strings with environment variables.

The block that we need to include in our polybar config therefore looks like this:

config (polybar)

type = custom/script
exec = ${env:TAG_CMD}
tail = true


With this, we now have a solution to use dwm with polybar. Admittedly this is hacky, and will break in some conditions relating to switching primary monitors, but works great for my daily usage.

Jonas Langlotz

Hey, I'm Jonas 👋

I'm a computer science student from Berlin, Germany. If you want to read more of my notes, you can find them here.