Skip to content
aykaramba edited this page Jun 19, 2022 · 8 revisions

This is a spot to share nice tricks and snippets of code you've discovered. RC files can get kind of long and intimidating at times, so it would be cool if folks could just share the occasional bit o' wisdom.


Someone's .stumpwmrc available online had the following useful functions in it:

    (defun shell-command (command) "Run a shell command and display output to screen.
    This must be used in a functional side-effects-free style! If a program does not
    exit of its own accord, Stumpwm might hang!"
           (check-type command string)
           (echo-string (current-screen) (run-shell-command command t)))

    (define-stumpwm-command "shell-command" ((command :string "sh: " :string))
      (check-type command string)
      (shell-command command))

    (defun cat (&rest strings) "Concatenates strings, like the Unix command 'cat'.
    A shortcut for (concatenate 'string foo bar)."
           (apply 'concatenate 'string strings))

Building on these, and using the (external) program xautomation, a functional (but ugly and non-native) equivalent to ratpoison's ratclick is possible like so:

    (defun stump-send-click (button iterations)
      "Send a click to the current pointer location.
    `button' is which mouse button to use and `iterations' is how many times to click
    (so twice would be a double-click, for example)."
      (loop while (> iterations 0) do
           (shell-command (cat "xte 'mouseclick " (write-to-string button) "'"))
           (setq iterations (- iterations 1))))

    (define-stumpwm-command "new-ratclick" ((button :number "Button: ") (iterations :number "How many times? "))
      (when (current-window)
         (stump-send-click button iterations)))

I prefer to bind these to a key, since I use a separate keymap for all my mouse control needs. If you provide no argument, it will prompt for both. If you provide one, it'll prompt for the second, and if you provide both it'll just click.

    (define-key *rat-map* (kbd "s") "new-ratclick 1 1")
    (define-key *rat-map* (kbd "d") "new-ratclick 2 1")
    (define-key *rat-map* (kbd "f") "new-ratclick 3 1")
    (define-key *rat-map* (kbd "S") "new-ratclick 1")
    (define-key *rat-map* (kbd "D") "new-ratclick 2")
    (define-key *rat-map* (kbd "F") "new-ratclick 3")

I spotted performance problems with StumpWM when running on SBCL. When the system gets under load, StumpWM gets quite sluggish and needs up to two seconds to respond in the worst case. Switching to CLISP fixes this, so I guess it is a memory problem. I have only 512MB main memory.

-- I had this problem on Linux, rebuilding SBCL from source seems to fix it.

This problem appears to occur when sbcl is compiled with threads. Compiling sbcl without threads seems to fix it.

-- Supposedly, recent versions of SBCL correct this.


Here there is a small starting applications menu. I have it bound to C-t . which is symmetrical to C-t c :-)

    (define-stumpwm-command "mymenu" ()
      (labels ((pick (options)
                 (let ((selection (stumpwm::select-from-menu (current-screen) options "")))
                   (cond
                     ((null selection)
                      (throw 'stumpwm::error "Abort."))
                     ((stringp (second selection))
                      (second selection))
                     (t
                      (pick (cdr selection)))))))
        (let ((choice (pick *app-menu*)))
          (run-shell-command choice))))

    (defparameter *app-menu* '(("INTERNET"
                              ;; sub menu
                        ("Firefox" "firefox")
                        ("Skype" "skype"))
                   ("FUN"
                    ;; sub menu
                        ("option 2" "xlogo")
                        ("GnuChess" "xboard"))
                       ("WORK"
                    ;;submenu
                    ("OpenOffice.org" "openoffice"))
                   ("GRAPHICS"
                    ;;submenu
                    ("GIMP" "gimp"))
                    ("K3B" "k3b")))

This defines two new StumpWM commands: gforward and gbackward that "move" current group. It's quite handy when you have many groups and want to reorder them.

    (defun swap-groups (group1 group2)
      (rotatef (slot-value group1 'number) (slot-value group2 'number)))

    (defun move-group-forward (&optional (group (current-group)))
      (swap-groups group (next-group group (sort-groups (current-screen)))))

    (defun move-group-backward (&optional (group (current-group)))
      (swap-groups group (next-group group (reverse (sort-groups (current-screen))))))

    (define-stumpwm-command "gforward" ()
      (move-group-forward)
      (echo-groups (current-screen) *group-format*))

    (define-stumpwm-command "gbackward" ()
      (move-group-backward)
      (echo-groups (current-screen) *group-format*))

This is the code that I use to put a "gmail notifier"-style indicator in my mode-line. It requires the XMLS and DRAKMA packages; both of these packages are pretty straightforward to download and install.

    (asdf:oos 'asdf:load-op '#:drakma) ; http client
    (asdf:oos 'asdf:load-op '#:xmls)   ; XML parser

    (defvar *gmail-cookies* (make-instance 'drakma:cookie-jar)
      "Contains cookies for talking to gmail server")
    (defvar *gmail-username* "<my gmail username>"
      "Username for gmail")
    (defvar *gmail-password* "<my gmail password>"
      "Password for gmail")

    (defun ping-gmail ()
      "Checks gmail's atom feed for new messages.  First return value is number of new messages,
       second is a list of (AUTHOR . TITLE) cons cells."
      (when (and *gmail-username* *gmail-password*)
        (multiple-value-bind (response-body response-code)
            (drakma:http-request "https://mail.google.com/mail/feed/atom" :cookie-jar *gmail-cookies*
                                 :basic-authorization (list *gmail-username* *gmail-password*))
          (if (= 401 response-code)
            :401-unauthorized
            (let* ((feed-tree (xmls:parse response-body))
                   (fullcount-tag (find "fullcount" (xmls:node-children feed-tree)
                                        :key 'xmls:node-name :test 'equal)))
              (assert (string= "feed" (xmls:node-name feed-tree)))
              (when (and fullcount-tag
                         (stringp (first (xmls:node-children fullcount-tag))))
                (values (or (read-from-string (first (xmls:node-children fullcount-tag)))
                            0)
                        (loop for child in (xmls:node-children feed-tree)
                              for title-tag = (when (equal (xmls:node-name child) "entry")
                                                (find "title" (xmls:node-children child)
                                                      :key 'xmls:node-name :test 'equal))
                              for author-tag = (when (equal (xmls:node-name child) "entry")
                                                 (find "author" (xmls:node-children child)
                                                       :key 'xmls:node-name :test 'equal))
                              when (and title-tag author-tag)
                              collect (cons
                                       (first (xmls:node-children (first (xmls:node-children author-tag))))
                                       (first (xmls:node-children title-tag)))))))))))

    (defparameter *gmail-show-titles* nil
      "When non-NIL, show the authors and titles whenever new mail arrives.")
    (defparameter *gmail-ping-period* (* 2 60 internal-time-units-per-second)
      "Time between each gmail server ping")

    (defvar *gmail-last-ping* 0
      "The internal time of the latest ping of the gmail server")
    (defvar *gmail-last-value* nil
      "The result of the latest ping of the gmail server")

    (defun format-gmail (stream)
      "Formats to STREAM a string representing the current status of the gmail mailbox.  Uses cached
          values if it is called more frequently than once every *GMAIL-PING-PERIOD*.  When new mail
          arrives, this function will also display a message containing all the current inbox items"
      (when (> (- (get-internal-real-time)
                  *gmail-last-ping*)
               *gmail-ping-period*)
        (multiple-value-bind (num-msgs msg-summaries)
            (ping-gmail)
          (when (and *gmail-show-titles*
                     num-msgs (> num-msgs 0)
                     (or (null *gmail-last-value*)
                         (/= num-msgs *gmail-last-value*)))
            (let ((*timeout-wait* (* *timeout-wait* (min 2 num-msgs)))) ; leave time to read the titles
              (message "~A"
                       (with-output-to-string (s)
                         (loop for (author . title) in msg-summaries
                               do (format s "~A - ~A~%" author title))))))
          (setf *gmail-last-value* num-msgs
                *gmail-last-ping* (get-internal-real-time))))
      (cond
        ((null *gmail-last-value*)
         (format stream "[mail:???]"))
        ((and (numberp *gmail-last-value*)
              (zerop *gmail-last-value*))
         (format stream "[mail: 0]"))
        ((numberp *gmail-last-value*)
         (format stream "[MAIL:~2D]" *gmail-last-value*))
        (t
         (format stream "[mail:ERR]"))))

    (setf *screen-mode-line-format* (list "%w   "
                                          ;; ... some other modeline settings ...
                                          '(:eval (format-gmail nil))))

This is the code I use in my .stumpwmrc to display the current CPU and I/O load in the mode-line:

    (defvar *prev-user-cpu* 0)
    (defvar *prev-sys-cpu* 0)
    (defvar *prev-idle-cpu* 0)
    (defvar *prev-iowait* 0)

    (defun current-cpu-usage ()
      "Return the average CPU usage since the last call.
       First value is percent of CPU in use.
       Second value is percent of CPU in use by system processes.
       Third value is percent of time since last call spent waiting for IO (or 0 if not available)."
      (let ((cpu-result 0)
            (sys-result 0)
            (io-result nil))
        (with-open-file (in #P"/proc/stat" :direction :input)
          (if (eq 'cpu (read in))
            (let* ((norm-user (read in))
                   (nice-user (read in))
                   (user (+ norm-user nice-user))
                   (sys (read in))
                   (idle (read in))
                   (iowait (or (ignore-errors (read in)) 0))
                   (step-denom (- (+ user sys idle iowait)
                                  (+ *prev-user-cpu* *prev-sys-cpu* *prev-idle-cpu* *prev-iowait*))))
              (setf cpu-result (/ (- (+ user sys)
                                     (+ *prev-user-cpu* *prev-sys-cpu*))
                                  step-denom)
                    sys-result (/ (- sys *prev-sys-cpu*)
                                  step-denom)
                    io-result (/ (- iowait *prev-iowait*)
                                 step-denom)
                    *prev-user-cpu* user
                    *prev-sys-cpu* sys
                    *prev-idle-cpu* idle
                    *prev-iowait* iowait))
            (warn "Unexpected header")))
        (values cpu-result sys-result io-result)))

    (defun format-current-cpu-usage (stream)
      "Formats a string representing the current processor usage to STREAM.
       Arguments are as those to FORMAT, so NIL returns a formatted string and T prints to standard
       output."
      (multiple-value-bind (cpu sys io) (current-cpu-usage)
        (declare (ignore sys))
        (format stream "[cpu:~3D%] [io:~3D%]" (truncate (* 100 cpu)) (if io (truncate (* 100 io)) 0))))

    (setf *screen-mode-line-format* (list "%w   "
                                          ;; ... some other modeline settings ...
                                          '(:eval (format-current-cpu-usage nil))))

Commands for controlling the volume.

I realized that I was only using xmodmap for volume control at this point, and decided to streamline a bit, especially since StumpWM can display arbitrary messages, which is useful to me. So here's a command to do a define-stumpwm-command for every combination amixer channel and volume-change/muting you want, without copying and pasting the same command again and again.

    ;;; A command to create volume-control commands
    (defun def-volcontrol (channel amount)
      "Commands for controling the volume"
      (define-stumpwm-command
        (concat "amixer-" channel "-" (or amount "toggle")) ()
        (echo-string
         (current-screen)
         (concat channel " " (or amount "toggled") "
    "
                 (run-shell-command
                  (concat "amixer sset " channel " " (or amount "toggle") "| grep '^[ ]*Front'") t)))))

    (defvar amixer-channels '("PCM" "Master" "Headphone"))
    (defvar amixer-options '(nil "1+" "1-"))

    (let ((channels amixer-channels))
      (loop while channels do
            (let ((options amixer-options))
              (loop while options do
                    (def-volcontrol (car channels) (car options))
                    (setq options (cdr options))))
            (setq channels (cdr channels))))

    (define-stumpwm-command "amixer-sense-toggle" ()
      (echo-string
       (current-screen)
       (concat "Headphone Jack Sense toggled
    "
               (run-shell-command "amixer sset 'Headphone Jack Sense' toggle" t))))

Note: If you want to use the XF86 volume keys, at present you'll need to add these lines to your .stumpwmrc, or you'll get errors.

    (define-keysym #x1008ff11 "XF86AudioLowerVolume")
    (define-keysym #x1008ff12 "XF86AudioMute")
    (define-keysym #x1008ff13 "XF86AudioRaiseVolume")

...and my setup.

    (define-key *top-map* (kbd "XF86AudioLowerVolume")   "amixer-PCM-1-")
    (define-key *top-map* (kbd "XF86AudioRaiseVolume")   "amixer-PCM-1+")
    (define-key *top-map* (kbd "XF86AudioMute")          "amixer-PCM-toggle")

    (define-key *top-map* (kbd "C-XF86AudioLowerVolume") "amixer-Master-1-")
    (define-key *top-map* (kbd "C-XF86AudioRaiseVolume") "amixer-Master-1+")
    (define-key *top-map* (kbd "C-XF86AudioMute")        "amixer-Master-toggle")

    (define-key *top-map* (kbd "M-XF86AudioLowerVolume") "amixer-Headphone-1-")
    (define-key *top-map* (kbd "M-XF86AudioRaiseVolume") "amixer-Headphone-1+")
    (define-key *top-map* (kbd "M-XF86AudioMute")        "amixer-Headphone-toggle")

    (define-key *top-map* (kbd "S-XF86AudioMute")        "amixer-sense-toggle")

I use groups a lot so I found I hardly used the stumpwm "select" command because it only works on the current group, so I made a different version that works for all groups in the current screen.

    (defun my-global-window-names ()
      "Returns a list of the names of all the windows in the current screen."
      (let ((groups (sort-groups (current-screen)))
            (windows nil))
        (dolist (group groups)
          (dolist (window (group-windows group))
            ;; Don't include the current window in the list
            (when (not (eq window (current-window)))
              (setq windows (cons (window-name window) windows)))))
        windows))

    (defun my-window-in-group (query group)
      "Returns a window matching QUERY in GROUP."
      (let ((match nil)
            (end nil)
            (name nil))
        (dolist (window (group-windows group))
          (setq name (window-name window)
                end (min (length name) (length query)))
          ;; Never match the current window
          (when (and (string-equal name query :end1 end :end2 end)
                     (not (eq window (current-window))))
            (setq match window)
            (return)))
        match))

    (define-stumpwm-type :my-global-window-names (input prompt)
      (or (argument-pop input)
          (completing-read (current-screen) prompt (my-global-window-names))))

    (define-stumpwm-command "global-select" ((query :my-global-window-names "Select: "))
      "Like select, but for all groups not just the current one."
      (let ((window nil))
        ;; Check each group to see if it's in
        (dolist (group (screen-groups (current-screen)))
          (setq window (my-window-in-group query group))
          (when window
            (switch-to-group group)
            (frame-raise-window group (window-frame window) window)
            (return)))))

This command runs the stumpwm "quit" command, but only if there aren't any windows open.

    (define-stumpwm-command "safequit" ()
      "Checks if any windows are open before quitting."
      (let ((win-count 0))

        ;; Count the windows in each group
        (dolist (group (screen-groups (current-screen)))
          (setq win-count (+ (length (group-windows group)) win-count)))

        ;; Display the number of open windows or quit
        (if (= win-count 0)
            (run-commands "quit")
          (message (format nil "You have ~d ~a open" win-count
                           (if (= win-count 1) "window" "windows"))))))

This displays a summary of all new mail in your mail spool. You can use it on a one-off basis or on a timer.

    (defvar *newmail-timer* nil
      "Runs the mail checker.")

    (defvar my-emails nil
      "The previous formatted contents of the mail spool.")

    (defun my-mail-popup (emails)
      "Displays a summary of all new email."
      (let ((summary nil))
        ;; Create the text for the summary
        (if emails
            (setq summary (concatenate 'string (format nil "^6*System mailbox (~a)^n~% ~%"
                                                       (length (split-string emails))) emails))
            (setq summary "^6*System mailbox (0)"))
        ;; Display the summary
        (message "~a" summary)))

    (defun my-get-mail ()
      "Returns the formatted contents of the mail spool."
      (let ((mail nil)
            (lines (run-prog-collect-output "/bin/grep" "-e" "^Subject:"
                                            "/var/spool/mail/USERNAME")))
        (when (not (string= lines ""))
          ;; Split the subjects by newline
          (setq lines (split-string lines))

          ;; Add each of the subject lines
          (dolist (line lines)
            (setq mail (concatenate 'string mail (format nil "~a~%" (subseq line 9))))))

        mail))

    (defun my-check-mail ()
      "Displays the mail popup if there's new email in the spool."
      (let ((newmail (my-get-mail)))
        (when (and newmail (not (string= my-emails newmail)))
          (my-mail-popup newmail))
        (setq my-emails newmail)))

    (define-stumpwm-command "mail" ()
      "Displays the mail popup."
      (setq my-emails (my-get-mail))
      (my-mail-popup my-emails))

    (defun my-stop-newmail-timer ()
      "Stops the newmail timer."
      (ignore-errors
        (cancel-timer *newmail-timer*)))

    (defun my-start-newmail-timer ()
      "Starts the newmail timer."
      (my-stop-newmail-timer)
      (setf *newmail-timer* (run-with-timer 10 10 'my-check-mail)))

    (define-stumpwm-command "mailstart" ()
      "Starts the newmail timer."
      (my-start-newmail-timer))

    (define-stumpwm-command "mailstop" ()
      "Stops the newmail timer."
      (my-stop-newmail-timer))

Fluxbox-style Alt-F# group ("desktop") switching.

    (dotimes (i 13)
      (unless (eq i 0) ; F0 is non-existant and will error.
        (define-key *top-map* (kbd (format nil "M-F~a" i)) (format nil "gselect ~a" i))))

If you ever forget which key map you're in, this works kind of like Emacs' mode-line except as messages.

    (defun show-key-seq (key seq val)
      (message (print-key-seq (reverse seq))))
    (add-hook *key-press-hook* 'show-key-seq)

Get EMMS to do its thing:

    (defvar *my-emms-bindings*
      '(("n" "emms-next")
        ("p" "emms-previous")
        ("s" "emms-stop")
        ("P" "emms-pause")))

    (stumpwm:define-key stumpwm:*root-map* (stumpwm:kbd "m")
      (let ((m (stumpwm:make-sparse-keymap)))
        (map nil #'(lambda (x)
                     (stumpwm:define-key m (stumpwm:kbd (car x))
                       (concat "exec emacsclient -e '(" (cadr x) ")'")))
             *my-emms-bindings*)
        m))

Allow windows to get raised even if they are in an inactive window group:

        (defun raise-urgent-window-hook (target)
          (gselect (window-group target))
          (really-raise-window target))
        (add-hook *urgent-window-hook* 'raise-urgent-window-hook)

Make screenshot commands (module depends on zpng):

(in-package :stumpwm)

(eval-when (:compile-toplevel :load-toplevel :execute)
  (require 'asdf)
  (asdf:load-system 'zpng))

(defcommand screenshot
    (filename)
    ((:rest "Filename: "))
  "Make screenshot of root window"
  (%screenshot-window (screen-root (current-screen)) filename))

(defcommand screenshot-window
    (filename)
    ((:rest "Filename: "))
  "Make screenshot of focus window"
  (%screenshot-window (window-xwin (current-window)) filename))

(defun %screenshot-window (drawable file &key (height (xlib:drawable-height drawable))
                                        (width (xlib:drawable-width drawable)))
  (let* ((png (make-instance 'zpng:pixel-streamed-png
                            :color-type :truecolor-alpha
                            :width width
                            :height height)))
    (multiple-value-bind (pixarray depth visual)
        (xlib:get-raw-image drawable :x 0 :y 0 :width width :height height
                :format :Z-PIXMAP)
      (with-open-file (stream file
                              :direction :output
                              :if-exists :supersede
                              :if-does-not-exist :create
                              :element-type '(unsigned-byte 8))
        (zpng:start-png png stream)
        ;;(zpng:write-row pixarray png)
        (case (xlib:display-byte-order (xlib:drawable-display drawable))
          (:lsbfirst
           (do ((i 0 (+ 4 i)))
               ((>= i (length pixarray)))
             (zpng:write-pixel (list (aref pixarray (+ 2 i))
                                     (aref pixarray (+ 1 i))
                                     (aref pixarray i)
                                     #xFF)
                               png)))
          (:msbfirst 
           (do ((i 0 (+ 4 i)))
               ((>= i (* height width 4)))
             (zpng:write-pixel (list (aref pixarray (1+ i))
                                     (aref pixarray (+ 2 i))
                                     (aref pixarray (+ 3 i))
                                     #xFF)
                               png)
             )))
        (zpng:finish-png png)))))

Left handed mouse:

(let ((mapping (xlib:pointer-mapping *display*)))
  (setf (elt mapping 0) 3)
  (setf (elt mapping 1) 2)
  (setf (elt mapping 2) 1)
  (setf (xlib:pointer-mapping *display*) mapping))

Trash group

A couple of functions for "banishing" the current window in a ".trash" group. Since this group is hidden, you can't switch to it with gnext/gprev and it won't appear in vgroups and such (unless list-hidden-groups is set). You can show the content of the trash with "trash-show". The group is automatically removed when its last window is destroyed (and recreated on demand).

    (defvar *trash-group* '()
      "Group containing the trashed windows")

    (define-stumpwm-command "trash-window" ()
      "Put the current window in the trash group. If it doesn't exist,
    create it"
      (unless (or (eq (current-group) *trash-group*)
              (not (current-window)))
        (unless *trash-group*
          (setf *trash-group* (add-group (current-screen) ".trash")))
        (move-window-to-group (current-window) *trash-group*)))

    (define-stumpwm-command "trash-show" ()
      "Switch to the trash group if it exists, call again to return to
    the previous group"
      (if *trash-group*
          (if (eq (current-group) *trash-group*)
          (switch-to-group (second (screen-groups (current-screen))))
          (switch-to-group *trash-group*))
          (message "The trash is empty!")))

    (defun clean-trash (w)
      "Called when a window is destroyed. If it was the last window of the
    trash group, destroy it"
      (let ((current-group (window-group w)))
        (when *trash-group*
          (when (and (eq current-group *trash-group*)
             (not (group-windows current-group)))
        (if (eq (current-group) *trash-group*)
            (let ((to-group (second (screen-groups (current-screen)))))
              (switch-to-group to-group)
              (kill-group *trash-group* to-group))
            (kill-group *trash-group* (current-group)))
        (setf *trash-group* nil)
        (message "The trash is empty")))))

    (add-hook *destroy-window-hook* #'clean-trash)

    ; sample bindings
    (define-key *root-map* (kbd "_") "trash-window")
    (define-key *root-map* (kbd "M-SPC") "trash-show")

Fixed window numbers for certain programs

The snippet below will ensure that certain window classes always get fixed window numbers, so that C-t 0 always go to console, C-t 1 to Emacs and so on. Customize as you see fit.

(defparameter *window-class-renumber*
  '(
    ("URxvt" . 0)
    ("Emacs" . 1)
    ("Firefox" . 2)
    ("Anki" . 3)
    ;; ("XLogo" . 9) ; for testing
    )
  "Alist of window classes to be renumbered, and their target numbers.")

(defun renumber-window-by-class (win)
  "Renumber window if its class matches *window-class-renumber*."

  (let* ((class (window-class win))
         (target-number (cdr (assoc class *window-class-renumber*
                                    :test #'string=))))

    (when target-number
      (let ((other-win (find-if #'(lambda (win)
                                    (= (window-number win) target-number))
                                (group-windows (window-group win)))))
        (if other-win
            (when (string-not-equal class (window-class other-win))
              ;; other window, different class; switch numbers
              (setf (window-number other-win) (window-number win))
              (setf (window-number win) target-number))
            ;; if there's already a window of this class, do nothing.
            ;; just keep the new number for this window.

            ;; else: no other window; target number is free.
            (setf (window-number win) target-number))

        ;; finally
        (update-all-mode-lines)))))

(add-hook *new-window-hook* 'renumber-window-by-class)

Automatic reordering of window numbers

Heavy multitasking can result in gaps in your window ordering. While the standard C-t <backspace> binding for repack-window-numbers solves this problem, it has to be invoked manually.

This hook automatically repacks your windows when one is killed or withdrawn:

(stumpwm:add-hook stumpwm:*destroy-window-hook*
                  #'(lambda (win) (stumpwm:repack-window-numbers)))

(Unfortunately it doesn't work when moving a window to another group.)

Que commands for a window that doesn't yet exist

Sometimes you want to do something to a window automatically, without needing to open the input bar and enter commands. For example, perhaps you'd like to float a window without needing to use float-this after the window is created. one example usage:

(with-open-window "emacs ~/.stumpwm.d/notes.org" nil
                  #'(lambda (cwin) 
                     (float-window cwin (current-group))
                     (float-window-move-resize :x 10 :y 70 :width 540 :height 400)))

This opens emacs to an org file, floats the window, then resizes and places it.

(defparameter *with-window*
;;  "function, arguments, class restrictor."
  '(nil nil nil))`

(defun with-open-window (cmd restrict-class function &rest args)
  "stores the {function}, {args}, and {restrict-class} variables in a dynamic
variable so that with-window-hanger can grab them. it then hangs 
with-window-hanger on focus-window-hook. then it checks if {cmd} is a string, 
in which case its a shell command and is run. otherwise its treated as a 
stumpwm command (or list of commands) and run that way."
  (progn
    (setf (first *with-window*) function)
    (setf (second *with-window*) args)
    (setf (third *with-window*) restrict-class)
    (add-hook *focus-window-hook* 'with-window-hanger)
    (if (stringp cmd)
        (run-shell-command cmd)
        (if (cdr cmd)
            (reduce #'run-commands cmd)
            (funcall #'run-commands (car cmd))))))

(defun with-window-hanger (cwin lwin)
  "this gets hung on focus-window-hook. it will call the function with {cwin}
and calls the function, and then removes itself from the hook. It gets hung 
on the *focus-window-hook* so that any command that foucuses a window
may be used, not just ones that create windows. 
update: added protection against bad functions that error via unwind-protect"
  (declare (ignore lwin))
  (let ((func (first *with-window*))
        (args (second *with-window*))
        (restrictor (third *with-window*)))
    (when (or (not restrictor) (equal restrictor (window-class cwin)))
      (unwind-protect
           (if args
               (reduce func (cons cwin args))
               (funcall func cwin))
        (remove-hook *focus-window-hook* 'with-window-hanger)))))

Raise/Pull a window or run command from menu

This builds on run-or-raise to allow one to pick a window from a menu or select "NEW" to run the command. The command is also run if no windows are found. If a window is selected and visible it is raised, otherwise it is pulled

(defun run-raise-pull-list (cmd props &key prompt
					(all-groups *run-or-raise-all-groups*)
					(all-screens *run-or-raise-all-screens*)
					(filter-pred *window-menu-filter*)
					(fmt *window-format*))
  "run-raise-pull-list opens a menu to choose either an existing window, or 
execute a command. If no windows match props, the command is run. This is built 
on run-or-raise"
  (let ((windows (find-matching-windows props all-groups all-screens)))
    (if (not windows)
	(run-shell-command cmd)
	(let* ((table  `(("NEW" ,cmd)
			 ,@(mapcar (lambda (el)
				     (list (format-expand
					    *window-formatters* fmt el)
					   el))
				   windows)))
	       (result
		(second
		 (select-from-menu (current-screen) table prompt
				   1 nil filter-pred))))
	  (cond ((not result)
		 '())
		((stringp result)
		 (run-shell-command result))
		((window-visible-p result)
		 (group-focus-window (current-group) result))
		(t 
		 (pull-window result)))))))

Grabbed Pointer

If you don't like the default "white square" cursor that appears after a part of key sequence is pressed, you can change it by modifying *grab-pointer-...* variables (except *grab-pointer-count* – it is an internal variable).

Foreground and background colors should be represented by CLX color objects and can be changed in several ways, for example (assuming that you are in :stumpwm package):

(setf *grab-pointer-foreground* (xlib:make-color :red 0.1 :green 0.25 :blue 0.5))
(setf *grab-pointer-background* (lookup-color (current-screen) "DeepSkyBlue"))

The default symbol of the cursor is a character 64 from the cursor font, its mask is the character 65. You can explore the characters with xfd utility (xfd -fn cursor). Play with the character and the mask and don't forget to try the following:

(setf *grab-pointer-character* 88)
(setf *grab-pointer-character-mask* 88)

You can also try to use a character from another suitable font, for example:

(setf *grab-pointer-font* "fixed")

Emacs

Emacsclient

If Emacs is invoked by emacslient from a command line, raise the emacs window. After press C-x #, the window turn auto back.

First: Add follow code to your .stumpwmrc

    (defvar *es-win* nil
      "to hold the windo called emacsclient")
    (defun save-es-called-win ()
      (setf *es-win* (current-window)))

    (defun return-es-called-win (win)
      (let* ((group (window-group win))
             (frame (window-frame win))
            (old-frame (tile-group-current-frame group)))
        (frame-raise-window group frame win)
        (focus-all win)
        (unless (eq frame old-frame)
          (show-frame-indicator group))))

Second: In you .emacs you have to include

    (add-hook 'after-init-hook 'server-start)
    (setq server-raise-frame t)

    (if window-system
        (add-hook 'server-done-hook
                  (lambda () (shell-command "stumpish 'eval (stumpwm::return-es-called-win stumpwm::*es-win*)'"))))

Third:

    cat > ~/bin/es << EOF
    #!/bin/sh

    stumpish 'eval (stumpwm::save-es-called-win)' > /dev/null
    emacsclient --alternate-editor=$ALTERNATE_EDITOR "$@"
    EOF

For example: You edit or view a file with command $: es xxxx, the emacs will raise up and after you finish with `C-x #' the window will turn back.

And you may set default editor to es.


    export EDITOR=es
    export ALTERNATE_EDITOR=vi
    export VISUAL=es

Note from Ejlflop (that's me!):

You may need to edit the es command (~/bin/es) and remove the backslash in "$@" Also, if you want to open a new emacs frame rather than reuse an existing one (which may involve a bit of tedious application switching, change "emacsclient" to "emacsclient -c".

If you are using the Firefox extension "ItsAllText", you may also want to set its editor to the "es" command -- it seemed to work fine when I did it.

Note: also, there is stumpmacs stumpwm module: https://gitorious.org/dss-project/stumpmacs/

Mounting Storage Devices

  • Mount

People coming from GNOME or KDE might be surprise at first when they try to access their USB flash drive, for example, and realize that they were not mount after insertion.

There is nothing wrong with that.

GNOME and KDE are Desktop Environments while StumpWM is just a Window Manager.

  • udisks

Many modern Linux distributions (including Ubuntu) use udisks to manage their storage devices. If you use one of these distributions, you can mount an USB flash drive using commands like:

udisks --mount /dev/sdb1

In Ubuntu there's no executable called udisks. If this is the case, use udiskctl instead:

udisksctl mount -b /dev/sdb1

  • udisks-glue

udisks-glue can be used to automount devices.


Create Groups on StumpWM Load

I find it useful to predfine groups in StumpWM for my workflow. I put all my email clients in one group, ticket tracker in another, sysadmin tools in another, and so forth.

In the following example, I create 9 workspaces, give them unique names and at the end switch to workspace 9 as my default group. My default workspace (in this example) is group 9 because the location of the 0 key throws off the symmetry of the default starting group being in any other numerical location. So, typically, I will be working in group 9, have a few predefined groups stacked in groups 1, 2, 3 (etc) and then any apps I need to keep open just get thrown into groups in between 9 and the predefined groups.

For me, this makes it mentally easy to visualize where everything is located and allows me enough flexibility to jumpt into an unused workgroup if I have an emergency and need to address an issue really quickly with tools that are not part of my normal workflow.

Here is the code I use for creating groups on startup, if there are more elegant ways to do this, please feel free to edit:

   (setf (group-name (first (screen-groups (current-screen)))) "comms")
     (run-commands
      "gnewbg Graphics"
      "gnewbg Email"
      "gnewbg System-Admin"
      "gnewbg ->"
      "gnewbg -->"
      "gnewbg --->"
      "gnewbg ---->"
      "gnewbg ----->"
      "gnewbg [..DEFAULT..]")
    (run-commands "gselect 9")