Ivory Siege Tower

mobile construct built of thoughts and parentheses

StumpWM Literate Config

project stumpwm stumpwm_config

StumpWM is my Window Manager of choice: manual tiling, lightweight, provisioned by Guix... Lisp! Yes, StumpWM is a Common Lisp software, and it reads a set of S-Expression forms as a configuration file, doing not less than extending a frozen lisp image. Which has an interesting consequence: drag a Slynk server inside, connect to it from an editor session - and you may have a live extending Window Environment where changes you make take place immediately, without any need of WM reload!

This is so cool that it's a pity that I use StumpWM mostly to draw me Emacs and browser interchangeably. But anyway, here I exploit another key feature of StumpWM: it is perfect in getting out of my way, a property of all efficient environments.

The best place to learn configuring Stump, as with a lot of FOSS nowadays, is ArchWiki. Here I leave a literate source of my .stumpwmrc

§ Header Section: Loading Stump

My getting into emacs/lisp world started from spacemacs bundle, and although I've grown out of it long ago, it's actually a secret behind my keybindings, particularly in keystroke prefixes.

Take it or leave it, I use Ctrl-Space as StumpWM prefix, Alt-Space as Emacs/evil-mode prefix, and Win-Space as keyboard layout switch.

Together with core StumpWM package I also use a number of the Contrib extensions. And since now everything is installed by Guix, the header also defines custom module loader macro that works with Stump modules as Guix packages. A post in backlinks elaborates on the problem.

  ;; This is a -*- lisp -*- file.
  (in-package :stumpwm)

  (require "asdf")                        ; to load contrib modules installed with Guix


  (defvar guix-sbcl-path 
    (format nil "~A/.guix-profile/share/common-lisp/sbcl" 
            (uiop:getenv "HOME"))
    "Location of CL packages obtained with Guix.")

  (defmacro load-guixified-module (name &optional package-name)
    "Allows to load StumpWM contrib modules installed via Guix."
    `(run-commands ,(format nil "add-to-load-path ~A/stumpwm-~A/" guix-sbcl-path name)
                   ,(format nil "load-module ~A" (or package-name name))))

  ;; Set ctrl-space as prefix sequence
  (set-prefix-key (kbd "C-SPC"))

§ Appearance

Colors, fonts and default message window position. To get something like this:

Beautiful, isn't it?

Used to like the bitmap Terminus font, but its TTF implementations do not scale well on larger screens (still remains my favorite raster font though).

As for vectorized, Iosevka has very few competitors from my point of view. The following sets up this TrueType font for StumpWM interfaces:

  (load-guixified-module "ttf-fonts")
  (setf xft:*font-dirs*
        (list (format nil "~A/.guix-profile/share/fonts/" (uiop:getenv "HOME"))))
  (setf clx-truetype:+font-cache-filename+
        ;;NOTE: ~/.local/share/fonts -> ~/.guix-profile/share/fonts
        ;; (format nil "~A/.local/share/fonts/font-cache.sexp" (uiop:getenv "HOME")))
        (format nil "~A/.local/share/font-cache.sexp" (uiop:getenv "HOME")))
  (xft:cache-fonts)
  (set-font (make-instance 'xft:font
                           :family "Iosevka Term"
                           :subfamily "Regular"
                           :size (parse-integer (uiop:getenv "STUMPWM_FONT_SIZE")))) ;HACK!

Neon colors are hardcoded in a "RetroWavy" feeling. Look here in the manual and also at this repo for extra colorscheme hints.

  ;;
  ;; Colorscheme
  (setf *colors*
        '("#000019"
          "#ff5555"
          "#a7f070"
          "#73eff7"
          "#63a6f6"
          "#ffb0ff"
          "#b7d7ec"
          "#f0f0ff"))

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

  (defmacro my-stump-set-color (val color)
    "Similar to `set-any-color', but without updating colors."
    `(dolist (s *screen-list*)
       (setf (,val s) (alloc-color s ,color))))

  (my-stump-set-color screen-fg-color "#73eff7")
  (my-stump-set-color screen-bg-color "#000019")
  (my-stump-set-color screen-focus-color  "#d57dff")
  (my-stump-set-color screen-unfocus-color "#000019")
  (my-stump-set-color screen-border-color "#8d74d4")
  (my-stump-set-color screen-float-focus-color "#b7d7ec")
  (my-stump-set-color screen-float-unfocus-color "#000019")
  (update-colors-all-screens)
  ;;
  ;; Grabbed mouse pointer
  (setq
   ,*grab-pointer-character* 40
   ,*grab-pointer-character-mask* 41
   ,*grab-pointer-foreground* (hex-to-xlib-color "#3db270")
   ,*grab-pointer-background* (hex-to-xlib-color "#2c53ca"))
  ;;
  ;; Gravity
  (set-normal-gravity :center)
  (setf *message-window-gravity* :center)
  (setf *input-window-gravity* :center)
  ;;
  ;; Window Border: tight (transparent gaps)
  (setf *window-border-style* :tight)
  ;;
  ;; Frame outline width set to 0 because with compton compositor it is
  ;; not erased correctly:
  (set-frame-outline-width 0)

§ Behavior

TODO: Refactor this section; comment on it...

Core StumpWM system commands and macro definitions. Time to read the FAQ.

§ StumpWM Input Tweaks

Display StumpWM controlling key sequence during input.

  ;; Message after a part of key sequence.
  (defun key-seq-msg (key key-seq cmd)
    "Show a message with current incomplete key sequence."
    (declare (ignore key))
    (or (eq *top-map* *resize-map*)
        (stringp cmd)
        (let ((*message-window-gravity* :center))
          (message "~A" (print-key-seq (reverse key-seq))))))

  (add-hook *key-press-hook* 'key-seq-msg)

§ Window Placement Rules

  ;; Clear rules
  (clear-window-placement-rules)
  
  ;;Set the mouse policy to focus follows mouse;
  (setf *mouse-focus-policy* :click) ;; :click, :ignore, :sloppy
 
  ;; Shortcuts to split windows: same as in tmux
  (define-key *root-map* (kbd "V")   "hsplit")
  (define-key *root-map* (kbd "H")   "vsplit")

  ;; Banish mouse cursor
  (define-key *root-map* (kbd "C-v") "banish")

§ Utility Commands and Shortcuts

Some shortcuts. TODO: extract a list of concretized shell commands.

  ;; Lock screen and Suspend
  (define-key *root-map* (kbd "M-l") "exec mate-screensaver-command -l && systemctl suspend")
  ;(define-key *root-map* (kbd "M-l") "exec systemctl suspend")

  ;; Screenshot
  (define-key *root-map* (kbd "M-s") "exec mate-screenshot -i")

  ;; Adjust brightness - now in redshift's settings
  ;; (define-key *root-map* (kbd "M-b") "colon exec xbacklight -set ")

  ;; Browse somewhere
  (define-key *root-map* (kbd "o") "colon exec sensible-browser https://www.")

Definitions for obtaining results of shell commands:

  (defun stumpwm-str-trim (s)
    "Remove whitespace at the beginning and end of S."
    (string-trim '(#\Space #\Newline #\Backspace #\Tab #\Linefeed #\Page #\Return #\Rubout) s))

  (defun shell-command-to-string (command)
    "Execute shell command COMMAND and return its output as a string."
    (stumpwm-str-trim (run-shell-command command t)))

Status Echo Commands:

  (defcommand echo-uptime () ()
    "Displays the string result of the `uptime' shell command."
    (shell-command-to-string "uptime"))

Interactively get selection results:

  (defun get-x-selection-interactive ()
    "Confirms selected text string and returns it."
    (let ((content
            (read-one-line
             (current-screen) "Selected: "
             :initial-input (get-x-selection))))
      content))

§ MSI Keyboard Colors

  (defcommand msi-keyboard-color () ()
    (let ((region
            (first
             (select-from-menu
              (current-screen)
              '("left" "middle" "right")
              "Region")))
          (color
            (first
             (select-from-menu
              (current-screen)
              '("off" "red" "orange" "yellow" "green"
                "sky" "blue" "purple" "white")
              "Color")))
          (intensity
            (first
             (select-from-menu
              (current-screen)
              '("high" "medium" "low" "light")
              "Intensity"))))
      (when (and region color intensity)
        (run-shell-command
         (format nil "msi-keyboard -m normal -c ~a"
                 (format nil "~a,~a,~a" region color intensity))))))


  (defcommand msi-keyboard-colors-default () ()
    (run-shell-command "msi-keyboard -m normal -c left,red,light -c middle,purple,medium -c right,sky,light"))

§ Terminal Emulator

It's handy to drag most used programs wherever they're needed. Luckily, defprogram-shortcut can define pull-able behavior as well.

Default behavior for terminal emulator program window is raise. See frame preference rules below.

  ;; terminal
  (defprogram-shortcut raise-term-emulator
    :command "mate-terminal"
    :props '(:role "terminal")
    :map *root-map*
    :key (kbd "t")
    :pullp t
    :pull-key (kbd "C-t"))

§ File Browser

  ;; file browser
  (defprogram-shortcut raise-file-browser
    :command "caja"
    :props '(:class "Caja")
    :map *root-map*
    :key (kbd "c")
    :pullp t
    :pull-key (kbd "C-c"))

§ Browser

Default behavior for browser window is raise. See frame preference rules below.

  ;; browser
  (defprogram-shortcut raise-browser
    :command "sensible-browser"
    :props '(:role "browser")
    :map *root-map*
    :key (kbd "b")
    :pullp t
    :pull-key (kbd "C-b"))

Shortcuts to search engines.

TODO: Refactor this section when I get bored.

Macro to create web search interaction commands.

  (defmacro make-web-search (name prefix &optional search-selected)
    "Make web search command. When SEARCH-SELECTED get default search text from clipboard."
      `(defcommand ,(intern name) () ()
         (let ((search (read-one-line
                        (current-screen)
                        ,(concatenate 'string name " query: ")
                        :initial-input
                        ,(if search-selected '(get-x-selection) ""))))
           (unless (or (null search) (string= "" search))
             (setf search (substitute #\+ #\Space search))
             (run-shell-command (concatenate 'string "sensible-browser " ,prefix search))))))

Register default search jumps.

  (make-web-search "google-search"
                   "https://www.google.com/search?q=")
  (make-web-search "duck-search"
                   "https://duckduckgo.com/?q=")
  (make-web-search "bing-search"
                   "https://www.bing.com/search?q=")
  (make-web-search "academia-search"
                   "https://www.google.com/scholar?q=")
  (make-web-search "goodreads-search"
                   "https://www.goodreads.com/search?q=")
  (make-web-search "youtube-search"
                   "https://www.youtube.com/results?search_query=")
  (make-web-search "imdb-search"
                   "https://www.imdb.com/find?q=")

  (defvar *web-search-bindings*
    (let ((m (make-sparse-keymap)))
      (define-key m (kbd "/") "google-search")
      (define-key m (kbd "d") "duck-search")
      (define-key m (kbd "b") "bing-search")
      (define-key m (kbd "a") "academia-search")
      (define-key m (kbd "r") "goodreads-search")
      (define-key m (kbd "y") "youtube-search")
      (define-key m (kbd "i") "imdb-search")
      m))

  (define-key *root-map* (kbd "/") '*web-search-bindings*)

Register search web jumps for selected text in the clipboard.

  (make-web-search "google-search-sel"
                   "https://www.google.com/search?q="
                   t)
  (make-web-search "duck-search-sel"
                   "https://duckduckgo.com/?q="
                   t)
  (make-web-search "bing-search-sel"
                   "https://www.bing.com/search?q="
                   t)
  (make-web-search "academia-search-sel"
                   "https://www.google.com/scholar?q="
                   t)
  (make-web-search "goodreads-search-sel"
                   "https://www.goodreads.com/search?q="
                   t)
  (make-web-search "youtube-search-sel"
                   "https://www.youtube.com/results?search_query="
                   t)
  (make-web-search "imdb-search-sel"
                   "https://www.imdb.com/find?q="
                   t)

  (defvar *web-search-sel-bindings*
    (let ((m (make-sparse-keymap)))
      (define-key m (kbd "/") "google-search-sel")
      (define-key m (kbd "d") "duck-search-sel")
      (define-key m (kbd "b") "bing-search-sel")
      (define-key m (kbd "a") "academia-search-sel")
      (define-key m (kbd "r") "goodreads-search-sel")
      (define-key m (kbd "y") "youtube-search-sel")
      (define-key m (kbd "i") "imdb-search-sel")
      m))

  (define-key *root-map* (kbd "C-/") '*web-search-sel-bindings*)

§ Emacs in Server Mode

Emacs editor to be run in server mode of course. It can be raised and pulled around as well.

  (defprogram-shortcut raise-emacs
    :command "exec emacsclient -c -a \"emacs --daemon\""
    :props '(:title "GNU Emacs")
    :map *root-map*
    :key (kbd "e")
    :pullp t
    :pull-key (kbd "C-e"))

Interfacing Emacsclient with Stump: details and sources reside in a separate blog post. I'm using noweb to tangle those definitions alongside with the rest of the .stumpwmrc config.

TODO: Customize export so that it unrolls the noweb source blocks on a webpage.

  <<./siegetower/infinite_staircase/interface-for-stumpwm-with-emacs.org:stumpwm-emacs-interface-wrapper()>>
  (defcommand emacs-link-from-selected () ()
    (emacs-store-link (get-x-selection-interactive)))

  (define-key *root-map* (kbd ",") "emacs-link-from-selected")
  (defcommand emacs-capture-selected () ()
    (emacs-push-to-template (get-x-selection-interactive)
                            (emacs-select-org-capture-template))
    (raise-emacs))

  (define-key *root-map* (kbd ".")  "emacs-capture-selected")

§ Message Timers with Notifications

Minimalistic timers that delay notification messages and sound by set minutes. A menu lists their remaining timeout and allows to cancel'em.

  ;; Timers with notification messages and sounds
  (defvar *notification-sound-file* #p"~/Music/bell.wav"
          "Notification sound file.")


  (defvar *sound-play-command* "aplay"
    "System command to play sounds.")


  (defun play-notifcation-sound ()
    "Play the *NOTIFICATION-SOUND-FILE* if exists."
    (when (probe-file *notification-sound-file*)
      (uiop:run-program
       (format nil "~a ~a" *sound-play-command*
               (namestring *notification-sound-file*)))))


  (defcommand set-message-timer (msg mins)
      ((:string "Message: ")
       (:number "Mins to wait: "))
    (run-with-timer
     (* 60 mins) nil
     (lambda (&rest args)
       (play-notifcation-sound)
       (uiop:run-program
        (format nil "notify-send -i emblem-mail \"~a\""
                msg)))
     :message msg)
    (message "timer for ^[^6\"~a\"^] triggers in^[^4 ~a ^]minutes!" msg mins))


  (defun %timer-remained-time-minutes (timer)
    (ceiling
     (/
      (- (timer-time timer)
         (get-internal-real-time)
         )
      internal-time-units-per-second
      60)))


  (defun %select-message-timer ()
    (select-from-menu
     (current-screen)
     (remove nil
             (mapcar
              (lambda (timer)
                (when (getf (timer-args timer) :message)
                  (list (format nil "~a mins: ~a"
                                (%timer-remained-time-minutes timer)
                                (getf (timer-args timer) :message))
                        timer)))
              ,*timer-list*))
     "^[^1RET^] to cancel timer; ^[^2ESC^] to quit:"))


  (defcommand list-message-timers () ()
    (when *timer-list*
      (let ((timer (%select-message-timer)))
        (when timer
          (cancel-timer (second timer))))))


  (define-key *root-map* (kbd "=") "set-message-timer") 
  (define-key *root-map* (kbd "\\") "list-message-timers") 

§ Contrib Add-Ons

Configuration of stumpwm-contrib modules.

I used to rely on many of them. However now only few of them remain here. Even my contrib Pomodoro tracker is replaced in favor to more generic timers defined above.

And since I've ditched mode-line, its plugins and notifications extensions also went away (for the latter dispatching to notify-send works okay-ish for me with default gnome/mate panel).

  ;; GnuPass interface. Needs full path stored in $PASSWORD_STORE_DIR
  (load-guixified-module "pass")
  (define-key *root-map* (kbd "Z") "pass-copy-menu")

§ Frame Preference Rules

Automatic GUI windows grouping. The Default group will receive the Desktop view, since it is listed as preferred for the Caja file explorer.

  ;;
  ;; Window Placement Rules
  
  (run-commands
   "gnewbg emacs"
   "gnewbg web"
   "gnewbg read"
   "gnewbg terminal")
 
 
  (define-frame-preference "emacs"
    (0 T T :class "Emacs"))

  (define-frame-preference "terminal"
    (0 T T :class "Mate-terminal")
    (0 T T :class "XTerm"))

  (define-frame-preference "web"
    (0 T T :class "Vivaldi-stable")
    (0 T T :class "firefox"))

  (define-frame-preference "read"
    (0 T T :class "Atril")
    (0 T T :class "Evince"))

  (define-frame-preference "Default"
    (0 T T :class "Caja"))