xesite/blog/why-i-use-suckless-tools-20...

310 lines
15 KiB
Markdown

---
title: "Why I Use Suckless Tools"
date: 2020-06-05
---
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][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][dwm] is just a window
manager. It doesn't handle things like transparency, compositing or volume
control. Their terminal [st][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]: https://suckless.org/
[dwm]: https://dwm.suckless.org/
[st]: https://st.suckless.org/
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][unixporndwm].
[unixporndwm]: https://www.reddit.com/r/unixporn/search?q=dwm&restrict_sr=1
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][dwmpatches] 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:
[dwmpatches]: https://dwm.suckless.org/patches/
- [alpha](https://dwm.suckless.org/patches/alpha/)
- [autostart](https://dwm.suckless.org/patches/autostart/)
- [bottomstack](https://dwm.suckless.org/patches/bottomstack/)
- [dwmc](https://dwm.suckless.org/patches/dwmc/)
- [pertag](https://dwm.suckless.org/patches/pertag/)
- [systray](https://dwm.suckless.org/patches/systray/)
- [uselessgap](https://dwm.suckless.org/patches/uselessgap/)
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][dwmconfig] is my config file. It's logically broken into two big sections:
[dwmconfig]: https://tulpa.dev/cadey/dwm/src/commit/8ea55d397459a865041b96d5b4933f426d010e6d/config.def.h
- Variables
- Keybinds
I'll go into more detail about these below.
### Variables
The main variables in my config control the following:
- border width
- size of the gaps when tiling windows
- the snap width
- system tray errata
- the location of the bar
- the fonts
- colors
- transparency values for the bar
- workspace names (mine are based off of the unicode emoticon `(ノ◕ヮ◕)ノ*:・゚✧`)
- app-specific hacks
- default settings for the tiling layouts
- if windows should be forced into place or not
- window layouts
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][hackfont] font on my
desktop and with emacs.
[hackfont]: https://sourcefoundry.org/hack/
### 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](https://github.com/Xe/nixos-configs/blob/master/common/programs/dwm.nix)
and [here](https://github.com/Xe/nixos-configs/blob/master/common/users/cadey/dwm.nix):
- [xrandr](https://wiki.archlinux.org/index.php/Xrandr) to set up my multiple
monitors and rotation for them
- [feh](https://feh.finalrewind.org/) to set my wallpaper
- [picom](https://github.com/yshui/picom) to handle compositing effects like
transparency, blur and drop shadows for windows
- [pasystray](https://github.com/christophgysin/pasystray) for controlling my
system volume
- [dunst](https://dunst-project.org/) for notifications
- [xmodmap](https://wiki.archlinux.org/index.php/Xmodmap) for rebinding the caps
lock key to the escape key
- [cabytcini](https://tulpa.dev/cadey/cabytcini) to show the current time and
weather in my dwm bar
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](https://tulpa.dev/cadey/dwm) 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](https://hisham.hm/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:
```c
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:
```c
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][xephyr] and [git
bisect][gitbisect].
[xephyr]: https://wiki.archlinux.org/index.php/Xephyr
[gitbisect]: https://www.metaltoad.com/blog/beginners-guide-git-bisect-process-elimination
I created two fake monitors with Xephyr:
```console
$ Xephyr -br -ac -noreset -screen 800x600 -screen 800x600 +xinerama :1 &
```
And then started to git bisect my dwm fork:
```console
$ 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](https://tulpa.dev/cadey/dwm/commit/cb3f58ad06993f7ef3a7d8f61468012e2b786cab))
and started to recreate the conditions of the hang.
I set the `DISPLAY` environment variable so that dwm would use the fake
monitors:
```console
$ export DISPLAY=:1
```
and then rebuilt/ran dwm:
```console
$ 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][attachbelow] patch was
the culprit.
[attachbelow]: https://dwm.suckless.org/patches/attachbelow/
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](https://github.com/Xe/xepkgs/commit/c3bffbc8a3ebbaf13bee60e00c8002934d89e803),
and then rebuilt my tower's config once it passed CI.
Being a good internet citizen, I reported this to the [suckless mailing
list](https://lists.suckless.org/dev/2006/33946.html) and then was able to get a
reply back not only confirming the bug, but also with [a patch for the
patch](https://lists.suckless.org/dev/2006/33947.html) 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.