UP | HOME
Kristian Alexander P

Kristian Alexander

Free Software Enthusiast | GNU Emacs User

Stumpwm

Published on Mar 19, 2022 by Kristian Alexander P.

Configuration file

header-args: :tangle ~/.config/stumpwm/config :mkdirp t

Header

Needed so Emacs will use lisp major-mode for this file since it’s not using .lisp extension.

;;;; Stumpwm configuration file
;;;
;;; This file is autogenerated, do not edit.
;;; see `https://java281.dynv6.net/~alexforsale/posts/Stumpwm.html' for details

Load quicklisp

Load my installation of quicklisp. The special operator let creates new variable quicklisp-init and executes a series of form (here just runs the macro when) that uses these bindings.

(let ((quicklisp-init
        (merge-pathnames ".local/share/quicklisp/setup.lisp" (user-homedir-pathname))))
  (when (probe-file quicklisp-init)
    (load quicklisp-init)))
  • merge-pathnames is similar to python’s os.path.join().
  • The variable user-homedir-pathname returns a pathname without any name, type or version component (those component are all nil) for the user’s home directory on host (not used here).
  • probe-file tests whether a file exists1

load stumpwm from quicklisp

(ql:quickload :stumpwm)

CANCELLED Swank

  • State “CANCELLED” from [2022-04-10 Sun 02:01]
    use sly instead

Requires swank (from the package slime) in order to run the function swank-loader. Again I use the special operator let to dynamically creates the stumpwm command swank, this command can be run inside stumpwm for toggling swank.

(require :swank)
(swank-loader:init)
(let ((server-running nil))
  (defcommand swank () ()
    "Toggle the swank server on/off"
    (if server-running
        (progn
          (swank:stop-server 4004)
          (echo-string
           (current-screen)
           "Stopping swank.")
          (setf server-running nil))
        (progn
          (swank:create-server :port 4004
                               :style swank:*communication-style*
                               :dont-close t)
          (echo-string
           (current-screen)
           "Starting swank. M-x slime-connect RET RET, then (in-package stumpwm).")
          (setf server-running t)))))

Sly

It has more features enabled without too much customization.

(ql:quickload :slynk)
(let ((server-running nil))
  (defcommand slynk () ()
    "Toggle the slynk server on/off"
    (if server-running
        (progn
          (slynk:stop-server 4003)
          (echo "Stopping slynk on port 4003.")
          (setf server-running nil))
        (progn
          (slynk:create-server :port 4003 :dont-close t)
          (echo "Starting slynk on port 4003.")
          (setf server-running t)))))

Stumpwm configuration

The macro in-package causes the package named to be the current packages; that is, the value of *package*. In short it’ll let us to call run-shell-command instead of stuwmpm:run-shell-command, this also works for variables.

(in-package :stumpwm)
*mouse-focus-policy*

This variable decide how the mouse affects input focus. Possible values are :ignore, :sloppy and :click.

(setf *mouse-focus-policy* :click)
*run-or-raise-all-groups*

Defines how the run-or-raise function searches, in this case all groups.

(setf *run-or-raise-all-groups* t)
*run-or-raise-all-screens*

Same as above, but to screens.

(setf *run-or-raise-all-screens* t)
*fload-window-modifier*
The keyboard modifier to use for resize and move floating windows without clicking on the top border. Valid values are :META :ALT :HYPER :SUPER, :ALTGR and :NUMLOCK.

Functions

empty-directory-p

(defun empty-directory-p (path)
  (and (null (directory (concatenate 'string path "/*")))
       (null (directory (concatenate 'string path "/*/")))))

check is process is active

(defun +config/is-active (process)
  "Returns PROCESS pid if it's active."
  (let ((+process
          (parse-integer
           (run-shell-command
            (format nil "pgrep ~A" process) t) :junk-allowed t)))
    (when +process)
    (print +process)))

Determine running OS

A simple method is by querying the output of uname -s:

(defparameter *os*
  (remove #\Newline
          (string-downcase
           (run-shell-command "uname -s" t)))
  "The currently running operating system in lowercase.")

pass-to-emacsclient

(defun pass-to-emacsclient (arg)
  (let ((emacs-cmd '("emacsclient")))
    (when *chemacs-profile*
      (push (format nil "-s ~A" *chemacs-profile*) (cdr (last emacs-cmd))))
    (push (format nil "--eval ~A" arg) (cdr (last emacs-cmd)))
    (run-shell-command (format nil "~A" emacs-cmd) t)))

emacs-emms

(defun emacs-emms (cmd)
  "Runs CMD for `Emacs's `EMMS'."
  (let ((is-running
          (pass-to-emacsclient "'emms-player-paused-p'")))
    (when is-running
      (cond ((string= (string-downcase cmd)
                      (or "play" "pause"))
             (pass-to-emacsclient "'(emms-pause)'"))
            ((string= (string-downcase cmd)
                      "next")
             (pass-to-emacsclient "'(emms-next)'"))
            ((string= (string-downcase cmd)
                      (or "prev" "previous"))
             (pass-to-emacsclient "'(emms-previous)'"))))))

restart computer

(defun restart-computer ()
  "Restart the computer."
  (run-shell-command "sudo -A reboot"))

shutdown computer

(defun shutdown-computer ()
  "Restart the computer."
  (run-shell-command "sudo -A poweroff"))

Global Variables

chemacs

(defparameter *chemacs-profile* nil
  "The current Chemacs profile")

(let ((chemacs-profile
       (cond ((probe-file
               (merge-pathnames
                ".config/chemacs/profile"
                (user-homedir-pathname)))
              (merge-pathnames
               ".config/chemacs/profile"
               (user-homedir-pathname)))
             ((probe-file
               (merge-pathnames
                ".emacs-profile"
                (user-homedir-pathname)))
              (merge-pathnames
               ".emacs-profile"
               (user-homedir-pathname))))))
  (when chemacs-profile
    (setf *chemacs-profile*
          (uiop:read-file-line chemacs-profile))))

emacs daemon name

(if *chemacs-profile*
    (defvar *emacs-daemon-name* *chemacs-profile*)
  (defvar *emacs-daemon-name* "default"))

set-transient-gravity

(setf set-transient-gravity :center)

Load contrib modules

Check several directories for modules, and add it to the stumpwm module-dir using the function set-module-dir.

(let ((module-dir
        (or (probe-file "/usr/share/stumpwm/contrib")
            (probe-file "/usr/local/share/stumpwm/contrib")
            (probe-file (merge-pathnames ".local/share/stumpwm/contrib"
                                         (user-homedir-pathname)))
            (probe-file (merge-pathnames ".config/stumpwm/modules"
                                         (user-homedir-pathname))))))
  (set-module-dir module-dir))

The modules directory itself is a git repository of the stumpwm-contrib. Currently the only dependency from the contrib modules is stumpish, an interactive shell for stumpwm.

if ! test -d ~/.config/stumpwm/modules; then
    git clone https://github.com/stumpwm/stumpwm-contrib.git ~/.config/stumpwm/modules
fi

Also create a symlink for stumpish to ~/.local/bin

[ ! -d ~/.local/bin ] && mkdir -p ~/.local/bin
pushd ~/.local/bin
[ ! -L stumpish ] && ln -sv ../../.config/stumpwm/modules/util/stumpish/stumpish stumpish
popd

additional module directory

This is for personal module

  • Set the variable

    Use this variable to set the directory, if there’s more than one directory, consider keep them inside one single directory and add the parent directory instead, since the set-module-dir function is recursive.

    (defvar *additional-module-dir* "~/Projects/lisp/playerctl-mode-line")
    
  • add when not empty
    (when (not (empty-directory-p *additional-module-dir*))
      (set-module-dir *additional-module-dir*))
    

Colors

Set the default color to base16-materia, palette from wal as a fallback if there’s no wal cache file yet.

(defvar default-colors
  '("#263238"
    "#EC5F67"
    "#8BD649"
    "#FFCC00"
    "#89DDFF"
    "#82AAFF"
    "#80CBC4"
    "#CDD3DE"
    "#707880"
    "#EC5F67"
    "#8BD649"
    "#FFCC00"
    "#89DDFF"
    "#82AAFF"
    "#80CBC4"
    "#FFFFFF"))

set the wal file

(defvar wal-colors-path "~/.cache/wal/colors")

Function to get the colors palette

(defun get-color-palette ()
  "Read colors from wal cache file or from `default-colors'."
  (let ((wal-colors (when (probe-file wal-colors-path)
                      (uiop:read-file-lines wal-colors-path))))
    (or wal-colors default-colors)))

Function to associate names into the colors palette

(defun get-color (name)
  "Map the colors from `get-color-palette'."
  (let ((colors (mapcar 'cons
                        '("color0"
                          "color1"
                          "color2"
                          "color3"
                          "color4"
                          "color5"
                          "color6"
                          "color7"
                          "color8"
                          "color9"
                          "color10"
                          "color11"
                          "color12"
                          "color13"
                          "color14"
                          "color15")
                        (get-color-palette))))
    (cdr (assoc name colors :test 'equal))))

Color Variable

(setf *background* (get-color "color0"))
(setf *foreground* (get-color "color7"))
(setf *color0* (get-color "color0"))
(setf *color1* (get-color "color1"))
(setf *color2* (get-color "color2"))
(setf *color3* (get-color "color3"))
(setf *color4* (get-color "color4"))
(setf *color5* (get-color "color5"))
(setf *color6* (get-color "color6"))
(setf *color7* (get-color "color7"))
(setf *color8* (get-color "color8"))
(setf *color9* (get-color "color9"))
(setf *color10* (get-color "color10"))
(setf *color11* (get-color "color11"))
(setf *color12* (get-color "color12"))
(setf *color13* (get-color "color13"))
(setf *color14* (get-color "color14"))
(setf *color15* (get-color "color15"))

Map the colors into *colors variable

(setq *colors*
      `(,*color0* ,*color1* ,*color2* ,*color3* ,*color4* ,*color5* ,*color6* ,*color7* ,*color8* ,*color9*))

Update the Color Map

(update-color-map (current-screen))

Set the foreground color for the message bar and input

(set-fg-color *color7*)

Set the background color for the message bar and input

(set-bg-color *color0*)

Set the border color for the message bar and input

(set-border-color *color2*)

Set the border color for focused windows.

This is only used when there is more than one frame.

(set-focus-color *color3*)

Set the border color for windows without focus.

This is only used when there is more than one frame.

(set-unfocus-color *color3*)

Set the border color for focused windows in a float group

(set-float-focus-color *color3*)

Set the border color for windows without focus in a float group.

(set-float-unfocus-color *color3*)

Autostarts

The variable *initializing* is only set to T when stumpwm is starting up. Which means once per session. Useful for starting up applications that only needed at startup.

(when *initializing*
  (progn
    (run-shell-command "xsetroot -cursor_name left_ptr")
    (run-shell-command "nm-applet")
    (run-shell-command "dunst")
    (run-shell-command "picom")
    (run-shell-command "unclutter")
    (run-shell-command "blueman-applet")
    (run-shell-command "xsettingsd")
    (run-shell-command "udiskie -t")
    (run-shell-command "~/.fehbg")
    (grename "Term") ; rename from Default
    (gnewbg "Dev")
    (gnewbg "Web")
    (gnewbg-float "Float") ; floating window
    (which-key-mode)
    (run-shell-command "emacs --daemon --debug-init")))

This also rename the Default frame to Term. And create additional frame named Dev, and Web. Usually these are all I needed in a session and my muscle memory set them to 1, 2, and 3 respectively.

mode-line helper functions

  • show-battery-charge

    (defun show-battery-charge ()
      (cond ((string= "freebsd" *os*)
             (let ((raw-battery
                     (remove
                      #\Newline
                      (run-shell-command "apm -l" t))))
               (format nil "~D%" raw-battery)))
            ((string= "linux" *os*)
             (let ((raw-battery
                     (run-shell-command
                      "acpi | cut -d, -f2" t)))
               (substitute #\Space #\Newline raw-battery)))))
    
  • show-battery-state

    (defun show-battery-state ()
      (cond ((string= "freebsd" *os*)
             (let ((ac-status
                     (remove
                      #\Newline
                      (run-shell-command
                       "apm -a" t))))
               (cond ((equal "0" ac-status)
                      (format nil "Discharging"))
                     ((equal "1" ac-status)
                      (format nil "Charging"))
                     ((equal "2" ac-status)
                      (format nil "Backup Power")))))
            ((string= "linux" *os*)
             (let ((raw-battery
                     (run-shell-command
                      "acpi | cut -d: -f2 | cut -d, -f1" t)))
               (substitute #\Space #\Newline raw-battery)))))
    
  • show-brightness-value

    (defun show-brightness-value ()
      (cond ((string= "freebsd" *os*)
             (let ((current-brightness
                     (remove
                      #\Newline
                      (run-shell-command
                       "backlight |sed 's/.*: //'" t))))
               (format nil "~A%" current-brightness)))
            ((string= "linux" *os*)
             (let ((max-brightness
                     (parse-integer
                      (run-shell-command
                       "brightnessctl max" t)))
                   (current-brightness
                     (parse-integer
                      (run-shell-command
                       "brightnessctl get" t))))
               (floor (* (/ current-brightness max-brightness) 100))))))
    
  • show-current-volume

    (defun show-current-volume ()
      (let ((current-volume
              (run-shell-command "pamixer --get-volume-human" t)))
        (substitute #\Space #\Newline current-volume)))
    

maildir contrib package

The maildir:*maildir-alist* already set “Mail” to “~/Mail“, which I don’t use, so I clear it first before adding my mail directories. Also the path listed must contains the mail directory cur, new, and tmp.

(load-module "maildir")
(setq maildir:*maildir-alist* nil)
;; most of my linux devices
(when (not (empty-directory-p  "~/.mail/gmail/inbox/"))
  (push (cons "gmail" (merge-pathnames ".mail/gmail/inbox/" (user-homedir-pathname)))
        maildir:*maildir-alist*))
(when (not (empty-directory-p  "~/.mail/ymail/inbox/"))
  (push (cons "ymail" (merge-pathnames ".mail/ymail/inbox/" (user-homedir-pathname)))
        maildir:*maildir-alist*))
(when (not (empty-directory-p  "~/.mail/hotmail/inbox/"))
  (push (cons "hotmail" (merge-pathnames ".mail/hotmail/inbox/" (user-homedir-pathname)))
        maildir:*maildir-alist*))
(when (not (empty-directory-p  "~/.mail/yahoo/inbox/"))
  (push (cons "yahoo" (merge-pathnames ".mail/yahoo/inbox/" (user-homedir-pathname)))
        maildir:*maildir-alist*))

;; freebsd
(let ((user-data-root (concat "/data/freebsd/"(getenv "USER"))))
  (when (string= "freebsd" *os*)
    (progn
      (when
          (not
           (empty-directory-p
            (concat
             user-data-root
             "/.mail/gmail/Inbox/")))
        (push (cons "gmail" (concat user-data-root "/.mail/gmail/Inbox/"))
              maildir:*maildir-alist*))
      (when
          (not
           (empty-directory-p
            (concat
             user-data-root
             "/.mail/ymail/Inbox/")))
        (push (cons "ymail" (concat user-data-root "/.mail/ymail/Inbox/"))
              maildir:*maildir-alist*))
      (when
          (not
           (empty-directory-p
            (concat
             user-data-root
             "/.mail/hotmail/Inbox/")))
        (push (cons "hotmail" (concat user-data-root "/.mail/hotmail/Inbox/"))
              maildir:*maildir-alist*))
      (when
          (not
           (empty-directory-p
            (concat
             user-data-root
             "/.mail/yahoo/Inbox/")))
        (push (cons "yahoo" (concat user-data-root "/.mail/yahoo/Inbox/"))
              maildir:*maildir-alist*)))))

MPD contrib package

This package also provides mode-line format.

(load-module "mpd")
(mpd:mpd-connect)

The mode-line format is by default %S [%s;%r;%F]: %a - %A - %t (%n/%p):

%S
Playing if playing, Paused if paused, else Stopped
%s
S if shuffle is on, _ otherwise
%r
R if repeat is on, _ otherwise
%F
F=# if crossfade is set, _ otherwise
%a
Artist
%A
Album
%t
Title
  • Other variables
    *mpd-volume-step*

    Default to 5

    (setf mpd:*mpd-volume-step* 1)
    
    *mpd-socket*
    Default to #(127 0 0 1)
    *mpd-port*
    Default to 6600
    *mpd-password*
    Set this if the mpd uses password

The mode-line variable

The mode line is a bar that runs across either the top or bottom of a head and is used to display information. By default the mode line displays the list of windows, similar to the output C-t w produces.

(setf *window-format* "%m%t")
(setf *screen-mode-line-format*
      (list
       "[^B^3%n^b] ^4%W"
       "^>"
       ;;"%m"
       '(:eval (format nil "^5|Volume: ~D" (show-current-volume)))
       '(:eval (when (or (not (empty-directory-p "/sys/class/backlight")) (not (empty-directory-p "/dev/backlight"))) (format nil "^6|Backlight: ~D%" (show-brightness-value))))
       '(:eval (when (or (not (empty-directory-p "/sys/class/power_supply")) (not (eq 255 (parse-integer (remove #\Newline (run-shell-command "apm -l" t)))))) (format nil "^5|Battery:~D" (show-battery-charge))))
       '(:eval (when (or (not (empty-directory-p "/sys/class/power_supply")) (not (eq 255 (parse-integer (remove #\Newline (run-shell-command "apm -l" t)))))) (format nil " ~D" (show-battery-state))))
       "^6|%D" ;maildir
       "^5|%d"
       ))
(setf *time-modeline-string* "%a %b %e %k:%M")
(setf *mode-line-timeout* 2)
(setf *mode-line-background-color* *background*)
(setf *mode-line-foreground-color* *foreground*)
(setf *mode-line-border-color* *color2*)
(setf *mode-line-border-width* 0)
(enable-mode-line (current-screen) (current-head) t)
*window-format*
This variable decides how the window list is formatted. It is a string with the following formatting options:
%m
Draw a # if the window is marked.
%n
Substitutes the window’s number translated via *window-number-map*, if there are more windows than *window-number-map* then will use the window-number.
%s
Substitute the window’s status. * means current window, + means last window, and - means any other window.
%c
Substitute the window’s class.
%t
Substitute the window’s name.
%i
Substitute the window’s resource ID.
*screen-mode-line-format*

This variable describes what will be displayed on the modeline for each screen. Turn it on with the function TOGGLE-MODE-LINE or the mode-line command. It is a list where each element may be a string, a symbol, or a list. For a symbol its value is used. For a list of the form (:eval FORM) FORM is evaluated and the result is used as a mode line element. If it is a string the string is printed with the following formatting options:

%n
The current group’s name.
%W
List all windows on the current head of the current group using *WINDOW-FORMAT*
%d
Using *TIME-MODELINE-STRING*, print the time.

This variable also uses with the color commands (see info (stumpwm)Colors):

^B
Turn on bright colors.
^b
Turn off bright colors.
^<digit>
Set the color to <digit> in the *colors variable.
%D
This is from the maildir contrib package.
%m
From the mpd contrib package.
*time-modeline-string*
The default time value to pass to the modeline. This is using the GNU coreutils format, see info "(coreutils)Date conversion specifiers" for details.
*mode-line-timeouts*
The modeline updates after each command, when a new window appears or an existing one disappears, and on a timer. This variable controls how many seconds elapse between each update. If this variable is changed while the modeline is visible, you must toggle the modeline to update timer.
*mode-line-background-color*
The background color for the modeline.
*mode-line-foreground-color*
The foreground color for the modeline.
*mode-line-border-color*
The color of the modeline border.
*mode-line-border-width*
The border width.

The last line (enable-mode-line (current-screen) (current-head) t) runs the mode-line.

Input and message.

*input-window-gravity*

This variable controls where the input window appears. Valid values are: :top-left, :top-right, :bottom-left, :bottom-right, :center, :top, :left, :right, and :bottom.

(setf *input-window-gravity* :top-right)
*message-window-padding*

The number of pixels that pad the text in the message window.

(setf *message-window-padding* 10)
*message-window-y-padding*

The number of pixels that pad the text in the message window vertically.

(setf *message-window-y-padding* 10)
message-window-gravity

This variable controls where the message window appears. Valid values are: :top-left, :top-right, :bottom-left, :bottom-right, :center, :top, :left, :right, and :bottom.

(setf *message-window-gravity* :top-right)
*message-window-input-gravity*

This variable controls where the message window appears when the input window is being displayed. Valid values are: :top-left, :top-right, :bottom-left, :bottom-right, :center, :top, :left, :right, and :bottom.

(setf *message-window-input-gravity* :top-left)
set-msg-border-width

Set the border width for the message bar, input bar and frame indicator.

(set-msg-border-width 0)

Window Appearance

*maxsize-border-width*

The width in pixels given to the borders of windows with maxsize or ratio hints.

(setf *maxsize-border-width* 0)
*transient-border-width*

The width in pixels given to the borders of transient or pop-up windows.

(setf *transient-border-width* 0)
*window-border-style*

This controls the appearance of the border around windows. Valid values are: :thick, :thin, :tight, and :none.

(setf *window-border-style* :none)

Font

In order to use TTF fonts, we need an additional package ttf-fonts, which is available in the contrib repository, or from quicklisp.

(ql:quickload :ttf-fonts)

This package also depends on clx-truetype which sadly is not in quicklisp repository, add it manually to the local-projects directory inside your quicklisp installation directory (when not set usually in ~/quicklisp)

*font-dir*

This is empty by default, so populate it first

(setf xft:*font-dirs* `("/usr/share/fonts/"
                        "/usr/local/share/fonts"
                        ,(merge-pathnames ".local/share/fonts"
                                          (user-homedir-pathname))))

The Keybindings

There are several variables that defines when a keybinding is used:

*root-map*
Known as the “prefix-map”.
*top-map*
This is where you’ll find the bindings for the “prefix-map”.
*group-map*
The keymap that group related key bindings sit on.
*group-top-map*
An alist of the top level maps for each group type.
*exchange-window-map*
The keymap that exchange-window key bindings sit on.

Sets the prefix

Here I’m using s-SPC (Super Key and the spacebar).

;; change the prefix key to something else
(set-prefix-key (kbd "s-SPC"))

The kbd itself is a function specific to stumpwm.

Sparse Keymaps

The function make-sparse-keymap used to create a new list of bindings in the key binding tree.

  • TODO screenshot
    • State “TODO” from [2022-04-10 Sun 03:26]

    For now I’ll use flameshot. But eventually I’ll use either the contrib module or using my own script.

    (defvar *my-screenshot-keymap*
      (let ((m (make-sparse-keymap)))
        (define-key m (kbd "d") "exec flameshot gui -d 3000")
        (define-key m (kbd "s") "exec flameshot full")
        (define-key m (kbd "S") "exec flameshot gui")
        m))
    
  • End-session

    This uses the end-session contrib module.

    ;;(load-module "end-session")
    (defvar *my-end-session-keymap*
      (let ((m (make-sparse-keymap)))
        (define-key m (kbd "q") "quit")
        ;;(define-key m (kbd "l") "logout")
        ;;(define-key m (kbd "s") "suspend-computer")
        (define-key m (kbd "S") "shutdown-computer")
        (define-key m (kbd "r") "loadrc")
        (define-key m (kbd "R") "reload")
        (define-key m (kbd "s-r") "restart-computer")
        m))
    

    And bind it to C-s-q (that Control, Shift, and q).

    (define-key *top-map* (kbd "C-s-q") '*my-end-session-keymap*)
    
  • Describe function
    (defvar *my-describe-bindings*
      (let ((m (make-sparse-keymap)))
        (define-key m (kbd "v") "describe-variable")
        (define-key m (kbd "f") "describe-function")
        (define-key m (kbd "c") "describe-command")
        (define-key m (kbd "k") "describe-key")
        m))
    
  • float keymap
    (defvar *my-frames-float-keymap*
      (let ((m (make-sparse-keymap)))
        (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 "s-f") "flatten-floats")
        m))
    
  • frame management
    (defvar *my-frames-management-keymap*
      (let ((m (make-sparse-keymap)))
        (define-key m (kbd "C-b") "move-focus left")
        (define-key m (kbd "C-n") "move-focus down")
        (define-key m (kbd "C-p") "move-focus up")
        (define-key m (kbd "C-f") "move-focus right")
        (define-key m (kbd "C-B") "move-window left")
        (define-key m (kbd "C-N") "move-window down")
        (define-key m (kbd "C-P") "move-window up")
        (define-key m (kbd "C-F") "move-window right")
        (define-key m (kbd "M-b") "exchange-direction left")
        (define-key m (kbd "M-n") "exchange-direction down")
        (define-key m (kbd "M-p") "exchange-direction up")
        (define-key m (kbd "M-f") "exchange-direction right")
        (define-key m (kbd "/") "hsplit-and-focus")
        (define-key m (kbd "-") "vsplit-and-focus")
        (define-key m (kbd "h") "hsplit")
        (define-key m (kbd "v") "vsplit")
        (define-key m (kbd "H") "hsplit-equally")
        (define-key m (kbd "V") "vsplit-equally")
        (define-key m (kbd ".") "iresize")
        (define-key m (kbd "+") "balance-frames")
        (define-key m (kbd "d") "remove-split")
        (define-key m (kbd "D") "only")
        (define-key m (kbd "e") "expose")
        (define-key m (kbd "f") "fullscreen")
        (define-key m (kbd "s-w") '*my-frames-float-keymap*)
        (define-key m (kbd "i") "info")
        (define-key m (kbd "I") "show-window-properties")
        (define-key m (kbd "m") "meta")
        (define-key m (kbd "s") "sibling")
        (define-key m (kbd "u") "next-urgent")
        (define-key m (kbd "U") "unmaximize")
        (define-key m (kbd "k") "delete-window")
        (define-key m (kbd "b") "windowlist-by-class")
        (define-key m (kbd "DEL") "repack-window-numbers")
        (define-key m (kbd "RET") "expose")
        m))
    
  • apps
    (defvar *my-app-bindings*
      (let ((m (make-sparse-keymap)))
        (define-key m (kbd "e") "emacs")
        (define-key m (kbd "w") "web")
        (define-key m (kbd "s-w") "exec $TERMINAL -e nmtui")
        (define-key m (kbd "g") "exec gimp")
        (define-key m (kbd "f") "exec thunar")
        (define-key m (kbd "s-p") "exec rofi-pass")
        (define-key m (kbd "p") "exec picard")
        (define-key m (kbd "h") "exec hakuneko-desktop")
        (define-key m (kbd "v") "exec vlc")
        (define-key m (kbd "S") "exec *my-screenshot-keymap*")
        (define-key m (kbd "t") "exec emacsclient -c -e \"(telega)\"")
        (define-key m (kbd "T") "exec telegram-desktop")
        m))
    

TODO Undefine default keybindings

  • State “TODO” from [2022-04-10 Sun 03:22]

Custom functions, commands and macros

  • colon1

    This is a custom command from the default configuration that prompt the user for an interactive command. The first arg is an optional initial contents.

    (defcommand colon1 (&optional (initial "")) (:rest)
      (let ((cmd (read-one-line (current-screen) ": " :initial-input initial)))
        (when cmd
          (eval-command cmd t))))
    

    Note that in stumpwm there’s also the command colon by default.

  • make-web-jump

    Also from the default configuration, currently sets for DuckDuckGo and IMDB.

    (defmacro make-web-jump (name prefix)
      `(defcommand ,(intern name) (search) ((:rest ,(concatenate 'string name " search: ")))
         (nsubstitute #\+ #\Space search)
         (run-shell-command (concatenate 'string ,prefix search))))
    
    (make-web-jump "duckduckgo" "firefox https://duckduckgo.com/?q=")
    (make-web-jump "imdb" "firefox http://www.imdb.com/find?q=")
    
  • Split window
    (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))
    
  • emacs

    Will start Emacs when there’s no server with the name *emacs-daemon-name*, else will start daemonized Emacs.

    (defcommand emacs () ()
      "Run or raise Emacs."
      (let ((cmd
              (if *chemacs-profile*
                  (format nil "emacsclient -s ~A -c -a \"emacs --daemon\"" *emacs-daemon-name*)
                  (format nil "emacsclient -c -a \"emacs --daemon\""))))
        (run-shell-command cmd)))
    
  • Browser

    Use the environment variable ${BROWSER} if set.

    (let ((browser (or (getenv "BROWSER")
                       "firefox")))
      (setf *web-browser* browser))
    
    (defcommand web () ()
      "Run or raise browser."
      (run-or-raise *web-browser* `(:class ,*web-browser*) t nil))
    
  • Terminal Emulator

    Although XTerm is my default terminal-emulator, I sometimes try tinkering with other programs, so I set the $TERMINAL environment variable to set my current terminal-emulator.

    (let ((terminal (or (getenv "TERMINAL")
                        "xterm")))
      (setf *terminal-emulator* terminal))
    
    (defcommand terminal () ()
      (run-or-raise *terminal-emulator* `(:class ,*terminal-emulator*)))
    

    And my default keybinding is s-RET.

    (define-key *top-map* (kbd "s-RET") "terminal")
    

Keybinding Tree

This is what in Emacs called “keychord” (I think). So, s-h v is for the function describe-variable and so on. Also note that since I’m already use s-d h this means I’m not using the usual window-manager vim keybindings (h, j, k, l). Also since “h” is actually already bound in *top-map* I need to unbind it in order to set it for *my-describe-bindings*.

  • Top Map

    This is the keybinding that resides on the *top-map*

    • *my-describe-bindings*
      (define-key *top-map* (kbd "s-h") '*my-describe-bindings*)
      
    • Media Keys
      • Volume

        Using PulseAudio.

        (define-key *top-map* (kbd "XF86AudioLowerVolume") "exec pactl -- set-sink-volume @DEFAULT_SINK@ -2%")
        (define-key *top-map* (kbd "XF86AudioRaiseVolume") "exec pactl -- set-sink-volume @DEFAULT_SINK@ +2%")
        (define-key *top-map* (kbd "XF86AudioMute") "exec pactl -- set-sink-mute @DEFAULT_SINK@ toggle")
        
      • Audio Player
        (define-key *top-map* (kbd "XF86AudioPlay") "exec playerctl play-pause")
        (define-key *top-map* (kbd "XF86AudioNext") "exec playerctl next")
        (define-key *top-map* (kbd "XF86AudioPrev") "exec playerctl previous")
        
      • Backlight
        (define-key *top-map* (kbd "XF86MonBrightnessDown") "exec brightnessctl set 1%-")
        (define-key *top-map* (kbd "XF86MonBrightnessUp") "exec brightnessctl set +1%")
        
    • Picom transparency
      (define-key *top-map* (kbd "s-F2") "exec picom-trans -c -5")
      (define-key *top-map* (kbd "s-F3") "exec picom-trans -c +5")
      
    • rofi
      (define-key *top-map* (kbd "s-d") "exec rofi -show drun")
      
    • colon

      I use this often enough it deserve it’s own key.

      (define-key *top-map* (kbd "s-M-x") "colon")
      
    • Mail

      Bind it to XF86Mail

      (define-key *top-map* (kbd "XF86Mail") "exec emacsclient -c -e \"(notmuch)\"")
      
    • Web

      Bound to XF86MyComputer

      (define-key *top-map* (kbd "XF86MyComputer") "web")
      
    • Group selections
      (define-key *top-map* (kbd "s-1") "gselect 1")
      (define-key *top-map* (kbd "s-2") "gselect 2")
      (define-key *top-map* (kbd "s-3") "gselect 3")
      (define-key *top-map* (kbd "s-4") "gselect 4")
      (define-key *top-map* (kbd "s-5") "gselect 5")
      (define-key *top-map* (kbd "s-6") "gselect 6")
      (define-key *top-map* (kbd "s-7") "gselect 7")
      (define-key *top-map* (kbd "s-8") "gselect 8")
      (define-key *top-map* (kbd "s-9") "gselect 9")
      (define-key *top-map* (kbd "s-0") "gselect 10")
      
      (define-key *top-map* (kbd "s-TAB") "gother")
      
    • Group move
      (define-key *top-map* (kbd "s-!") "gmove-and-follow 1")
      (define-key *top-map* (kbd "s-@") "gmove-and-follow 2")
      (define-key *top-map* (kbd "s-#") "gmove-and-follow 3")
      (define-key *top-map* (kbd "s-$") "gmove-and-follow 4")
      (define-key *top-map* (kbd "s-%") "gmove-and-follow 5")
      (define-key *top-map* (kbd "s-^") "gmove-and-follow 6")
      (define-key *top-map* (kbd "s-&") "gmove-and-follow 7")
      (define-key *top-map* (kbd "s-*") "gmove-and-follow 8")
      (define-key *top-map* (kbd "s-(") "gmove-and-follow 9")
      (define-key *top-map* (kbd "s-)") "gmove-and-follow 10")
      
    • Binding for *my-frame-management-keymap*
      (define-key *top-map* (kbd "s-w") '*my-frames-management-keymap*)
      
    • Binding for *my-app-bindings
      (define-key *top-map* (kbd "s-x") '*my-app-bindings*)
      
    • escape-key
      (define-key *top-map* (kbd "s-quoteleft") "send-raw-key")
      
    • Screenshot

      Most keyboard have the Print key, so I’ll bind it there

      (define-key *top-map* (kbd "Print") '*my-screenshot-keymap*)
      
    • Frame movement

      Using Emacs keys

      (define-key *top-map* (kbd "s-p") "move-focus up")
      (define-key *top-map* (kbd "s-n") "move-focus down")
      (define-key *top-map* (kbd "s-f") "move-focus right")
      (define-key *top-map* (kbd "s-b") "move-focus left")
      

      Using numbers

      (define-key *top-map* (kbd "s-M-0") "pull 0")
      (define-key *top-map* (kbd "s-M-1") "pull 1")
      (define-key *top-map* (kbd "s-M-2") "pull 2")
      (define-key *top-map* (kbd "s-M-3") "pull 3")
      (define-key *top-map* (kbd "s-M-4") "pull 4")
      (define-key *top-map* (kbd "s-M-5") "pull 5")
      (define-key *top-map* (kbd "s-M-6") "pull 6")
      (define-key *top-map* (kbd "s-M-7") "pull 7")
      (define-key *top-map* (kbd "s-M-8") "pull 8")
      (define-key *top-map* (kbd "s-M-9") "pull 9")
      
  • Group Map
    • vgroups
      (define-key *groups-map* (kbd "G") "vgroups")
      
  • Root Map
    • Binding for *group-map*

      This is already set by default.

      (define-key *root-map* (kbd "g") '*groups-map*)
      
    • Slynk
      (define-key *root-map* (kbd "s-s") "slynk")
      

Function key

This keymap is for keyboard with no media keys. On my laptop the media key is mapped to the Function keys:

Function Key Media Key Note
F1 XF86Sleep Already working
F2 XF86MonBrightnessDown bound
F3 XF86MonBrightnessUp bound
F4 XF86Display unbound
F5 XF86Mail bound
F6 XF86MyComputer bound
F7 XF86AudioMute bound
F8 XF86AudioLowerVolume bound
F9 XF86AudioRaiseVolume bound
F10 XF86AudioPrev bound
F11 XF86AudioPlay bound
F12 XF86AudioNext bound
  • Media
    (define-key *top-map* (kbd "s-F7") "exec pactl -- set-sink-mute @DEFAULT_SINK@ toggle")
    (define-key *top-map* (kbd "s-F8") "exec pactl -- set-sink-volume @DEFAULT_SINK@ -1%")
    (define-key *top-map* (kbd "s-F9") "exec pactl -- set-sink-volume @DEFAULT_SINK@ +1%")
    (define-key *top-map* (kbd "s-F10") "exec playerctl previous")
    (define-key *top-map* (kbd "s-F11") "exec playerctl play-pause")
    (define-key *top-map* (kbd "s-F12") "exec playerctl next")
    
  • Brightneess
    (define-key *top-map* (kbd "C-s-F2") "exec brightnessctl set 1%-")
    (define-key *top-map* (kbd "C-s-F3") "exec brightnessctl set +1%")
    

Remapped Keys

The define-remapped-keys function can be configured to translate certain familiar top level keybindings to alternative key sequences that are understood by specific applications. One example is to force the Emacs keybinding C-n and C-p to move up and down for any specified application. To use the original application we can use send-raw-key. And for temporarily disable all remapped keys, set the *remapped-keys-enabled-p* to nil.

(define-remapped-keys
    `((,(lambda (win)
          (string-equal "Firefox" (window-class win)))
        ("C-n"   . "Down")
        ("C-p"   . "Up")
        ("C-f"   . "Right")
        ("C-b"   . "Left")
        ("C-v"   . "Next")
        ("M-v"   . "Prior")
        ("M-w"   . "C-c")
        ("C-w"   . "C-x")
        ("C-y"   . "C-v")
        ("M-<"   . "Home")
        ("M->"   . "End")
        ("C-M-b" . "M-Left")
        ("C-M-f" . "M-Right")
        ("M-f"   . "C-Right")
        ("M-b"   . "C-Left")
        ("C-k"   . ("C-S-End" "C-x")))))

CANCELLED imdb and duckduckgo

  • State “CANCELLED” from [2022-04-10 Sun 13:28]
    not used

This is from the default configuration, I don’t use this often.

(define-key *root-map* (kbd "M-s") "duckduckgo")
(define-key *root-map* (kbd "i") "imdb")

Window rules

First we need to clear the previous rules in order to create a new one.

(clear-window-placement-rules)

Frame preferences

(define-frame-preference "Term"
  (1 t t :class "XTerm")
  (1 t t :class "Termite"))

(define-frame-preference "Ardour"
  (0 t   t   :instance "ardour_editor" :type :normal)
  (0 t   t   :title "Ardour - Session Control")
  (0 nil nil :class "XTerm")
  (1 t   nil :type :normal)
  (1 t   t   :instance "ardour_mixer")
  (2 t   t   :instance "jvmetro")
  (1 t   t   :instance "qjackctl")
  (3 t   t   :instance "qjackctl" :role "qjackctlMainForm"))

(define-frame-preference "Dev"
  (2 t t :restore "emacs-editing-dump" :title "...xdvi")
  (2 t t :create "emacs-dump" :class "Emacs"))

(define-frame-preference "Web"
  (3 t t :create "nyxt-dump" :class "Nyxt")
  (3 t t :create "firefox-dump" :class "firefox"))

(define-frame-preference "Files"
  (4 t t :create "thunar-dump" :class "Thunar"))

(define-frame-preference "Message"
  (5 t t :create "telegramdesktop-dump" :class "TelegramDesktop"))

(define-frame-preference "Multimedia"
  (6 t t :create "cheese-dump" :class "Cheese")
  (6 t t :create "spotify-dump" :class "Spotify")
  (6 t t :create "picard-dump" :class "Picard")
  (6 t t :create "mpv-dump" :class "mpv")
  (6 t t :create "vlc-dump" :class "vlc")
  (6 t t :create "hakuneko-dump" :class "hakuneko-desktop")
  (6 t t :create "gimp-dump" :class "Gimp"))

(define-frame-preference "Remote"
  (7 t t :create "virtmanager-dump" :class "Virt-manager")
  (7 t t :create "vncviewer-dump" :class "Vncviewer"))

Running Stumpwm

There are various methods of starting a stumpwm X session. The easiest way is to install the stumpwm package using your distro package manager, then you can run the executable stumpwm using xinit, startx, or via display managers, since the package will likely to also provide a .desktop file. The other (recommended) way is to clone the official repository2, install the required dependencies, build and install stumpwm (usually into /usr/local/ so other user can also use it). Then start it by either startx or xinit, or create additional .desktop file so the display manager can also use it. Other method (the one I use), is to install stumpwm via sbcl

(ql:quickload "stumpwm")

And create a startstumpwm file in /usr/local/bin:

;;;; -*-lisp-*-
(require :stumpwm)
(stumpwm:stumpwm)

This file then called from ~/.xinitrc:

exec sbcl --load /usr/local/bin/startstumpwm

or if using display manager create a .desktop file:

[Desktop Entry]
Name=StumpWM
Type=XSession
Exec=/usr/local/bin/sbcl --load /usr/local/bin/startstumpwm
Name=StumpWM
Comment=Stump window manager

The display manager itself must be configured to also pick up the /usr/local/share/xsession directory, I prefer to put my own compiled packages in /usr/local to avoid conflict with the ones installed by the distro package manager. Example in lightdm is to add:

sessions-directory=/usr/share/lightdm/sessions:/usr/share/xsessions:/usr/share/wayland-sessions:/usr/local/share/xsessions

in the [LightDM] section. This method, and the method using git repository, has the benefit of having all the dependencies in their latest version. This is useful when using sly or slime in emacs.

Footnotes: