dwm

dwm is my favorite window manager. Its homepage is here: https://dwm.suckless.org/. Though the name annoys me a bit, that website has a number of great tools written with the same philosophy. I will mention a few here. Admittedly, their philosophy is not a great one for every type of tool, but it is a great one for a window manager.

Why I ❤️ dwm

  1. It’s fast. My computer boots almost immediately, which begs the question, what is GNOME loading?
  2. It’s tiling. I think floating windows are basically useless. dwm does support floating windows if you really need them, but I don’t think I have ever used that feature.
  3. It has tags. Tags are like workspaces, but you can enable multiple tags at the same time and/or assign windows to multiple tags.
  4. It’s customizable. It takes a little love to get everything working but once it does, it works exactly how you want it to work. It’s not GNOME’s fault, by the way. I think how you use your computer is such a personal experience, that it’s impossible to have a once-size-fits-all set of knobs. But because dwm is customized in the source, no concessions have to be made.
  5. It has a great community. There are patches online for most things you can think of. And, the source it pretty straight forward, so it’s easy to make your own patch if it doesn’t exist.

How I install dwm

You can install dwm from your favorite package manager, but it is one of the very few tools I don’t recommend this for. It is best to build it from source, because it is the only way to customize it, and that’s the whole point. (Most tools from this community work this way, which is one of the reasons I use only a small number of them).

What I would do is fork the upstream repo at https://git.suckless.org/dwm. In particular, I made myself a repo at dimitar/dwm on the CBA GitLab server and then:

git clone https://git.suckless.org/dwm
cd dwm
git remote rename origin upstream
git remote add origin ssh://git@gitlab.cba.mit.edu:846/dimitar/dwm.git
git fetch origin
git branch -m main  # optional, but `main` is the new standard
git push --set-upstream origin main

After you’re set up, copy config.def.h to config.h and commit that. This will happen at maketime anyway, so better be explicit about it I think. You can also add a README to document your changes and a .gitignore such as:

*.o
dwm

Commit and push your changes. This has nothing to do with dwm, but it is what you should always do :)

Then just make clean install. Run this command with root privileges using e.g. sudo because the Makefile will copy the binary to /usr/local/bin/ and install a manpage. Like everything else about this suite of tools, the Makefile is super simple if you want to see what it’s doing.

Most of the changes you’re going to want to make can be made in config.h. More complex logic changes can be made in dwm.c (the entirety of the source code for the window manager is 2,164 lines of C, with comments). Any time you change something, just make clean install and relaunch dwm if you’re already running it.

How you actually launch dwm is up to you. I have no desktop environment, and my computer drops me into a login shell when it boots up. I have a ~/.xinitrc file containing:

exec dwm

So, after I log in to the shell, I just type startx and it comes up. There are 1,000 ways to automate this if you would like to.

To quit dwm, press Alt-Shift-q.

What it looks like

When you launch dwm, you’ll see a blank screen with a bar at the top. The bar can be toggled on an off with Alt-b. (I have mine set to be off by default, by setting showbar to 0 in config.h). All the way on the left, the bar shows your tags, which I will discuss in a second. By default they are the numbers 1-9, but that can also be changed in config.h. Then, there is a “button” for the mode, which you will likely never use. Then, the bar displays the title of the current window – whatever would appear in the top bar of the window in e.g. GNOME. Finally there is a “status bar” which is just the string dwm-6.0 by default, and is useless unless modified.

Opening windows

You can open a terminal emulator with the shortcut Alt-Shift-Enter. (You can also do this by clicking the status in the bar with the middle button of the mouse.) By default, it looks for the terminal emulator program st. This is another tool from the suite and one I like. You can install it from your package manager if you don’t want to customize it. If you do, follow the steps for installing dwm from above. The remote address is https://git.suckless.org/st. If you don’t want to use st, change termcmd in config.h.

To open non-terminal windows, you have three options:

  1. Open them from a terminal by typing the program name
  2. Add shortcut keys to config.h by following the example for st
  3. Use dmenu

I go for the last option. dmenu is yet another tool from the suite. (This one I install using apt because there is nothing about it I’ve cared to customize so far.) If you have demnu installed, Alt-p will launch at the top of the screen (over the bar if you have it on). You can then just type a command and it will launch a window. It also has a nice built-in search function. Just use the left and right arrow keys after you type a few letters and press enter.

To close a window, press Alt-Shit-c. I also mapped Alt-F4 to this because of muscle memory, but note that that’s not mapped by default.

How tags work

Each window (or “client”) is assigned to some number of tags. You select which tags you want to make visible. All clients assigned to any tag selected are displayed.

To select tag N as the only visible tag, press Alt-N or left-click the tag in the bar.

To toggle the visibility of tag N, press Alt-Ctrl-N or right-click the tag in the bar. At least one tag has to be visible, so if only one tag is visible, you can’t toggle it to not be visible.

The shortcut Alt-Tab toggles back and forth between the current set of selected tags and the set of tags selected before the last change.

Of all the clients displayed, exactly one client is “focused”. This is the client that your keyboard controls, and the title of which appears in the bar. You can select which client is focused using Alt-j and Alt-k or by hovering over it with your mouse.

To assign the current focused client to only tag N, press Alt-Shift-N or left-click the tag in the bar while holding Alt.

To toggle whether or not the current focused client is assigned to tag N, press Alt-Shift-Ctrl-N or right-click the tag in the bar while holding Alt. Each client has to be assigned to at least one tag, so if the current focused client is assigned to only one tag, you can’t toggle it to not be assigned.

To select all tags as visible, press Alt-0. To assign the current focused client to all tags, press Alt-Shift-0.

It’s up to you how you use this feature. I generally have about 1 client per tag. If that’s your workflow, you can add more tags (you just have to pick shortcut keys that make sense).

How the tiling works

By default, there are two columns of clients. The column on the left is the “master” and it has a maximum number of clients. This maximum is 1 when dwm starts and can be adjusted dynamically. By default, Alt-i increases the maximum number of clients in the master, and Alt-d decreases the maximum number of clients in the master. All other clients are in the “stack”, which is the the column on the right.

When dwm starts, the master takes up 55% of the horizontal real estate, while the stack takes up 45%. Alt-h decreases the size of the master and increases the size of the stack by 5% points. Alt-l increases the size of the master and decreases the size of the stack by 5% points.

Alt-Enter moves the currently-focused client to the top position in the master. You can also do this by clicking the title in the bar with the middle button of the mouse (although I don’t think I’ve ever used that shortcut). If the focused client starts in the stack, the bottom client from the master gets moved to the stack instead.

Floating clients

You can make individual clients floating if needed. Press Alt-Space with the client focused, or click it with the middle button of the mouse while holding Alt. Both methods will also convert a floating client back to tiling.

To move a floating client, drag it with the left button of the mouse while holding Alt (dwm doesn’t draw an individual bar, so just grab the client anywhere). To resize a floating client, drag it with the right button of the mouse while holding Alt.

You can also convert a tiled client to a floating client by attempting to move it as described above.

Floating and Monocle Modes

The behavior described so far is known at “tiling mode”. There is also a “floating mode” and “monocle mode”.

Floating mode means that all clients are floating. It is activated using Alt-f. The mode symbol in the bar becomes ><>. Clients initially remain where they were, but are floating, so if you move one, the others won’t re-tile.

Monocle mode means that a single client is full-screened. It is activated using Alt-m. The mode symbol in the bar becomes [M], where M is the client number.

Tiling mode is activated using Alt-t. The mode symbol in the bar is []=.

You can toggle back and forth between the current and last mode using Alt-Space, or by left-clicking the mode symbol in the bar. Right-clicking it always activates monocle mode.

Reminder about configurability

Everything I said above is configurable easily. For example, if you want the number of clients in the master at the start to not be 1 on boot, simply change this line in config.h.

static const int nmaster     = 1;    /* number of clients in master area */

If you want to change the default master size, change mfact in config.h, and so on. All the shortcut keys can also very easily be edited in config.h.

Multiple monitors

There is out-of-the-box support for multiple monitors, in a way. If you extend your desktop across monitors, dwm basically runs a new instance on every monitor. You can focus a monitor with Alt-, and Alt-., or by hovering the mouse in it. You can move the current focused client to another monitor using Alt-Shift-, and Alt-Shift-.. Otherwise, everything above applies within the focused monitor. Admittedly, this leaves a bit to be desired, but at least it’s simple.

One thing dwm won’t do for you is automatically enable a monitor when you plug it in. I use xrandr directly to do this. For example, when I’m at my desk at home, I run:

xrandr --auto
xrandr --output DP-1-1 --auto --above eDP-1 --mode 1920x1080
xrandr --output DP-1-2 --auto --right-of DP-1-1 --mode 1920x1080

When I unplug, I run:

xrandr --auto

It’s a bit annoying, but to be honest, I feel like multiple monitors always are. It’s like Bluetooth and like printing – problems humanity will never solve. There are many ways documented on the internet to automate multiple monitors.

Additions

Everyone will find that things are missing out of the box. I think, though, it will be a different set of things for everybody. In this section, I will document the things I have added on my setup.

Screencaps

I love taking screencaps. I picked the tool scrot. Because the commands are kind of long, I created variables for them:

static const char *scrotcmd[]  = { "scrot", "-F", "/home/dsd36/Screencaps/%y%m%d_%H%M%S.png", "NULL" };
static const char *scrotscmd[]  = { "scrot", "-f", "-s", "-F", "/home/dsd36/Screencaps/%y%m%d_%H%M%S.png", "NULL" };

(Expanding the home directory is possible too, and I should do that to make my config more portable.)

The first command takes a screencap of the whole screen, and the second takes a screencap of a rectangular selection box (and freezes the screen). I mapped them to PrtSc and Shift-PrtSc respectively:

{ 0,                            XK_Print,  spawn,          {.v = scrotcmd } },
{ ShiftMask,                    XK_Print,  spawn,          {.v = scrotscmd } },

Locking

I also like locking my laptop when I step away. I picked the tool slock and mapped it to Alt-Shift-L:

{ MODKEY|ShiftMask,             XK_l,      spawn,          {.v = (const char*[]){ "slock", "NULL" } } },

This is kind of a silly tool because it makes your screen totally blank until you type in the correct passwords. There are many other options out there for this if that’s not your cup of tea.

Brightness Control

You can control brightness through sysfs (/sys/class/backlight/*/brightness). However, I chose to install a utility (brightnessctl) to do this. I used to have a script, but the issue is that that file is normally owned by root:root, so you need a trick. brightnessctl includes a udev rule that puts the file in the video group, which is in my opinion the best way to deal with the problem, and there’s not much more than that in the package, so I figured it’s cleaner to install it than to implement it myself.

When I went to map the brightness keys on my keyboard, I found that they were not defined in /usr/include/X11/keysymdef.h. (You can figure out what value X sees when you press any key using the xev utility.) One option is to use the keysym values directly:

{ 0,                            0x1008ff03, spawn, {.v = (const char*[]){ "brightnessctl", "set", "7500-", "NULL" } } },
{ 0,                            0x1008ff02,   spawn, {.v = (const char*[]){ "brightnessctl", "set", "+7500", "NULL" } } },

I found, however, that the keys were defined in the vendor-specific /usr/include/X11/XF86keysym.h:

darter:/usr/include/X11$ rg 0x1008ff11
XF86keysym.h
39:#define XF86XK_AudioLowerVolume      0x1008ff11  /* Volume control down        */

So, I included that file in dwm.c:

#include <X11/XF86keysym.h>

And then:

{ 0,                            XF86XK_MonBrightnessDown, spawn, {.v = (const char*[]){ "brightnessctl", "set", "7500-", "NULL" } } },
{ 0,                            XF86XK_MonBrightnessUp,   spawn, {.v = (const char*[]){ "brightnessctl", "set", "+7500", "NULL" } } },

I wanted each click to be 1/16th of the total brightness, like it is in macOS. I got the value 7500 from echo $(( $(cat /sys/class/backlight/intel_backlight/max_brightness) / 16 )).

Volume Control

I use amixer to control my volume. I chose 3dB per click and mapped the volume keys on my keyboard as so:

{ 0,                            XF86XK_AudioMute, spawn, {.v = (const char*[]){ "amixer", "set", "Master", "toggle", "NULL" } } },
{ 0,                            XF86XK_AudioLowerVolume, spawn, {.v = (const char*[]){ "amixer", "set", "Master", "3dB-", "unmute", "NULL" } } },
{ 0,                            XF86XK_AudioRaiseVolume, spawn, {.v = (const char*[]){ "amixer", "set", "Master", "3dB+", "unmute", "NULL" } } },

Status Bar

I picked slstatus for my status bar. This is another tool you want to build from source, so I used the same procedure as I did for dwm. Upstream is at https://git.suckless.org/slstatus.

I added date and time, battery percentage, and current volume, in dB:

{ datetime, "%s",           "%F %T" },
{ battery_perc, " | %s%%",       "BAT0" },
{ run_command, " | %s%",       "amixer sget Master | tail -1 | awk '{print $5 }' | sed 's@\\(\\[\\|\\]\\)@@g'" },

Note that there is a built-in function for volume called vol_perc. However, it depends on /dev/mixer, which is ancient. I figured it’s easier to run amixer directly. (awk command courtesy of https://stackoverflow.com/questions/56714376/how-to-set-vol-perc-in-slstatus-a-suckless-tool)

Finally, I added this line to the start of my .xinitrc, so that I don’t have to start slstatus manually:

slstatus &

Notifications

I picked the utility dunst for notifications. I didn’t customize anything, so I just installed the Debian package. I also installed libnotify-bin (which is not a dependency, but is required to hook dunst up to e.g. Firefox). I start dunst from my .xinitrc as well:

dunst &

My sources

In case it helps, I’m linking my setup here: