Sawfish
Scripts quick access edit this
  • Author: Jens Thiele
  • Version: unknown
  • License: GPLv3

Synopsis[]

Selectively delegate window management to a "sub window manager". (another process, emacs :-)

Description[]

Selective delegeate window management to another process ("sub window manager"). At the moment there is only one sub window manager: emacs. WARNING: This is an evil hack and I only distribute that code because I really use that crap and find it useful enough to share it.

Installation[]

There are two parts: sawfish side and emacs side. Put both in their corresponding lisp directory and require them.

Configuration[]

You have to edit the code.

Screencast[]

http://karme.de/delme/subwm.ogv (screencast, ogg theora video)

Sawfish Code[]

;;; subwm.jl -- selectively delegate window management to a sub
;;; window manager (another process, emacs :-)

;; Copyright (C) 2010  Jens Thiele

;; Author: Jens Thiele <karme@berlios.de>

;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.

;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING.  If not, write to
;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.

;;; Commentary:

;; at the moment there is only one subwm and that is emacs
;; EVIL HACK FOLLOWS!
;; I only distribute that code because I really use that crap and find
;; it useful enough to share it

(defun syslog l
  (apply call-process
	 (append (list (make-process
			standard-output) nil)
		 (list "/usr/bin/logger" "sawfish:") l)))

(defun subwm-debug l
  ;; (format standard-error "%s sawfish: %s\n" (current-time) l)
  ;; (apply syslog l)
  l)

(defun kill-process-gracefully-1 (p timeout signals)
  (if (process-in-use-p p)
      (if signals
	  (progn
	    (signal-process p (car signals))
	    (apply accept-process-output timeout)
	    (kill-process-gracefully-1 p timeout (cdr signals)))
	nil)
    t))

(defun kill-process-gracefully (p timeout)
  (kill-process-gracefully-1 p timeout '(INT TERM KILL)))

;; note: timeout is more like a hint
(defun call-process-timeout (timeout p dummy #!rest args)
  (apply start-process (cons p args))
  (while (process-in-use-p p)
    (when (apply accept-process-output timeout)
      ;; timeout reached, kill that sucker
      (kill-process-gracefully p timeout)))
  (process-exit-value p))

(defun shell-command-to-string-timeout (timeout #!rest l)
  (let* ((b (make-string-output-stream))
	 (args (append (list timeout (make-process b) nil) l))
	 (dummy (subwm-debug (format nil "calling (call-process %s)" args)))
	 (ret (apply call-process-timeout args)))
    (subwm-debug (format nil "subprocess returned: %s" ret))
    (list (get-output-stream-string b) (= ret 0) ret)))

(define (subwm-place-mode w)
  (let ((subwm (window-get w 'subwm-dim)))
    (subwm-debug (format nil "subwm-place-mode %s" subwm))
    (window-put w 'ignore-program-position t)
    (window-put w 'fixed-position t)
    (window-put w 'never-maximize t)
    (move-resize-window-to w (car subwm) (cadr subwm) (caddr subwm) (cadddr subwm))
    t))

(define-placement-mode 'subwm subwm-place-mode #:for-normal t #:for-dialogs t)

(defun intersection (x y)
  "intersection of 2 lists"
  (filter (lambda(x) (member x y)) x))

;; todo: at the moment only the window id of the first entry is used
(setq subwm-registry (list))

(defun subwm-register (wid hooks)
  "Register a sub-window manager"
  (subwm-debug (format nil "New tiled sub-window-manager in window %s registered" wid))
  (setq subwm-registry (cons (cons wid hooks) subwm-registry))
  (mapcar (lambda(h)
	    (add-hook (car h) (cdr h))) hooks)
  t)

(defun subwm-unregister (wid)
  "Unregister a sub-window manager"
  (subwm-debug (format nil "tiled sub-window-manager in window %s unregistered" wid))
  ;; remove old hooks!
  (let ((swm (assoc wid subwm-registry)))
    (when swm
      (mapcar (lambda(h)
		(remove-hook (car h) (cdr h))) (cdr swm))))
  (setq subwm-registry
	(filter (lambda(x) (not (equal (car x) wid))) subwm-registry)))

;; todo: this is not yet correct
(defun fix-window-at (wid x1 y1 ww wh)
  (let ((w (get-window-by-id wid)))
    (when w
      (set-window-type w 'unframed)
      (window-put w 'place-mode 'subwm)
      (window-put w 'ignore-program-position t)
      (when (window-iconified-p w)
	(uniconify-window w))
      (when (window-get w 'subwm)
	(move-resize-window-to w x1 y1 ww wh))
      (window-put w 'subwm-dim (list x1 y1 ww wh))
      (window-put w 'subwm t))))

;; emacs as a sub window manager

;; todo: ugly
;; maybe use emacs.jl for that part
(defun emacs-eval-read (l)
  (let ((eret (shell-command-to-string-timeout '(2 . 0) "/usr/bin/emacsclient" "-e" (prin1-to-string l))))
    (subwm-debug (format nil "eret: %s" eret))
    (if (and
	 (cadr eret)
	 (not (equal (car eret) "")))
	(read (make-string-input-stream (car eret)))
      nil)))

(defun emacs-eval-async (sexp)
  "Pass sexp to emacs for asynchronous evaluation"
  (start-process 
   (make-process standard-output) 
   "/usr/bin/emacsclient" "-e" (prin1-to-string sexp))
  nil)

(defun emacs-place-window (w)
  (subwm-debug "emacs-place-window: new window")
  (window-put w 'subwm nil)
  (condition-case data
      (let* ((ewid (car (car subwm-registry)))
	     (ew (get-window-by-id ewid)))
	(when (intersection (window-workspaces w)
			    (window-workspaces ew))
	  (eval (emacs-eval-read (list 'x-window-place-hook
				       (window-id w)
				       (window-name w)
				       (window-class w)
				       (cons 'list (window-workspaces w))
				       (cons 'list (window-workspaces ew))
				       (car (window-dimensions ew))
				       (cdr (window-dimensions ew))
				       (car (window-position ew))
				       (cdr (window-position ew)))))))
    (error
     ;; Default handler
     (let* ((b (make-string-output-stream))
	    (standard-output b))
       (format standard-output "some error in my place-window-hook: %s" data)
       (print "backtrace:\n")
       (backtrace)
       (subwm-debug (get-output-stream-string b)))
     nil)))

(defun emacs-destroy-notify (w)
  ;; unfortunately the window is not passed 
  ;; (somehow w is #<window 0>) :-(
  (subwm-debug (format nil "window %s destroyed (%s)" w (window-id w)))
  (emacs-eval-async '(x-window-destroy-hook)))

;; hack to prevent endless recursion
(setq in-window-moved-or-resized nil)

;; todo: this is not yet correct
(defun emacs-window-moved-or-resized (w moved)
  (let ((action (if moved "moved" "resized")))
    (subwm-debug (format nil "emacs-window-moved-or-resized: window %s %s, rec? %s subwm? %s fixed? %s client? %s"
			 w 
			 action 
			 in-window-moved-or-resized
			 (window-get w 'subwm)
			 (window-get w 'fixed-position)
			 (window-get w 'client-set-position)
			 ))
    (subwm-debug (format nil "pos: %s dim: %s placed?: %s" 
			 (window-position w)
			 (window-dimensions w)
			 (window-get w 'placed)
			 ))

    (when (and (not in-window-moved-or-resized)
	       (window-get w 'subwm)
	       (window-get w 'client-set-position))
      (subwm-debug (format nil "stupid app moving its window %s?" w))
      (let ((subwm (window-get w 'subwm-dim)))
	(setq in-window-moved-or-resized t)
	(move-resize-window-to w (car subwm) (cadr subwm) (caddr subwm) (cadddr subwm))
	(setq in-window-moved-or-resized nil))))
  nil)

(defun emacs-window-moved (w)
  (emacs-window-moved-or-resized w t))

(defun emacs-window-resized (w)
  (emacs-window-moved-or-resized w nil))

;; todo: use server-name!
(defun subwm-register-emacs (wid server-name)
  "Register a emacs sub-window-manager"
  (subwm-register wid `((place-window-hook . ,emacs-place-window)
			(destroy-notify-hook . ,emacs-destroy-notify)
			(window-moved-hook . ,emacs-window-moved)
			(window-resized-hook . ,emacs-window-resized))))

Emacs Code[]

;;; subwm.el --- emacs as sub window-manager for sawfish

;; Copyright (C) 2010  Jens Thiele

;; Author: Jens Thiele <karme@berlios.de>
;; Keywords: 

;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.

;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING.  If not, write to
;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.

;;; Commentary:

;; emacs as sub window manager (EVIL HACK!)

;;; Code:

(eval-when-compile (require 'cl))
;; todo: get rid of ecb again?
(require 'ecb)
(require 'sawfish)

;; uhh - better way?
(defun server-start-if-not-running ()
  (when (or (not (boundp 'server-process))
	    (not (processp server-process)))
    (server-start)))

(defecb-window-dedicator ecb-set-xwindow-buffer " *xwindow*"
  (switch-to-buffer (get-buffer-create " *xwindow*")))

(defun frame->x-window-id ()
  "hack to get x window id for current frame"
  (getenv "WINDOWID"))

(defun sawfish-runningp ()
  (sawfish-eval-read t))

(defun x-subwm-register ()
  "register with x window manager as sub-window manager"
  (setq buffer-x-window-mapping '())
  (sawfish-eval-read (list 'subwm-register-emacs
			   (string-to-number (frame->x-window-id))
			   server-name)))

(defun x-subwm-unregister ()
  "unregister with x window manager as sub-window manager"
  (sawfish-eval-read (list 'subwm-unregister
			   (string-to-number
			    (frame->x-window-id)) server-name)))

(defun window->x-window (x-width x-height x-offx x-offy t-width t-height)
  (list (+ (* (nth 0 (window-inside-edges)) (/ x-width t-width))    x-offx)
	(+ (* (nth 1 (window-inside-edges)) (/ x-height t-height))  x-offy)
	(+ (* (- (nth 2 (window-inside-edges)) (if (= (nth 2 (window-inside-edges)) t-width) 0 1))
	      (/ x-width t-width))    x-offx -0)
	(+ (* (- (nth 3 (window-inside-edges)) 0) (/ x-height t-height))  x-offy -0)))

(defun x-window-move (wid l)
  "return a sawfish program to move a x-window"
  (list 'fix-window-at wid (nth 0 l) (nth 1 l) (- (nth 2 l) (nth 0 l)) (- (nth 3 l) (nth 1 l))))

(defun x-window-catch (wid x-width x-height x-offx x-offy)
  "return a sawfish program to place a x-window into current buffer"
  (x-window-move wid (window->x-window x-width x-height x-offx x-offy (frame-width) (frame-height))))

(defun x-window-hide (wid)
  "return a sawfish program to hide a x-window"
  `(iconify-window (get-window-by-id ,wid)))

(defun x-window-close (wid)
  "close x window referenced by id (if it exists)"
  (sawfish-eval-read `(if (get-window-by-id ,wid)
			  (delete-window (get-window-by-id ,wid)
					 t))))

(defun x-subwm-first-x-buffer ()
  "find first buffer with an x-window attached"
  (or (some (lambda(bn) (if (string-match "^\*x-" bn) bn '()))
	    (mapcar 'buffer-name (buffer-list)))
      " *xwindow*"))

(defun my-debug (s)
  ;; XXX: quote !!
  ;;(shell-command-to-string (format "logger 'emacs: %s'" s))
  )

(defun my-ecb-switch-window-buffer (window nbuf)
  "switch buffer in ecb window"
  (my-debug (format "my-ecb-switch-window-buffer %s %s" window nbuf))
  (if window
      (ecb-with-some-adviced-functions nil
	(progn ;;	      (with-selected-window window (switch-to-buffer nbuf))
	  (with-selected-window window
	    (my-debug "try to switch ecb window")
	    (dedicated-mode 1)
	    (dedicated-mode -1)
	    ;; hmm switching buffers will call back sawfish via catch-x-windows?!
	    (if (not (ignore-errors (switch-to-buffer nbuf)))
		(my-debug "my-ecb-switch-window-buffer failed to switch buffer"))
	    (dedicated-mode 1))))
    (my-debug "my-ecb-switch-window-buffer called without valid window")))

(defun my-kill-buffer-hook ()
  (let ((bw (assoc (buffer-name) buffer-x-window-mapping)))
    (when bw
      ;; first remove from mapping
      (setq buffer-x-window-mapping (delete* bw buffer-x-window-mapping))
      ;; now delete x-window (this will call us back!)
      (x-window-close (cdr bw)))))

(defun x-window-place-hook (wid wname wclass wdesktops fdesktops x-width x-height x-offx x-offy)
  ;; ignore windows on other desktops and windows of apps not working well with tabbed window managers
  ;;   (my-debug (format "new window %s (%s) %s: class: %s desktops: %s %s intersection: %s"
  ;; 		    wid wname (string-match "VLC" wname) wclass wdesktops fdesktops
  ;; 		    (intersection wdesktops fdesktops)))
  ;;   (my-debug (format "v.digit match: %s" (= (string-match "^v.digit" wname) 0)))
  (if (and (intersection wdesktops fdesktops)
 	   (or (string= wname "xclock")
 	       (string-match "^Xpdf" wname)
 	       (string-match "^gv:" wname)
 	       (string-match "MPlayer" wname)
 	       (string-match "^Xdvi" wname)
 	       (string-match "^Advi" wname)
 	       (string-match "^Figure 1" wname)
 	       (string-match "^GNUPlot" wname)
 	       (string-match "my-image-viewer" wname)
 	       (string-match "sgachine" wname)
 	       (string-match "GRASS" wname)
 	       (string-match "conkeror" wname)
	       (string-match "^v.digit -" wname)
 	       (string-match "^VLC" wname)
 	       (and wclass (or
			    (string-match "GQview" wclass)
			    (string-match "Epiphany" wclass)
			    (string-match "^Gnome-www-browser" wclass)
			    (string-match "^Eog" wclass)
			    (string-match "^X-www-browser" wclass)))
 	       (and wclass (string-match "Evince" wclass) (not (string-match "Drucken" wname)))
 	       ;;	       (string-match "feh" wclass)
 	       ;;	       (string-match "Gliv" wclass)
 	       ;; imagemagick doesn't play nice
 	       ;;	       (string-match "^ImageMagick" wname)
 	       ))
      ;; XXX: disabled: bug here
      ;; a new window might be created using the same id as a window already
      ;; destroyed but we do not know yet!
      ;;(not (assoc (format "*x-%s<%s>*" wname wid) buffer-x-window-mapping)))
      ;;  (print (format "new window: %d" wid))
      ;; create new buffer - insert into assoc list
      (progn
	(my-debug "create buffer for window")
	(let ((nbuf (get-buffer-create (format "*x-%s<%s>*" wname wid))))
	  (setq buffer-x-window-mapping (append buffer-x-window-mapping (list (cons (buffer-name nbuf) wid))))
	  (my-debug (format "created buffer for window %s. new map:%s" wid buffer-x-window-mapping))
	  ;; XXX: shit => disable hook at that point
	  (remove-hook 'window-configuration-change-hook 'catch-x-windows)
	  ;; quite a hack:
	  ;; if there already is a window showing a x window use that one
	  (let ((window
		 (some
		  (lambda(x)
		    (if
			(or
			 ;;		       (and
			 (string-match "^\*x-" (buffer-name (window-buffer x)))
			 ;;			(with-current-buffer x (not dedicated-mode)))
			 (string-match " *xwindow" (buffer-name (window-buffer x))))
			x
		      nil))
		  (window-list))))
	    (if window
		(my-ecb-switch-window-buffer window nbuf)
	      ;; create a new window (todo: add some placement constraints for different apps)
	      ;; (for example it would be nice to set the size)
	      (display-buffer nbuf t)))
	  (add-hook 'window-configuration-change-hook 'catch-x-windows)
	  (with-current-buffer nbuf
	    (add-hook 'kill-buffer-hook
		      'my-kill-buffer-hook))
	  (append (catch-x-windows-sync x-width x-height x-offx x-offy) (list '()))))
    (progn
      (my-debug "ignoring this window")
      '())))

(defun x-window-destroyed (wid)
  (my-debug (format "window %s destroyed" wid))
  (let ((bw (rassoc wid buffer-x-window-mapping)))
    (if bw
	(progn
	  (my-debug (format "lost one of our windows kill corresponding buffer"))
	  (setq buffer-x-window-mapping (delete* bw buffer-x-window-mapping))
	  ;; todo: it might be a dedicated ecb window!
	  (ecb-with-some-adviced-functions nil
	    (if (not (ignore-errors (kill-buffer (car bw))))
		(progn
		  (my-debug
		   (format
		    "failed to kill buffer probably ecb window 2nd try »%s« in window »%s«"
		    (car bw) (get-buffer-window (car bw))))
		  ;; XXX: shit => disable hook at that point
		  (remove-hook 'window-configuration-change-hook 'catch-x-windows)
		  (my-ecb-switch-window-buffer
		   (get-buffer-window (car bw)) " *xwindow*")
		  (my-debug "switched to special")
		  (add-hook 'window-configuration-change-hook 'catch-x-windows)
		  (if (not (ignore-errors (kill-buffer (car bw))))
		      (my-debug "finally failed to kill buffer")
		    (progn
		      (my-debug "finally killed the buffer now try to switch to next x buffer")
		      (my-ecb-switch-window-buffer
		       (get-buffer-window " *xwindow*") (x-subwm-first-x-buffer))))
		  )
	      (my-debug "killed buffer")))))))

(defun x-window-exists (wid)
  (sawfish-eval-read `(get-window-by-id ,wid)))

(defun x-window-destroy-hook () ;; (wid)
  ;; (my-debug (format "got message that window %s was destroyed" wid))
  (my-debug (format "got message that a window was destroyed"))
  ;; (if wid
  ;;    (x-window-destroyed wid)
  ;; unfortunately we have to find the window ourself
  (mapc (lambda(bw)
	  (if (not (x-window-exists (cdr bw)))
	      (x-window-destroyed (cdr bw))))
	buffer-x-window-mapping)
  '())

(defun catch-x-windows-sync (x-width x-height x-offx x-offy)
  (let ((value (list 'progn)))
    (dolist (bw buffer-x-window-mapping value)
      (if (get-buffer (car bw))
	  (setq value
		(append value
			(if (get-buffer-window (car bw))
			    (list (with-selected-window (get-buffer-window (car bw))
				    (x-window-catch (cdr bw) x-width x-height x-offx x-offy)))
			  (list (x-window-hide (cdr bw))))))))))

(defun catch-x-windows ()
  (let ((ew (string-to-number (frame->x-window-id))))
    (my-debug (format "catch-x-windows called with selected frame %s" (selected-frame)))
    (my-debug
     (format "sawfish returned: %s"
	     (sawfish-eval-read
	      (apply 'catch-x-windows-sync
		     (sawfish-eval-read `(let ((ew (get-window-by-id ,ew)))
					   (list (car (window-dimensions ew))
						 (cdr (window-dimensions ew))
						 (car (window-position   ew))
						 (cdr (window-position   ew)))))))))))

(defun my-on-window-size-change (frame)
  (catch-x-windows))

;; todo: at the moment only one emacs subwm is supported and we want
;; to start the corresponding server process
(when (and (getenv "DISPLAY")
	   ;; do not start a second emacs server
	   (not (equal (shell-command-to-string "emacsclient -n -e 1 && echo -n ok") "ok"))
	   (sawfish-runningp))
  (server-start-if-not-running) ;; in fact we know that no server is running, yet
  (add-hook 'suspend-hook 'x-subwm-unregister)
  (add-hook 'kill-emacs-hook 'x-subwm-unregister)
  (add-hook 'suspend-resume-hook 'x-subwm-register)
  (x-subwm-register)
  (setq buffer-x-window-mapping '())
  (add-hook 'window-size-change-functions 'my-on-window-size-change))

(provide 'subwm)
;;; subwm.el ends here