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:
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"))