Search code examples
common-lispstumpwm

Assigning Spotify window to a group in StumpWM


I am currently starting to set up stumpwm, and I would like to assign a specific window to a particular group. So far I have this:

(define-frame-preference "Spotify"
    (0 t t :class "Spotify")
    )

So essentially, I would expect that that would set the windows with the class Spotify to the group Spotify, this however does not happen.

Can anybody help me on this? Thank you!


Solution

  • The relationship between X11 windows and Linux processes is thin: things are asynchronous, you start a process and some time later zero, one or more windows are created. You have to work with callbacks, there is no easy way to create a process and synchronously have all its windows in return.

    Some processes are nice enough to set the _NET_WM_PID property on windows (it looks like the "Spotify" application does it). You can retrieve this property as follows:

    (first (xlib:get-property (window-xwin w) :_net_wm_pid))
    

    Placement rules cannot help here, given how Spotify fails to set the class property early enough (see comments and other answer). But you can use a custom hook:

    STUMPWM-USER> (let ((out *standard-output*))
                    (push (lambda (&rest args) (print args out))
                          *new-window-hook*))
    (#<CLOSURE (LAMBDA (&REST ARGS)) {101A92388B}>)
    

    Notice how I first evaluate *standard-output* to bind it lexically to out, so that the function can use it as a stream when printing informations. This is because the hook might be run in another thread, where the dynamic binding of the standard output might not be the one I want here (this ensures debugging in done in the Slime REPL, in my case).

    When I start for example xclock, the following is printed in the REPL:

    (#S(TILE-WINDOW "xclock" #x380000A)) 
    

    So I can change the hook so that instead if does other things. This is a bit experimental but for example, you can temporarily modify the *new-window-hook* to react on a particular window event:

    (in-package :stumpwm-user)
    
    (let ((process (sb-ext:run-program "xclock" () :search t :wait nil))
          (hook))
      (sb-ext:process-kill process sb-unix:sigstop)
      (flet ((hook (w)
               (when (find
                      (sb-ext:process-pid process)
                      (xlib:get-property (window-xwin w) :_net_wm_pid))
                 (move-window-to-group w (add-group (current-screen) "XCLOCK"))
                 (setf *new-window-hook* (remove hook *new-window-hook*)))))
        (setf hook #'hook)
        (push #'hook *new-window-hook*))
      (sb-ext:process-kill process sb-unix:sigcont))
    

    Basically: create a process, stop it to minimize race conditions, define a hook that checks if the PID associated in the client matches the one of the process, execute some rules, then remove the hook from the list of hooks. This is fragile, since if the hook is never run, it stays in the list, and in case of errors, it also stays in the list. At the end of the expression, the hook is added and the process resumes execution.