config.phundrak.com/org/config/stumpwm.org

35 KiB
Raw Blame History

StumpWM config

Introduction

What is StumpWM?

StumpWM is a tiling window manager inheriting from RatPoison, written entirely in Common Lisp and compiled with SBCL. While it is not an dynamic tiling window manager like Awesome is, its ability of managing windows in frames and using keychords with keymaps like Emacs does is a huge plus for me, not to mention the fact its configuration file is written in Common Lisp, a general programming language, a bit like Awesome. This makes it an i3 on steroids, sort of. It also uses a lot of Emacs concepts, which is great for an Emacs user such as myself.

Why not EXWM then?

Sometimes, some actions within Emacs are blocking actions, making the computer not usable while the command runs. It also does not play nice with video games (pun intended), which is also a negative point for me. And I also find EXWM more confusing overall than StumpWM.

What this file is for

This file has two main goals:

  • This will be the actual source code of my StumpWM configuration, thanks to Emacs org-mode, and thanks to org-modes literate config capabilities. Almost all of the visible source blocks if not all will be included in my configuration files through tangling, which can be done in Emacs when this file is opened through M-x org-babel-tangle, which will write my configuration files based on the source blocks present in this document. This file is not only my configs documentation, it is my configuration.
  • Be my documentation on my StumpWM configuration. That way, Ill never forget which block of code does what. And maybe, hopefully, someone could learn a thing or two if they want to get into StumpWM but dont know where to begin. You should be able to read this document as a book, with each chapter dedicated to a different aspect of StumpWM.a

Organization of my files

While I could make this file write everything to the same file (the actual source will be in a single file after all), I find it easier to debug StumpWM if everythings split up. For now, my configuration follows this architecture:

init.el
My main configuration file, glues everything together. It loads all of my configuration files as well as some modules I find useful;
colors.lisp
In this file are defined colors that will be used in common in my theme.lisp and modeline.lisp files. Lets make my code DRY, or as I prefer to say, DRYD (Dont Repeat Yourself Dummy).
commands.lisp
Lisp commands, in case I want to bind some complicated actions to a keybind that is not just a simple shell command;
keybindings.lisp
My list of keymaps and keybinds which make StumpWM actually usable;
modeline.lisp
This defines the modeline, a concept taken from Emacs which can display various information such as a list of workspaces, including the current one;
placement.lisp
This file manages my workspaces and the default placement of various windows;
theme.lisp
manages the color theme of StumpWM, the default placement of some windows and StumpWMs gaps.

Init file

As mentioned in the documentation, the configuration files can be in different locations, but I chose an Emacs-like configuration: put everything in ~/.stumpwm.d/. We begin by indicating quicklisp how to properly initialize:

  #-quicklisp
  (let ((quicklisp-init (merge-pathnames "quicklisp/setup.lisp"
                                         (user-homedir-pathname))))
    (when (probe-file quicklisp-init)
      (load quicklisp-init)))

Then, our first StumpWM-related code is declaring we are using the stumpwm package, and this is also our default package. This will allow us to avoid using the prefix stumpwm: each time we are using a function or a variable from this package.

  (in-package :stumpwm)
  (setf *default-package* :stumpwm)

Since I install StumpWM with my package manager (I use the AURs stumpwm-git package), StumpWMs modules are installed to /usr/share/stupmwm/contrib/utils/, lets indicate that to StumpWM.

  (set-module-dir "/usr/share/stupmwm/contrib/")

A startup message can be used when initializing StumpWM. For now, lets set it to nil.

(setf *startup-message* nil)

The first thing I want to do after that is to set some decent cursor pointer as well as get a bunch of stuff started. To see whats in the autostart script, see here.

  (run-shell-command "xsetroot -cursor_name left_ptr")
  (run-shell-command "autostart")

Now, well load a couple of my custom files that will be described below:

File to be loaded
commands.lisp
placement.lisp
keybindings.lisp
theme.lisp
modeline.lisp
  (mapconcat (lambda (file)
               (format "(load \"~/.stumpwm.d/%s\")" (car file)))
             files
             "\n")

This is equivalent to the Common Lisp code:

(load "~/.stumpwm.d/commands.lisp")
(load "~/.stumpwm.d/placement.lisp")
(load "~/.stumpwm.d/keybindings.lisp")
(load "~/.stumpwm.d/theme.lisp")
(load "~/.stumpwm.d/modeline.lisp")

Once the modeline file is loaded, lets indicate StumpWM to activate it:

  (when *initializing*
    (mode-line))

Another thing I want to set is how focus is linked to my mouse: only on click. I HATE it when focus follows my mouse like some damn dog after its ball. Also, the meta key will be used to move floating windows.

  (setf *mouse-focus-policy*    :click
        ,*float-window-modifier* :META)

Next, some modules will be loaded from the stumpwm-contrib package (which is included in stumpwm-git). Here is a short list including a short description of what they are for:

Module Name Why It Is Loaded
alert-me Creates notifications, can also create timed notifications
battery-portable Get information on the battery level of a laptop
beckon Bring the mouse cursor to the current window
cpu Get the CPU usage of the computer
end-session Gracefully end programs when ending user session
globalwindows Navigate between windows from all workspaces
mem Get the memory usage of the computer
stump-backlight Native management of backlight in StumpWM
urgentwindows Get urgent windows
  (mapconcat (lambda (module)
               (format "(load-module \"%s\")" (car module)))
             modules
             "\n")
(load-module "alert-me")
(load-module "battery-portable")
(load-module "beckon")
(load-module "cpu")
(load-module "end-session")
(load-module "globalwindows")
(load-module "mem")
(load-module "stump-backlight")
(load-module "urgentwindows")

Finally, we can notify the user everything is ready.

  (setf *startup-message* "StumpWM is ready!")

And its done! We can now move on to the creation of the other CLisp files.

Commands

This file is going to be short. The only two custom command I have is for Firefox, in order to either invoke a new Firefox window, or raise it if it already exists, and for Emacs to invoke the Emacs client or a new Emacs instance if the server isnt running. This is done like so:

  (defcommand firefox () ()
              "Run or raise Firefox."
              (run-or-raise "firefox" '(:class "Firefox") t nil))

And done, next!

Colors

If youve taken a look at the rest of my dotfiles, you may have noticed I really like the Nord theme. No wonder we can find it here again! Here is a small table listing the Nord colors:

Name Value
nord0 #2e3440
nord1 #3b4252
nord2 #434c5e
nord3 #4c566a
nord4 #d8dee9
nord5 #e5e9f0
nord6 #eceff4
nord7 #8fbcbb
nord8 #88c0d0
nord9 #81a1c1
nord10 #5e81ac
nord11 #bf616a
nord12 #d08770
nord13 #ebcb8b
nord14 #a3be8c
nord15 #b48ead

Ill prefix the variables name with phundrak- just in case it might conflict with another package I might use in the future, so the CLisp code looks like so:

  (mapconcat (lambda (color)
               (format "(defvar phundrak-%s \"%s\")" (car color) (cadr color)))
             colors
             "\n")
(defvar phundrak-nord0 "#2e3440")
(defvar phundrak-nord1 "#3b4252")
(defvar phundrak-nord2 "#434c5e")
(defvar phundrak-nord3 "#4c566a")
(defvar phundrak-nord4 "#d8dee9")
(defvar phundrak-nord5 "#e5e9f0")
(defvar phundrak-nord6 "#eceff4")
(defvar phundrak-nord7 "#8fbcbb")
(defvar phundrak-nord8 "#88c0d0")
(defvar phundrak-nord9 "#81a1c1")
(defvar phundrak-nord10 "#5e81ac")
(defvar phundrak-nord11 "#bf616a")
(defvar phundrak-nord12 "#d08770")
(defvar phundrak-nord13 "#ebcb8b")
(defvar phundrak-nord14 "#a3be8c")
(defvar phundrak-nord15 "#b48ead")

And with that were done!

Modeline

The modeline is pretty easy. First, lets load the colors.lisp file we just created:

  (load "~/.stumpwm.d/colors.lisp")

Next, we can set some colors for the modeline. Lets set the background of the modeline to Nord1 and the foreground to Nord5, I think this is a pretty good combination.

  (setf *mode-line-background-color* phundrak-nord1
        ,*mode-line-foreground-color* phundrak-nord5)

We could also use some borders in the modeline. But we wont. Lets still set its color to Nord1, just in case.

  (setf *mode-line-border-color* phundrak-nord1
        ,*mode-line-border-width* 0)

The timeout of the modeline indicates how often it refreshes in seconds. I think one second is good.

  (setf *mode-line-timeout* 1)

Next we get to the content of the modeline. This format follows the format indicated in the manpage of date.

  (setf *time-modeline-string* "%F %H:%M")

Lets also indicate how the groupname is displayed.

  (setf *group-format* " %t ")

The window format should display first its window number, then its titled, limited to 30 characters.

  (setf *window-format* "%n: %30t")

We can indicate what to display in our modeline. Be aware the ^> string will align the rest of the string to the right of the modeline. %g will display the group list, while %v will display the list of windows that are in the current group, with the active one highlighted, and %u will display urgent windows if there are any. %d on the other hand will display the date in the format set above, while %B will display the battery level of the laptop.

  (setf *screen-mode-line-format* (list "%g %v ^> %C | %M | %B | %d"))

This variable as you can see is a list of elements, although here I am only using one string. But it is completely possible to insert some CLisp code in here that returns some string if the user needs some code to return data that cannot be easily accesible otherwise. I might add some at some point, but not today yet.

Placement

Ive been used to ten groups, or workspaces, or tags, since I began using tiling window managers. I shall then continue this habit. Here is the list of groups I will be using:

Groups
term
emacs
www
files
media
graphics
VMs
games
private
discord
  (string-join `(,(format "(grename \"%s\")" (car (car groups)))
                 ,@(mapcar (lambda (group)
                             (format "(gnewbg \"%s\")" (car group)))
                           (cdr groups)))
               "\n")
(grename "term")
(gnewbg "emacs")
(gnewbg "www")
(gnewbg "files")
(gnewbg "media")
(gnewbg "graphics")
(gnewbg "VMs")
(gnewbg "games")
(gnewbg "private")
(gnewbg "discord")

Groups are specified this way:

  (when *initializing*
    <<gen-groups()>>)

Next, lets make sure no previous window placement rule is in place, this will avoid unexpected and hard-to-debug behavior.

  (clear-window-placement-rules)

Now we can define our window placement preferences. For now, all rely on the windows class, so it will be pretty straightforward to write.

Window Class Group
Emacs emacs
Firefox browser
Nemo files
Gimp media
Signal private
lightcord discord
Steam games
Virt-manager VMs
  (mapconcat (lambda (rule)
               (let ((class (car rule))
                     (group (cadr rule)))
                 (format "(define-frame-preference \"%s\"
      (nil t t :class \"%s\"))" group class)))
             rules
             "\n")

This can be written this way:

(define-frame-preference "emacs"
    (nil t t :class "Emacs"))
(define-frame-preference "browser"
    (nil t t :class "Firefox"))
(define-frame-preference "files"
    (nil t t :class "Nemo"))
(define-frame-preference "media"
    (nil t t :class "Gimp"))
(define-frame-preference "private"
    (nil t t :class "Signal"))
(define-frame-preference "discord"
    (nil t t :class "lightcord"))
(define-frame-preference "games"
    (nil t t :class "Steam"))
(define-frame-preference "VMs"
    (nil t t :class "Virt-manager"))

Theme

As in the modeline file, the first thing well do is to load our colors.

  (load "~/.stumpwm.d/colors.lisp")

We can now go onto more serious business.

Fonts

This gave me quite the headache when I tried to set this up: in order to use TTF or OTF fonts, we need to use the ttf-fonts module which relies on the clx-truetype library. A few years back, it should have been possible to get it installed with a call to src_lisp[:exports code]{(ql:quickload :clx-truetype)}, but it is no longer available! Theres a quickfix available while we wait for clx-truetype to be once again available: clone it in quicklisps local projects. You will obviously need to have quicklisp installed (for that, follow the official instructions), then execute the following shell commands:

  cd ~/quicklisp/local-projects/
  git clone https://github.com/lihebi/clx-truetype.git

This will make clx-truetype available to quicklisp, and you can run again

(ql:quickload :clx-truetype)
without an issue (running it again is necessary to install its dependencies).

Now that this is out of the way, lets add two lines so we can use TTF and OTF fonts:

  (ql:quickload :clx-truetype)
  (load-module "ttf-fonts")

Something that didnt click immediately for me (and I think StumpWMs documentation on this could be improved) is that set-font can be used to set either one main font for StumpWM, as one might guess reading the documentation — or you can set a list of them! And this is great, since my main font does not support some characters I regularly have in my windows title, such as CJK characters, emojis and all! Here is my list of fonts I want loaded:

Family Subfamily Size
DejaVu Sans Book 9
IPAMincho Regular 11
  (format "(set-font `(%s))"
          (mapconcat (lambda (font)
                      (let ((family    (nth 0 font))
                            (subfamily (nth 1 font))
                            (size      (nth 2 font)))
                        (format ",%s" `(make-instance 'xft:font
                                                      :family ,(format "\"%s\"" family)
                                                      :subfamily ,(format "\"%s\"" subfamily)
                                                      :size ,size
                                                      :antialias t))))
                    fonts
                    "\n            "))

The code equivalent of this table can be seen below:

(set-font `(,(make-instance 'xft:font :family "DejaVu Sans" :subfamily "Book" :size 11 :antialias t)
            ,(make-instance 'xft:font :family "IPAMincho" :subfamily "Regular" :size 11 :antialias t)))

Colors

We can now set a couple of colors for StumpWM. Not that we will see them often since I dont like borders on my windows, but in case I want to get them back, theyll be nice to have.

  (set-border-color        phundrak-nord1)
  (set-focus-color         phundrak-nord1)
  (set-unfocus-color       phundrak-nord3)
  (set-float-focus-color   phundrak-nord1)
  (set-float-unfocus-color phundrak-nord3)

Lets also set the colors of the message and input windows:

  (set-fg-color phundrak-nord4)
  (set-bg-color phundrak-nord1)

As I said, I dont like borders, so Ill remove them. Ill still keep the windows title bar available when its floating, and this is also where I can set the format of its title: its number as well as its name, limited to thirty characters.

  (setf *normal-border-width*       0
        ,*float-window-border*       0
        ,*float-window-title-height* 15
        ,*window-border-style*       :none
        ,*window-format*             "%n:%30t")

Message and Input Windows

The Input windows as well as the message windows should both be at the top of my screen. And I believe a padding of five pixels for the message windows is good.

  (setf *input-window-gravity*     :top
        ,*message-window-padding*   10
        ,*message-window-y-padding* 10
        ,*message-window-gravity*   :top)

Gaps Between Frames

I love gaps. When I was using i3, I used the i3-gaps package, not just plain i3. In Awesome, I still have gaps. And in StumpWM, I shall still use gaps. In order to use them, lets load a module dedicated to gaps in StumpWM:

  (load-module "swm-gaps")

Now that this is done, I can now set some variables bound to this package.

  (setf swm-gaps:*head-gaps-size*  0
        swm-gaps:*inner-gaps-size* 5
        swm-gaps:*outer-gaps-size* 15)

Finally, lets enable our gaps:

  (when *initializing*
    (swm-gaps:toggle-gaps))

Keybinds

Buckle up, this chapter is going to be long, because me loves LOTS of keybinds.

First, lets declare again we are using the default package stumpwm:

  (in-package :stumpwm)

This will avoid us always repeating stumpwm:define-key or stumpwm:kbd instead of simply define-key and kbd.

StumpWM behaves a bit like Emacs in terms of keybinds. You have keymaps, which are a collection of keybinds, which in turn call CLisp functions. However, unlike Emacs, you have to declare a lot of keymaps, because StumpWM cannot (yet) understand keybinds such as

(kbd "C-x c l")
, so you end up creating a keybind to a keymap which contains other keybinds, which might contain a couple of keybinds to other keymaps. I hope this will get improved soon.

There are also two keymaps you need to be aware of:

*top-map*
This is the keymap available litteraly everywhere. With this keymap, you can emulate most of your keybinds you have in other window managers. For instance, I cannot live without s-RET for creating new shells, so Ill bind it to *top-map*. But its good practice to avoid polluting *top-map* with too many keybinds.
*root-map*
This keymap is the default keymap that is already somewhat populated. It is available after hitting the prefix key set with set-prefix-key which we will see just below.

It is interesting to note that once you entered any keymap, except *top-map*, if you hit ? you will see the list of available keybinds. Id like it if something similar to general in Emacs too could be implemented: give any arbitrary name to the keybind you just declared which would be displayed instead of the actual function or keymap called by keybind. It would be nicer to see frames rather than *my-frames-management-keymap*.

Anyways, as mentionned above, *root-map* is already pre-populated with some cool stuff for you, and you can access it with a prefix which is by default C-t. But if this doesnt suit you, you can always redefine it with set-prefix-key. I personally like to have my space key as a leader key, but in order to not have it conflict with Emacs, I also need to press the super key too.

  (set-prefix-key (kbd "s-SPC"))

Also, lets enable which-key:

  (which-key-mode)
  (mapconcat (lambda (keybind)
               (format "%s" (let ((key    (string-replace "~" "" (car keybind)))
                                  (function (string-replace "~" "" (cadr keybind))))
                              `(define-key ,map
                                 (kbd ,(format "\"%s\"" key))
                                 ,(if (string-prefix-p "'" function t)
                                      function
                                    (format "\"%s\"" function))))))
             keybinds
             "\n")
(define-key m (kbd "f") "float-this")
(define-key m (kbd "F") "unfloat-this")
(define-key m (kbd "u") "unfloat-this")
(define-key m (kbd "C-f") "flatten-floats")

Frames and Windows management

As youll see, I have loads of keybinds related to frames and windows management. They are all categorized in a specific keymap, called *my-frames-management-keymap*. But before that, lets define the keymap *my-frames-float-keymap*, with keybinds dedicated to actions related with floating windows and frames.

Keychord Function
f float-this
F unfloat-this
u unfloat-this
C-f flatten-floats
*my-frames-float-keymap*

We can now pass onto *my-frames-management-keymap*. My keybinds are organized this way:

Keychord Function
c move-focus left
t move-focus down
s move-focus up
r move-focus right
C move-window left
T move-window down
S move-window up
R move-window right
C-c exchange-direction left
C-t exchange-direction down
C-s exchange-direction up
C-r exchange-direction right
n next
p prev
/ hsplit
- vsplit
h hsplit
v vsplit
H hsplit-equally
V vsplit-equally
. iresize
d remove-split
D only
e expose
f fullscreen
F '*my-frames-float-keymap*
i info
I show-window-properties
m meta
o other-window
q delete-window
Q kill-window
s sibling
u next-urgent
U unmaximize
*my-frames-management-keymap*

As you can see, with the binding to F, we make use of the *my-frames-float-keymap* keymap declared above, which means if we find ourselves in *my-frames-management-keymap*, pressing F will bring us in *my-frames-float-keymap*.

  (defvar *my-frames-float-keymap*
    (let ((m (make-sparse-keymap)))
      <<keybinds-gen(map="m", keybinds=frames-float)>>
      m))

  (defvar *my-frames-management-keymap*
    (let ((m (make-sparse-keymap)))
      <<keybinds-gen(map="m", keybinds=frames-and-window-management)>>
      m))

Lets bind *my-frames-management-keymap* in *root-keymap*:

  (define-key *root-map* (kbd "w") '*my-frames-management-keymap*)

That way, if we want for instance to split our current frame vertically, well be able to type s-SPC w - and vsplit will be called.

I also bound a couple of these functions to the top keymap for easier access:

Keychord Function
s-c move-focus left
s-t move-focus down
s-s move-focus up
s-r move-focus right
s-C move-window left
s-T move-window down
s-S move-window up
s-R move-window right
s-M-c exchange-direction left
s-M-t exchange-direction down
s-M-s exchange-direction up
s-M-r exchange-direction right

This translates to:

  <<keybinds-gen(map="*top-map*", keybinds=top-window-map)>>

Being a bépo layout user, the hjkl keys dont exactly fit me, as you might have noticed with my use of ctsr which is its equivalent. Due to this, the interactive keymap for iresize is not ideal for me, let me redefine it:

  (define-interactive-keymap (iresize tile-group) (:on-enter #'setup-iresize
                                                   :on-exit  #'resize-unhide
                                                   :abort-if #'abort-resize-p)
    ((kbd "c") "resize-direction left")
    ((kbd "t") "resize-direction down")
    ((kbd "s") "resize-direction up")
    ((kbd "r") "resize-direction right"))

Applications

When I speak about applications, I speak about programs and scripts in general. With these keymaps, I can launch programs I often have use for, but I can also launch some scripts as well as take screenshots.

First, lets create my rofi scripts keymap.

Keychord Function
a exec awiki
r exec rofi -combi-modi drun,window -show combi
s exec rofi -show ssh
p exec rofi-pass -t
P exec rofi-pass
e exec rofi-emoji
m exec rofi-mount
u exec rofi-umount
w exec wacom-setup
y exec ytplay
Y exec rofi-ytdl
*my-rofi-keymap*

Heres the equivalent in Common Lisp.

  (defvar *my-rofi-keymap*
    (let ((m (make-sparse-keymap)))
      <<keybinds-gen(map="m", keybinds=rofi-scripts)>>
      m))

Lets also create a keymap for screenshots.

Keychord Function
d exec scrot -d 3 -e 'mv $f ~/Pictures/Screenshots'
s exec scrot -e 'mv $f ~/Pictures/Screenshots'
S exec scrot -s -e 'mv $f ~/Pictures/Screenshots'
g exec scrot -e 'gimp $f; mv $f ~/Pictures/Screenshots'
*my-screenshot-keymap*

Heres the equivalent in Common Lisp.

  (defvar *my-screenshot-keymap*
    (let ((m (make-sparse-keymap)))
      <<keybinds-gen(map="m", keybinds=screenshot-keymap)>>
      m))

We can now define our applications keymap which will reference both the above keymaps.

Keychord Function
b firefox
d exec lightcord
e emacs
g exec gimp
n exec nemo
r '*my-rofi-keymap*
s '*my-screenshot-keymap*
*my-applications-keymap*

This translates to:

  (defvar *my-applications-keymap*
    (let ((m (make-sparse-keymap)))
      <<keybinds-gen(map="m", keybinds=application-keymap)>>
      m))

The application keymap can now be bound to the top map like so:

  (define-key *top-map* (kbd "s-a") '*my-applications-keymap*)

I will also bind to the top map s-RET in order to open a new terminal window. The screenshot keymap is also bound to the ScreenPrint key.

  (define-key *top-map* (kbd "s-RET") "exec kitty")
  (define-key *top-map* (kbd "Print") '*my-screenshot-keymap*)

End of Session, Powering Off, and the Likes

The module end-session provides functions for gracefully ending the user session, powering off, restarting, and suspending the computer. It also provides a function that interactively asks what the user whishes to do.

Keychord Function
q end-session
l logout
s suspend-computer
S shutdown-computer
r restart-computer

This translates to:

  (defvar *my-end-session-keymap*
    (let ((m (make-sparse-keymap)))
      <<keybinds-gen(map="m", keybinds=end-session-keymap)>>
      m))

Which is bound in the root map to q:

  (define-key *root-map* (kbd "q") '*my-end-session-keymap*)

Misc

Finally, some misc keybinds on the root map which dont really fit anywhere else:

Keychord Function
B beckon
l exec plock
r reload
  <<keybinds-gen(map="*root-map*", keybinds=misc-root-map)>>