diff --git a/blog/why-i-use-suckless-tools-2020-06-05.markdown b/blog/why-i-use-suckless-tools-2020-06-05.markdown new file mode 100644 index 0000000..a736848 --- /dev/null +++ b/blog/why-i-use-suckless-tools-2020-06-05.markdown @@ -0,0 +1,309 @@ +--- +title: "Why I Use Suckless Tools" +date: 2020-06-05 +--- + +# Why I Use Suckless Tools + +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: + +- [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 tenants 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 hightly 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.