From 030a4fb5107ffd888d83a3546945e3cc03b55354 Mon Sep 17 00:00:00 2001 From: Ian Keane Date: Mon, 10 Aug 2026 07:35:24 -0400 Subject: [PATCH] Floating window fix, dynamic keymap hints --- config/stumpwm/config.lisp | 124 +++++++++++++++++++++++++++---------- 1 file changed, 91 insertions(+), 33 deletions(-) diff --git a/config/stumpwm/config.lisp b/config/stumpwm/config.lisp index 7cf2fa7..f4eca9d 100644 --- a/config/stumpwm/config.lisp +++ b/config/stumpwm/config.lisp @@ -33,6 +33,8 @@ (setf *ignore-wm-inc-hints* t) (set-font "-b&h-luxi mono-medium-r-normal--20-*-*-*-m-0-iso10646-1") (setf *message-window-padding* 16) +(setf *message-window-margin* 20) +(setf *message-window-y-margin* 20) (setf *message-window-gravity* :center) (setf *input-window-gravity* :center) @@ -47,12 +49,20 @@ (setf *new-frame-action* :empty) (setf *suppress-frame-indicator* t) +;;; Groups — must be set up before define-frame-preference so names match +(grename "1") +(run-commands "gnewbg 2" "gnewbg 3" "gnewbg 4") + ;;; Window placement policy -(define-frame-preference "Default" - (0 t nil :class "Konqueror" :role "...konqueror-mainwindow") - (1 t nil :class "XTerm") - (:float t t :class "alsamixer-scratch") - (:float t t :class "ncmpcpp-scratch")) +;; "Default" is the initial group name before grename runs at the bottom. +;; These float rules must use "Default" — StumpWM evaluates frame preferences +;; at window-open time against the group name, and the first group starts as +;; "Default" until grename changes it at startup (but the rule is registered +;; at load time when the name is still being matched by content, not snapshot). +;; In practice (:float t t) with target-group matching works correctly here. +(define-frame-preference nil + (:float t nil :class "alsamixer-scratch") + (:float t nil :class "ncmpcpp-scratch")) (define-frame-preference "scratch-emacs-1" (0 t t :title "scratch-emacs-1")) @@ -243,23 +253,40 @@ If PERSISTENT is t, smart-kill will hide the window instead of killing it." ;; This causes other windows to bleed through when transparency is used. ;; This hook hides all frame siblings when focus changes so only the ;; focused window and the wallpaper are visible through any transparency. -(add-hook *focus-window-hook* - (lambda (new old) - (declare (ignore old)) - (when new - (let* ((group (window-group new)) - (frame (window-frame new)) - (siblings (remove new (frame-windows group frame)))) - (mapc #'hide-window siblings) - (unhide-window new))))) +;; Named function (not lambda) so loadrc replaces it instead of accumulating copies. +(defun hide-unfocused-frame-siblings (new old) + (declare (ignore old)) + (when (and new (typep new 'stumpwm::tile-window)) + (let* ((group (window-group new)) + (frame (window-frame new)) + (siblings (remove new (frame-windows group frame)))) + (mapc #'hide-window siblings) + (unhide-window new)))) + +(remove-hook *focus-window-hook* 'hide-unfocused-frame-siblings) +(add-hook *focus-window-hook* 'hide-unfocused-frame-siblings) + +(defun float-window-to-current-group (win) + "Move a float window to the current group by directly manipulating group membership, + bypassing group-add-window (which would retrigger frame preferences and group switches)." + (let ((current (current-group)) + (old (window-group win))) + (unless (eq old current) + (setf (group-windows old) (remove win (group-windows old))) + (setf (window-group win) current) + (push win (group-windows current))))) (defun spawn-or-focus (class command) - "Unhide or focus existing window with CLASS, or spawn COMMAND if none exists." + "Unhide or focus existing window with CLASS, or spawn COMMAND if none exists. + Float windows are moved to the current group directly to avoid group switches." (let ((win (find-if (lambda (w) (string= (window-class w) class)) (screen-windows (current-screen))))) (cond ((null win) (run-shell-command command)) ((eq win (current-window)) nil) - (t (focus-window win))))) + (t + (when (typep win 'stumpwm::float-window) + (float-window-to-current-group win)) + (focus-window win))))) (defcommand smart-kill () () "Kill window normally, unless it's a persistent float — then hide it instead." @@ -285,10 +312,19 @@ If PERSISTENT is t, smart-kill will hide the window instead of killing it." (defcommand ncmpcpp-float () () "Open ncmpcpp in a persistent floating wezterm window." - (spawn-or-focus "ncmpcpp-scratch" "wezterm start --class ncmpcpp-scratch -- ncmpcpp")) -;;; Config keymap -;;; Appearance sub-map (s-W t / s-W T) -(defvar *appearance-map* (make-sparse-keymap)) + (spawn-or-focus "ncmpcpp-scratch" "wezterm start --class ncmpcpp-scratch -- ncmpcpp" :persistent t)) +;;; Keymaps +;; +;; All named keymaps are defined here. Add a keymap to *hint-keymaps* to make +;; it auto-display its bindings in the top-left corner when its prefix key is +;; pressed, instead of requiring '?'. Remove from the list to disable hints. + +(defvar *config-map* (make-sparse-keymap)) +(defvar *brightness-map* (make-sparse-keymap)) +(defvar *gaps-map* (make-sparse-keymap)) +(defvar *window-map* (make-sparse-keymap)) +(defvar *media-map* (make-sparse-keymap)) +(defvar *appearance-map* (make-sparse-keymap)) (defvar *appearance-all-map* (make-sparse-keymap)) ;; Global opacity — stored so new windows can inherit it via *new-window-hook* @@ -361,12 +397,39 @@ If PERSISTENT is t, smart-kill will hide the window instead of killing it." (define-key *appearance-all-map* (kbd "8") "global-opacity-80") (define-key *appearance-all-map* (kbd "9") "global-opacity-90") (define-key *appearance-all-map* (kbd "0") "global-opacity-100") +(defvar *brightness-map* (make-sparse-keymap)) +(defvar *gaps-map* (make-sparse-keymap)) +(defvar *window-map* (make-sparse-keymap)) +(defvar *media-map* (make-sparse-keymap)) +;; Keymaps that automatically show their bindings when activated. +;; Add or remove keymaps here to control which ones show hints. +(defvar *hint-keymaps* + (list *config-map* + *window-map* + *media-map* + *brightness-map* + *gaps-map* + *appearance-map* + *appearance-all-map*)) + +;; Show bindings in top-left for any keymap in *hint-keymaps*. +(add-hook *key-press-hook* + (lambda (key key-seq cmd) + (declare (ignore key cmd)) + (let* ((oriented (reverse key-seq)) + (maps (get-kmaps-at-key-seq (dereference-kmaps (top-maps)) oriented))) + (when-let ((hint-maps (remove-if-not + (lambda (m) (member m *hint-keymaps*)) + maps))) + (let ((*message-window-gravity* :top-left)) + (apply #'display-bindings-for-keymaps oriented hint-maps)))))) + +;;; Config map bindings (define-key *config-map* (kbd "a") "alsamixer-float") +(define-key *config-map* (kbd "b") *brightness-map*) -(define-key *window-map* (kbd "t") *appearance-map*) -(define-key *window-map* (kbd "T") *appearance-all-map*) -(defvar *brightness-map* (make-sparse-keymap)) +;;; Brightness map bindings (define-key *brightness-map* (kbd "1") "brightness-10") (define-key *brightness-map* (kbd "2") "brightness-20") (define-key *brightness-map* (kbd "3") "brightness-30") @@ -377,21 +440,20 @@ If PERSISTENT is t, smart-kill will hide the window instead of killing it." (define-key *brightness-map* (kbd "8") "brightness-80") (define-key *brightness-map* (kbd "9") "brightness-90") (define-key *brightness-map* (kbd "0") "brightness-100") -(define-key *config-map* (kbd "b") *brightness-map*) -;;; Keymaps - -(defvar *gaps-map* (make-sparse-keymap)) +;;; Gaps map bindings (define-key *gaps-map* (kbd "0") "gaps-off") (define-key *gaps-map* (kbd "1") "gaps-small") (define-key *gaps-map* (kbd "2") "gaps-medium") (define-key *gaps-map* (kbd "3") "gaps-large") -(defvar *window-map* (make-sparse-keymap)) +;;; Window map bindings (define-key *window-map* (kbd "r") "iresize") (define-key *window-map* (kbd "g") *gaps-map*) +(define-key *window-map* (kbd "t") *appearance-map*) +(define-key *window-map* (kbd "T") *appearance-all-map*) -(defvar *media-map* (make-sparse-keymap)) +;;; Media map bindings (define-key *media-map* (kbd "v") "mpv-pick") (define-key *media-map* (kbd "m") "mpv-url") (define-key *media-map* (kbd "d") "yt-download") @@ -415,10 +477,6 @@ If PERSISTENT is t, smart-kill will hide the window instead of killing it." (define-key *top-map* (kbd "s-W") *window-map*) (define-key *top-map* (kbd "s-m") *media-map*) -;;; Groups -(grename "1") -(run-commands "gnewbg 2" "gnewbg 3" "gnewbg 4") - ;;; Group switching (define-key *top-map* (kbd "s-1") "gselect 1") (define-key *top-map* (kbd "s-2") "gselect 2")