#+TITLE: StumpWM config
#+setupfile: headers
#+OPTIONS: unique-id:t
#+HTML_HEAD_EXTRA:
#+HTML_HEAD_EXTRA:
#+HTML_HEAD_EXTRA:
#+property: header-args:emacs-lisp :tangle no :exports results :cache yes :noweb yes
[[file:img/stumpwm.png]]
* Introduction
:PROPERTIES:
:CUSTOM_ID: Introduction-9vda1z81u5j0
:END:
** What is StumpWM?
:PROPERTIES:
:CUSTOM_ID: Introduction-What-is-StumpWM-oyycyb91u5j0
:END:
[[https://stumpwm.github.io/][StumpWM]] is a tiling window manager inheriting from [[http://www.nongnu.org/ratpoison/][RatPoison]], written
entirely in [[https://common-lisp.net/][Common Lisp]] and compiled with [[http://www.sbcl.org/][SBCL]]. While it is not an
dynamic tiling window manager like [[file:Deprecated/awesome.org][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 [[file:Deprecated/i3.org][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?
:PROPERTIES:
:CUSTOM_ID: Introduction-Why-not-EXWM-then-670dyb91u5j0
:END:
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
:PROPERTIES:
:CUSTOM_ID: Introduction-What-this-file-is-for-pnyg92a1u5j0
:END:
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-mode’s
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 config’s documentation,
it /*is*/ my configuration.
- Be my documentation on my StumpWM configuration. That way, I’ll
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 don’t 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.
** Organization of my files
:PROPERTIES:
:CUSTOM_ID: Introduction-Organization-of-my-files-40vjne91u5j0
:END:
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 everything’s 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~ :: This file defines colors that will be used in my
~theme.lisp~ and ~modeline.lisp~ files. Let’s make my code DRY, or as I
prefer to say, DRYD (/Don’t 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;
- ~utilities.lisp~ :: Here you can find my StumpWM configuration that
isn’t really related to the rest of the config, for instance utility
code for connecting by SSH to some host.
- ~theme.lisp~ :: manages the color theme of StumpWM, the default
placement of some windows and StumpWM’s gaps.
You will also find below my ~xinit~ file for StumpWM, exported to
~$HOME/.xinitrc.stumpwm~, which I use to start Stump through ~startx
~/.xinitrc.stumpwm~.
#+begin_src sh :tangle ~/.xinitrc.stumpwm :shebang "#!/bin/sh"
# this makes it work in Ubuntu
xhost +SI:localuser:$USER
# Set fallback pointer
xsetroot -cursor_name left_ptr
# Fix scrolling on some GTK3 applications
export GDK_CORE_DEVICE_EVENTS=1
# in case Java applications display /nothing/
# wmname LG3D
# export _JAVA_AWT_WM_NONREPARENTING=1
autorandr -l home
exec stumpwm
#+end_src
* Init file
:PROPERTIES:
:CUSTOM_ID: Init-file-l3q4snd1u5j0
:header-args:lisp: :mkdirp yes :tangle ~/.stumpwm.d/init.lisp
:END:
As mentioned in [[https://stumpwm.github.io/git/stumpwm-git_1.html#Init-File][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:
#+begin_src lisp
#-quicklisp
(let ((quicklisp-init (merge-pathnames "quicklisp/setup.lisp"
(user-homedir-pathname))))
(when (probe-file quicklisp-init)
(load quicklisp-init)))
#+end_src
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.
#+begin_src lisp
(in-package :stumpwm)
(setf *default-package* :stumpwm)
#+end_src
Since I install StumpWM with my package manager (I use the AUR’s
~stumpwm-git~ package), StumpWM’s modules are installed to
~/usr/share/stupmwm/contrib/utils/~, let’s indicate that to StumpWM.
#+begin_src lisp
(set-module-dir "/usr/share/stupmwm/contrib/")
#+end_src
A startup message can be used when initializing StumpWM. For now,
let’s set it to ~nil~.
#+begin_src lisp
(setf *startup-message* nil)
#+end_src
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 what’s in the
~autostart~ script, [[file:bin.org::#Autostart-a99e99e7][see here]].
#+begin_src lisp
(run-shell-command "autostart")
#+end_src
Next I need to register the AltGr key so it works correctly when used.
On my system, the value of ~*altgr-offset*~ is 4, but on yours it might
be 6, so be careful and refer to the manual on that matter.
#+begin_src lisp
(setf *altgr-offset* 4)
(register-altgr-as-modifier)
#+end_src
Now, we’ll load a couple of my custom files that will be described below:
#+name: first-loaded-files
| File to be loaded |
|-------------------|
| bluetooth.lisp |
| commands.lisp |
| placement.lisp |
| keybindings.lisp |
| theme.lisp |
| utilities.lisp |
| modeline.lisp |
| systemd.lisp |
#+name: gen-load-files
#+header: :wrap src lisp
#+begin_src emacs-lisp :var files=first-loaded-files
(mapconcat (lambda (file)
(format "(load \"~/.stumpwm.d/%s\")" (car file)))
files
"\n")
#+end_src
This is equivalent to the Common Lisp code:
#+RESULTS[ed4f3fe4f7f82b11cd3cd262578abc7146f5807d]: gen-load-files
#+begin_src lisp
(load "~/.stumpwm.d/bluetooth.lisp")
(load "~/.stumpwm.d/commands.lisp")
(load "~/.stumpwm.d/placement.lisp")
(load "~/.stumpwm.d/keybindings.lisp")
(load "~/.stumpwm.d/theme.lisp")
(load "~/.stumpwm.d/utilities.lisp")
(load "~/.stumpwm.d/modeline.lisp")
(load "~/.stumpwm.d/systemd.lisp")
#+end_src
Once the modeline file is loaded, let’s indicate StumpWM to activate
it:
#+begin_src lisp
(when *initializing*
(mode-line))
#+end_src
Another thing I want to set is to use the super key to move floating
windows and window focus to transfer from one window to another only
on click.
#+begin_src lisp
(setf *mouse-focus-policy* :click
,*float-window-modifier* :SUPER)
#+end_src
Next, some modules will be loaded from the ~stumpwm-contrib~ package
(which is included in ~stumpwm-git~ in the AUR). Here is a short list
including a short description of what they are for:
#+name: loaded-modules
| Module Name | Why It Is Loaded |
|-----------------+------------------------------------------------------------|
| beckon | Bring the mouse cursor to the current window |
| end-session | Gracefully end programs when ending user session |
| globalwindows | Navigate between windows from all workspaces |
| mpd | Interact with MPD |
| stump-backlight | Native management of backlight in StumpWM |
| urgentwindows | Get urgent windows |
#+name: gen-load-modules
#+header: :wrap src lisp
#+begin_src emacs-lisp :var modules=loaded-modules
(mapconcat (lambda (module)
(format "(load-module \"%s\")" (car module)))
modules
"\n")
#+end_src
#+RESULTS[0cbba236372280cb2eb6a1e277cda84938e15d46]: gen-load-modules
#+begin_src lisp
(load-module "beckon")
(load-module "end-session")
(load-module "globalwindows")
(load-module "mpd")
(load-module "stump-backlight")
(load-module "urgentwindows")
#+end_src
In order to be able to use MPD from StumpWM itself, we’ll need to
connect to it.
#+begin_src lisp
(mpd:mpd-connect)
#+end_src
Finally, we can notify the user everything is ready.
#+begin_src lisp
(setf *startup-message* "StumpWM is ready!")
#+end_src
And it’s done! We can now move on to the creation of the other CLisp files.
* Commands
:PROPERTIES:
:CUSTOM_ID: Commands-1wagy001v5j0
:header-args:lisp: :mkdirp yes :tangle ~/.stumpwm.d/commands.lisp
:END:
The first command I declare in this file is a command that will avoid
me invoking too many Firefox instances. Either Firefox is not already
running and an instance is launched, or one already is and we are
brought to it. This is done like so:
#+begin_src lisp
(defcommand firefox () ()
"Run or raise Firefox."
(sb-thread:make-thread (lambda () (run-or-raise "firefox" '(:class "Firefox") t nil))))
#+end_src
Next, this command will not only close the current window, but it will
also close the current frame.
#+begin_src lisp
(defcommand delete-window-and-frame () ()
"Delete the current frame with its window."
(delete-window)
(remove-split))
#+end_src
The two following commands will create a new frame to the right and
below the current frame respectively, then focus it.
#+begin_src lisp
(defcommand hsplit-and-focus () ()
"Create a new frame on the right and focus it."
(hsplit)
(move-focus :right))
(defcommand vsplit-and-focus () ()
"Create a new frame below and move focus to it."
(vsplit)
(move-focus :down))
#+end_src
Now, let’s create a command for invoking the terminal, optionally with
a program.
#+begin_src lisp
(defcommand term (&optional program) ()
"Invoke a terminal, possibly with a @arg{program}."
(sb-thread:make-thread
(lambda ()
(run-shell-command (if program
(format nil "kitty ~A" program)
"kitty")))))
#+end_src
And done! Next!
* Colors
:PROPERTIES:
:CUSTOM_ID: Colors-w5493d01v5j0
:header-args:lisp: :mkdirp yes :tangle ~/.stumpwm.d/colors.lisp
:END:
If you’ve taken a look at the rest of my dotfiles, you may have
noticed I really like the [[https://www.nordtheme.com/][Nord theme]]. No wonder we can find it here
again! Here is a small table listing the Nord colors:
#+name: 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 |
I’ll 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:
#+name: gen-colors
#+header: :wrap src lisp
#+begin_src emacs-lisp :var colors=nord-colors
(mapconcat (lambda (color)
(format "(defvar phundrak-%s \"%s\")" (car color) (cadr color)))
colors
"\n")
#+end_src
#+RESULTS[08b3db7a2b4f31d641bcd096ff265eae06879244]: gen-colors
#+begin_src lisp
(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")
#+end_src
Finally, let’s also modify the default colors StumpWM has. I’ll try to
respect the original colors while converting them to Nord. We also
need to reload them now that we modified them.
#+begin_src lisp
(setq *colors*
`(,phundrak-nord1 ;; 0 black
,phundrak-nord11 ;; 1 red
,phundrak-nord14 ;; 2 green
,phundrak-nord13 ;; 3 yellow
,phundrak-nord10 ;; 4 blue
,phundrak-nord14 ;; 5 magenta
,phundrak-nord8 ;; 6 cyan
,phundrak-nord5)) ;; 7 white
(when *initializing*
(update-color-map (current-screen)))
#+end_src
And with that we’re done!
* Mode-Line
:PROPERTIES:
:CUSTOM_ID: Modeline-g2ofyw01v5j0
:header-args:lisp: :mkdirp yes :tangle ~/.stumpwm.d/modeline.lisp
:END:
The timeout of the modeline indicates how often it refreshes in
seconds. I think two seconds is good.
#+begin_src lisp
(setf *mode-line-timeout* 2)
#+end_src
** Formatting Options
:PROPERTIES:
:CUSTOM_ID: Mode-Line-Formatting-Options-3n494y814aj0
:END:
Next we get to the content of the modeline. This format follows the
format indicated in the manpage of ~date~.
#+begin_src lisp
(setf *time-modeline-string* "%F %H:%M")
#+end_src
Let’s also indicate how the groupname is displayed.
#+begin_src lisp
(setf *group-format* "%t")
#+end_src
The window format should display first its window number, then its
titled, limited to 30 characters.
#+begin_src lisp
(setf *window-format* "%n: %30t")
#+end_src
** Mode-Line Theme
:PROPERTIES:
:CUSTOM_ID: Mode-Line-Mode-Line-Theme-4mm37x814aj0
:END:
The modeline is pretty easy. First, let’s load the ~colors.lisp~ file we just created:
#+begin_src lisp
(load "~/.stumpwm.d/colors.lisp")
#+end_src
Next, we can set some colors for the modeline. Let’s set the
background of the modeline to Nord1 and the foreground to Nord5, I
think this is a pretty good combination.
#+begin_src lisp
(setf *mode-line-background-color* phundrak-nord1
,*mode-line-foreground-color* phundrak-nord5)
#+end_src
We /could/ also use some borders in the modeline. But we won’t. Let’s
still set its color to Nord1, just in case.
#+begin_src lisp
(setf *mode-line-border-color* phundrak-nord1
,*mode-line-border-width* 0)
#+end_src
** Mode-Line Modules
:PROPERTIES:
:CUSTOM_ID: Mode-Line-Mode-Line-Modules-tlihmy814aj0
:END:
Here are some modules that we will load for the modeline:
#+name: modeline-modules
| Module Name | Why Do I Need It? |
|------------------+--------------------------------------------------|
| battery-portable | Get information on the battery level of a laptop |
| cpu | Get the CPU usage |
| mpd | Display MPD’s status |
| mem | Get the memory usage |
#+name: gen-load-modeline-modules
#+header: :wrap src lisp
#+begin_src emacs-lisp :var modules=modeline-modules
(mapconcat (lambda (module)
(format "(load-module \"%s\")" (car module)))
modules
"\n")
#+end_src
#+RESULTS[20a1d5d9c6e0136d6a130b1c1b4bd4d742aead8a]: gen-load-modeline-modules
#+begin_src lisp
(load-module "battery-portable")
(load-module "cpu")
(load-module "mpd")
(load-module "mem")
#+end_src
We need to set some variables so modules can be displayed correctly.
Note that the character between the font switchers in the second CPU
formatter is U+E082, which symbolizes the CPU.
#+begin_src lisp
(setf cpu::*cpu-modeline-fmt* "%c"
cpu::*cpu-usage-modeline-fmt* "^f2^f0^[~A~2D%^]"
mem::*mem-modeline-fmt* "%a%p"
mpd:*mpd-modeline-fmt* "%a - %t"
mpd:*mpd-status-fmt* "%a - %t"
,*hidden-window-color* "^**"
,*mode-line-highlight-template* "«~A»")
#+end_src
** Generating the Mode-Line
:PROPERTIES:
:CUSTOM_ID: Modeline-Generating-the-Mode-Line-daw8qp814aj0
:END:
We can indicate what to display in our modeline. Each formatter will
be separated by a Powerline separator with the code point ~0xE0B0~ in
the font I am using (see §[[#Theme-Fonts-28pc8141v5j0]]).
#+name: modeline-format
| Formatter | What it does | Command? |
|------------------+-------------------------------------------------------+----------|
| ~%g~ | Display list of groups | |
| ~%W~ | Display list of windows in the current group and head | |
| ~^>~ | Rest of the modeline align to the right | |
| ~docker-running~ | Display number of docker containers currently running | yes |
| ~mu-unread~ | Display number of unread emails | yes |
| ~%m~ | Display current MPD song | |
| ~%C~ | Display CPU usage | |
| ~%M~ | Display RAM usage | |
| ~%B~ | Display battery status | |
| ~%d~ | Display date | |
#+name: modeline-format-gen
#+begin_src emacs-lisp :var elements=modeline-format :exports none
(mapcar (lambda (element)
(cons (format "\"%s\""
(string-replace (regexp-quote "~")
""
(car element)))
(string= "yes" (caddr element))))
elements)
#+end_src
#+RESULTS[4246baab1293d54bcd2223590f274152f24934c3]: modeline-format-gen
: (("%g") ("%W") ("^>") ("docker-running" . t) ("mu-unread" . t) ("%m") ("%C") ("%M") ("%B") ("%d"))
#+begin_src lisp :noweb yes
(defvar *mode-line-formatter-list*
'<>
"List of formatters for the modeline.")
#+end_src
As you can see, ~generate-modeline~ generates the string defining
~*screen-mode-line-format*~ from the list of formatters we gave it with
the table [[modeline-format]].
#+begin_src lisp
(defun generate-modeline (elements &optional not-invertedp rightp)
"Generate a modeline for StumpWM.
ELEMENTS should be a list of `cons'es which `car' is the modeline
formatter or the shell command to run, and their `cdr' is either nil
when the `car' is a formatter and t when it is a shell command."
(when elements
(cons (format nil
" ^[~A^]^(:bg \"~A\") "
(format nil "^(:fg \"~A\")^(:bg \"~A\")^f1~A^f0"
(if (xor not-invertedp rightp) phundrak-nord1 phundrak-nord3)
(if (xor not-invertedp rightp) phundrak-nord3 phundrak-nord1)
(if rightp "" ""))
(if not-invertedp phundrak-nord3 phundrak-nord1))
(let* ((current-element (car elements))
(formatter (car current-element))
(commandp (cdr current-element)))
(cons (if commandp
`(:eval (run-shell-command ,formatter t))
(format nil "~A" formatter))
(generate-modeline (cdr elements)
(not not-invertedp)
(if (string= "^>" (caar elements)) t rightp)))))))
#+end_src
It is then easy to define a command that can call this function and
set this variable so we can sort of reload the mode-line.
#+begin_src lisp
(defcommand reload-modeline () ()
"Reload modeline."
(sb-thread:make-thread
(lambda ()
(setf *screen-mode-line-format*
(cdr (generate-modeline *mode-line-formatter-list*))))))
#+end_src
And actually, let’s reload the modeline immediately.
#+begin_src lisp
(reload-modeline)
#+end_src
** TODO Investigate why ~stumptray~ acts up :noexport:
:PROPERTIES:
:CUSTOM_ID: Modeline-Investigate-why-stumptray-doesn-t-work-0juh13g0m6j0
:END:
Systray overlaps with the far-right part of the modeline.
# Also, let’s enable a system tray.
# #+begin_src lisp
# (load-module "stumptray")
# (stumptray::stumptray)
# #+end_src
# Don’t forget to run src_lisp[:exports code]{(ql:quickload :xembed)} in
# ~sbcl~ at least once to install its dependencies.
* Groups and placement
:PROPERTIES:
:CUSTOM_ID: Placement-mhc3sr21v5j0
:header-args:lisp: :mkdirp yes :tangle ~/.stumpwm.d/placement.lisp :noweb yes
:END:
I’ve 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:
#+name: list-groups
| Groups | Number | Windows | Type |
|---------+--------+---------+--------|
| [EMACS] | 1 | | Tiling |
| [TERM] | 2 | | Tiling |
| [WWW] | 3 | | Tiling |
| [PRIV] | 4 | | Tiling |
| [FILES] | 5 | Nemo | Tiling |
#+name: gen-groups
#+header: :exports none
#+begin_src emacs-lisp :var groups=list-groups
(let ((make-group (lambda (group &optional first-p)
(let ((group-name (car group))
(group-type (nth 3 group)))
(format "%S" `(,(if first-p 'grename
(pcase group-type
("Dynamic" 'gnewbg-dynamic)
("Floating" 'gnewbg-float)
(otherwise 'gnewbg)))
,group-name))))))
(string-join `(,(funcall make-group (car groups) t)
,@(mapcar (lambda (group)
(funcall make-group group))
(cdr groups)))
"\n"))
#+end_src
#+RESULTS[7c58aa808149319f652155b05c9324b841324f23]: gen-groups
: (grename "[EMACS]")
: (gnewbg "[TERM]")
: (gnewbg "[WWW]")
: (gnewbg "[PRIV]")
: (gnewbg "[FILES]")
Groups are specified this way:
#+begin_src lisp
(when *initializing*
<>)
#+end_src
By default, if nothing is specified as per the group type, my groups
are manual tiling groups. Otherwise, as you can see above, they can
also be dynamic tiling groups or floating groups.
Next, let’s make sure no previous window placement rule is in place,
this will avoid unexpected and hard-to-debug behavior.
#+begin_src lisp
(clear-window-placement-rules)
#+end_src
As you can see in the table [[list-groups]] above, I also indicated my
window placement preferences. For now, they all rely on the window’s
class, so it will be pretty straightforward to the corresponding code.
#+name: gen-rules
#+header: :wrap src lisp
#+begin_src emacs-lisp :var rules=list-groups
(require 'seq)
(let ((output "")
(rules (seq-filter (lambda (rule) rule)
(mapcar (lambda (line)
(let ((classes (caddr line)))
(unless (string= "" classes)
(cons
(split-string classes "," t "[[:space:]]*")
(car line)))))
rules))))
(progn
(seq-do (lambda (rule)
(let ((classes (car rule))
(group (cdr rule)))
(dolist (class classes)
(setf output (format "%s\n%s"
`(define-frame-preference ,(format "\"%s\"" group)
(nil t t :class ,(format "\"%s\"" class)))
output)))))
rules)
output))
#+end_src
This can be written this way:
#+RESULTS[6121129a3a3aa07c6decc78cb46a1520dcae1156]: gen-rules
#+begin_src lisp
(define-frame-preference "[FILES]" (nil t t :class "Nemo"))
#+end_src
Dynamic groups, if any is created, should have a split ratio of half
of the available space.
#+begin_src lisp
(setf *dynamic-group-master-split-ratio* 1/2)
#+end_src
* Theme
:PROPERTIES:
:CUSTOM_ID: Theme-1x3c2u31v5j0
:header-args:lisp: :mkdirp yes :tangle ~/.stumpwm.d/theme.lisp :noweb yes
:END:
As in the modeline file, the first thing we’ll do is to load our colors.
#+begin_src lisp
(load "~/.stumpwm.d/colors.lisp")
#+end_src
We can now go onto more serious business.
** Fonts
:PROPERTIES:
:CUSTOM_ID: Theme-Fonts-28pc8141v5j0
:END:
This gave me quite the headache when I tried to set this up: in order
to use TTF fonts (note: it is not possible to use OTF fonts, see
below), 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!
There’s a quickfix available while we wait for ~clx-truetype~ to be once
again available: clone it in quicklisp’s local projects. You will
obviously need to have quicklisp installed (for that, follow the
[[https://www.quicklisp.org/beta/#installation][official instructions]]), then execute the following shell commands:
#+begin_src sh :dir ~/quicklisp/local-projects
cd ~/quicklisp/local-projects/
git clone https://github.com/lihebi/clx-truetype.git
#+end_src
This will make ~clx-truetype~ available to quicklisp, and you can run
again src_lisp[:exports code]{(ql:quickload :clx-truetype)} without an
issue (running it again is necessary to install its dependencies).
In order for it to work, install [[https://www.quicklisp.org/beta/][quicklisp]] and don’t forget to run
src_lisp[:exports code]{(ql:add-to-init-file)} so it is loaded each
time you start your Lisp interpreter. ~SBCL~ should be your CommonLisp
interpreter of choice since StumpWM is generally compiled with it. The
main advantage is also that SBCL supports multithreading, unlike
clisp. In case StumpWM doesn’t find your font, spin up SBCL and
execute the following lines:
#+begin_src lisp :tangle no
(ql:quickload :clx-truetype)
(xft:cache-fonts)
#+end_src
If you want a list of font families available, you can execute the
following:
#+begin_src lisp :tangle no
(clx-truetype:get-font-families)
#+end_src
If you want to know the subfamilies of a certain family, you can
execute this:
#+begin_src lisp :tangle no
(clx-truetype:get-font-subfamilies "Family Name")
#+end_src
Now that this is out of the way, let’s add two lines so we can use TTF
fonts:
#+begin_src lisp
(ql:quickload :clx-truetype)
(load-module "ttf-fonts")
#+end_src
The documentation says we should be able to also use OTF fonts, but so
far I’ve had no luck loading one.
Loading more than one font to use
some fallback fonts also doesn’t seem to work, unlike specified in the
documentation (I wanted to use a CJK font, but it doesn’t appear to
work), we need to manually change the font used which isn’t very
user-friendly, especially if you might have CJK characters appear in
otherwise regular text.
Something that didn’t click immediately for me (and I think StumpWM’s
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! However, be aware
*the second font and further aren’t fallback fonts*. They are additional
fonts you can switch to manually through the use of ~^f~ (~~ being
the desired’s font index in the 0-indexed font list). But if a font
cannot render a character, it will simply display an empty rectangle
instead of falling back to another font. That’s annoying… Here is my
list of fonts I want loaded:
#+name: list-fonts
| Family | Subfamily | Size |
|----------------------------------+-----------+------|
| Unifont-JP | Regular | 10 |
| DejaVu Sans Mono for Powerline | Book | 8.5 |
| siji | Medium | 10 |
| FantasqueSansMono Nerd Font Mono | Regular | 9.5 |
#+name: gen-fonts
#+header: :wrap src lisp
#+begin_src emacs-lisp :var fonts=list-fonts
(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 "))
#+end_src
The code equivalent of this table can be seen below:
#+RESULTS[1693001a9a9c0e274a9b7097665e9795783ae8a2]: gen-fonts
#+begin_src lisp
(set-font `(,(make-instance 'xft:font :family "Unifont-JP" :subfamily "Regular" :size 10 :antialias t)
,(make-instance 'xft:font :family "DejaVu Sans Mono for Powerline" :subfamily "Book" :size 8.5 :antialias t)
,(make-instance 'xft:font :family "siji" :subfamily "Medium" :size 10 :antialias t)
,(make-instance 'xft:font :family "FantasqueSansMono Nerd Font Mono" :subfamily "Regular" :size 9.5 :antialias t)))
#+end_src
As far as I know, Unifont is the only font I’ve tested that displays
monospaced Japanese characters in StumpWM. I tried DejaVu, IBM Plex,
and a couple of others but only this one works correctly. DejaVu is
here for the Powerline separator. If you know of another monospaced
font that displays Japanese characters, or even better CJK characters,
please tell me! My email address is at the bottom of this webpage.
** Colors
:PROPERTIES:
:CUSTOM_ID: Theme-Colors-ctlclb51v5j0
:END:
We can now set a couple of colors for StumpWM. Not that we will see
them often since I don’t like borders on my windows, but in case I
want to get them back, they’ll be nice to have.
#+begin_src lisp
(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)
#+end_src
Let’s also set the colors of the message and input windows:
#+begin_src lisp
(set-fg-color phundrak-nord4)
(set-bg-color phundrak-nord1)
#+end_src
As I said, I don’t like borders, so I’ll remove them. I’ll still keep
the window’s title bar available when it’s 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.
#+begin_src lisp
(setf *normal-border-width* 0
,*float-window-border* 0
,*float-window-title-height* 15
,*window-border-style* :none
,*window-format* "%n:%t")
#+end_src
I also have a [[https://github.com/Phundrak/stumpwm/tree/feature/no-hardcoded-which-key-format][StumpWM fork]] that introduces two new variables for
customizing which-key keybindings. I submitted a [[https://github.com/stumpwm/stumpwm/pull/931][pull request]], so it
might come one day to StumpWM.
#+begin_src lisp
(setf *key-seq-color* "^2")
(setf *which-key-format* (concat *key-seq-color* "*~5a^n ~a"))
#+end_src
** Message and Input Windows
:PROPERTIES:
:CUSTOM_ID: Theme-Message-and-Input-Windows-jxwhch51v5j0
:END:
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.
#+begin_src lisp
(setf *input-window-gravity* :top
,*message-window-padding* 10
,*message-window-y-padding* 10
,*message-window-gravity* :top)
#+end_src
** Gaps Between Frames
:PROPERTIES:
:CUSTOM_ID: Theme-Gaps-Between-Frames-bqngnt51v5j0
:END:
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, let’s load a module dedicated to gaps
in StumpWM:
#+begin_src lisp
(load-module "swm-gaps")
#+end_src
Now that this is done, I can now set some variables bound to this
package.
#+begin_src lisp
(setf swm-gaps:*head-gaps-size* 0
swm-gaps:*inner-gaps-size* 5
swm-gaps:*outer-gaps-size* 40)
#+end_src
Finally, let’s enable our gaps:
#+begin_src lisp
(when *initializing*
(swm-gaps:toggle-gaps))
#+end_src
* Keybinds
:PROPERTIES:
:CUSTOM_ID: Keybinds-c6wgf961v5j0
:header-args:lisp: :mkdirp yes :tangle ~/.stumpwm.d/keybindings.lisp :noweb yes
:END:
Buckle up, this chapter is going to be *long*, because me loves LOTS of keybinds.
First, let’s declare again we are using the default package ~stumpwm~:
#+begin_src lisp
(in-package :stumpwm)
#+end_src
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
src_lisp[:exports code]{(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 I’ll bind it to ~*top-map*~. But it’s 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.
I’d 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 doesn’t 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.
#+begin_src lisp
(set-prefix-key (my/kbd "s-SPC"))
#+end_src
Also, let’s enable ~which-key~:
#+begin_src lisp
(which-key-mode)
#+end_src
Lastly, before we get more into details, keep in mind that I use the
[[https://bepo.fr][bépo]] layout, as I often say in my different documents. This means the
characters found in the numbers’ row when pressing shift are actually
the numbers themselves. Also, some characters are not recognized as is
by ~kbd~, so we need to use a special name (not fun…). Below are the
following characters:
#+name: number-to-char-table
| Number | Character |
|--------+-----------|
| 1 | ~"~ |
| 2 | ~«~ |
| 3 | ~»~ |
| 4 | ~(~ |
| 5 | ~)~ |
| 6 | ~@~ |
| 7 | ~+~ |
| 8 | ~-~ |
| 9 | ~/~ |
| 0 | ~*~ |
So if you see any weird keybind involving these characters, this is
because of my layout.
Something a bit annoying though is Lisp doesn’t know some characters
by their actual name, rather by another one that I find too long and
too bothersome to remember. So here’s a list, if you see any of the
characters on the left column in my config, with the function
described below, my actual config will use their name as specified in
the right column.
#+name: tbl-char-to-name
| Character | Name |
|-----------+------------------|
| ~«~ | ~guillemotleft~ |
| ~»~ | ~guillemotright~ |
#+name: chars-table-to-list
#+header: :exports none :noweb yes :results verbatim
#+begin_src emacs-lisp :var chars=tbl-char-to-name
;; chars
(let ((filter (lambda (str)
(replace-regexp-in-string "^~\\|~$" "" str))))
(mapcar (lambda (row)
`(,(apply filter `(,(car row))) . ,(apply filter `(,(cadr row)))))
chars))
#+end_src
#+RESULTS[8ceb9b882276931ad0dba7dcf38d163f7674f547]: chars-table-to-list
: (("«" . "guillemotleft") ("»" . "guillemotright"))
To convert these characters, I have my own macro which is a wrapper
around the function ~kbd~.
#+name: my-kbd-defun
#+begin_src lisp :noweb yes
(defun my/kbd (keys)
"Prepares KEYS for function `stumpwm:kbd'.
If a character declared in the car of a member of the variable char,
it is replaced with its cdr. This allows the user to input characters
such as « or » and have them replaced with their actual name when
`stumpwm:kbd' is called."
(kbd (let ((chars '<>))
(dolist (row chars keys)
(setf keys (cl-ppcre:regex-replace-all (car row) keys (cdr row)))))))
#+end_src
#+header: :exports none
#+begin_src lisp :noweb yes :tangle ~/.stumpwm.d/bluetooth.lisp
<>
#+end_src
#+header: :exports none
#+begin_src lisp :noweb yes :tangle ~/.stumpwm.d/utilities.lisp
<>
#+end_src
** Applications
:PROPERTIES:
:CUSTOM_ID: Keybinds-Applications-2t512k00w5j0
:END:
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, let’s create my ~rofi~ scripts keymap.
#+name: rofi-scripts
#+caption: ~*my-rofi-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~ |
Here’s the equivalent in Common Lisp.
#+begin_src lisp
(defvar *my-rofi-keymap*
(let ((m (make-sparse-keymap)))
<>
m))
#+end_src
Let’s also create a keymap for screenshots.
#+name: screenshot-keymap
#+caption: ~*my-screenshot-keymap*~
| Keychord | Function |
|----------+----------------------------|
| ~d~ | ~exec flameshot gui -d 3000~ |
| ~s~ | ~exec flameshot full~ |
| ~S~ | ~exec flameshot gui~ |
Here’s the equivalent in Common Lisp.
#+begin_src lisp
(defvar *my-screenshot-keymap*
(let ((m (make-sparse-keymap)))
<>
m))
#+end_src
We can now define our applications keymap which will reference both
the above keymaps.
#+name: application-keymap
#+caption: ~*my-applications-keymap*~
| Keychord | Function |
|----------+-------------------------|
| ~b~ | ~firefox~ |
| ~B~ | ~exec qutebrowser~ |
| ~d~ | ~exec discord~ |
| ~e~ | ~exec emacsclient -c~ |
| ~g~ | ~exec gimp~ |
| ~n~ | ~exec nemo~ |
| ~r~ | ~'*my-rofi-keymap*~ |
| ~s~ | ~'*my-screenshot-keymap*~ |
| ~w~ | ~exec select-pape~ |
This translates to:
#+begin_src lisp
(defvar *my-applications-keymap*
(let ((m (make-sparse-keymap)))
<>
m))
#+end_src
The application keymap can now be bound to the root map like so:
#+begin_src lisp
(define-key *root-map* (my/kbd "a") '*my-applications-keymap*)
#+end_src
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,
and the ~XF86Mail~ key opens mu4e in Emacs.
#+begin_src lisp
(define-key *top-map* (my/kbd "s-RET") "term")
(define-key *top-map* (my/kbd "Print") '*my-screenshot-keymap*)
(define-key *top-map* (my/kbd "XF86Mail") "exec emacsclient -c -e \"(mu4e)\"")
#+end_src
** End of Session, Powering Off, and the Likes
:PROPERTIES:
:CUSTOM_ID: Keybinds-End-of-Session-Powering-Off-and-the-Likes-mgz02z40w5j0
:END:
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.
#+name: end-session-keymap
| Keychord | Function |
|----------+-------------------|
| ~q~ | ~end-session~ |
| ~l~ | ~logout~ |
| ~s~ | ~suspend-computer~ |
| ~S~ | ~shutdown-computer~ |
| ~r~ | ~loadrc~ |
| ~R~ | ~restart-hard~ |
| ~C-r~ | ~restart-computer~ |
This translates to:
#+begin_src lisp
(defvar *my-end-session-keymap*
(let ((m (make-sparse-keymap)))
<>
m))
#+end_src
Which is bound in the root map to ~q~:
#+begin_src lisp
(define-key *root-map* (my/kbd "q") '*my-end-session-keymap*)
#+end_src
** Groups
:PROPERTIES:
:CUSTOM_ID: Keybinds-Groups-daxfwu40a7j0
:END:
A basic keybind I need for groups is to be able to switch from one
another. I’m very used to the ability of being able to jump between
them with the keybind Super + /number of the group/, so let’s define
this:
#+name: group-keybind-gen
#+header: :noweb no :results verbatim :exports none :var convert="no"
#+begin_src emacs-lisp :var groups=list-groups mod="s" action="gselect" map="*top-map*" convert="yes"
(mapconcat (lambda (group)
(let ((group-nbr (nth 1 group)))
(format "%S" `(define-key
,(make-symbol map)
(my/kbd ,(format "%s-%s"
mod
(if (string= "yes" convert)
(format "<>" group-nbr)
(number-to-string group-nbr))))
,(format "%s %d" action group-nbr)))))
groups
"\n")
#+end_src
#+RESULTS[282113d17ea21f02dc7c87059d787e62b5886b16]: group-keybind-gen
: "(define-key *top-map* (my/kbd \"s-1\") \"gselect 1\")
: (define-key *top-map* (my/kbd \"s-2\") \"gselect 2\")
: (define-key *top-map* (my/kbd \"s-3\") \"gselect 3\")
: (define-key *top-map* (my/kbd \"s-4\") \"gselect 4\")
: (define-key *top-map* (my/kbd \"s-5\") \"gselect 5\")"
#+header: :cache yes :noweb yes :wrap src lisp
#+begin_src emacs-lisp
<>
#+end_src
#+RESULTS[35268fd11d1fe4fa4065c98a0b7bc723a56c09a7]:
#+begin_src lisp
(define-key *top-map* (my/kbd "s-<>") "gselect 1")
(define-key *top-map* (my/kbd "s-<>") "gselect 2")
(define-key *top-map* (my/kbd "s-<>") "gselect 3")
(define-key *top-map* (my/kbd "s-<>") "gselect 4")
(define-key *top-map* (my/kbd "s-<>") "gselect 5")
#+end_src
Another batch of keybinds I use a lot is keybinds to send the
currently active window to another group, using Super + Shift + /number
of the group/. As mentioned before, due to my keyboard layout Shift +
/number/ is actually just /number/ for me (e.g. Shift + ~"~ results in ~1~),
so there’s no need to convert the group number to another character.
#+begin_src emacs-lisp :wrap src lisp
<>
#+end_src
#+RESULTS[bd9b499dfad0fdf1f54db146ded1a60c6674f8ea]:
#+begin_src lisp
(define-key *top-map* (my/kbd "s-1") "gmove-and-follow 1")
(define-key *top-map* (my/kbd "s-2") "gmove-and-follow 2")
(define-key *top-map* (my/kbd "s-3") "gmove-and-follow 3")
(define-key *top-map* (my/kbd "s-4") "gmove-and-follow 4")
(define-key *top-map* (my/kbd "s-5") "gmove-and-follow 5")
#+end_src
If I want to send a window to another group without following it, I’ll
use ~s-S-C-~, which gives us the following:
#+begin_src emacs-lisp :wrap src lisp
<>
#+end_src
#+RESULTS[68f918c24a6cc0efa1503ed57841c40f4ec2ec4a]:
#+begin_src lisp
(define-key *top-map* (my/kbd "s-C-1") "gmove-and-follow 1")
(define-key *top-map* (my/kbd "s-C-2") "gmove-and-follow 2")
(define-key *top-map* (my/kbd "s-C-3") "gmove-and-follow 3")
(define-key *top-map* (my/kbd "s-C-4") "gmove-and-follow 4")
(define-key *top-map* (my/kbd "s-C-5") "gmove-and-follow 5")
#+end_src
And if I want to bring the windows of another group into the current
group, I’ll use ~s-C-~:
#+begin_src emacs-lisp :wrap src lisp :exports results
<>
#+end_src
#+RESULTS[36b3ded631cd0bad400c4847f6937cde4f11b4b7]:
#+begin_src lisp
(define-key *top-map* (my/kbd "s-C-<>") "gmove-and-follow 1")
(define-key *top-map* (my/kbd "s-C-<>") "gmove-and-follow 2")
(define-key *top-map* (my/kbd "s-C-<>") "gmove-and-follow 3")
(define-key *top-map* (my/kbd "s-C-<>") "gmove-and-follow 4")
(define-key *top-map* (my/kbd "s-C-<>") "gmove-and-follow 5")
#+end_src
StumpWM also has already a nice keymap for managing groups called
~*groups-map*~, so let’s bind it to ~*root-map*~ too! (It’s actually
already bound, but since I plan on erasing ~*root-map*~ in the near
future before binding stuff to it, I prefer to bind it already)
#+begin_src lisp
(define-key *root-map* (my/kbd "g") '*groups-map*)
#+end_src
And a binding to ~vgroups~ is done on ~*groups-map*~ in order to regroup
similar keybinds.
#+begin_src lisp
(define-key *groups-map* (my/kbd "G") "vgroups")
#+end_src
I grew accustomed to ~s-ESC~ bringing me to the previous group when
using AwesomeWM, so let’s define that:
#+begin_src lisp
(define-key *top-map* (my/kbd "s-ESC") "gother")
#+end_src
** Frames and Windows management
:PROPERTIES:
:CUSTOM_ID: Keybinds-Frames-and-Windows-management-g4s6j371v5j0
:END:
As you’ll 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, let’s define the
keymap ~*my-frames-float-keymap*~, with keybinds dedicated to actions
related with floating windows and frames.
#+name: frames-float
#+caption: ~*my-frames-float-keymap*~
| Keychord | Function |
|----------+----------------|
| ~f~ | ~float-this~ |
| ~F~ | ~unfloat-this~ |
| ~u~ | ~unfloat-this~ |
| ~C-f~ | ~flatten-floats~ |
We can now pass onto ~*my-frames-management-keymap*~. My keybinds are organized this way:
#+name: frames-and-window-management
#+caption: ~*my-frames-management-keymap*~
| 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~ |
| ~/~ | ~hsplit-and-focus~ |
| ~-~ | ~vsplit-and-focus~ |
| ~h~ | ~hsplit~ |
| ~v~ | ~vsplit~ |
| ~H~ | ~hsplit-equally~ |
| ~V~ | ~vsplit-equally~ |
| ~.~ | ~iresize~ |
| ~+~ | ~balance-frames~ |
| ~d~ | ~remove-split~ |
| ~D~ | ~only~ |
| ~e~ | ~expose~ |
| ~f~ | ~fullscreen~ |
| ~F~ | ~'*my-frames-float-keymap*~ |
| ~i~ | ~info~ |
| ~I~ | ~show-window-properties~ |
| ~m~ | ~meta~ |
| ~s~ | ~sibling~ |
| ~u~ | ~next-urgent~ |
| ~U~ | ~unmaximize~ |
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*~.
#+begin_src lisp
(defvar *my-frames-float-keymap*
(let ((m (make-sparse-keymap)))
<>
m))
(defvar *my-frames-management-keymap*
(let ((m (make-sparse-keymap)))
<>
m))
#+end_src
Let’s bind ~*my-frames-management-keymap*~ in ~*root-keymap*~:
#+begin_src lisp
(define-key *root-map* (my/kbd "w") '*my-frames-management-keymap*)
#+end_src
That way, if we want for instance to split our current frame
vertically, we’ll 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:
#+name: top-window-map
| 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:
#+begin_src lisp
<>
#+end_src
Being a [[https://bepo.fr/wiki/Accueil][bépo layout]] user, the ~hjkl~ keys don’t 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:
#+begin_src lisp
(define-interactive-keymap (iresize tile-group) (:on-enter #'setup-iresize
:on-exit #'resize-unhide
:abort-if #'abort-resize-p
:exit-on ((kbd "RET") (kbd "ESC")
(kbd "C-g") (kbd "q")))
((my/kbd "c") "resize-direction left")
((my/kbd "t") "resize-direction down")
((my/kbd "s") "resize-direction up")
((my/kbd "r") "resize-direction right"))
#+end_src
As with groups management, I grew used to ~s-TAB~ in AwesomeWM bringing
me back to the previously focused window, and I also grew used to ~s-o~
doing the same thing.
#+begin_src lisp
(define-key *top-map* (my/kbd "s-TAB") "other-window")
(define-key *top-map* (my/kbd "s-o") "other-window")
#+end_src
** Windows management
:PROPERTIES:
:CUSTOM_ID: Keybinds-Windows-management-ylf903j0x5j0
:END:
When it comes to windows management, I will treat them a bit like I do
with Emacs’ buffers.
#+name: window-management
#+caption: ~*my-buffers-management-keymap*~
| Keychord | Function |
|----------+-------------------------|
| ~b~ | ~windowlist~ |
| ~d~ | ~delete-window~ |
| ~D~ | ~delete-window-and-frame~ |
| ~k~ | ~kill-window~ |
| ~n~ | ~next~ |
| ~o~ | ~other-window~ |
| ~p~ | ~prev~ |
#+begin_src lisp
(defvar *my-buffers-management-keymap*
(let ((m (make-sparse-keymap)))
<>
m))
(define-key *root-map* (my/kbd "b") '*my-buffers-management-keymap*)
#+end_src
** Media and Media Control
:PROPERTIES:
:CUSTOM_ID: Keybinds-Media-and-Media-Control-hbv5uk91z5j0
:END:
My music is managed through MPD, and I often use ~playerctl~ commands in
order to interact with it without any GUI application. So, we’ll see a
lot of its usage here, and numerous commands used here come from the
~mpd~ minor mode loaded [[#Init-file-l3q4snd1u5j0][above]].
First, let’s declare an interactive keymap in order to easily change
several times in a row either the current song playing or the volume
of MPD.
#+name: inter-mpc
#+caption: Interactive keybinds for ~mpc~
| Keychord | Function |
|----------+-----------------|
| ~c~ | ~mpd-prev~ |
| ~t~ | ~mpd-volume-down~ |
| ~s~ | ~mpd-volume-up~ |
| ~r~ | ~mpd-next~ |
This can be translated in CommonLisp as:
#+begin_src lisp
<>
#+end_src
We need to indicate also how much the volume is affected by
~mpd-volume-down~ and ~mpd-volume-up~.
#+begin_src lisp
(setf *mpd-volume-step* 2)
#+end_src
Another one will be defined for the general audio of my computer. And
I know it isn’t technically media keybinds, but I’ll add in keybinds
for my screen’s backlight.
#+name: inter-media
#+caption: Interactive keybinds for general media interaction
| Keys | Function |
|------+--------------------------------------|
| ~c~ | ~exec xbacklight -perceived -dec 2~ |
| ~t~ | ~exec amixer -q set Master 2%- unmute~ |
| ~s~ | ~exec amixer -q set Master 2%+ unmute~ |
| ~r~ | ~exec xbacklight -perceived -inc 2~ |
| ~m~ | ~exec amixer -q set Master 1+ toggle~ |
#+begin_src lisp
<>
#+end_src
Then, let’s declare a keymap for our media controls.
#+name: mpd-add-map
#+caption: ~*my-mpd-add-map*~
| Keychord | Function |
|----------+---------------------------|
| ~a~ | ~mpd-search-and-add-artist~ |
| ~A~ | ~mpd-search-and-add-album~ |
| ~f~ | ~mpd-search-and-add-file~ |
| ~F~ | ~mpd-add-file~ |
| ~g~ | ~mpd-search-and-add-genre~ |
| ~t~ | ~mpd-search-and-add-title~ |
#+name: mpd-browse-map
#+caption: ~*my-mpd-browse-map*~
| Keychord | Function |
|----------+---------------------|
| ~a~ | ~mpd-browse-artists~ |
| ~A~ | ~mpd-browse-albums~ |
| ~g~ | ~mpd-browse-genres~ |
| ~p~ | ~mpd-browse-playlist~ |
| ~t~ | ~mpd-browse-tracks~ |
#+name: media-management
#+caption: ~*my-media-keymap*~
| Keychord | Function |
|----------+-----------------------------------|
| ~.~ | ~media-interactive~ |
| ~«~ | ~exec playerctl previous~ |
| ~»~ | ~exec playerctl next~ |
| ~a~ | ~'*my-mpd-add-map*~ |
| ~b~ | ~'*my-mpd-browse-map*~ |
| ~c~ | ~mpd-clear~ |
| ~m~ | ~mpc-interactive~ |
| ~p~ | ~exec playerctl play-pause~ |
| ~s~ | ~exec playerctl stop~ |
| ~u~ | ~mpd-update~ |
| ~n~ | ~exec kitty ncmpcpp -q~ |
| ~v~ | ~exec kitty ncmpcpp -qs visualizer~ |
Let’s translate this table in CommonLisp:
#+begin_src lisp
(defvar *my-mpd-add-map*
(let ((m (make-sparse-keymap)))
<>
m))
(defvar *my-mpd-browse-map*
(let ((m (make-sparse-keymap)))
<>
m))
(defvar *my-media-keymap*
(let ((m (make-sparse-keymap)))
<>
m))
(define-key *root-map* (my/kbd "m") '*my-media-keymap*)
#+end_src
I will also define on ~*top-map*~ some basic volume management keybinds
so that they are immediately accessible. Again, this isn’t technically
media-related, but I’ll add keybinds for my screen’s backlight.
#+name: media-top-level
#+caption: Top-level media keys
| Keychord | Function |
|-----------------------+-----------------------------------|
| ~XF86AudioPlay~ | ~exec playerctl play-pause~ |
| ~XF86AudioPause~ | ~exec playerctl pause~ |
| ~XF86AudioStop~ | ~exec playerctl stop~ |
| ~XF86AudioPrev~ | ~exec playerctl previous~ |
| ~XF86AudioNext~ | ~exec playerctl next~ |
| ~XF86AudioRewind~ | ~exec playerctl position -1~ |
| ~XF86AudioForward~ | ~exec playerctl position +1~ |
| ~XF86AudioRaiseVolume~ | ~exec pamixer -i 2~ |
| ~XF86AudioLowerVolume~ | ~exec pamixer -d 2~ |
| ~XF86AudioMute~ | ~exec pamixer -t~ |
| ~XF86MonBrightnessDown~ | ~exec xbacklight -perceived -dec 2~ |
| ~XF86MonBrightnessUp~ | ~exec xbacklight -perceived -inc 2~ |
#+begin_src lisp
<>
#+end_src
** Misc
:PROPERTIES:
:CUSTOM_ID: Keybinds-Misc-455iuh50w5j0
:END:
Finally, some misc keybinds on the root map which don’t really fit
anywhere else:
#+name: misc-root-map
| Keychord | Function |
|----------+------------|
| ~SPC~ | ~colon~ |
| ~B~ | ~beckon~ |
| ~C-b~ | ~banish~ |
| ~l~ | ~exec plock~ |
| ~r~ | ~reload~ |
#+begin_src lisp
<>
#+end_src
From time to time, I need to switch between different keyboard
layouts, especially to the US Qwerty layout when I’m playing some
games and the bépo layout most of the time. I’ll use the command
~switch-layout~ defined above.
#+name: keyboard-layout-map
| Keychord | Function |
|----------+------------------------------|
| ~b~ | ~exec setxkbmap fr bepo_afnor~ |
| ~u~ | ~exec setxkbmap us~ |
#+begin_src lisp
(defvar *my-keyboard-layout-keymap*
(let ((m (make-sparse-keymap)))
<>
m))
(define-key *root-map* (my/kbd "k") '*my-keyboard-layout-keymap*)
#+end_src
* Utilities
:PROPERTIES:
:CUSTOM_ID: Utilities-vrggajs0z9j0
:header-args:lisp: :mkdirp yes :tangle ~/.stumpwm.d/utilities.lisp :noweb yes
:END:
Part of my configuration is not really related to StumpWM itself, or
rather it adds new behavior StumpWM doesn’t have. ~utilities.lisp~
stores all this code in one place.
** Binwarp
:PROPERTIES:
:CUSTOM_ID: Utilities-Binwarp-0wrbg1v0z9j0
:END:
Binwarp allows the user to control their mouse from the keyboard,
basically eliminating the need for a physical mouse in daily usage of
the workstation (though a physical mouse stays useful for games and
such).
#+begin_src lisp
(load-module "binwarp")
#+end_src
Next, I’ll define my keybinds for when using Binwarp for emulating
mouse clicks as well as bépo-compatible mouse movements. This new
Binwarp mode is now available from the keybind ~s-m~ at top level.
#+begin_src lisp
(binwarp:define-binwarp-mode my-binwarp-mode "s-m" (:map *top-map*)
((my/kbd "SPC") "ratclick 1")
((my/kbd "RET") "ratclick 3")
((my/kbd "c") "binwarp left")
((my/kbd "t") "binwarp down")
((my/kbd "s") "binwarp up")
((my/kbd "r") "binwarp right")
((my/kbd "i") "init-binwarp")
((my/kbd "q") "exit-binwarp"))
#+end_src
** Bluetooth
:PROPERTIES:
:CUSTOM_ID: Utilities-Bluetooth-rns0nr902aj0
:header-args:lisp: :mkdirp yes :tangle ~/.stumpwm.d/bluetooth.lisp :noweb yes
:END:
Although there is a bluetooth module for the modeline, this is about
the extent to which StumpWM can interact with the system’s bluetooth.
However, I wish for some more interecactivity, like powering on and
off bluetooth, connecting to devices and so on.
First, out code relies on ~cl-ppcre~, so let’s quickload it.
#+begin_src lisp
(ql:quickload :cl-ppcre)
#+end_src
Let’s indicate which command we’ll be using.
#+begin_src lisp
(defvar *bluetooth-command* "bluetoothctl"
"Base command for interacting with bluetooth.")
#+end_src
*** Utilities
:PROPERTIES:
:CUSTOM_ID: Utilities-Bluetooth-Utilities-3zicf7k03aj0
:END:
We’ll need a couple of functions that will take care of stuff for us
so we don’t have to repeat ourselves. The first one is a way for us to
share a message. The function ~bluetooth-message~ will first display
~Bluetooth:~ in green, then it will display the message we want it to
display.
#+begin_src lisp
(defun bluetooth-message (&rest message)
(message (format nil
"^2Bluetooth:^7 ~{~A~^ ~}"
message)))
#+end_src
This function is a builder function which will create our commands.
For instance, src_lisp[:exports code]{(bluetooth-make-command "power"
"on")} will return ~"bluetoothctl power on"~ with ~*bluetooth-ctl*~ set as
~"bluetoothctl"~ --- simply put, it joins ~*bluetooth-command*~ with ~args~
with a space as their separator.
#+begin_src lisp
(defun bluetooth-make-command (&rest args)
(format nil
"~a ~{~A~^ ~}"
,*bluetooth-command*
args))
#+end_src
Now we can put ~bluetooth-make-command~ to use with ~bluetooth-command~
which will actually run the result of the former. As you can see, it
also collects the output so we can display it later in another
function.
#+begin_src lisp
(defmacro bluetooth-command (&rest args)
`(run-shell-command (bluetooth-make-command ,@args) t))
#+end_src
Finally, ~bluetooth-message-command~ is the function that both executes
and also displays the result of the bluetooth command we wanted to see
executed. Each argument of the command is a separate string. For
instance, if we want to power on the bluetooth on our device, we can
call src_lisp[:exports code]{(bluetooth-message-command "power"
"on")}.
#+begin_src lisp
(defmacro bluetooth-message-command (&rest args)
`(bluetooth-message (bluetooth-command ,@args)))
#+end_src
*** Toggle Bluetooth On and Off
:PROPERTIES:
:CUSTOM_ID: Utilities-Bluetooth-Toggle-Bluetooth-On-and-Off-9pyfbtd02aj0
:END:
This part is easy. Now that we can call our bluetooth commands easily,
we can easily define how to turn on bluetooth.
#+begin_src lisp
(defcommand bluetooth-turn-on () ()
"Turn on bluetooth."
(bluetooth-message-command "power" "on"))
#+end_src
And how to power it off.
#+begin_src lisp
(defcommand bluetooth-turn-off () ()
"Turn off bluetooth."
(bluetooth-message-command "power" "off"))
#+end_src
*** Bluetooth Devices
:PROPERTIES:
:CUSTOM_ID: Utilities-Bluetooth-Bluetooth-Devices-196gbtd02aj0
:END:
In order to manipulate bluetooth device, which we can represent as a
MAC address and a name, we can create a structure that will make use
of a constructor for simpler use. The constructor
~make-bluetooth-device-from-command~ expects an entry such as ~Device
00:00:00:00:00:00 Home Speaker~. The constructor discards the term
~Device~ and stores the MAC address separately from the rest of the
string which is assumed to be the full name of the device.
#+begin_src lisp
(defstruct (bluetooth-device
(:constructor
make-bluetooth-device (&key (address "")
(name nil)))
(:constructor
make-bluetooth-device-from-command
(&key (raw-name "")
&aux (address (cadr (cl-ppcre:split " " raw-name)))
(full-name (format nil "~{~A~^ ~}" (cddr (cl-ppcre:split " " raw-name)))))))
address
(full-name (progn
(format nil "~{~A~^ ~}" name))))
#+end_src
We can now collect our devices easily.
#+begin_src lisp
(defun bluetooth-get-devices ()
(let ((literal-devices (bluetooth-command "devices")))
(mapcar (lambda (device)
(make-bluetooth-device-from-command :raw-name device))
(cl-ppcre:split "\\n" literal-devices))))
#+end_src
*** Connect to a device
:PROPERTIES:
:CUSTOM_ID: Utilities-Bluetooth-Connect-to-a-device-tjqcf7k03aj0
:END:
When we want to connect to a bluetooth device, we always need
bluetooth turned on, so ~bluetooth-turn-on~ will always be called. Then
the function will attempt to connect to the device specified by the
~device~ argument, whether the argument is a bluetooth structure as
defined above or a plain MAC address.
#+begin_src lisp
(defun bluetooth-connect-device (device)
(progn
(bluetooth-turn-on)
(cond ((bluetooth-device-p device) ;; it is a bluetooth-device structure
(bluetooth-message-command "connect"
(bluetooth-device-address device)))
((stringp device) ;; assume it is a MAC address
(bluetooth-message-command "connect" device))
(t (message (format nil "Cannot work with device ~a" device))))))
#+end_src
The command to connect to a device displays a choice between the
collected bluetooth device and the user only has to select it. It will
then attempt to connect to it.
#+begin_src lisp
(defcommand bluetooth-connect () ()
(sb-thread:make-thread
(lambda ()
(let* ((devices (bluetooth-get-devices))
(choice (cadr (stumpwm:select-from-menu
(stumpwm:current-screen)
(mapcar (lambda (device)
`(,(bluetooth-device-full-name device) ,device))
devices)))))
(bluetooth-connect-device choice)))))
#+end_src
*** Keybinds
:PROPERTIES:
:CUSTOM_ID: Utilities-Bluetooth-Keybinds-gxjaagl05aj0
:END:
It’s all nice and all, but typing manually the commands with ~s-SPC ;~
is a bit tiring, so let’s define our bluetooth keymap which we will
bind to ~s-SPC B~.
#+name: bluetooth-keymap
| Keychord | Command |
|----------+--------------------|
| ~c~ | ~bluetooth-connect~ |
| ~o~ | ~bluetooth-turn-on~ |
| ~O~ | ~bluetooth-turn-off~ |
#+begin_src lisp
(defvar *my-bluetooth-keymap*
(let ((m (make-sparse-keymap)))
<>
m))
(define-key *root-map* (my/kbd "B") '*my-bluetooth-keymap*)
#+end_src
** NetworkManager integration
:PROPERTIES:
:CUSTOM_ID: Utilities-NetworkManager-integration-nm7jxbt0z9j0
:END:
It is possible to have some kind of integration between StumpWM and
NetworkManager. To do so, we have to load the related module, then
create the two keybinds described in [[nm-keybinds]].
#+name: nm-keybinds
#+caption: ~*my-nm-keybinds*~
| Keychord | Command |
|----------+---------------------------|
| ~W~ | ~nm-list-wireless-networks~ |
A call to src_lisp[:exports code]{(ql:quickload :dbus)} is necessary
for this module. Installing the ~dbus~ module in turn requires the
library ~libfixposix~ installed on the user’s machine. On Arch, you can
install it like so using ~paru~:
#+begin_src fish
paru -S libfixposix --noconfirm
#+end_src
#+begin_src lisp
(ql:quickload :dbus)
(load-module "stump-nm")
<>
#+end_src
** Pinentry
:PROPERTIES:
:CUSTOM_ID: Utilities-Pinentry-o6v95fu0z9j0
:END:
Out with GTK2’s pinentry program! Let’s use StumpWM’s! At least that’s
what I’d like to say, but unfortunately there is a bug in the text
reading devices of StumpWM that prevent the user from using modifiers
when entering a password such as AltGr, so I can’t use it : /
#+begin_src lisp
;; (load-module "pinentry")
#+end_src
** Sly
:PROPERTIES:
:CUSTOM_ID: Utilities-Sly-kkok6oi0yaj0
:END:
[[https://github.com/joaotavora/sly][Sly]] is a fork of SLIME with which I can connect StumpWM and Emacs
together. Technically this is already done to some level with
~stumpwm-mode~, but the latter doesn’t provide auto-completion or stuff
like that.
The first thing to do is load ~slynk~, SLY’s server:
#+begin_src lisp
(ql:quickload :slynk)
#+end_src
Now we can define a command to launch the server. I don’t want it to
run all the time, just when I need it.
#+begin_src lisp
(stumpwm:defcommand sly-start-server () ()
"Start a slynk server for sly."
(sb-thread:make-thread (lambda () (slynk:create-server :dont-close t))))
(stumpwm:defcommand sly-stop-server () ()
"Stop current slynk server for sly."
(sb-thread:make-thread (lambda () (slynk:stop-server 4005))))
#+end_src
** ~swm-ssh~
:PROPERTIES:
:CUSTOM_ID: Utilities-swm-ssh-s14ahrs0z9j0
:END:
This module from the contrib repository scans the user’s ssh
configuration file and offers them a quick way of connecting to their
remote hosts.
#+begin_src lisp
(load-module "swm-ssh")
#+end_src
The default terminal needs to be set, otherwise the module will try to
call ~urxvtc~ which is not installed on my system.
#+begin_src lisp
(setq swm-ssh:*swm-ssh-default-term* "kitty")
#+end_src
Now, to call the main command of this module we can define the
following keybind.
#+begin_src lisp
(define-key *root-map* (my/kbd "s") "swm-ssh-menu")
#+end_src
** Systemd
:PROPERTIES:
:CUSTOM_ID: UtilitiesSystemd-rfb6hs30hmj0
:header-args:lisp: :mkdirp yes :tangle ~/.stumpwm.d/systemd.lisp :noweb yes
:END:
I’m currently in the process of writing functions to interact with
Systemd directly through StumpWM. For now, not much work is done, but
it’s a start.
First of all, I have the following function that lists all the system
or user services.
#+begin_src lisp
(defun systemd-get-services (&key user-p)
"Collect all systemd services running.
If USER-P is t, collect user services, otherwise collect system
services.
The value returned is a list of lists. The first element is the
service’s name, the second is its load state, the third the high-level
activation state of the service, and the fourth its low-level
activation state."
(mapcar (lambda (elt)
(multiple-value-bind (_ result)
(ppcre:scan-to-strings "(.*\\.service) *([^ ]+) *([^ ]+) *([^ ]+).*"
elt)
result))
(ppcre:split
" *\\n●? *"
(ppcre:regex-replace
"^ *"
(run-shell-command (concat "systemctl list-units --type service --all -q"
(if user-p " --user" ""))
t)
""))))
#+end_src
The only command I have right now is for listing the system or user
services with ~message~. Unfortunately, if there are too many services,
the list will overflow the screen. I do not know how to fix that yet.
I set the timeout to 600 seconds in order to have all the time in the
world to read the services list. It goes away as soon as something
else appears, such as a ~s-SPC C-g~ since I have ~which-key-mode~ enabled.
#+begin_src lisp
(defcommand systemd-list-services (user-p) ((:y-or-n "User services? "))
(let ((stumpwm::*timeout-wait* 600))
(message (format nil "~{~a~^~&~}"
(mapcar (lambda (service)
(let ((name (aref service 0))
(load (aref service 1))
(active (aref service 2))
(sub (aref service 3)))
(cond ((member load '("not-found" "bad-setting"
"error" "masked")
:test #'string=)
(format nil
"^~A~A^0 ^> Load: ~12@A"
(if (string= "masked" load) 4 1)
name load))
((member active '("failed" "reloading" "activating"
"deactivating" "inactive")
:test #'string=)
(format nil "^~A~A^0 ^>Active: ~12@A"
(case active
("failed" 1)
("inactive" 0)
(t 3))
name
active))
(t (format nil "^2~A^0 ^> Sub: ~12@A" name sub)))))
(systemd-get-services :user-p user-p))))))
#+end_src
* org functions :noexport:
:PROPERTIES:
:CUSTOM_ID: org-functions-syqgzgg0m6j0
:END:
#+name: keybinds-gen
#+header: :wrap "src lisp :exports none" :exports none :noweb yes
#+begin_src emacs-lisp :var map="m" keybinds=media-management
(mapconcat (lambda (keybind)
(format "%s" (let* ((filter (lambda (str)
(replace-regexp-in-string "^~\\|~$" "" str)))
(key (funcall filter (car keybind)))
(function (funcall filter (cadr keybind))))
`(define-key ,map
(my/kbd ,(format "\"%s\"" key))
,(if (string-prefix-p "'" function t)
function
(format "\"%s\"" function))))))
keybinds
"\n")
#+end_src
#+name: interactive-gen
#+begin_src emacs-lisp :var name="inter" keys=inter-mpc
(format "%s"
`(define-interactive-keymap ,name
"\n (:exit-on ((kbd \"RET\") (kbd \"ESC\")"
"\n (kbd \"C-g\") (kbd \"q\")))"
"\n "
,(mapconcat (lambda (keybind)
(format "%s"
(let* ((filter (lambda (str)
(replace-regexp-in-string "^~\\|~$" "" str)))
(key (funcall filter (car keybind)))
(command (funcall filter (cadr keybind))))
`((my/kbd ,(format "\"%s\"" key))
,(format "\"%s\"" command)))))
keys
"\n ")))
#+end_src
#+RESULTS[b7d91bafe659a77aef5059ae17859a7fc715255e]: interactive-gen
#+begin_src lisp
(define-interactive-keymap inter
(:exit-on ((kbd "RET") (kbd "ESC")
(kbd "C-g") (kbd "q")))
((my/kbd "c") "mpd-prev")
((my/kbd "t") "mpd-volume-down")
((my/kbd "s") "mpd-volume-up")
((my/kbd "r") "mpd-next"))
#+end_src
#+name: num-to-char
#+begin_src emacs-lisp :var table=number-to-char-table num=2
(let* ((filter (lambda (str)
(replace-regexp-in-string "^~\\|~$" "" str)))
(char (funcall filter (cadr (assoc num table)))))
(if (string= char "\"")
"\\\""
char))
#+end_src
#+RESULTS[f66a1c4f98f4e8def9867862da252249b6a65749]: num-to-char
: «