Why I Use Suckless Tools

Published on , 2080 words, 8 minutes to read

Software is complicated. Foundational building blocks of desktop environments tend to grow year over year until it's difficult to understand or maintain them. Suckless offers an alternative to this continuous cycle of bloat and meaningless redesign. Suckless tools aim to keep things simple, minimal, usable and hackable by default. Their window manager dwm is just a window manager. It doesn't handle things like transparency, compositing or volume control. Their terminal st is just a terminal. It doesn't handle fancy things like ancient terminal kinds that died out long ago. It just displays text. It doesn't handle things that tmux or similar could take care of, because tmux can do a better job at that than st ever could on its own.

Suckless tools are typically configured in C, the language they are written in. However as a side effect of suckless tools having their configuration baked into the executable at compile time, they start up instantly. If something goes wrong while using them, you can easily jump right into the code that implements them and nail down issues using basic debugger skills.

However, even though the window manager is meager, it still offers places for you to make it look beautiful. For examples of beautiful dwm setups, see this search of /r/unixporn on reddit.

I would like to walk through my dwm setup, how I have it configured all of the parts at play as well as an example of how I debug problems in my dwm config.

My dwm Config

As dwm is configured in C, there's also a community of people creating patches for dwm that add extra features like additional tiling methods, the ability to automatically start things with dwm, transparency for the statusbar and so much more. I use the following patches:

This combination of patches allows me to make things feel comfortable and predictable enough that I can rely entirely on muscle memory for most of my window management. Nearly all of it is done with the keyboard too.

Here is my config file. It's logically broken into two big sections:

I'll go into more detail about these below.

Variables

The main variables in my config control the following:

All of these things control various errata. As a side effect of making them all compile time constants, these settings don't have to be loaded into the program because they're already a part of it. I use the Hack font on my desktop and with emacs.

Keybinds

The real magic of tiling window managers is that all of the window management commands are done with my keyboard. Alt is the key I have devoted to controlling the window manager. All of my window manager control chords use the alt key.

Here are the main commands and what they do:

Command Effect
Alt-p Spawn a program by name
Alt-Shift-Enter Open a new terminal window
Alt-b Hide the bar if it is shown, show the bar if it is hidden
Alt-j Move focus down the stack of windows
Alt-k Move focus up the stack of windows
Alt-i Increase the number of windows in the primary area
Alt-d Decrease the number of windows in the primary area
Alt-h Make the primary area smaller by 5%
Alt-l Make the primary area larger by 5%
Alt-Enter Move the currently active window into the primary area
Alt-Tab Switch to the most recently active workspace
Alt-Shift-C Nicely ask a window to close
Alt-t Select normal tiling mode for the current workspace
Alt-f Select floating (non-tiling) mode for the current workspace
Alt-m Select monocle (fullscreen active window) mode for the current workspace
Alt-u Select bottom-stacked tiling mode for the current workspace
Alt-o Select bottom-stacked horizontal tiling mode for the current workspace (useful on vertical monitors)
Alt-e Open a new emacs window
Alt-Space Switch to the most recently used tiling method
Alt-Shift-Space Detach the currently active window from tiling
Alt-1 thru Alt-9 Switch to a given workspace
Alt-Shift-1 thru Alt-Shift-9 Move the active window to a given workspace
Alt-0 Show all windows on all workspaces
Alt-Shift-0 Show the active window on all workspaces
Alt-Comma and Alt-Period Move focus to the other monitor
Alt-Shift-Comma and Alt-Shift-Period Move the active window to the other monitor
Alt-Shift-q Uncleanly exit dwm and kill the session

This is just enough commands that I can get things done, but not so many that I get overwhelmed and forget what keybind does what. I have most of this committed to muscle memory (and had to look at the config file to write out this table), and as a result nearly all of my window management is done with my keyboard.

The rest of my config handles things like Alt-Right-Click to resize windows arbitrarily, signals with dwmc and other overhead like that.

The Other Parts

The rest of my desktop environment is built up using a few other tools that build on top of dwm. You can see the NixOS modules I've made for it here and here:

Each of these tools has their own place in the stack and they all work together to give me a coherent and cohesive environment that I can use for Netflix, programming, playing Steam games and more.

cabytcini is a program I created for myself as part of my goal to get more familiar with Rust. As of the time of this post being written, it uses only 11 megabytes of ram and is configured using a config file located at ~/.config/cabytcini/gaftercu'a.toml. It scrapes data from the API server I use for my wall-mounted clock to show me the weather in Montreal. I've been meaning to write more about it, but it's currently only documented in Lojban.

Debugging dwm

Software is imperfect, even smaller programs like dwm can still have bugs in them. Here's the story of how I debugged and bisected a problem with my dwm config recently.

I had just gotten the second monitor set up and noticed that whenever I sent a window to it, the entire window manager seemed to get locked up. I tried sending the quit command to see if it would respond to that, and it failed. I opened up a virtual terminal with control-alt-F1 and logged in there, then I launched htop to see if the process was blocked.

It reported dwm was using 100% CPU. This was odd. I then decided to break out the debugger and see what was going on. I attached to the dwm process with gdb -p (pgrep dwm) and then ran bt full to see where it was stuck.

The backtrace revealed it was stuck in the drawbar() function. It was stuck in a loop that looked something like this:

for (c = m->clients; c; c = c->next) {
    occ |= c->tags;
    if (c->isurgent)
            urg |= c->tags;
}

dwm stores the list of clients per tag in a singly linked list, so the root cause could be related to a circular linked list somehow, right?

I decided to check this by printing c and c->next in GDB to see what was going on:

gdb> print c
0xfad34f
gdb> print c->next
0xfad34f

The linked list was circular. dwm was stuck iterating an infinite loop. I looked at the type of c and saw it was something like this:

struct Client {
	char name[256];
	float mina, maxa;
	float cfact;
	int x, y, w, h;
	int oldx, oldy, oldw, oldh;
	int basew, baseh, incw, inch, maxw, maxh, minw, minh;
	int bw, oldbw;
	unsigned int tags;
	int isfixed, isfloating, isurgent, neverfocus, oldstate, isfullscreen;
	Client *next;
	Client *snext;
	Monitor *mon;
	Window win;
};

So, next is a pointer to the next client (if it exists). Setting the pointer to NULL would probably break dwm out of the infinite loop. So I decided to test that by running:

gdb> set var c->next = 0x0

To set the next pointer to null. dwm immediately got unstuck and exited (apparently my quit command from earlier got buffered), causing the login screen to show up. I was able to conclude that something was wrong with my dwm setup.

I know this behavior worked on release versions of dwm, so I decided to load up KDE and then take a look at what was going on with Xephyr and git bisect.

I created two fake monitors with Xephyr:

$ Xephyr -br -ac -noreset -screen 800x600 -screen 800x600 +xinerama :1 &

And then started to git bisect my dwm fork:

$ cd ~/code/cadey/dwm
$ git bisect init
$ git bisect bad HEAD
$ git bisect good cb3f58ad06993f7ef3a7d8f61468012e2b786cab

I registered the bad commit (the current one) and the last known good commit (from when dwm 6.2 was released) and started to recreate the conditions of the hang.

I set the DISPLAY environment variable so that dwm would use the fake monitors:

$ export DISPLAY=:1

and then rebuilt/ran dwm:

$ make clean && rm config.h && make && ./dwm

Once I had dwm up and running, I created a terminal window and tried to send it to the other screen. If it worked, I marked the commit as good with git bisect good, and if it hung I marked the commit as bad with git bisect bad. 7 iterations later and I found out that the attachbelow patch was the culprit.

I reverted the patch on the master branch, rebuilt and re-ran dwm and tried to send the terminal window between the fake monitors. It worked every time. Then I committed the revert of attachbelow, pushed it to my NUR repo, and then rebuilt my tower's config once it passed CI.

Being a good internet citizen, I reported this to the suckless mailing list and then was able to get a reply back not only confirming the bug, but also with a patch for the patch to fix the behavior forever. I have yet to integrate this meta-patch into my dwm fork, but I'll probably get around to it someday.

This really demonstrates one of the core tenets of the suckless philosophy perfectly. I am not very familiar with how the dwm codebase works, but I am able to dig into its guts and diagnose/fix things because it is intentionally kept as simple as possible.

If you use Linux on a desktop/laptop, I highly suggest taking a look at suckless software and experimenting with it. It is super optimized for understandability and hacking, which is a huge breath of fresh air these days.


Facts and circumstances may have changed since publication. Please contact me before jumping to conclusions if something seems wrong or unclear.

Tags: