1 ;;; winner.el --- Restore old window configurations
3 ;; Copyright (C) 1997-1998, 2001-2017 Free Software Foundation, Inc.
5 ;; Author: Ivar Rummelhoff <ivarru@math.uio.no>
6 ;; Created: 27 Feb 1997
7 ;; Keywords: convenience frames
9 ;; This file is part of GNU Emacs.
11 ;; GNU Emacs is free software: you can redistribute it and/or modify
12 ;; it under the terms of the GNU General Public License as published by
13 ;; the Free Software Foundation, either version 3 of the License, or
14 ;; (at your option) any later version.
16 ;; GNU Emacs is distributed in the hope that it will be useful,
17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 ;; GNU General Public License for more details.
21 ;; You should have received a copy of the GNU General Public License
22 ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
26 ;; Winner mode is a global minor mode that records the changes in the
27 ;; window configuration (i.e. how the frames are partitioned into
28 ;; windows) so that the changes can be "undone" using the command
29 ;; `winner-undo'. By default this one is bound to the key sequence
30 ;; ctrl-c left. If you change your mind (while undoing), you can
31 ;; press ctrl-c right (calling `winner-redo'). Even though it uses
32 ;; some features of Emacs20.3, winner.el should also work with
33 ;; Emacs19.34 and XEmacs20, provided that the installed version of
34 ;; custom is not obsolete.
36 ;; Winner mode was improved August 1998.
37 ;; Further improvements February 2002.
41 (eval-when-compile (require 'cl-lib
))
43 (defun winner-active-region ()
44 (declare (gv-setter (lambda (store)
45 (if (featurep 'xemacs
)
46 `(if ,store
(zmacs-activate-region)
47 (zmacs-deactivate-region))
48 `(if ,store
(activate-mark) (deactivate-mark))))))
51 (defalias 'winner-edges
52 (if (featurep 'xemacs
) 'window-pixel-edges
'window-edges
))
53 (defalias 'winner-window-list
54 (if (featurep 'xemacs
)
55 (lambda () (delq (minibuffer-window) (window-list nil
0)))
56 (lambda () (window-list nil
0))))
61 "Restoring window configurations."
64 (defcustom winner-dont-bind-my-keys nil
65 "Non-nil means do not bind keys in Winner mode."
69 (defcustom winner-ring-size
200
70 "Maximum number of stored window configurations per frame."
74 (defcustom winner-boring-buffers
'("*Completions*")
75 "List of buffer names whose windows `winner-undo' will not restore.
76 You may want to include buffer names such as *Help*, *Apropos*,
77 *Buffer List*, *info* and *Compile-Log*."
78 :type
'(repeat string
)
83 ;;;; Saving old configurations (internal variables and subroutines)
86 ;;; Current configuration
88 ;; List the windows according to their edges.
89 (defun winner-sorted-window-list ()
90 (sort (winner-window-list)
92 (cl-loop for a in
(winner-edges x
)
93 for b in
(winner-edges y
)
95 finally return
(< a b
)))))
97 (defun winner-win-data ()
98 ;; Essential properties of the windows in the selected frame.
99 (cl-loop for win in
(winner-sorted-window-list)
100 collect
(cons (winner-edges win
) (window-buffer win
))))
102 ;; This variable is updated with the current window configuration
103 ;; every time it changes.
104 (defvar winner-currents nil
)
106 ;; The current configuration (+ the buffers involved).
107 (defsubst winner-conf
()
108 (cons (current-window-configuration)
112 ;; Save current configuration.
113 ;; (Called below by `winner-save-old-configurations').
114 (defun winner-remember ()
115 (setf (alist-get (selected-frame) winner-currents
) (winner-conf)))
117 ;; Consult `winner-currents'.
118 (defun winner-configuration (&optional frame
)
119 (or (cdr (assq (or frame
(selected-frame)) winner-currents
))
120 (with-selected-frame frame
125 ;;; Saved configurations
127 ;; This variable contains the window configuration rings.
128 ;; The key in this alist is the frame.
129 (defvar winner-ring-alist nil
)
131 ;; Find the right ring. If it does not exist, create one.
132 (defsubst winner-ring
(frame)
133 (or (cdr (assq frame winner-ring-alist
))
134 (let ((ring (make-ring winner-ring-size
)))
135 (ring-insert ring
(winner-configuration frame
))
136 (push (cons frame ring
) winner-ring-alist
)
140 ;; If the same command is called several times in a row,
141 ;; we only save one window configuration.
142 (defvar winner-last-command nil
)
144 ;; Frames affected by the previous command.
145 (defvar winner-last-frames nil
)
148 (defsubst winner-equal
(a b
)
149 "Check whether two Winner configurations (as produced by
150 `winner-conf') are equal."
151 (equal (cdr a
) (cdr b
)))
154 ;; Save the current window configuration, if it has changed.
155 ;; If so return frame, otherwise return nil.
156 (defun winner-insert-if-new (frame)
157 (unless (or (memq frame winner-last-frames
)
158 (eq this-command
'winner-redo
))
159 (let ((conf (winner-configuration frame
))
160 (ring (winner-ring frame
)))
161 (when (and (not (ring-empty-p ring
))
162 (winner-equal conf
(ring-ref ring
0)))
163 ;; When the previous configuration was very similar,
164 ;; keep only the latest.
165 (ring-remove ring
0))
166 (ring-insert ring conf
)
167 (push frame winner-last-frames
)
174 ;; Frames affected by the current command.
175 (defvar winner-modified-list nil
)
177 ;; Called whenever the window configuration changes
178 ;; (a `window-configuration-change-hook').
179 (defun winner-change-fun ()
182 (setq winner-modified-list
183 (cl-loop for frame in winner-modified-list
184 if
(frame-live-p frame
) collect frame
))
186 (unless (or (memq (selected-frame) winner-modified-list
)
187 (/= 0 (minibuffer-depth)))
188 (push (selected-frame) winner-modified-list
)))
190 ;; A `post-command-hook' for emacsen with
191 ;; `window-configuration-change-hook'.
192 (defun winner-save-old-configurations ()
193 (when (zerop (minibuffer-depth))
194 (unless (eq this-command winner-last-command
)
195 (setq winner-last-frames nil
)
196 (setq winner-last-command this-command
))
197 (dolist (frame winner-modified-list
)
198 (winner-insert-if-new frame
))
199 (setq winner-modified-list nil
)
202 ;; A `minibuffer-setup-hook'.
203 (defun winner-save-unconditionally ()
204 (unless (eq this-command winner-last-command
)
205 (setq winner-last-frames nil
)
206 (setq winner-last-command this-command
))
207 (winner-insert-if-new (selected-frame))
210 ;; A `post-command-hook' for other emacsen.
211 ;; Also called by `winner-undo' before "undoing".
212 (defun winner-save-conditionally ()
213 (when (zerop (minibuffer-depth))
214 (winner-save-unconditionally)))
219 ;;;; Restoring configurations
221 ;; Works almost as `set-window-configuration',
222 ;; but does not change the contents or the size of the minibuffer,
223 ;; and tries to preserve the selected window.
224 (defun winner-set-conf (winconf)
225 (let* ((miniwin (minibuffer-window))
226 (chosen (selected-window))
227 (minisize (window-height miniwin
)))
228 (cl-letf (((window-buffer miniwin
))
229 ((window-point miniwin
)))
230 (set-window-configuration winconf
))
232 ((window-live-p chosen
) (select-window chosen
))
233 ((window-minibuffer-p) (other-window 1)))
234 (when (/= minisize
(window-height miniwin
))
235 (with-selected-window miniwin
236 (setf (window-height) minisize
)))))
240 (defvar winner-point-alist nil
)
241 ;; `set-window-configuration' restores old points and marks. This is
242 ;; not what we want, so we make a list of the "real" (i.e. new) points
243 ;; and marks before undoing window configurations.
245 ;; Format of entries: (buffer (mark . mark-active) (window . point) ..)
247 (defun winner-make-point-alist ()
250 for win in
(winner-window-list)
252 (or (assq (window-buffer win
) alist
)
253 (car (push (list (set-buffer (window-buffer win
))
254 (cons (mark t
) (winner-active-region)))
256 do
(push (cons win
(window-point win
))
258 finally return alist
)))
260 (defun winner-get-point (buf win
)
261 ;; Consult (and possibly extend) `winner-point-alist'.
262 ;; Returns nil if buf no longer exists.
263 (when (buffer-name buf
)
264 (let ((entry (assq buf winner-point-alist
)))
267 (or (cdr (assq win
(cddr entry
)))
268 (cdr (assq nil
(cddr entry
)))
269 (with-current-buffer buf
270 (push (cons nil
(point)) (cddr entry
))
272 (t (with-current-buffer buf
274 (cons (mark t
) (winner-active-region))
280 ;; Make sure point does not end up in the minibuffer and delete
281 ;; windows displaying dead or boring buffers
282 ;; (c.f. `winner-boring-buffers'). Return nil if all the windows
283 ;; should be deleted. Preserve correct points and marks.
284 (defun winner-set (conf)
285 ;; For the format of `conf', see `winner-conf'.
288 ;; Possibly update `winner-point-alist'
289 (cl-loop for buf in
(mapcar 'cdr
(cdr conf
))
290 for pos
= (winner-get-point buf nil
)
291 if
(and pos
(not (memq buf buffers
)))
292 do
(push buf buffers
)
294 (winner-set-conf (car conf
))
295 (let (xwins) ; to be deleted
298 (dolist (win (winner-sorted-window-list))
299 (unless (and (pop alive
)
300 (setf (window-point win
)
301 (winner-get-point (window-buffer win
) win
))
302 (not (member (buffer-name (window-buffer win
))
303 winner-boring-buffers
)))
304 (push win xwins
))) ; delete this window
307 ;; `winner-undo' shouldn't update the selection (Bug#28631) when
308 ;; select-enable-primary is non-nil.
309 (unless select-enable-primary
311 (cl-loop for buf in buffers
312 for entry
= (cadr (assq buf winner-point-alist
))
313 do
(progn (set-buffer buf
)
314 (set-mark (car entry
))
315 (setf (winner-active-region) (cdr entry
))))))
316 ;; Delete windows, whose buffers are dead or boring.
317 ;; Return t if this is still a possible configuration.
320 (mapc 'delete-window
(cdr xwins
)) ; delete all but one
321 (unless (one-window-p t
)
322 (delete-window (car xwins
))
327 ;;;; Winner mode (a minor mode)
329 (defcustom winner-mode-hook nil
330 "Functions to run whenever Winner mode is turned on or off."
334 (define-obsolete-variable-alias 'winner-mode-leave-hook
335 'winner-mode-off-hook
"24.3")
337 (defcustom winner-mode-off-hook nil
338 "Functions to run whenever Winner mode is turned off."
342 (defvar winner-mode-map
343 (let ((map (make-sparse-keymap)))
344 (unless winner-dont-bind-my-keys
345 (define-key map
[(control c
) left
] 'winner-undo
)
346 (define-key map
[(control c
) right
] 'winner-redo
))
348 "Keymap for Winner mode.")
352 (define-minor-mode winner-mode
353 "Toggle Winner mode on or off.
354 With a prefix argument ARG, enable Winner mode if ARG is
355 positive, and disable it otherwise. If called from Lisp, enable
356 the mode if ARG is omitted or nil, and toggle it if ARG is ‘toggle’.
358 Winner mode is a global minor mode that records the changes in
359 the window configuration (i.e. how the frames are partitioned
360 into windows) so that the changes can be \"undone\" using the
361 command `winner-undo'. By default this one is bound to the key
362 sequence `C-c <left>'. If you change your mind (while undoing),
363 you can press `C-c <right>' (calling `winner-redo')."
367 (add-hook 'window-configuration-change-hook
'winner-change-fun
)
368 (add-hook 'post-command-hook
'winner-save-old-configurations
)
369 (add-hook 'minibuffer-setup-hook
'winner-save-unconditionally
)
370 (setq winner-modified-list
(frame-list))
371 (winner-save-old-configurations))
372 (remove-hook 'window-configuration-change-hook
'winner-change-fun
)
373 (remove-hook 'post-command-hook
'winner-save-old-configurations
)
374 (remove-hook 'minibuffer-setup-hook
'winner-save-unconditionally
)))
376 ;; Inspired by undo (simple.el)
378 (defvar winner-undo-frame nil
)
380 (defvar winner-pending-undo-ring nil
381 "The ring currently used by `winner-undo'.")
382 (defvar winner-undo-counter nil
)
383 (defvar winner-undone-data nil
) ; There confs have been passed.
385 (defun winner-undo ()
386 "Switch back to an earlier window configuration saved by Winner mode.
387 In other words, \"undo\" changes in window configuration."
390 ((not winner-mode
) (error "Winner mode is turned off"))
391 (t (unless (and (eq last-command
'winner-undo
)
392 (eq winner-undo-frame
(selected-frame)))
393 (winner-save-conditionally) ; current configuration->stack
394 (setq winner-undo-frame
(selected-frame))
395 (setq winner-point-alist
(winner-make-point-alist))
396 (setq winner-pending-undo-ring
(winner-ring (selected-frame)))
397 (setq winner-undo-counter
0)
398 (setq winner-undone-data
(list (winner-win-data))))
399 (cl-incf winner-undo-counter
) ; starting at 1
400 (when (and (winner-undo-this)
401 (not (window-minibuffer-p)))
402 (message "Winner undo (%d / %d)"
404 (1- (ring-length winner-pending-undo-ring
)))))))
409 (defun winner-undo-this () ; The heart of winner undo.
412 ((>= winner-undo-counter
(ring-length winner-pending-undo-ring
))
413 (message "No further window configuration undo information")
416 ((and ; If possible configuration
417 (winner-set (ring-ref winner-pending-undo-ring
418 winner-undo-counter
))
419 ; .. and new configuration
420 (let ((data (winner-win-data)))
421 (and (not (member data winner-undone-data
))
422 (push data winner-undone-data
))))
423 (cl-return t
)) ; .. then everything is fine.
424 (t ;; Otherwise, discharge it (and try the next one).
425 (ring-remove winner-pending-undo-ring winner-undo-counter
)))))
428 (defun winner-redo () ; If you change your mind.
429 "Restore a more recent window configuration saved by Winner mode."
432 ((eq last-command
'winner-undo
)
434 (if (zerop (minibuffer-depth))
435 (ring-remove winner-pending-undo-ring
0)
436 (ring-ref winner-pending-undo-ring
0)))
437 (unless (eq (selected-window) (minibuffer-window))
438 (message "Winner undid undo")))
439 (t (user-error "Previous command was not a `winner-undo'"))))
442 ;;; winner.el ends here