4673e928c08e

Split Stump config
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Tue, 19 Mar 2024 13:55:42 -0400
parents 3a03464b6914
children dedc81b8510c
branches/tags (none)
files bin/bootstrap.sh fish/config.fish fish/functions/mutt.fish gitignore stumpwm/applications.lisp stumpwm/brightness.lisp stumpwm/budget.lisp stumpwm/clipboard.lisp stumpwm/config.lisp stumpwm/external-screens.lisp stumpwm/icelandic.lisp stumpwm/key-mapping.lisp stumpwm/local-share-stumpwm/522.dump stumpwm/local-share-stumpwm/522.lisp.dump stumpwm/miscellaneous.lisp stumpwm/modeline.lisp stumpwm/modules.lisp stumpwm/package.lisp stumpwm/passwords.lisp stumpwm/posture.lisp stumpwm/screenshots.lisp stumpwm/sensors.lisp stumpwm/sound.lisp stumpwm/stumpconfig.asd stumpwm/stumpwmrc stumpwm/terminal-fonts.lisp stumpwm/timers.lisp stumpwm/utils.lisp stumpwm/vlime.lisp stumpwmrc vim/custom-dictionary.utf-8.add vim/vimrc

Changes

--- a/bin/bootstrap.sh	Mon Mar 18 12:49:56 2024 -0400
+++ b/bin/bootstrap.sh	Tue Mar 19 13:55:42 2024 -0400
@@ -56,7 +56,7 @@
 ensure_link "src/dotfiles/sbclrc"                      ".sbclrc"
 ensure_link "src/dotfiles/shellcheckrc"                ".shellcheckrc"
 ensure_link "src/dotfiles/sqliterc"                    ".sqliterc"
-ensure_link "src/dotfiles/stumpwmrc"                   ".stumpwmrc"
+ensure_link "src/dotfiles/stumpwm/stumpwmrc"           ".stumpwmrc"
 ensure_link "src/dotfiles/stumpwm/local-share-stumpwm" ".local/share/stumpwm"
 ensure_link "src/dotfiles/vim"                         ".vim"
 ensure_link "src/dotfiles/vim/vimrc"                   ".vimrc"
--- a/fish/config.fish	Mon Mar 18 12:49:56 2024 -0400
+++ b/fish/config.fish	Tue Mar 19 13:55:42 2024 -0400
@@ -8,7 +8,7 @@
 function ei; hg -R ~/src/inventory/ pull -u; and nvim ~/src/inventory/inventory.markdown; and hg -R ~/src/inventory/ ci -m 'Update inventory'; and hg -R ~/src/inventory/ push; end
 function el; cd ~/Dropbox/life; nvim .; end
 function em; nvim ~/.mutt/muttrc; end
-function es; pushd ~/src/stumpwm; e ~/.stumpwmrc; popd; end
+function es; pushd ~/src/dotfiles/stumpwm; e stumpwmrc; popd; end
 function ev; nvim ~/.vimrc; end
 function evm; nvim ~/.vimrc-minimal; end
 function eff; nvim ~/.config/fish/functions; end
--- a/fish/functions/mutt.fish	Mon Mar 18 12:49:56 2024 -0400
+++ b/fish/functions/mutt.fish	Tue Mar 19 13:55:42 2024 -0400
@@ -1,5 +1,5 @@
 set -g -x MUTT_BIN (which neomutt)
 
 function mutt
-    bash --login -c "cd ~/Downloads; $MUTT_BIN \$@" custom_mutt $argv
+    bash --login -c "cd ~/downloads; $MUTT_BIN \$@" custom_mutt $argv
 end
--- a/gitignore	Mon Mar 18 12:49:56 2024 -0400
+++ b/gitignore	Tue Mar 19 13:55:42 2024 -0400
@@ -18,5 +18,7 @@
 .sjl-rsync-exclude
 sjl-jupyter
 sjl-sync-*.sh
+sjl-*-push.sh
+sjl-*-pull.sh
 
 *.waiting
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/applications.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,30 @@
+(in-package :stumpwm-user)
+
+(defcommand spotify () ()
+  (run-or-raise "spotify" '(:class "Spotify")))
+
+(defcommand files () ()
+  (run-shell-command "open $HOME"))
+
+(defcommand browser () ()
+  (run-or-raise "firefox" '(:class "firefox")))
+
+(defcommand vlc () ()
+  (run-or-raise "vlc" '(:class "vlc")))
+
+(defcommand terminal () ()
+  (run-shell-command (format nil "st -f 'Ubuntu Mono:size=~D'" *terminal-font-size*)))
+
+(defcommand terminal-apl () ()
+  (run-shell-command "st -f 'BQN386 Unicode:style=Regular:size=12'"))
+
+(defcommand gcontrol () ()
+  (run-or-raise "gcontrol" '(:class "Gnome-control-center")))
+
+(defcommand zoom () ()
+  (when-let-window (w "^Zoom Meeting.*")
+    (focus-window w t)))
+
+(defcommand papers () ()
+  (run-or-raise "jabref" '(:class "org.jabref.gui.MainApplication")))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/brightness.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,28 @@
+(in-package :stumpwm-user)
+
+(defparameter *brightness-values* #(0 1 5 10 20 30 40 55 70 85 100))
+(defvar *brightness-index* 5)
+
+(defun brightness ()
+  (aref *brightness-values* *brightness-index*))
+
+(defun set-brightness (value)
+  (run-and-echo-shell-command
+    (hostcase
+      ((:gro :juss) (format nil "xrandr --output ~A --brightness ~D"
+                      (hostcase ((:gro :juss) "eDP"))
+                      (/ value 100.0)))
+      (t (message "Not sure how to set brightness on this machine.")))))
+
+(defun rotate-brightness (delta)
+  (setf *brightness-index*
+        (mod+ *brightness-index* delta (length *brightness-values*)))
+  (set-brightness (brightness)))
+
+
+(defcommand rotate-brightness-up () ()
+  (rotate-brightness 1))
+
+(defcommand rotate-brightness-down () ()
+  (rotate-brightness -1))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/budget.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,51 @@
+(in-package :stumpwm-user)
+
+(defparameter *tz/eastern*
+  (local-time:find-timezone-by-location-name "US/Eastern"))
+
+(defparameter *budget/start*
+  (local-time:encode-timestamp 0 0 0 0 29 8 2023 :timezone *tz/eastern*))
+
+(defun budget/per-day ()
+  (first (losh:read-all-from-file "/home/sjl/Sync/budget/per-day")))
+
+(defun budget/elapsed ()
+  (local-time:timestamp-difference (local-time:now) *budget/start*))
+
+(defun budget/days-elapsed ()
+  (floor (/ (budget/elapsed) (* 60 60 24))))
+
+(defun budget/in ()
+  (* (budget/days-elapsed) (budget/per-day)))
+
+(defun budget/out ()
+  (loop :for path :in (directory "/home/sjl/Sync/budget/hosts/*/total")
+        :summing (print (first (read-all-from-file (print path))))))
+
+(defun budget/current ()
+  (- (budget/in) (budget/out)))
+
+(defcommand budget-dump () ()
+  (message
+    (sh '("sh" "-c" "tail -n 5 /home/sjl/Sync/budget/hosts/*/records")
+        :result-type 'string)))
+
+(defcommand budget () ()
+  (message "$~D" (budget/current)))
+
+(defmacro with-budget-file ((f file &rest open-args) &body body)
+  `(with-open-file
+     (,f (format nil "/home/sjl/Sync/budget/hosts/~(~A~)/~A" *host* ,file)
+      ,@open-args)
+     ,@body))
+
+(defcommand spend (amount what) ((:integer "Amount: $") (:string "For: "))
+  (let ((current (with-budget-file (total "total")
+                   (first (read-all-from-file total))))
+        (timestamp (local-time:to-rfc3339-timestring (local-time:now))))
+    (with-budget-file (total "total" :direction :output :if-exists :supersede)
+      (print (+ current amount) total))
+    (with-budget-file (records "records" :direction :output :if-exists :append :if-does-not-exist :create)
+      (print (list timestamp amount what) records))
+    (message "Spent $~D for ~A at ~A" amount what timestamp)))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/clipboard.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,22 @@
+(in-package :stumpwm-user)
+
+(load-module "clipboard-history")
+(clipboard-history:start-clipboard-manager)
+
+(defcommand generate-random-uuid () ()
+  (run-shell-command "uuidgen | tr -d '\\n' | ~/src/dotfiles/bin/pbcopy")
+  (message "Copied random UUID to clipboard."))
+
+(defcommand bee-movie-script () ()
+  (run-shell-command "pbeecopy")
+  (message "Copied the entire Bee Movie script to clipboard."))
+
+(defcommand urlize-jira-issue () ()
+  (let ((issue (str:trim (pbpaste))))
+    (if (ppcre:scan "^[A-Z0-9]+-\\d+$" issue)
+      (let* ((endpoint (str:trim (run-shell-command "grep endpoint .jira.d/config.yml | sed -e 's/.*: //'" t)))
+             (url (format nil "~A/browse/~A" endpoint issue)))
+        (pbcopy url)
+        (message "Copied ~A to the clipboard." url))
+      (message "Clipboard does not look like a JIRA issue."))))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/config.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,32 @@
+(in-package :stumpwm-user)
+
+(set-prefix-key (kbd "C-space"))
+(local-time:reread-timezone-repository)
+
+(set-focus-color "#aaaaaa")
+(set-win-bg-color "#111111")
+(set-unfocus-color "#444444")
+(setf *normal-border-width* 1
+      *default-bg-color* #x222222
+      *window-border-style* :thin
+      (xlib:window-background (screen-root (current-screen))) *default-bg-color*)
+
+(defvar *redirected* (redirect-all-output (data-dir-file "debug" "log")))
+
+(setf *mouse-focus-policy* :click
+      *message-window-gravity* :center
+      *input-window-gravity* :center
+      *debug-level* 0
+      *resize-increment* 75
+      *new-frame-action* :empty
+      *window-format* "(%n%m%20t)"
+      *window-name-source* :title
+      *maximum-completions* 20
+      *shell-program* "/home/sjl/src/dotfiles/bin/bash-dammit"
+      losh:*pbcopy-command* "/home/sjl/src/dotfiles/bin/pbcopy"
+      losh:*pbpaste-command* "/home/sjl/src/dotfiles/bin/pbpaste")
+
+(defun stumpwm::input-insert-hyphen-or-space (input key)
+  ;; Unbreak typing
+  (declare (ignore key))
+  (input-insert-char input #\space))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/external-screens.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,26 @@
+(in-package :stumpwm-user)
+
+(defcommand screen-laptop () ()
+  (only)
+  (hostcase
+    ((:gro :juss) (loop :with laptop = "eDP"
+                        :with extern = (hostcase (:gro "DisplayPort-0")
+                                                 (:juss "HDMI-A-0"))
+                        :for (output commands) :in `((,laptop ("--auto"))
+                                                     (,laptop ("--primary"))
+                                                     (,extern ("--off")))
+                        :do (progn (uiop:run-program `("xrandr" "--output" ,output ,@commands)))))
+    (t (message "Not configured on this system."))))
+
+(defcommand screen-external () ()
+  (only)
+  (hostcase
+    ((:gro :juss) (loop :with laptop = "eDP"
+                        :with extern = (hostcase (:gro "DisplayPort-0")
+                                                 (:juss "HDMI-A-0"))
+                        :for (output commands) :in `((,extern ("--auto"))
+                                                     (,extern ("--primary"))
+                                                     (,laptop ("--off")))
+                        :do (uiop:run-program `("xrandr" "--output" ,output ,@commands))))
+    (t (message "Not configured on this system."))))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/icelandic.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,66 @@
+(in-package :stumpwm-user)
+
+(defcommand send-key (key &optional (win (current-window))) (:key)
+  "Send key press and key release events for KEY to window WIN."
+  ;; from https://github.com/alezost/stumpwm-config/blob/master/utils.lisp
+  (let ((xwin (window-xwin win)))
+    (multiple-value-bind (code state) (stumpwm::key-to-keycode+state key)
+      (flet ((send (event)
+               (xlib:send-event xwin event (xlib:make-event-mask event)
+                                :display *display*
+                                :root (screen-root (window-screen win))
+                                :x 0 :y 0 :root-x 0 :root-y 0
+                                :window xwin :event-window xwin
+                                :code code
+                                :state state)))
+        (send :key-press)
+        (send :key-release)
+        (xlib:display-finish-output *display*)))))
+
+(defun send-keys (keys &key (win (current-window)) (sleep 0))
+  (dolist (k keys)
+    (send-key (kbd k) win)
+    (sleep sleep)))
+
+(defmacro defmultikey (name key compose-keys)
+  ;; Unfortunately we can't reliably autogen the name with something like
+  ;; (symb 'mk- compose-key) here because things like đ (th) and Đ (TH) would
+  ;; case fold to the same name.
+  `(progn
+     (defcommand ,name () ()
+       (send-keys '("Multi_key" ,@(map 'list #'string compose-keys))))
+     (define-key *top-map*
+       (kbd ,key) ,(string name))))
+
+(defmacro defmultikeys (&rest bindings)
+  `(progn ,@(loop for binding :in bindings :collect `(defmultikey ,@binding))))
+
+(defmultikeys
+  (isk-l-á "M-a" "'a")
+  (isk-u-Á "M-A" "'A")
+  (isk-l-é "M-e" "'e")
+  (isk-u-É "M-E" "'E")
+  (isk-l-í "M-i" "'i")
+  (isk-u-Í "M-I" "'I")
+  (isk-l-ó "M-o" "'o")
+  (isk-u-Ó "M-O" "'O")
+  (isk-l-ö "M-m" "\"o")
+  (isk-u-Ö "M-M" "\"O")
+  (isk-l-ú "M-u" "'u")
+  (isk-u-Ú "M-U" "'U")
+  (isk-l-ý "M-y" "'y")
+  (isk-u-Ý "M-Y" "'Y")
+  (isk-l-þ "M-t" "th")
+  (isk-u-Þ "M-T" "TH")
+  (isk-l-đ "M-d" "dh")
+  (isk-u-Đ "M-D" "DH")
+  (isk-l-æ "M-h" "ae")
+  (isk-u-Æ "M-H" "AE"))
+
+
+(defcommand thinkpad-ret () ()
+  (send-key (kbd "RET")))
+
+(defcommand thinkpad-bs () ()
+  (send-key (kbd "BackSpace")))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/key-mapping.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,223 @@
+(in-package :stumpwm-user)
+
+;;; Conventions:
+;;;
+;;; * Hyper-dir: move focus
+;;; * Hyper-Shift-dir: move window
+;;; * Hyper-Shift-Control-dir: swap window
+;;; * Hyper-F*: hardware
+;;; * Shift-F*: timers
+;;; * Hyper-Super-*: layout
+;;; * Hyper-*: miscellaneous
+;;; * Super-GAMERMOUSE: duplicate stuff to avoid swapping back
+
+
+(defmacro define-top-keys (&body keyforms)
+  `(progn ,@(loop :for form :in keyforms
+                  :collect `(define-key *top-map*
+                              (kbd ,(first form))
+                              ,(second form)))))
+
+
+
+(define-top-keys ;; miscellaneous
+  ("H-m" "terminal")
+  ("H-M" "mark")
+  ("H-SunPageUp" "st-font-up")
+  ("H-SunPageDown" "st-font-down")
+  ("H-Home" "st-font-reset")
+  ("H-F4" "switch-yubikeys")
+  ("H-\\" "pass-personal")
+  ("H-|" "generate-password")
+  ("s-1" "pass-um-1")
+  ("s-2" "pass-um-2")
+  ("H-b" "browser")
+  ("H-O" "spotify")
+  ("H-o" "files")
+  ("H-z" "zoom")
+  ("H-Z" "toggle-zoom-mute")
+  ("C-H-Z" "end-zoom")
+  ("F26"   "prev")
+  ("S-F26" "next")
+  ("H-q" "exec lock-screen")
+  ("H-y" "screenshot")
+  ("H-g" "gcontrol")
+  ("H-f" "save-fucked-screenshot")
+  ("H-F" "delete-fucked-screenshot")
+  ("H-r" "rain")
+  ("H-V" "vlc")
+  ("H-e" "budget")
+  ("H-E" "spend")
+  ("C-BackSpace" "clear-notifications")
+  )
+
+(define-top-keys ;; clipboard
+  ("H-c" "show-clipboard-history")
+  ("H-C" "clear-clipboard-history")
+  ("H-u" "generate-random-uuid")
+  ("H-B" "bee-movie-script")
+  ("M-H-u" "urlize-jira-issue"))
+
+(define-top-keys ;; movement
+  ("H-h" "move-focus* left")
+  ("H-j" "move-focus down")
+  ("H-k" "move-focus up")
+  ("H-l" "move-focus* right")
+
+  ("H-H" "move-window left")
+  ("H-J" "move-window down")
+  ("H-K" "move-window up")
+  ("H-L" "move-window right")
+
+  ("H-1" "gselect 1")
+  ("H-2" "gselect 2")
+  ("H-3" "gselect 3")
+  ("H-4" "gselect 4")
+  ("H-5" "gselect 5")
+  ("H-6" "gselect 6")
+
+  ("KP_End"       "gselect 1")
+  ("KP_Down"      "gselect 2")
+  ("KP_Page_Down" "gselect 3")
+  ("KP_Left"      "gselect 4")
+  ("KP_Begin"     "gselect 5")
+  ("KP_Right"     "gselect 6")
+  ("KP_Home"      "gselect 7")
+  ("KP_Up"        "gselect 8")
+  ("KP_Page_Up"   "gselect 9")
+
+  ("H-!" "gmove 1")
+  ("H-@" "gmove 2")
+  ("H-#" "gmove 3")
+  ("H-$" "gmove 4")
+  ("H-%" "gmove 5")
+  ("H-^" "gmove 6")
+
+  ("C-H-H" "exchange-direction left")
+  ("C-H-J" "exchange-direction down")
+  ("C-H-K" "exchange-direction up")
+  ("C-H-L" "exchange-direction right")
+
+  ("H-`" "next")
+  ("S-H-`" "prev")
+  ("H-n" "next-in-frame")
+  ("H-p" "prev-in-frame")
+  ("H-N" "pull-hidden-next")
+  ("H-P" "papers")
+
+  ("H-," "pull-from-windowlist"))
+
+
+(define-top-keys ;; splitting
+  ("H-s" "sane-vsplit")
+  ("H-v" "sane-hsplit")
+  ("H-=" "balance-frames"))
+
+(define-top-keys ;; killing
+  ("H-w" "delete")
+  ("H-W" "kill")
+  ("H-BackSpace" "remove")
+  ("S-H-BackSpace" "kill-and-remove"))
+
+;; (define-top-keys ;; broken thinkpad keys
+;;   ("s-m" "thinkpad-ret")
+;;   ("s-Delete" "thinkpad-bs"))
+
+
+(define-top-keys ;; naming
+  ("H-'" "title"))
+
+(define-top-keys ;; sound
+  ("H-F1" "mute")
+  ("H-F2" "volume-down")
+  ("H-F3" "volume-up")
+  ("XF86AudioMute" "exec mute")
+  ("XF86AudioRaiseVolume" "volume-down") ; todo unfuck the backwards mapping in qmk
+  ("XF86AudioLowerVolume" "volume-up"))
+
+(define-top-keys ;; screen
+  ("H-F5" "rotate-brightness-down")
+  ("H-F6" "rotate-brightness-up")
+  ("H-F7" "screen-laptop")
+  ("H-F8" "screen-external"))
+
+(define-top-keys ;; layout
+  ("s-H-o" "only")
+  ("s-H-t" "restore-from-file thirds")
+  ("s-H-m" "restore-from-file dev")
+  ("s-H-s" "restore-from-file streaming")
+  ("s-H-w" "restore-from-file work")
+  ("s-H-z" "restore-from-file zoom"))
+
+(define-top-keys ;; timers
+  ("s-F7"  "tea-timer")
+  ("s-F9"  "run-pop-timer")
+  ("s-F8"  "set-pop-timer")
+  ("s-p"   "posture-start")
+  ("s-P"   "posture-stop")
+  ("s-y"   "posture-answer-yes")
+  ("s-h"   "posture-answer-meh")
+  ("s-n"   "posture-answer-no")
+  ("s-\\"  "posture-toggle-pause")
+  ("s-o"   "posture-snooze"))
+
+(define-top-keys ;; stump
+  ("Pause" "terminal") ; jesus christ
+  ("H-F9"  "sleep-machine")
+  ("H-F10" "toggle-stumptray")
+  ("H-F11" "toggle-current-mode-line")
+  ("H-F12" "refresh-heads"))
+
+
+(define-top-keys ;; GAMER MOUSE
+  ("s-$" "move-focus* left")
+  ("s-*" "move-focus down")
+  ("s-@" "move-focus up")
+  ("s-^" "move-focus* right")
+  ("M-s-Left"  "gprev")
+  ("M-s-Right" "gnext")
+  ("s-Home" "next-in-frame")
+  ("s-End"  "prev-in-frame")
+  ("s-)" "sane-hsplit")
+  ("s-_" "sane-vsplit")
+  ("s-Delete" "remove"))
+
+
+;; (stumpwm::unbind-remapped-keys)
+(define-remapped-keys
+  '(("st-256color"
+     ("s-c" . "C-C")
+     ("s-v" . "C-V")
+     ("C-=" . "S-C-SunPageUp")
+     ("C--" . "S-C-SunPageDown")
+     ("C-0" . "S-C-Home"))
+    ("(firefox|Google-chrome|Chromium-browser)"
+     ("s-[" . "C-S-Tab")
+     ("s-]" . "C-Tab")
+     ("C-a" . "Home")
+     ("C-e" . "End")
+     ;; I always try to hit ctrl-d to kill a browser window because I'm so used
+     ;; to terminal windows, and it ends up bookmarking the damn page.  In the
+     ;; interest of not having a random collection of bookmarks grow over time,
+     ;; I'll just add a mapping to compensate for my stupid brain.
+     ("C-d" . "C-w")
+     ;; todo debug why this breaks a really fast C-a-k roll
+     ;; ("C-a" . "Home")
+     ;; ("C-e" . "End")
+     ("s-a" . "C-a")
+     ("s-d" . "C-d")
+     ("s-l" . "C-l")
+     ("s-t" . "C-t")
+     ("s-w" . "C-w")
+     ("s-r" . "C-r")
+     ("s-f" . "C-f")
+     ("s-z" . "C-z")
+     ("s-x" . "C-x")
+     ("s-c" . "C-c")
+     ("s-v" . "C-v"))
+    (""
+     ("s-z" . "C-z")
+     ("s-x" . "C-x")
+     ("s-c" . "C-c")
+     ("s-v" . "C-v"))))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/local-share-stumpwm/522.dump	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,52 @@
+#S(GDUMP
+   :NUMBER 3
+   :NAME "code"
+   :TREE (((#S(FDUMP
+               :NUMBER 0
+               :X 0
+               :Y 0
+               :WIDTH 1200
+               :HEIGHT 1020
+               :WINDOWS (31457288)
+               :CURRENT 31457288)
+            #S(FDUMP
+               :NUMBER 3
+               :X 0
+               :Y 1020
+               :WIDTH 1200
+               :HEIGHT 420
+               :WINDOWS (29360133)
+               :CURRENT 29360133))
+           ((#S(FDUMP
+                :NUMBER 1
+                :X 1200
+                :Y 0
+                :WIDTH 1735
+                :HEIGHT 1440
+                :WINDOWS (33554437)
+                :CURRENT 33554437)
+             (#S(FDUMP
+                 :NUMBER 4
+                 :X 2935
+                 :Y 0
+                 :WIDTH 985
+                 :HEIGHT 720
+                 :WINDOWS (37748741)
+                 :CURRENT 37748741)
+              #S(FDUMP
+                 :NUMBER 5
+                 :X 2935
+                 :Y 720
+                 :WIDTH 985
+                 :HEIGHT 720
+                 :WINDOWS NIL
+                 :CURRENT NIL)))
+            #S(FDUMP
+               :NUMBER 2
+               :X 3920
+               :Y 0
+               :WIDTH 1200
+               :HEIGHT 1440
+               :WINDOWS (16977044)
+               :CURRENT 16977044))))
+   :CURRENT 1)
\ No newline at end of file
--- a/stumpwm/local-share-stumpwm/522.lisp.dump	Mon Mar 18 12:49:56 2024 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
-#S(GDUMP
-   :NUMBER 3
-   :NAME "code"
-   :TREE (((#S(FDUMP
-               :NUMBER 0
-               :X 0
-               :Y 0
-               :WIDTH 1200
-               :HEIGHT 1020
-               :WINDOWS (31457288)
-               :CURRENT 31457288)
-            #S(FDUMP
-               :NUMBER 3
-               :X 0
-               :Y 1020
-               :WIDTH 1200
-               :HEIGHT 420
-               :WINDOWS (29360133)
-               :CURRENT 29360133))
-           ((#S(FDUMP
-                :NUMBER 1
-                :X 1200
-                :Y 0
-                :WIDTH 1735
-                :HEIGHT 1440
-                :WINDOWS (33554437)
-                :CURRENT 33554437)
-             (#S(FDUMP
-                 :NUMBER 4
-                 :X 2935
-                 :Y 0
-                 :WIDTH 985
-                 :HEIGHT 720
-                 :WINDOWS (37748741)
-                 :CURRENT 37748741)
-              #S(FDUMP
-                 :NUMBER 5
-                 :X 2935
-                 :Y 720
-                 :WIDTH 985
-                 :HEIGHT 720
-                 :WINDOWS NIL
-                 :CURRENT NIL)))
-            #S(FDUMP
-               :NUMBER 2
-               :X 3920
-               :Y 0
-               :WIDTH 1200
-               :HEIGHT 1440
-               :WINDOWS (16977044)
-               :CURRENT 16977044))))
-   :CURRENT 1)
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/miscellaneous.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,97 @@
+(in-package :stumpwm-user)
+
+(defcommand sane-hsplit () ()
+  (hsplit)
+  (move-focus :right))
+
+(defcommand sane-vsplit () ()
+  (vsplit)
+  (move-focus :down))
+
+
+(defcommand move-focus* (direction)
+    ((:direction "Enter a direction: "))
+  (labels ((in-float-p ()
+             (typep (current-group) 'stumpwm::float-group))
+           (focus-first-frame ()
+             (unless (in-float-p)
+               ;; After moving to a new group we don't know which frame is
+               ;; focused, and unfortunately Stump doesn't give us a nice way to
+               ;; say "focus the leftmost frame" so we'll just move the focus
+               ;; a bunch of times and hope it's enough.  Sigh.
+               (loop :repeat 15
+                     :until (eql (current-frame)
+                                 (progn (move-focus (ecase direction
+                                                      (:left :right)
+                                                      (:right :left)))
+                                        (current-frame))))))
+           (next-group ()
+             (ecase direction
+               (:right (gnext))
+               (:left (gprev)))
+             (focus-first-frame)))
+    (unless (in-float-p)
+      (banish))
+    (if (in-float-p)
+      (next-group)
+      (let ((frame (current-frame)))
+        (move-focus direction)
+        (when (eql frame (current-frame))
+          (next-group))))))
+
+(defcommand toggle-current-mode-line () ()
+  (toggle-mode-line (current-screen) (current-head)))
+
+(defcommand toggle-stumptray () ()
+  (run-commands "stumptray"))
+
+(defcommand kill-and-remove () ()
+  (run-commands "kill" "remove"))
+
+(defcommand sleep-machine ()
+    ()
+  (hostcase
+    ((:gro :juss)
+     (run-shell-command "exec lock-screen")
+     (run-shell-command "systemctl suspend"))
+    (t (message "Not sleeping this machine for safety."))))
+
+(defcommand copy-clhs-url (s)
+    ((:string "Symbol: "))
+  (run-shell-command (format nil "clhs --url 'http://www.lispworks.com/documentation/HyperSpec/' --quiet --open echon '~A' | pbcopy" s)))
+
+(defcommand describe-window () ()
+  (show-window-properties))
+
+(defcommand rain () ()
+  (_ '("/home/sjl/src/dotfiles/lisp/bin/weather" "48105" "-H" "36")
+    (losh:sh _ :result-type 'list)
+    (mapcar (lambda (line) (ppcre:regex-replace " 1[0-9]:00 " line "^6\\&^*")) _)
+    (message "~{~A~^~%~}" _)))
+
+(defcommand mark (thing) ((:string "Mark: "))
+  (run-shell-command (format nil "mark ~A" thing)))
+
+(defcommand toggle-zoom-mute () ()
+  (when-let-window (win "^Zoom Meeting.*")
+    ;; Zoom stupidly won't accept the shortcut unless it's in focus
+    (unless (eql (window-group win) (current-group))
+      ;;        jesus            christ        stump just export switch-to-group come on
+      (gselect (princ-to-string (group-number (window-group win)))))
+    (focus-window win t)
+    (meta (kbd "M-a"))))
+
+(defcommand end-zoom () ()
+  (when-let-window (win "^Zoom Meeting.*")
+    (kill-window win))
+  (sleep 2)
+  (when-let-window (win "^Zoom -.*")
+    (kill-window win)))
+
+(defcommand clear-notifications () ()
+  (run-shell-command "dunstctl close-all"))
+
+(defcommand start-vm () ()
+  (echo "Starting VM.")
+  (run-shell-command "/home/sjl/vms/run"))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/modeline.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,52 @@
+(in-package :stumpwm-user)
+
+(defun ensure-mode-line ()
+  (when (not (stumpwm::head-mode-line (current-head)))
+    (toggle-mode-line (current-screen) (current-head))))
+
+
+(defun configure-modeline ()
+  (setf
+    *time-modeline-string*
+    "%a %b %e %H:%M"
+
+    cpu::*cpu-usage-modeline-fmt*
+    "^[~A~3D%^]"
+
+    cpu::*cpu-modeline-fmt*
+    "[%c] [%f]"
+
+    mem::*mem-modeline-fmt*
+    "%b"
+
+    *screen-mode-line-format*
+    (append
+      (list "[^B"
+            '(:eval (princ-to-string (group-number (current-group))))
+            ":%n^b@%h] %W^>")
+
+      #+todo-some-day (list ;; "(V "
+                            ;; ;; '(:eval (volume))
+                            ;; ")"
+                            " ")
+
+      ;; battery and brightness for laptops
+      (hostcase
+        ((:gro :juss)
+         '("(B %B)"
+           " (BR "
+           (:eval (princ-to-string (brightness)))
+           "%)")))
+
+      ;; temp, cpu, mem, time, tray
+      #+no (list "(TEMP %S) (CPU %C) (MEM %M) %d %T")
+      (list "(CPU %C) (MEM %M) %d %T")
+      ))
+
+  (setf *mode-line-timeout* 10)
+  (setf *mode-line-background-color* "#111111")
+
+  (ensure-mode-line))
+
+(configure-modeline)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/modules.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,8 @@
+(in-package :stumpwm-user)
+
+(load-module "pass")
+(load-module "battery-portable")
+(load-module "cpu")
+(load-module "hostname")
+(load-module "mem")
+(load-module "stumptray")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/package.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,5 @@
+(in-package :stumpwm-user)
+
+(shadow :window)
+
+(use-package :losh)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/passwords.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,23 @@
+(in-package :stumpwm-user)
+
+(defcommand pass-personal () ()
+  (let ((pass:*password-store* "/home/sjl/.password-store/")
+        (pass:*pass-notification-message* t))
+    (pass:pass-copy)))
+
+(defcommand pass-um-1 () ()
+  (echo "Copying UM level 1 password, touch key.")
+  (run-shell-command "pass -c umich.edu/slosh"))
+
+(defcommand pass-um-2 () ()
+  (echo "Copying UM level 2 password, touch key.")
+  (run-shell-command "pass -c umich.edu/l2"))
+
+(defcommand switch-yubikeys () ()
+  (echo (run-shell-command "switch-yubikeys" t)))
+
+(defcommand generate-password () ()
+  (run-shell-command "genpass | pbc")
+  (message "Generated a fresh password and copied it to the clipboard."))
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/posture.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,84 @@
+(in-package :stumpwm-user)
+
+(defparameter *posture-thread* nil)
+(defparameter *posture-should-stop* nil)
+(defparameter *posture-paused* nil)
+(defparameter *posture-snooze* nil)
+(defparameter *posture-current* 30)
+(defparameter *posture-min* 5)
+(defparameter *posture-max* (hours->seconds 2))
+
+(defun posture-paused-p ()
+  ;; this is the dumbest shit ever, but I can't figure out how to call into
+  ;; stumpish from the setguid slock process
+  (or *posture-paused* (probe-file "/tmp/.posture-pause")))
+
+(defun posture-snoozed-p ()
+  (and *posture-snooze*
+       (< (get-universal-time) *posture-snooze*)))
+
+(defcommand posture-pause () ()
+  (message "Pausing posture.")
+  (setf *posture-paused* t))
+
+(defcommand posture-unpause () ()
+  (message "Unpausing posture.")
+  (setf *posture-paused* nil))
+
+(defcommand posture-toggle-pause () ()
+  (if (setf *posture-paused* (not *posture-paused*))
+    (message "Posture is now paused.")
+    (message "Posture is now unpaused.")))
+
+(defcommand posture-snooze (hours)
+    ((:real "Snooze for how many hours? "))
+  (setf *posture-snooze* (+ (hours->seconds hours) (get-universal-time))))
+
+(defun posture-update (delta)
+  (setf *posture-current*
+        (clamp *posture-min* *posture-max* (* *posture-current* delta))))
+
+(defun posture-query ()
+  (speak "Is your posture okay?"))
+
+(defcommand posture-answer-yes () ()
+  (message "Good work.")
+  (run-shell-command "echo $(epochseconds) 1.0 >> ~/.posture.log")
+  (posture-update 11/10))
+
+(defcommand posture-answer-meh () ()
+  (message "Better than nothing.")
+  (run-shell-command "echo $(epochseconds) 0.5 >> ~/.posture.log"))
+
+(defcommand posture-answer-no () ()
+  (message "Try harder.")
+  (run-shell-command "echo $(epochseconds) 0.0 >> ~/.posture.log")
+  (posture-update 8/10))
+
+(defun posture% ()
+  (if *posture-should-stop*
+    nil
+    (progn (unless (or (posture-paused-p) (posture-snoozed-p))
+             (posture-query)
+             (sleep 10))
+           *posture-current*)))
+
+(defun posture-running-p ()
+  (and *posture-thread* (sb-thread:thread-alive-p *posture-thread*)))
+
+(defcommand posture-stop () ()
+  (setf *posture-should-stop* t))
+
+(defcommand posture-start () ()
+  (setf *posture-should-stop* nil)
+  (if (posture-running-p)
+    (message "Posture loop was already running.")
+    (setf *posture-thread*
+          (sb-thread:make-thread
+            (lambda ()
+              (loop :for seconds = (posture%)
+                    :while seconds
+                    :do (sleep seconds))
+              (message "Posture loop exiting."))
+            :name "Posture thread"))))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/screenshots.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,11 @@
+(in-package :stumpwm-user)
+
+(defcommand screenshot () ()
+  (run-shell-command "screenshot"))
+
+(defcommand save-fucked-screenshot () ()
+  (run-shell-command "broken-screenshot"))
+
+(defcommand delete-fucked-screenshot () ()
+  (run-shell-command "delete-broken-screenshot"))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/sensors.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,53 @@
+(in-package :stumpwm-user)
+
+(defun ? (obj &rest keys)
+  (if (null keys)
+      obj
+      (apply #'? (etypecase obj
+                   (hash-table (gethash (first keys) obj)))
+             (rest keys))))
+
+(defun parse-sensors ()
+  ;; sensors -j is stupid and will output errors before the actual output on
+  ;; standard out, instead of putting them on standard err like a reasonable
+  ;; program, e.g.:
+  ;;
+  ;;     ERROR: Can't get value of subfeature temp1_input: Can't read
+  ;;     {
+  ;;        "iwlwifi_1-virtual-0":{ … },
+  ;;        …
+  ;;
+  ;; So we'll have to drop the `ERROR` lines before we can get to the actual
+  ;; goddamn JSON.  UNIX programs are so great.
+  (let ((s (losh:sh '("sensors" "-j") :result-type 'stream)))
+    (loop :while (char= #\E (peek-char nil s)) :do (read-line s))
+    (jarl:read t s)))
+
+(defparameter *sensors-refresh-delay* 5.0 "How long between sensor refreshes (in seconds).")
+(defparameter *sensors-next-refresh* nil)
+(defparameter *sensors-cache* nil)
+
+(defun sensors% (&aux (sensors (parse-sensors)))
+  (hostcase
+    (:ouroboros (format nil "[CPU ~D°C] [GPU ~D°C ~D°C ~D°C]"
+                        (round (? sensors "nct6779-isa-0290" "CPUTIN" "temp2_input"))
+                        (round (? sensors "amdgpu-pci-4500"  "edge"     "temp1_input"))
+                        (round (? sensors "amdgpu-pci-4500"  "junction" "temp2_input"))
+                        (round (? sensors "amdgpu-pci-4500"  "mem"      "temp3_input"))))
+    ((:gro :juss) (format nil "[CPU ~D°C] [GPU ~D°C]"
+                        (round (? sensors "thinkpad-isa-0000" "CPU"  "temp1_input"))
+                        (round (? sensors "amdgpu-pci-0600"   "edge" "temp1_input"))))
+    (t "?")))
+
+(defun sensors (&aux (now (get-internal-real-time)))
+  (if (or (null *sensors-next-refresh*)
+          (>= now *sensors-next-refresh*))
+      (setf *sensors-next-refresh* (+ now (* internal-time-units-per-second *sensors-refresh-delay*))
+            *sensors-cache* (sensors%))
+      *sensors-cache*))
+
+(defun sensors-modeline (ml)
+  (declare (ignore ml))
+  (sensors))
+
+(add-screen-mode-line-formatter #\S #'sensors-modeline)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/sound.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,14 @@
+(in-package :stumpwm-user)
+
+(defcommand mute () ()
+  (run-shell-command "mute")
+  (echo "Muted."))
+
+(defcommand volume-up () ()
+  (run-shell-command "amixer -q sset Master 5%+")
+  (message "Volume: ~D%" (volume)))
+
+(defcommand volume-down () ()
+  (run-shell-command "amixer -q sset Master 5%-")
+  (message "Volume: ~D%" (volume)))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/stumpconfig.asd	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,36 @@
+(asdf:defsystem :stumpconfig
+  :description "My StumpWM configuration."
+  :author "Steve Losh <steve@stevelosh.com>"
+
+  :depends-on (:losh
+               :split-sequence
+               :alexandria
+               :parse-number
+               :str
+               :cl-ppcre
+               :bordeaux-threads
+               :jarl
+               :local-time)
+
+  :serial t
+  :components ((:file "package")
+               (:file "modules")
+               (:file "config")
+               (:file "utils")
+               (:file "posture")
+               (:file "budget")
+               (:file "screenshots")
+               (:file "sound")
+               (:file "brightness")
+               (:file "passwords")
+               (:file "terminal-fonts")
+               (:file "clipboard")
+               (:file "applications")
+               (:file "timers")
+               (:file "icelandic")
+               (:file "sensors")
+               (:file "modeline")
+               (:file "vlime")
+               (:file "external-screens")
+               (:file "miscellaneous")
+               (:file "key-mapping")))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/stumpwmrc	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,12 @@
+(in-package :stumpwm-user)
+
+(ql:quickload :stumpconfig)
+
+(defvar *tray-loaded*
+  (run-commands "stumptray"))
+
+(defvar *dunst*
+  (run-shell-command "/usr/bin/dunst -conf ~/.dunstrc"))
+
+(when (probe-file "/home/sjl/.stumpwmrc.local")
+  (load "/home/sjl/.stumpwmrc.local"))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/terminal-fonts.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,27 @@
+(in-package :stumpwm-user)
+
+(defcommand reload-terminal-font-size ()
+    ()
+  (setf *terminal-font-size*
+        (if (probe-file "/home/sjl/.terminal-font")
+          (with-open-file (f "/home/sjl/.terminal-font")
+            (read f))
+          11)))
+
+(defparameter *terminal-font-size* (if (probe-file "/home/sjl/.terminal-font")
+                                     (with-open-file (f "/home/sjl/.terminal-font")
+                                       (read f))
+                                     11))
+
+(defcommand st-font-up ()
+    ()
+  (loop :repeat 7 :do (meta (kbd "C-S-SunPageUp"))))
+
+(defcommand st-font-down ()
+    ()
+  (loop :repeat 7 :do (meta (kbd "C-S-SunPageDown"))))
+
+(defcommand st-font-reset ()
+    ()
+  (meta (kbd "C-S-Home")))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/timers.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,38 @@
+(in-package :stumpwm-user)
+
+(defparameter *pop-timer-minutes* nil)
+(defparameter *pop-timer-seconds* nil)
+
+(defun pop-timer ()
+  (if (or (null *pop-timer-minutes*)
+          (null *pop-timer-seconds*))
+    (message "Pop timer is not configured.")
+    (progn
+      (message "Setting pop timer for ~D:~2,'0D."
+               *pop-timer-minutes* *pop-timer-seconds*)
+      (let* ((warning-time 30)
+             (total-time (+ (* *pop-timer-minutes* 60) *pop-timer-seconds*))
+             (initial-time (- total-time warning-time)))
+        (sb-thread:make-thread
+          (lambda ()
+            (if (plusp initial-time)
+              (progn (sleep initial-time)
+                     (speak "Pop soon.")
+                     (sleep warning-time))
+              (sleep total-time))
+            (speak "Pop!"))
+          :name "Pop Timer")))))
+
+(defcommand run-pop-timer () ()
+  (pop-timer))
+
+(defcommand set-pop-timer (minutes seconds)
+    ((:integer "Minutes: ")
+     (:integer "Seconds: "))
+  (setf *pop-timer-minutes* minutes
+        *pop-timer-seconds* seconds))
+
+(defcommand tea-timer (seconds)
+    ((:integer "Seconds: "))
+  (run-shell-command (format nil "tea ~D" seconds)))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/utils.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,126 @@
+(in-package :stumpwm-user)
+
+(defun string-contains (needle string)
+  (and (search needle string :test #'char=) t))
+
+(defun string-grep (needle text &key first-only)
+  (_ text
+    (split-sequence:split-sequence #\newline _)
+    (if first-only
+      (find needle _ :test #'string-contains)
+      (remove-if-not (alexandria:curry #'string-contains needle) _))))
+
+(defun string-split (delimiters string)
+  (split-sequence:split-sequence delimiters string
+                                 :test (lambda (bag ch)
+                                         (find ch bag :test #'char=))))
+
+(defun run-and-echo-shell-command (command &rest args)
+  (message command)
+  (apply #'run-shell-command command args))
+
+
+(defun mod+ (n increment modulo)
+  (mod (+ n increment) modulo))
+
+
+(defun volume ()
+  (_ (run-shell-command "amixer sget Master" t)
+    (string-grep "Front Left:" _ :first-only t)
+    (string-split "[]" _)
+    second
+    (string-trim "%" _)
+    parse-integer))
+
+
+(defun current-frame ()
+  (stumpwm::tile-group-current-frame (current-group)))
+
+
+(defun keywordize (string)
+  (_ string
+    (string-trim (string #\newline) _)
+    string-upcase
+    (intern _ (find-package :keyword))))
+
+(defparameter *host* (keywordize (machine-instance)))
+
+
+(defmacro ehostcase (&body clauses)
+  `(ecase *host* ,@clauses))
+
+(defmacro hostcase (&body clauses)
+  `(case *host* ,@clauses))
+
+
+(defcommand speak (text)
+    ((:string "Text: "))
+  (message text)
+  (run-shell-command (format nil "~~/src/dotfiles/bin/say '~A'" text)))
+
+
+(defun seconds->hours (seconds)
+  (/ seconds 60 60))
+
+(defun hours->seconds (hours)
+  (* hours 60 60))
+
+
+(define-stumpwm-type :integer (input prompt)
+  ;; Annoyingly, StumpWM's built-in :number type isn't actually number, but is
+  ;; actually just integers.  Define a better-named type here.
+  (when-let ((n (or (argument-pop input)
+                    (read-one-line (current-screen) prompt))))
+    (handler-case
+        (parse-integer n)
+      (parse-error (c)
+        (declare (ignore c))
+        (throw 'error "Integer required.")))))
+
+(define-stumpwm-type :real (input prompt)
+  (when-let ((n (or (argument-pop input)
+                    (read-one-line (current-screen) prompt))))
+    (handler-case
+        (let ((result (parse-number:parse-number n)))
+          (assert (typep result 'real))
+          result)
+      (error (c)
+        (declare (ignore c))
+        (throw 'error "Real required.")))))
+
+
+(defun window-match-p (query window)
+  "Return whether `window` matches `query`.
+
+  `query` must be of the form `(query-type query-value)`.
+
+  `query-type` must be one of `:title` or `:class`.
+
+  `query-value` must either be a string (which must be matched exactly) or
+  a PPCRE scanner.
+
+  "
+  (destructuring-bind (query-type query) query
+    (let ((value (ecase query-type
+                   (:title (window-title window))
+                   (:class (window-class window)))))
+      (etypecase query
+        (string (string= query value))
+        (function (ppcre:scan query value))))))
+
+(defun all-windows ()
+  "Return a fresh list of all windows on all screens.  Yes, all of them."
+  (mapcan #'screen-windows *screen-list*))
+
+(defun find-window (query)
+  "Find and return the first window that matches `query` under `window-match-p`."
+  (find-if (lambda (w) (window-match-p query w)) (all-windows)))
+
+(defun find-windows (query)
+  "Find and return a fresh list of all windows that match `query` under `window-match-p`."
+  (remove-if-not (lambda (w) (window-match-p query w)) (all-windows)))
+
+(defmacro when-let-window ((symbol title-query) &body body)
+  `(when-let ((,symbol (find-window `(:title ,(ppcre:create-scanner ,title-query)))))
+     ,@body))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stumpwm/vlime.lisp	Tue Mar 19 13:55:42 2024 -0400
@@ -0,0 +1,15 @@
+(in-package :stumpwm-user)
+
+(defcommand vlime () ()
+  (load "~/src/dotfiles/vim/bundle/vlime/lisp/start-vlime.lisp")
+  (message "Started VLIME"))
+
+(defcommand vlime-port (port) ((:integer "Port: "))
+  "Start VLIME on the given port.
+
+  Good for bootstrapping a VLIME connection when you accidentally started a
+  VLIME instance on another port that you don't want to mess with.
+
+  "
+  (funcall (read-from-string "vlime-loader::run") port)
+  (message "Started VLIME"))
--- a/stumpwmrc	Mon Mar 18 12:49:56 2024 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1041 +0,0 @@
-(in-package :stumpwm-user)
-(shadow :window)
-
-(ql:quickload '(:losh :split-sequence :alexandria :parse-number :str :cl-ppcre :bordeaux-threads :jarl :local-time)
-              :silent t)
-
-(use-package :losh)
-
-;;;; Config -------------------------------------------------------------------
-(set-prefix-key (kbd "C-space"))
-(local-time:reread-timezone-repository)
-
-(set-focus-color "#aaaaaa")
-(set-win-bg-color "#111111")
-(set-unfocus-color "#444444")
-(setf *normal-border-width* 1
-      *default-bg-color* #x222222
-      *window-border-style* :thin
-      (xlib:window-background (screen-root (current-screen))) *default-bg-color*)
-
-(defvar *redirected* (redirect-all-output (data-dir-file "debug" "log")))
-
-(setf *mouse-focus-policy* :click
-      *message-window-gravity* :center
-      *input-window-gravity* :center
-      *debug-level* 0
-      *resize-increment* 75
-      *new-frame-action* :empty
-      *window-format* "(%n%m%20t)"
-      *window-name-source* :title
-      *maximum-completions* 20
-      *shell-program* "/home/sjl/src/dotfiles/bin/bash-dammit"
-      losh:*pbcopy-command* "/home/sjl/src/dotfiles/bin/pbcopy"
-      losh:*pbpaste-command* "/home/sjl/src/dotfiles/bin/pbpaste")
-
-
-;;;; Utils --------------------------------------------------------------------
-(defun string-contains (needle string)
-  (and (search needle string :test #'char=) t))
-
-(defun string-grep (needle text &key first-only)
-  (_ text
-    (split-sequence:split-sequence #\newline _)
-    (if first-only
-      (find needle _ :test #'string-contains)
-      (remove-if-not (alexandria:curry #'string-contains needle) _))))
-
-(defun string-split (delimiters string)
-  (split-sequence:split-sequence delimiters string
-                                 :test (lambda (bag ch)
-                                         (find ch bag :test #'char=))))
-
-(defun run-and-echo-shell-command (command &rest args)
-  (message command)
-  (apply #'run-shell-command command args))
-
-
-(defun mod+ (n increment modulo)
-  (mod (+ n increment) modulo))
-
-
-(defun volume ()
-  (_ (run-shell-command "amixer sget Master" t)
-    (string-grep "Front Left:" _ :first-only t)
-    (string-split "[]" _)
-    second
-    (string-trim "%" _)
-    parse-integer))
-
-
-(defun current-frame ()
-  (stumpwm::tile-group-current-frame (current-group)))
-
-
-(defun keywordize (string)
-  (_ string
-    (string-trim (string #\newline) _)
-    string-upcase
-    (intern _ (find-package :keyword))))
-
-(defparameter *host* (keywordize (machine-instance)))
-
-
-(defmacro ehostcase (&body clauses)
-  `(ecase *host* ,@clauses))
-
-(defmacro hostcase (&body clauses)
-  `(case *host* ,@clauses))
-
-
-(defcommand speak (text)
-    ((:string "Text: "))
-  (message text)
-  (run-shell-command (format nil "~~/src/dotfiles/bin/say '~A'" text)))
-
-
-(defun seconds->hours (seconds)
-  (/ seconds 60 60))
-
-(defun hours->seconds (hours)
-  (* hours 60 60))
-
-
-(define-stumpwm-type :integer (input prompt)
-  ;; Annoyingly, StumpWM's built-in :number type isn't actually number, but is
-  ;; actually just integers.  Define a better-named type here.
-  (when-let ((n (or (argument-pop input)
-                    (read-one-line (current-screen) prompt))))
-    (handler-case
-        (parse-integer n)
-      (parse-error (c)
-        (declare (ignore c))
-        (throw 'error "Integer required.")))))
-
-(define-stumpwm-type :real (input prompt)
-  (when-let ((n (or (argument-pop input)
-                    (read-one-line (current-screen) prompt))))
-    (handler-case
-        (let ((result (parse-number:parse-number n)))
-          (assert (typep result 'real))
-          result)
-      (error (c)
-        (declare (ignore c))
-        (throw 'error "Real required.")))))
-
-
-(defun window-match-p (query window)
-  "Return whether `window` matches `query`.
-
-  `query` must be of the form `(query-type query-value)`.
-
-  `query-type` must be one of `:title` or `:class`.
-
-  `query-value` must either be a string (which must be matched exactly) or
-  a PPCRE scanner.
-
-  "
-  (destructuring-bind (query-type query) query
-    (let ((value (ecase query-type
-                   (:title (window-title window))
-                   (:class (window-class window)))))
-      (etypecase query
-        (string (string= query value))
-        (function (ppcre:scan query value))))))
-
-(defun all-windows ()
-  "Return a fresh list of all windows on all screens.  Yes, all of them."
-  (mapcan #'screen-windows *screen-list*))
-
-(defun find-window (query)
-  "Find and return the first window that matches `query` under `window-match-p`."
-  (find-if (lambda (w) (window-match-p query w)) (all-windows)))
-
-(defun find-windows (query)
-  "Find and return a fresh list of all windows that match `query` under `window-match-p`."
-  (remove-if-not (lambda (w) (window-match-p query w)) (all-windows)))
-
-(defmacro when-let-window ((symbol title-query) &body body)
-  `(when-let ((,symbol (find-window `(:title ,(ppcre:create-scanner ,title-query)))))
-     ,@body))
-
-
-;;;; Posture ------------------------------------------------------------------
-(defparameter *posture-thread* nil)
-(defparameter *posture-should-stop* nil)
-(defparameter *posture-paused* nil)
-(defparameter *posture-snooze* nil)
-(defparameter *posture-current* 30)
-(defparameter *posture-min* 5)
-(defparameter *posture-max* (hours->seconds 2))
-
-(defun posture-paused-p ()
-  ;; this is the dumbest shit ever, but I can't figure out how to call into
-  ;; stumpish from the setguid slock process
-  (or *posture-paused* (probe-file "/tmp/.posture-pause")))
-
-(defun posture-snoozed-p ()
-  (and *posture-snooze*
-       (< (get-universal-time) *posture-snooze*)))
-
-(defcommand posture-pause () ()
-  (message "Pausing posture.")
-  (setf *posture-paused* t))
-
-(defcommand posture-unpause () ()
-  (message "Unpausing posture.")
-  (setf *posture-paused* nil))
-
-(defcommand posture-toggle-pause () ()
-  (if (setf *posture-paused* (not *posture-paused*))
-    (message "Posture is now paused.")
-    (message "Posture is now unpaused.")))
-
-(defcommand posture-snooze (hours)
-    ((:real "Snooze for how many hours? "))
-  (setf *posture-snooze* (+ (hours->seconds hours) (get-universal-time))))
-
-(defun posture-update (delta)
-  (setf *posture-current*
-        (clamp *posture-min* *posture-max* (* *posture-current* delta))))
-
-(defun posture-query ()
-  (speak "Is your posture okay?"))
-
-(defcommand posture-answer-yes () ()
-  (message "Good work.")
-  (run-shell-command "echo $(epochseconds) 1.0 >> ~/.posture.log")
-  (posture-update 11/10))
-
-(defcommand posture-answer-meh () ()
-  (message "Better than nothing.")
-  (run-shell-command "echo $(epochseconds) 0.5 >> ~/.posture.log"))
-
-(defcommand posture-answer-no () ()
-  (message "Try harder.")
-  (run-shell-command "echo $(epochseconds) 0.0 >> ~/.posture.log")
-  (posture-update 8/10))
-
-(defun posture% ()
-  (if *posture-should-stop*
-    nil
-    (progn (unless (or (posture-paused-p) (posture-snoozed-p))
-             (posture-query)
-             (sleep 10))
-           *posture-current*)))
-
-(defun posture-running-p ()
-  (and *posture-thread* (sb-thread:thread-alive-p *posture-thread*)))
-
-(defcommand posture-stop () ()
-  (setf *posture-should-stop* t))
-
-(defcommand posture-start () ()
-  (setf *posture-should-stop* nil)
-  (if (posture-running-p)
-    (message "Posture loop was already running.")
-    (setf *posture-thread*
-          (sb-thread:make-thread
-            (lambda ()
-              (loop :for seconds = (posture%)
-                    :while seconds
-                    :do (sleep seconds))
-              (message "Posture loop exiting."))
-            :name "Posture thread"))))
-
-
-;;;; Budget ------------------------------------------------------------------
-(defparameter *tz/eastern*
-  (local-time:find-timezone-by-location-name "US/Eastern"))
-
-(defparameter *budget/start*
-  (local-time:encode-timestamp 0 0 0 0 29 8 2023 :timezone *tz/eastern*))
-
-(defun budget/per-day ()
-  (first (losh:read-all-from-file "/home/sjl/Sync/budget/per-day")))
-
-(defun budget/elapsed ()
-  (local-time:timestamp-difference (local-time:now) *budget/start*))
-
-(defun budget/days-elapsed ()
-  (floor (/ (budget/elapsed) (* 60 60 24))))
-
-(defun budget/in ()
-  (* (budget/days-elapsed) (budget/per-day)))
-
-(defun budget/out ()
-  (loop :for path :in (directory "/home/sjl/Sync/budget/hosts/*/total")
-        :summing (print (first (read-all-from-file (print path))))))
-
-(defun budget/current ()
-  (- (budget/in) (budget/out)))
-
-(defcommand budget-dump () ()
-  (message
-    (sh '("sh" "-c" "tail -n 5 /home/sjl/Sync/budget/hosts/*/records")
-        :result-type 'string)))
-
-(defcommand budget () ()
-  (message "$~D" (budget/current)))
-
-(defmacro with-budget-file ((f file &rest open-args) &body body)
-  `(with-open-file
-     (,f (format nil "/home/sjl/Sync/budget/hosts/~(~A~)/~A" *host* ,file)
-      ,@open-args)
-     ,@body))
-
-(defcommand spend (amount what) ((:integer "Amount: $") (:string "For: "))
-  (let ((current (with-budget-file (total "total")
-                   (first (read-all-from-file total))))
-        (timestamp (local-time:to-rfc3339-timestring (local-time:now))))
-    (with-budget-file (total "total" :direction :output :if-exists :supersede)
-      (print (+ current amount) total))
-    (with-budget-file (records "records" :direction :output :if-exists :append :if-does-not-exist :create)
-      (print (list timestamp amount what) records))
-    (message "Spent $~D for ~A at ~A" amount what timestamp)))
-
-
-;;;; Load ---------------------------------------------------------------------
-(load-module "pass")
-
-;;;; Screenshotting -----------------------------------------------------------
-(defcommand screenshot () ()
-  (run-shell-command "screenshot"))
-
-(defcommand save-fucked-screenshot () ()
-  (run-shell-command "broken-screenshot"))
-
-(defcommand delete-fucked-screenshot () ()
-  (run-shell-command "delete-broken-screenshot"))
-
-
-;;;; Sound --------------------------------------------------------------------
-(defcommand mute () ()
-  (run-shell-command "mute")
-  (echo "Muted."))
-
-(defcommand volume-up () ()
-  (run-shell-command "amixer -q sset Master 5%+")
-  (message "Volume: ~D%" (volume)))
-
-(defcommand volume-down () ()
-  (run-shell-command "amixer -q sset Master 5%-")
-  (message "Volume: ~D%" (volume)))
-
-
-;;;; Brightness ---------------------------------------------------------------
-(defparameter *brightness-values* #(0 1 5 10 20 30 40 55 70 85 100))
-(defvar *brightness-index* 5)
-
-(defun brightness ()
-  (aref *brightness-values* *brightness-index*))
-
-(defun set-brightness (value)
-  (run-and-echo-shell-command
-    (hostcase
-      ((:gro :juss) (format nil "xrandr --output ~A --brightness ~D"
-                      (hostcase ((:gro :juss) "eDP"))
-                      (/ value 100.0)))
-      (t (message "Not sure how to set brightness on this machine.")))))
-
-(defun rotate-brightness (delta)
-  (setf *brightness-index*
-        (mod+ *brightness-index* delta (length *brightness-values*)))
-  (set-brightness (brightness)))
-
-
-(defcommand rotate-brightness-up () ()
-  (rotate-brightness 1))
-
-(defcommand rotate-brightness-down () ()
-  (rotate-brightness -1))
-
-
-;;;; Miscellaneous ------------------------------------------------------------
-(defcommand sane-hsplit () ()
-  (hsplit)
-  (move-focus :right))
-
-(defcommand sane-vsplit () ()
-  (vsplit)
-  (move-focus :down))
-
-
-(defcommand move-focus* (direction)
-    ((:direction "Enter a direction: "))
-  (labels ((in-float-p ()
-             (typep (current-group) 'stumpwm::float-group))
-           (focus-first-frame ()
-             (unless (in-float-p)
-               ;; After moving to a new group we don't know which frame is
-               ;; focused, and unfortunately Stump doesn't give us a nice way to
-               ;; say "focus the leftmost frame" so we'll just move the focus
-               ;; a bunch of times and hope it's enough.  Sigh.
-               (loop :repeat 15
-                     :until (eql (current-frame)
-                                 (progn (move-focus (ecase direction
-                                                      (:left :right)
-                                                      (:right :left)))
-                                        (current-frame))))))
-           (next-group ()
-             (ecase direction
-               (:right (gnext))
-               (:left (gprev)))
-             (focus-first-frame)))
-    (unless (in-float-p)
-      (banish))
-    (if (in-float-p)
-      (next-group)
-      (let ((frame (current-frame)))
-        (move-focus direction)
-        (when (eql frame (current-frame))
-          (next-group))))))
-
-(defcommand screen-laptop () ()
-  (only)
-  (hostcase
-    ((:gro :juss) (loop :with laptop = "eDP"
-                        :with extern = (hostcase (:gro "DisplayPort-0")
-                                                 (:juss "HDMI-A-0"))
-                        :for (output commands) :in `((,laptop ("--auto"))
-                                                     (,laptop ("--primary"))
-                                                     (,extern ("--off")))
-                        :do (progn (uiop:run-program `("xrandr" "--output" ,output ,@commands)))))
-    (t (message "Not configured on this system."))))
-
-(defcommand screen-external () ()
-  (only)
-  (hostcase
-    ((:gro :juss) (loop :with laptop = "eDP"
-                        :with extern = (hostcase (:gro "DisplayPort-0")
-                                                 (:juss "HDMI-A-0"))
-                        :for (output commands) :in `((,extern ("--auto"))
-                                                     (,extern ("--primary"))
-                                                     (,laptop ("--off")))
-                        :do (uiop:run-program `("xrandr" "--output" ,output ,@commands))))
-    (t (message "Not configured on this system."))))
-
-(defcommand vlime () ()
-  (load "~/src/dotfiles/vim/bundle/vlime/lisp/start-vlime.lisp")
-  (message "Started VLIME"))
-
-(defcommand vlime-port (port) ((:integer "Port: "))
-  "Start VLIME on the given port.
-
-  Good for bootstrapping a VLIME connection when you accidentally started a
-  VLIME instance on another port that you don't want to mess with.
-
-  "
-  (funcall (read-from-string "vlime-loader::run") port)
-  (message "Started VLIME"))
-
-(defcommand toggle-current-mode-line () ()
-  (toggle-mode-line (current-screen) (current-head)))
-
-(defcommand toggle-stumptray () ()
-  (run-commands "stumptray"))
-
-(defcommand pass-personal () ()
-  (let ((pass:*password-store* "/home/sjl/.password-store/")
-        (pass:*pass-notification-message* t))
-    (pass:pass-copy)))
-
-(defcommand pass-um-1 () ()
-  (echo "Copying UM level 1 password, touch key.")
-  (run-shell-command "pass -c umich.edu/slosh"))
-
-(defcommand pass-um-2 () ()
-  (echo "Copying UM level 2 password, touch key.")
-  (run-shell-command "pass -c umich.edu/l2"))
-
-(defcommand switch-yubikeys () ()
-  (echo (run-shell-command "switch-yubikeys" t)))
-
-(defcommand generate-password () ()
-  (run-shell-command "genpass | pbc")
-  (message "Generated a fresh password and copied it to the clipboard."))
-
-(defcommand kill-and-remove () ()
-  (run-commands "kill" "remove"))
-
-(defcommand sleep-machine ()
-    ()
-  (hostcase
-    ((:gro :juss)
-     (run-shell-command "exec lock-screen")
-     (run-shell-command "systemctl suspend"))
-    (t (message "Not sleeping this machine for safety."))))
-
-(defcommand copy-clhs-url (s)
-    ((:string "Symbol: "))
-  (run-shell-command (format nil "clhs --url 'http://www.lispworks.com/documentation/HyperSpec/' --quiet --open echon '~A' | pbcopy" s)))
-
-(defcommand describe-window () ()
-  (show-window-properties))
-
-(defcommand rain () ()
-  (_ '("/home/sjl/src/dotfiles/lisp/bin/weather" "48105" "-H" "36")
-    (losh:sh _ :result-type 'list)
-    (mapcar (lambda (line) (ppcre:regex-replace " 1[0-9]:00 " line "^6\\&^*")) _)
-    (message "~{~A~^~%~}" _)))
-
-(defcommand mark (thing) ((:string "Mark: "))
-  (run-shell-command (format nil "mark ~A" thing)))
-
-(defcommand toggle-zoom-mute () ()
-  (when-let-window (win "^Zoom Meeting.*")
-    ;; Zoom stupidly won't accept the shortcut unless it's in focus
-    (unless (eql (window-group win) (current-group))
-      ;;        jesus            christ        stump just export switch-to-group come on
-      (gselect (princ-to-string (group-number (window-group win)))))
-    (focus-window win t)
-    (meta (kbd "M-a"))))
-
-(defcommand end-zoom () ()
-  (when-let-window (win "^Zoom Meeting.*")
-    (kill-window win))
-  (sleep 2)
-  (when-let-window (win "^Zoom -.*")
-    (kill-window win)))
-
-(defcommand clear-notifications () ()
-  (run-shell-command "dunstctl close-all"))
-
-(defcommand rstudio () ()
-  (run-shell-command "rstudio"))
-
-(defcommand start-vm () ()
-  (echo "Starting VM.")
-  (run-shell-command "/home/sjl/vms/run"))
-
-
-
-;;;; Terminal Fonts -----------------------------------------------------------
-(defcommand reload-terminal-font-size ()
-    ()
-  (setf *terminal-font-size*
-        (if (probe-file "/home/sjl/.terminal-font")
-          (with-open-file (f "/home/sjl/.terminal-font")
-            (read f))
-          11)))
-
-(defparameter *terminal-font-size* (if (probe-file "/home/sjl/.terminal-font")
-                                     (with-open-file (f "/home/sjl/.terminal-font")
-                                       (read f))
-                                     11))
-
-(defcommand st-font-up ()
-    ()
-  (loop :repeat 7 :do (meta (kbd "C-S-SunPageUp"))))
-
-(defcommand st-font-down ()
-    ()
-  (loop :repeat 7 :do (meta (kbd "C-S-SunPageDown"))))
-
-(defcommand st-font-reset ()
-    ()
-  (meta (kbd "C-S-Home")))
-
-
-;;;; Clipboard/Data Generation ------------------------------------------------
-(load-module "clipboard-history")
-(clipboard-history:start-clipboard-manager)
-
-(defcommand generate-random-uuid () ()
-  (run-shell-command "uuidgen | tr -d '\\n' | ~/src/dotfiles/bin/pbcopy")
-  (message "Copied random UUID to clipboard."))
-
-(defcommand bee-movie-script () ()
-  (run-shell-command "pbeecopy")
-  (message "Copied the entire Bee Movie script to clipboard."))
-
-(defcommand urlize-jira-issue () ()
-  (let ((issue (str:trim (pbpaste))))
-    (if (ppcre:scan "^[A-Z0-9]+-\\d+$" issue)
-      (let* ((endpoint (str:trim (run-shell-command "grep endpoint .jira.d/config.yml | sed -e 's/.*: //'" t)))
-             (url (format nil "~A/browse/~A" endpoint issue)))
-        (pbcopy url)
-        (message "Copied ~A to the clipboard." url))
-      (message "Clipboard does not look like a JIRA issue."))))
-
-
-;;;; Applications -------------------------------------------------------------
-(defcommand spotify () ()
-  (run-or-raise "spotify" '(:class "Spotify")))
-
-(defcommand files () ()
-  (run-shell-command "open $HOME"))
-
-(defcommand browser () ()
-  (run-or-raise "firefox" '(:class "firefox")))
-
-(defcommand vlc () ()
-  (run-or-raise "vlc" '(:class "vlc")))
-
-(defcommand terminal () ()
-  (run-shell-command (format nil "st -f 'Ubuntu Mono:size=~D'" *terminal-font-size*)))
-
-(defcommand terminal-apl () ()
-  (run-shell-command "st -f 'BQN386 Unicode:style=Regular:size=12'"))
-
-(defcommand gcontrol () ()
-  (run-or-raise "gcontrol" '(:class "Gnome-control-center")))
-
-(defcommand zoom () ()
-  (when-let-window (w "^Zoom Meeting.*")
-    (focus-window w t)))
-
-(defcommand papers () ()
-  (run-or-raise "jabref" '(:class "org.jabref.gui.MainApplication")))
-
-
-;;;; Timers -------------------------------------------------------------------
-(defparameter *pop-timer-minutes* nil)
-(defparameter *pop-timer-seconds* nil)
-
-(defun pop-timer ()
-  (if (or (null *pop-timer-minutes*)
-          (null *pop-timer-seconds*))
-    (message "Pop timer is not configured.")
-    (progn
-      (message "Setting pop timer for ~D:~2,'0D."
-               *pop-timer-minutes* *pop-timer-seconds*)
-      (let* ((warning-time 30)
-             (total-time (+ (* *pop-timer-minutes* 60) *pop-timer-seconds*))
-             (initial-time (- total-time warning-time)))
-        (sb-thread:make-thread
-          (lambda ()
-            (if (plusp initial-time)
-              (progn (sleep initial-time)
-                     (speak "Pop soon.")
-                     (sleep warning-time))
-              (sleep total-time))
-            (speak "Pop!"))
-          :name "Pop Timer")))))
-
-(defcommand run-pop-timer () ()
-  (pop-timer))
-
-(defcommand set-pop-timer (minutes seconds)
-    ((:integer "Minutes: ")
-     (:integer "Seconds: "))
-  (setf *pop-timer-minutes* minutes
-        *pop-timer-seconds* seconds))
-
-(defcommand tea-timer (seconds)
-    ((:integer "Seconds: "))
-  (run-shell-command (format nil "tea ~D" seconds)))
-
-
-;;;; Isk ----------------------------------------------------------------------
-(defcommand send-key (key &optional (win (current-window))) (:key)
-  "Send key press and key release events for KEY to window WIN."
-  ;; from https://github.com/alezost/stumpwm-config/blob/master/utils.lisp
-  (let ((xwin (window-xwin win)))
-    (multiple-value-bind (code state) (stumpwm::key-to-keycode+state key)
-      (flet ((send (event)
-               (xlib:send-event xwin event (xlib:make-event-mask event)
-                                :display *display*
-                                :root (screen-root (window-screen win))
-                                :x 0 :y 0 :root-x 0 :root-y 0
-                                :window xwin :event-window xwin
-                                :code code
-                                :state state)))
-        (send :key-press)
-        (send :key-release)
-        (xlib:display-finish-output *display*)))))
-
-(defun send-keys (keys &key (win (current-window)) (sleep 0))
-  (dolist (k keys)
-    (send-key (kbd k) win)
-    (sleep sleep)))
-
-(defmacro defmultikey (name key compose-keys)
-  ;; Unfortunately we can't reliably autogen the name with something like
-  ;; (symb 'mk- compose-key) here because things like đ (th) and Đ (TH) would
-  ;; case fold to the same name.
-  `(progn
-     (defcommand ,name () ()
-       (send-keys '("Multi_key" ,@(map 'list #'string compose-keys))))
-     (define-key *top-map*
-       (kbd ,key) ,(string name))))
-
-(defmacro defmultikeys (&rest bindings)
-  `(progn ,@(loop for binding :in bindings :collect `(defmultikey ,@binding))))
-
-(defmultikeys
-  (isk-l-á "M-a" "'a")
-  (isk-u-Á "M-A" "'A")
-  (isk-l-é "M-e" "'e")
-  (isk-u-É "M-E" "'E")
-  (isk-l-í "M-i" "'i")
-  (isk-u-Í "M-I" "'I")
-  (isk-l-ó "M-o" "'o")
-  (isk-u-Ó "M-O" "'O")
-  (isk-l-ö "M-m" "\"o")
-  (isk-u-Ö "M-M" "\"O")
-  (isk-l-ú "M-u" "'u")
-  (isk-u-Ú "M-U" "'U")
-  (isk-l-ý "M-y" "'y")
-  (isk-u-Ý "M-Y" "'Y")
-  (isk-l-þ "M-t" "th")
-  (isk-u-Þ "M-T" "TH")
-  (isk-l-đ "M-d" "dh")
-  (isk-u-Đ "M-D" "DH")
-  (isk-l-æ "M-h" "ae")
-  (isk-u-Æ "M-H" "AE"))
-
-
-(defcommand thinkpad-ret () ()
-  (send-key (kbd "RET")))
-
-(defcommand thinkpad-bs () ()
-  (send-key (kbd "BackSpace")))
-
-
-;;;; Key Mapping --------------------------------------------------------------
-;;; Conventions:
-;;;
-;;; * Hyper-dir: move focus
-;;; * Hyper-Shift-dir: move window
-;;; * Hyper-Shift-Control-dir: swap window
-;;; * Hyper-F*: hardware
-;;; * Shift-F*: timers
-;;; * Hyper-Super-*: layout
-;;; * Hyper-*: miscellaneous
-
-(defmacro define-top-keys (&body keyforms)
-  `(progn ,@(loop :for form :in keyforms
-                  :collect `(define-key *top-map*
-                              (kbd ,(first form))
-                              ,(second form)))))
-
-
-(define-top-keys ;; miscellaneous
-  ("H-m" "terminal")
-  ("H-M" "mark")
-  ("H-SunPageUp" "st-font-up")
-  ("H-SunPageDown" "st-font-down")
-  ("H-Home" "st-font-reset")
-  ("H-F4" "switch-yubikeys")
-  ("H-\\" "pass-personal")
-  ("H-|" "generate-password")
-  ("s-1" "pass-um-1")
-  ("s-2" "pass-um-2")
-  ("H-b" "browser")
-  ("H-O" "spotify")
-  ("H-o" "files")
-  ("H-z" "zoom")
-  ("H-Z" "toggle-zoom-mute")
-  ("C-H-Z" "end-zoom")
-  ("F26"   "prev")
-  ("S-F26" "next")
-  ("H-q" "exec lock-screen")
-  ("H-y" "screenshot")
-  ("H-g" "gcontrol")
-  ("H-f" "save-fucked-screenshot")
-  ("H-F" "delete-fucked-screenshot")
-  ("H-R" "rstudio")
-  ("H-r" "rain")
-  ("H-V" "vlc")
-  ("H-e" "budget")
-  ("H-E" "spend")
-  ("C-BackSpace" "clear-notifications")
-  )
-
-(define-top-keys ;; clipboard
-  ("H-c" "show-clipboard-history")
-  ("H-C" "clear-clipboard-history")
-  ("H-u" "generate-random-uuid")
-  ("H-B" "bee-movie-script")
-  ("M-H-u" "urlize-jira-issue"))
-
-(define-top-keys ;; movement
-  ("H-h" "move-focus* left")
-  ("H-j" "move-focus down")
-  ("H-k" "move-focus up")
-  ("H-l" "move-focus* right")
-
-  ("H-H" "move-window left")
-  ("H-J" "move-window down")
-  ("H-K" "move-window up")
-  ("H-L" "move-window right")
-
-  ("H-1" "gselect 1")
-  ("H-2" "gselect 2")
-  ("H-3" "gselect 3")
-  ("H-4" "gselect 4")
-  ("H-5" "gselect 5")
-  ("H-6" "gselect 6")
-
-  ("KP_End"       "gselect 1")
-  ("KP_Down"      "gselect 2")
-  ("KP_Page_Down" "gselect 3")
-  ("KP_Left"      "gselect 4")
-  ("KP_Begin"     "gselect 5")
-  ("KP_Right"     "gselect 6")
-  ("KP_Home"      "gselect 7")
-  ("KP_Up"        "gselect 8")
-  ("KP_Page_Up"   "gselect 9")
-
-  ("H-!" "gmove 1")
-  ("H-@" "gmove 2")
-  ("H-#" "gmove 3")
-  ("H-$" "gmove 4")
-  ("H-%" "gmove 5")
-  ("H-^" "gmove 6")
-
-  ("C-H-H" "exchange-direction left")
-  ("C-H-J" "exchange-direction down")
-  ("C-H-K" "exchange-direction up")
-  ("C-H-L" "exchange-direction right")
-
-  ("H-`" "next")
-  ("S-H-`" "prev")
-  ("H-n" "next-in-frame")
-  ("H-p" "prev-in-frame")
-  ("H-N" "pull-hidden-next")
-  ("H-P" "papers")
-
-  ("H-," "pull-from-windowlist"))
-
-
-(define-top-keys ;; splitting
-  ("H-s" "sane-vsplit")
-  ("H-v" "sane-hsplit")
-  ("H-=" "balance-frames"))
-
-(define-top-keys ;; killing
-  ("H-w" "delete")
-  ("H-W" "kill")
-  ("H-BackSpace" "remove")
-  ("S-H-BackSpace" "kill-and-remove"))
-
-(define-top-keys ;; broken thinkpad keys
-  ("s-m" "thinkpad-ret")
-  ("s-Delete" "thinkpad-bs"))
-
-
-(define-top-keys ;; naming
-  ("H-'" "title"))
-
-(define-top-keys ;; sound
-  ("H-F1" "mute")
-  ("H-F2" "volume-down")
-  ("H-F3" "volume-up")
-  ("XF86AudioMute" "exec mute")
-  ("XF86AudioRaiseVolume" "volume-down") ; todo unfuck the backwards mapping in qmk
-  ("XF86AudioLowerVolume" "volume-up"))
-
-(define-top-keys ;; screen
-  ("H-F5" "rotate-brightness-down")
-  ("H-F6" "rotate-brightness-up")
-  ("H-F7" "screen-laptop")
-  ("H-F8" "screen-external"))
-
-(define-top-keys ;; layout
-  ("s-H-o" "only")
-  ("s-H-t" "restore-from-file thirds")
-  ("s-H-m" "restore-from-file dev")
-  ("s-H-s" "restore-from-file streaming")
-  ("s-H-w" "restore-from-file work")
-  ("s-H-z" "restore-from-file zoom"))
-
-(define-top-keys ;; timers
-  ("s-F7"  "tea-timer")
-  ("s-F9"  "run-pop-timer")
-  ("s-F8"  "set-pop-timer")
-  ("s-p"   "posture-start")
-  ("s-P"   "posture-stop")
-  ("s-y"   "posture-answer-yes")
-  ("s-h"   "posture-answer-meh")
-  ("s-n"   "posture-answer-no")
-  ("s-\\"  "posture-toggle-pause")
-  ("s-o"   "posture-snooze"))
-
-(define-top-keys ;; stump
-  ("Pause" "terminal") ; jesus christ
-  ("H-F9"  "sleep-machine")
-  ("H-F10" "toggle-stumptray")
-  ("H-F11" "toggle-current-mode-line")
-  ("H-F12" "refresh-heads"))
-
-
-;; (stumpwm::unbind-remapped-keys)
-(define-remapped-keys
-  '(("st-256color"
-     ("s-c" . "C-C")
-     ("s-v" . "C-V")
-     ("C-=" . "S-C-SunPageUp")
-     ("C--" . "S-C-SunPageDown")
-     ("C-0" . "S-C-Home"))
-    ("(firefox|Google-chrome|Chromium-browser)"
-     ("s-[" . "C-S-Tab")
-     ("s-]" . "C-Tab")
-     ("C-a" . "Home")
-     ("C-e" . "End")
-     ;; I always try to hit ctrl-d to kill a browser window because I'm so used
-     ;; to terminal windows, and it ends up bookmarking the damn page.  In the
-     ;; interest of not having a random collection of bookmarks grow over time,
-     ;; I'll just add a mapping to compensate for my stupid brain.
-     ("C-d" . "C-w")
-     ;; todo debug why this breaks a really fast C-a-k roll
-     ;; ("C-a" . "Home")
-     ;; ("C-e" . "End")
-     ("s-a" . "C-a")
-     ("s-d" . "C-d")
-     ("s-l" . "C-l")
-     ("s-t" . "C-t")
-     ("s-w" . "C-w")
-     ("s-r" . "C-r")
-     ("s-f" . "C-f")
-     ("s-z" . "C-z")
-     ("s-x" . "C-x")
-     ("s-c" . "C-c")
-     ("s-v" . "C-v"))
-    (""
-     ("s-z" . "C-z")
-     ("s-x" . "C-x")
-     ("s-c" . "C-c")
-     ("s-v" . "C-v"))))
-
-
-;;;; Sensors ------------------------------------------------------------------
-(defun ? (obj &rest keys)
-  (if (null keys)
-      obj
-      (apply #'? (etypecase obj
-                   (hash-table (gethash (first keys) obj)))
-             (rest keys))))
-
-(defun parse-sensors ()
-  ;; sensors -j is stupid and will output errors before the actual output on
-  ;; standard out, instead of putting them on standard err like a reasonable
-  ;; program, e.g.:
-  ;;
-  ;;     ERROR: Can't get value of subfeature temp1_input: Can't read
-  ;;     {
-  ;;        "iwlwifi_1-virtual-0":{ … },
-  ;;        …
-  ;;
-  ;; So we'll have to drop the `ERROR` lines before we can get to the actual
-  ;; goddamn JSON.  UNIX programs are so great.
-  (let ((s (losh:sh '("sensors" "-j") :result-type 'stream)))
-    (loop :while (char= #\E (peek-char nil s)) :do (read-line s))
-    (jarl:read t s)))
-
-(defparameter *sensors-refresh-delay* 5.0 "How long between sensor refreshes (in seconds).")
-(defparameter *sensors-next-refresh* nil)
-(defparameter *sensors-cache* nil)
-
-(defun sensors% (&aux (sensors (parse-sensors)))
-  (hostcase
-    (:ouroboros (format nil "[CPU ~D°C] [GPU ~D°C ~D°C ~D°C]"
-                        (round (? sensors "nct6779-isa-0290" "CPUTIN" "temp2_input"))
-                        (round (? sensors "amdgpu-pci-4500"  "edge"     "temp1_input"))
-                        (round (? sensors "amdgpu-pci-4500"  "junction" "temp2_input"))
-                        (round (? sensors "amdgpu-pci-4500"  "mem"      "temp3_input"))))
-    ((:gro :juss) (format nil "[CPU ~D°C] [GPU ~D°C]"
-                        (round (? sensors "thinkpad-isa-0000" "CPU"  "temp1_input"))
-                        (round (? sensors "amdgpu-pci-0600"   "edge" "temp1_input"))))
-    (t "?")))
-
-(defun sensors (&aux (now (get-internal-real-time)))
-  (if (or (null *sensors-next-refresh*)
-          (>= now *sensors-next-refresh*))
-      (setf *sensors-next-refresh* (+ now (* internal-time-units-per-second *sensors-refresh-delay*))
-            *sensors-cache* (sensors%))
-      *sensors-cache*))
-
-(defun sensors-modeline (ml)
-  (declare (ignore ml))
-  (sensors))
-
-(add-screen-mode-line-formatter #\S #'sensors-modeline)
-
-
-;;;; Modeline -----------------------------------------------------------------
-(load-module "battery-portable")
-(load-module "cpu")
-(load-module "hostname")
-(load-module "mem")
-
-(defun ensure-mode-line ()
-  (when (not (stumpwm::head-mode-line (current-head)))
-    (toggle-mode-line (current-screen) (current-head))))
-
-
-(defun configure-modeline ()
-  (setf
-    *time-modeline-string*
-    "%a %b %e %H:%M"
-
-    cpu::*cpu-usage-modeline-fmt*
-    "^[~A~3D%^]"
-
-    cpu::*cpu-modeline-fmt*
-    "[%c] [%f]"
-
-    mem::*mem-modeline-fmt*
-    "%b"
-
-    *screen-mode-line-format*
-    (append
-      (list "[^B"
-            '(:eval (princ-to-string (group-number (current-group))))
-            ":%n^b@%h] %W^>")
-
-      #+todo-some-day (list ;; "(V "
-                            ;; ;; '(:eval (volume))
-                            ;; ")"
-                            " ")
-
-      ;; battery and brightness for laptops
-      (hostcase
-        ((:gro :juss)
-         '("(B %B)"
-           " (BR "
-           (:eval (princ-to-string (brightness)))
-           "%)")))
-
-      ;; temp, cpu, mem, time, tray
-      #+no (list "(TEMP %S) (CPU %C) (MEM %M) %d %T")
-      (list "(CPU %C) (MEM %M) %d %T")
-      ))
-
-  (setf *mode-line-timeout* 10)
-  (setf *mode-line-background-color* "#111111")
-
-  (ensure-mode-line)
-  )
-
-(configure-modeline)
-
-
-;;;; System Tray --------------------------------------------------------------
-(load-module "stumptray")
-(defvar *tray-loaded* (run-commands "stumptray"))
-
-
-;;;; Unbreak Typing -----------------------------------------------------------
-(defun stumpwm::input-insert-hyphen-or-space (input key)
-  (declare (ignore key))
-  (input-insert-char input #\space))
-
-
-;;;; Startup ------------------------------------------------------------------
-;; (defvar *dropbox*
-  ;; (run-shell-command "~/.dropbox-dist/dropboxd"))
-
-(defvar *dunst*
-  (run-shell-command "/usr/bin/dunst -conf ~/.dunstrc"))
-
-(when (probe-file "/home/sjl/.stumpwmrc.local")
-  (load "/home/sjl/.stumpwmrc.local"))
-
-
-#;;; Scratch ------------------------------------------------------------------
-
-(group-number (current-group))
-
-(message "~D" *terminal-font-size*)
--- a/vim/custom-dictionary.utf-8.add	Mon Mar 18 12:49:56 2024 -0400
+++ b/vim/custom-dictionary.utf-8.add	Tue Mar 19 13:55:42 2024 -0400
@@ -429,3 +429,7 @@
 trimethylated
 BIOINF
 astrocyte
+Armis
+Taubman
+H3K27M
+polycomb
--- a/vim/vimrc	Mon Mar 18 12:49:56 2024 -0400
+++ b/vim/vimrc	Tue Mar 19 13:55:42 2024 -0400
@@ -563,6 +563,7 @@
     au BufNewFile,BufRead *.paren set filetype=lisp
     au BufNewFile,BufRead .abclrc set filetype=lisp
     au BufNewFile,BufRead .lisprc set filetype=lisp
+    au BufNewFile,BufRead stumpwmrc set filetype=lisp
     au BufNewFile,BufRead .stumpwmrc set filetype=lisp
     au BufNewFile,BufRead .stumpwmrc.local set filetype=lisp