Skip to content

Commit

Permalink
Create window-purpose layer
Browse files Browse the repository at this point in the history
- Proper integration with popwin.
- Helm commands (a-la helm-buffers-list)
- Correct display of *LV* window: affects
  `spacemacs/window-manipulation-micro-state` and anything that uses
  corelv.el
- Integrate with opening a new eyebrowse workspace
- Enable purpose-x-kill: purpose-aware replacement of a window's buffer
  when a  buffer is killed
  • Loading branch information
Bar Magal committed Sep 18, 2015
1 parent d0d2b15 commit 2c00d24
Show file tree
Hide file tree
Showing 3 changed files with 539 additions and 0 deletions.
117 changes: 117 additions & 0 deletions layers/+window-management/window-purpose/README.org
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#+TITLE: window-purpose contribution layer for Spacemacs

* Table of Contents :TOC@4:
- [[#description][Description]]
- [[#purposes][Purposes]]
- [[#switch-to-buffer-and-display-buffer][switch-to-buffer and display-buffer]]
- [[#features][Features]]
- [[#window-purpose-purpose-mode][window-purpose (purpose-mode)]]
- [[#purpose-popwinel-pupo-mode][purpose-popwin.el (pupo-mode)]]
- [[#misc][misc]]
- [[#install][Install]]
- [[#usage][Usage]]
- [[#key-bindings][Key Bindings]]
- [[#caveats][Caveats]]
- [[#popwin-and-guide-key][Popwin and guide-key]]
- [[#packages-that-do-display-management][Packages that do display management]]

* Description

This layer enables [[https://github.com/bmag/emacs-purpose][window-purpose]], which provides an alternative, purpose-based
window manager for Emacs. With this layer, your window layout should be robust
and shouldn't change too much when opening all sorts of buffers.

Regular [[https://github.com/m2ym/popwin-el][popwin]] is not triggered when window-purpose is enabled. However, the
window-purpose layer provides a purpose-popwin extension, which brings popwin's
behavior to window-purpose and solves that problem.

** Purposes

window-purpose contains a configuration which assigns a purpose for each buffer.
Later, when Emacs needs to display a buffer in a window, its purpose helps make
a better decision of which window to use.

For example, consider the following case: Emacs frame shows three windows - one
for code, one for a terminal and one general-purpose window. The general window
is selected and you want to open a code file. How do you ensure that the code
file will be displayed in the code window? With window-purpose you don't need to
worry about it - you open the file and window-purpose places it in the correct
window.

Additionally, you can dedicate a window to a purpose - so that window is
reserved only for buffers that share that purpose.

** switch-to-buffer and display-buffer

In regular Emacs, =switch-to-buffer= follows different rules than the other
switching and popping commands, because it doesn't use =display-buffer= (which
the other commands do). With window-purpose, this behavior is fixed. The result
is a better control over how buffers are displayed, since =switch-to-buffer=
doesn't ignore the user's customizations anymore.

* Features

** window-purpose (purpose-mode)
- window layout is more robust and less likely to change unintentionally
- dedicate window to a purpose
- user-defined purposes
- extensible window display behavior
- empty =purpose-mode-map=, to avoid conflicts with other key maps

** purpose-popwin.el (pupo-mode)
- replicate popwin behavior for purpose-mode - almost no regression in popup
behavior from using window-purpose.
- reuses popwin's settings: =popwin:special-display-config=,
=popwin:popup-window-height= and =popwin:popup-window-width=.
- difference from popwin: when several windows are open, popup window is
sometimes bigger than with regular popwin in the same situation.

** misc
- specialized helm source similar to =helm-source-buffers-list=

* Install

To use this contribution add it to your =~/.spacemacs=

#+BEGIN_SRC emacs-lisp
(setq-default dotspacemacs-configuration-layers '(window-purpose))
#+END_SRC

* Usage

With window-purpose layer installed, =purpose-mode= and =pupo-mode= are enabled.
You can toggle =purpose-mode= (~SPC : purpose-mode~) at any time to return to
purpose-less behavior. You can toggle =pupo-mode= (~SPC : pupo-mode~) to turn
off only the purpose-popwin integration.

If you change =popwin:special-display-config= in your =dotspacemacs/config=, you
should call =pupo/update-purpose-config= to update purpose-popwin with those
changes.

See [[https://github.com/bmag/emacs-purpose/wiki][window-purpose wiki]] to learn more about window-purpose.

* Key Bindings

| Key Binding | Description |
|-------------+-------------------------------------------------------------------------------------|
| ~SPC r b~ | Open a buffer. Only buffers with the same purpose as the current buffer are listed. |
| ~SPC r B~ | Open any buffer and ignore window-purpose when displaying the buffer. |
| ~SPC r d~ | Toggle dedication of selected window to its current purpose. |
| ~SPC r D~ | Delete all non-dedicated windows. |
| ~SPC r p~ | Choose a purpose and open a buffer with that purpose. |
| ~SPC r P~ | Change the purpose of the selected window. Changes the window's buffer accordingly. |

* Caveats

** Popwin and guide-key

If a buffer is displayed in two different windows, and only one of those windows
is purpose-dedicated, then invoking guide-key will cause both windows to become
purpose-dedicated.

** Packages that do display management

Some packages that manage how windows are displayed, such as =gdb= with
=gdb-many-windows=, might not play nicely with =window-purpose=. However, it is
usually possible to find a solution. After all, even =helm= and =popwin= work
with =window-purpose=.
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
;;; purpose-popwin.el --- Purpose extension to act like Popwin -*- lexical-binding: t -*-

;;; Commentary:

;;; Code:

(defconst pupo--direction-to-purpose '((left . popl)
(right . popr)
(top . popt)
(bottom . popb))
"Mapping of popwin positions to purposes.")

(defconst pupo--purposes
(loop for (direction . purpose) in pupo--direction-to-purpose collect purpose)
"List of purposes used to present popwin positions.")

(defvar pupo--windows nil
"List of popup windows.")
(defvar pupo--auto-windows nil
"List of popup windows that should be closed automatically.")
(defvar pupo--saved-buffers nil
"Temporary list of displayed popup buffers.")
(defvar pupo--saved-auto-buffers nil
"Temporary list of non-sticky displayed popup buffers.")

(defun pupo//popup-function (position size)
"Generate a display function to create a popup window.
POSITION should be one of bottom, top, left and right.
SIZE should be either a positive number of nil. Size is interpreted as
width or height depending on POSITION."
(let* ((size (cl-case position
('left (purpose--normalize-width (or size
popwin:popup-window-width)))
('right (purpose--normalize-width (or size
popwin:popup-window-width)))
('top (purpose--normalize-height (or size
popwin:popup-window-height)))
('bottom (purpose--normalize-height (or size
popwin:popup-window-height)))))
(size (when size (- size)))
(side (cl-case position
('left 'left)
('right 'right)
('top 'above)
('bottom 'below))))
(lambda (buffer alist)
(let ((window (ignore-errors
(split-window (frame-root-window) size side))))
(when window
(purpose-change-buffer buffer window 'window alist))))))

(defun pupo//position-to-display-function (position width height)
"Generate a display function for creating a popup window.
POSITION defaults to bottom.
WIDTH and HEIGHT should be either a positive number or nil."
(cl-case (or position 'bottom)
((left right) (pupo//popup-function position width))
((top bottom) (pupo//popup-function position height))))

(defun pupo//position-to-purpose (position)
"Translate POSITION to a purpose.
Direction -> purpose:
left -> popl
right -> popr
top -> popt
bottom -> popb
POSITION defaults to bottom."
(cl-case (or position 'bottom)
;; names are short so they don't take much room in the mode-line
('left 'popl)
('right 'popr)
('top 'popt)
('bottom 'popb)))

(defun pupo//actions (settings)
"Generate list of display functions for displaying a popup window.
SETTINGS is the settings for the popup buffer, and corresponds to what
popwin calls \"config keywords\"."
(delq nil
(list #'purpose-display-reuse-window-buffer
(unless (plist-get settings :dedicated)
#'purpose-display-reuse-window-purpose)
(pupo//position-to-display-function (plist-get settings :position)
(plist-get settings :width)
(plist-get settings :height)))))

(defun pupo/display-condition (_purpose buffer _alist)
"A condition to be used in `purpose-special-action-sequences'.
Return non-nil if BUFFER is a popup buffer, according to the settings in
`popwin:special-display-config'.
See `purpose-special-action-sequences' for a description of _PURPOSE,
BUFFER and _ALIST."
(popwin:match-config buffer))

(defun pupo/display-function (buffer alist)
"A display function to be used in `purpose-special-action-sequences'.
Display BUFFER as a popup buffer, according to the settings in
`popwin:special-display-config'.
See `purpose-special-action-sequences' for a description of BUFFER and
ALIST."
(do ((display-fns (pupo//actions (cdr (popwin:match-config buffer)))
(cdr display-fns))
(window nil (and display-fns (funcall (car display-fns) buffer alist))))
((or window (null display-fns)) window)))

(defun pupo/after-display (window)
"Additional initialization for popup windows.
Sets properties for WINDOW and updates some variables, if WINDOW is a
popup window.
This function should be hooked to `purpose-display-buffer-functions'."
(let* ((buffer (window-buffer window))
(config (popwin:match-config buffer))
(settings (cdr (popwin:listify config))))
(when config
(setq pupo--windows (delete window pupo--windows))
(push window pupo--windows)
(when (plist-get settings :dedicated)
(set-window-dedicated-p window t))
(unless (plist-get settings :stick)
(push window pupo--auto-windows))
(unless (or (minibuffer-window-active-p (selected-window))
(plist-get settings :noselect))
;; popwin selects window unless :noselect is t
;; in contrast, popwin doesn't prevent selection when :noselect is nil
(select-window window))
;; make \\[C-g] delete last popup window
(global-set-key [remap keyboard-quit] #'pupo/close-window))))

(defun pupo//safe-delete-window (&optional window)
"Delete WINDOW if possible.
Return t if successful, nil otherwise.
WINDOW defaults to the selected window."
(ignore-errors (delete-window window) t))

(defun pupo/auto-delete-windows (window)
"Delete all non-sticky popup windows, unless WINDOW is a popup window.
This function should be hooked to `purpose-display-buffer-functions'."
(unless (member (purpose-window-purpose window) pupo--purposes)
(mapc #'pupo//safe-delete-window pupo--auto-windows)
(setq pupo--auto-windows nil)))

(defun pupo/close-window ()
"Close most recent popup window.
This command can be used repeatedly to close all popup windows."
(interactive)
(let ((searching t))
(while (and pupo--windows searching)
(when (window-live-p (car pupo--windows))
(pupo//safe-delete-window (car pupo--windows))
(setq searching nil))
(pop pupo--windows))
(unless pupo--windows
;; no more popup windows, revert \\[C-g] to `keyboard-quit'
(global-set-key [remap keyboard-quit] nil))))

(defun pupo/close-all-windows ()
"Close all popup windows."
(interactive)
(dolist (purpose pupo--purposes)
(mapc #'pupo//safe-delete-window (purpose-windows-with-purpose purpose))))

(defun pupo/popwin-config-to-purpose-config ()
"Create a purpose configuration matching current popwin's settings.
Return a `purpose-conf' object.
Popwin's settings are taken from `popwin:special-display-config'."
(let (mode-purposes name-purposes regexp-purposes)
(loop for config-entry in popwin:special-display-config
for (pattern . settings) = (popwin:listify config-entry)
do
(push (cons pattern
(pupo//position-to-purpose (plist-get settings :position)))
(cond ((symbolp pattern) mode-purposes)
((plist-get settings :regexp) regexp-purposes)
(t name-purposes))))
(purpose-conf "pupo"
:mode-purposes mode-purposes
:name-purposes name-purposes
:regexp-purposes regexp-purposes)))

(defun pupo/update-purpose-config ()
"Update purpose configuration according to current popwin's settings.
Popwin's settings are taken from `popwin:special-display-config'."
(purpose-set-extension-configuration :pupo (pupo/popwin-config-to-purpose-config)))

(define-minor-mode pupo-mode
"Minor mode for combining `purpose-mode' and `popwin-mode'."
:global t
(if pupo-mode
(progn
(pupo/update-purpose-config)
(push '(pupo/display-condition pupo/display-function)
purpose-special-action-sequences)
(add-hook 'purpose-display-buffer-functions #'pupo/after-display)
(add-hook 'purpose-display-buffer-functions #'pupo/auto-delete-windows))
(purpose-del-extension-configuration :pupo)
(setq purpose-special-action-sequences
(delete '(pupo/display-condition pupo/display-function)
purpose-special-action-sequences))
(remove-hook 'purpose-display-buffer-functions #'pupo/after-display)
(remove-hook 'purpose-display-buffer-functions #'pupo/auto-delete-windows)))

(defadvice popwin:create-popup-window (before pupo/before-popwin-create)
"Save current popup windows for later restoration.
The windows are restored in `pupo/after-popwin-create'.
Note that the windows themselves aren't saved, but some internal
variables are updated instead."
(setq pupo--saved-buffers (mapcar #'window-buffer pupo--windows))
(setq pupo--saved-auto-buffers (mapcar #'window-buffer pupo--auto-windows)))

(defadvice popwin:create-popup-window (after pupo/after-popwin-create)
"Restore popup windows.
The windows were saved in `pupo/before-popwin-create'.
Note that the windows themselves aren't restored, but some internal
variables are updated instead."
(setq pupo--windows nil)
(loop for buffer in pupo--saved-buffers
do (setq pupo--windows
(append pupo--windows
(get-buffer-window-list buffer))))
(setq pupo--auto-windows nil)
(loop for buffer in pupo--saved-auto-buffers
do (setq pupo--auto-windows
(append pupo--auto-windows
(get-buffer-window-list buffer)))))

(defun pupo/sync-advices ()
(if pupo-mode
(progn
(ad-enable-advice 'popwin:create-popup-window 'before 'pupo/before-popwin-create)
(ad-enable-advice 'popwin:create-popup-window 'after 'pupo/after-popwin-create)
(ad-update 'popwin:create-popup-window)
(ad-activate 'popwin:create-popup-window))
(ad-disable-advice 'popwin:create-popup-window 'before 'pupo/before-popwin-create)
(ad-disable-advice 'popwin:create-popup-window 'after 'pupo/after-popwin-create)
(ad-update 'popwin:create-popup-window)))
(add-hook 'pupo-mode-hook #'pupo/sync-advices)

(provide 'purpose-popwin)
;;; purpose-popwin.el ends here
Loading

0 comments on commit 2c00d24

Please sign in to comment.