1 ;;; ace-window.el --- Quickly switch windows. -*- lexical-binding: t -*-
3 ;; Copyright (C) 2015 Free Software Foundation, Inc.
5 ;; Author: Oleh Krehel <ohwoeowho@gmail.com>
6 ;; Maintainer: Oleh Krehel <ohwoeowho@gmail.com>
7 ;; URL: https://github.com/abo-abo/ace-window
9 ;; Package-Requires: ((avy "0.2.0"))
10 ;; Keywords: window, location
12 ;; This file is part of GNU Emacs.
14 ;; This file is free software; you can redistribute it and/or modify
15 ;; it under the terms of the GNU General Public License as published by
16 ;; the Free Software Foundation; either version 3, or (at your option)
19 ;; This program is distributed in the hope that it will be useful,
20 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
21 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 ;; GNU General Public License for more details.
24 ;; For a full copy of the GNU General Public License
25 ;; see <http://www.gnu.org/licenses/>.
29 ;; The main function, `ace-window' is meant to replace `other-window'.
30 ;; In fact, when there are only two windows present, `other-window' is
31 ;; called. If there are more, each window will have its first
32 ;; character highlighted. Pressing that character will switch to that
35 ;; To setup this package, just add to your .emacs:
37 ;; (global-set-key (kbd "M-p") 'ace-window)
39 ;; replacing "M-p" with an appropriate shortcut.
41 ;; Depending on your window usage patterns, you might want to set
43 ;; (setq aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l))
45 ;; This way they are all on the home row, although the intuitive
48 ;; If you don't want the gray background that makes the red selection
49 ;; characters stand out more, set this:
51 ;; (setq aw-background nil)
53 ;; If you want to know the selection characters ahead of time, you can
54 ;; turn on `ace-window-display-mode'.
56 ;; When prefixed with one `universal-argument', instead of switching
57 ;; to selected window, the selected window is swapped with current one.
59 ;; When prefixed with two `universal-argument', the selected window is
67 (defgroup ace-window nil
68 "Quickly switch current window."
72 (defcustom aw-keys '(?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9)
73 "Keys for selecting window.")
75 (defcustom aw-scope 'global
76 "The scope used by `ace-window'."
78 (const :tag "global" global)
79 (const :tag "frame" frame)))
81 (defcustom aw-ignored-buffers '("*Calc Trail*" "*LV*")
82 "List of buffers to ignore when selecting window."
83 :type '(repeat string))
85 (defcustom aw-ignore-on t
86 "When t, `ace-window' will ignore `aw-ignored-buffers'.
87 Use M-0 `ace-window' to toggle this value."
90 (defcustom aw-ignore-current nil
91 "When t, `ace-window' will ignore `selected-window'."
94 (defcustom aw-background t
95 "When t, `ace-window' will dim out all buffers temporarily when used.'."
98 (defcustom aw-leading-char-style 'char
99 "Style of the leading char overlay."
101 (const :tag "single char" 'char)
102 (const :tag "full path" 'path)))
104 (defcustom aw-dispatch-always nil
105 "When non-nil, `ace-window' will issue a `read-char' even for one window.
106 This will make `ace-window' act different from `other-window' for
110 (defface aw-leading-char-face
111 '((((class color)) (:foreground "red"))
112 (((background dark)) (:foreground "gray100"))
113 (((background light)) (:foreground "gray0"))
114 (t (:foreground "gray100" :underline nil)))
115 "Face for each window's leading char.")
117 (defface aw-background-face
118 '((t (:foreground "gray40")))
119 "Face for whole window background during selection.")
121 (defface aw-mode-line-face
122 '((t (:inherit mode-line-buffer-id)))
123 "Face used for displaying the ace window key in the mode-line.")
126 (defun aw-ignored-p (window)
127 "Return t if WINDOW should be ignored."
128 (or (and aw-ignore-on
129 (member (buffer-name (window-buffer window))
131 (and aw-ignore-current
132 (equal window (selected-window)))))
134 (defun aw-window-list ()
135 "Return the list of interesting windows."
139 (let ((f (window-frame w)))
140 (or (not (and (frame-live-p f)
141 (frame-visible-p f)))
142 (string= "initial_terminal" (terminal-name f))
146 (cl-mapcan #'window-list (frame-list)))
150 (error "Invalid `aw-scope': %S" aw-scope))))
153 (defvar aw-overlays-back nil
154 "Hold overlays for when `aw-background' is t.")
156 (defvar ace-window-mode nil
157 "Minor mode during the selection process.")
159 ;; register minor mode
160 (or (assq 'ace-window-mode minor-mode-alist)
161 (nconc minor-mode-alist
162 (list '(ace-window-mode ace-window-mode))))
164 (defvar aw-empty-buffers-list nil
165 "Store the read-only empty buffers which had to be modified.
166 Modify them back eventually.")
169 "Clean up mode line and overlays."
171 (aw-set-mode-line nil)
173 (mapc #'delete-overlay aw-overlays-back)
174 (setq aw-overlays-back nil)
175 (avy--remove-leading-chars)
176 (dolist (b aw-empty-buffers-list)
177 (with-current-buffer b
178 (when (string= (buffer-string) " ")
179 (let ((inhibit-read-only t))
180 (delete-region (point-min) (point-max))))))
181 (setq aw-empty-buffers-list nil))
183 (defun aw--lead-overlay (path leaf)
184 "Create an overlay using PATH at LEAF.
186 (let ((wnd (cdr leaf)))
187 (with-selected-window wnd
188 (when (= 0 (buffer-size))
189 (push (current-buffer) aw-empty-buffers-list)
190 (let ((inhibit-read-only t))
192 (let* ((pt (car leaf))
193 (ol (make-overlay pt (1+ pt) (window-buffer wnd)))
196 (with-selected-window wnd
197 (buffer-substring pt (1+ pt))))
201 (cl-case aw-leading-char-style
203 (apply #'string (last path)))
205 (apply #'string (reverse path)))
207 (error "Bad `aw-leading-char-style': %S"
208 aw-leading-char-style)))
209 (cond ((string-equal old-str "\t")
210 (make-string (1- tab-width) ?\ ))
211 ((string-equal old-str "\n")
215 (max 0 (1- (string-width old-str)))
217 (overlay-put ol 'face 'aw-leading-char-face)
218 (overlay-put ol 'window wnd)
219 (overlay-put ol 'display new-str)
220 (push ol avy--overlays-lead)))))
222 (defun aw--make-backgrounds (wnd-list)
223 "Create a dim background overlay for each window on WND-LIST."
225 (setq aw-overlays-back
227 (let ((ol (make-overlay
231 (overlay-put ol 'face 'aw-background-face)
235 (define-obsolete-variable-alias
236 'aw-flip-keys 'aw--flip-keys "0.1.0"
237 "Use `aw-dispatch-alist' instead.")
239 (defvar aw-dispatch-function 'aw-dispatch-default
240 "Function to call when a character not in `aw-keys' is pressed.")
242 (defvar aw-action nil
243 "Function to call at the end of `aw-select'.")
245 (defun aw-set-mode-line (str)
246 "Set mode line indicator to STR."
247 (setq ace-window-mode str)
248 (force-mode-line-update))
250 (defvar aw-dispatch-alist
251 '((?x aw-delete-window " Ace - Delete Window")
252 (?m aw-swap-window " Ace - Swap Window")
254 (?v aw-split-window-vert " Ace - Split Vert Window")
255 (?b aw-split-window-horz " Ace - Split Horz Window")
256 (?i delete-other-windows " Ace - Maximize Window")
257 (?o delete-other-windows))
258 "List of actions for `aw-dispatch-default'.")
260 (defun aw-dispatch-default (char)
261 "Perform an action depending on CHAR."
262 (let ((val (cdr (assoc char aw-dispatch-alist))))
264 (if (and (car val) (cadr val))
265 (prog1 (setq aw-action (car val))
266 (aw-set-mode-line (cadr val)))
269 (avy-handler-default char))))
271 (defun aw-select (mode-line &optional action)
272 "Return a selected other window.
273 Amend MODE-LINE to the mode line for the duration of the selection."
274 (setq aw-action action)
275 (let ((start-window (selected-window))
276 (next-window-scope (cl-case aw-scope
279 (wnd-list (aw-window-list))
282 (cond ((<= (length wnd-list) 1)
283 (when aw-dispatch-always
287 (funcall aw-dispatch-function (read-char)))
289 (when (eq aw-action 'exit)
290 (setq aw-action nil)))
291 (or (car wnd-list) start-window))
292 ((and (= (length wnd-list) 2)
293 (not aw-dispatch-always)
294 (not aw-ignore-current))
295 (let ((wnd (next-window nil nil next-window-scope)))
296 (while (and (aw-ignored-p wnd)
297 (not (equal wnd start-window)))
298 (setq wnd (next-window wnd nil next-window-scope)))
301 (let ((candidate-list
302 (mapcar (lambda (wnd)
303 (cons (aw-offset wnd) wnd))
305 (aw--make-backgrounds wnd-list)
306 (aw-set-mode-line mode-line)
307 ;; turn off helm transient map
308 (remove-hook 'post-command-hook 'helm--maybe-update-keymap)
310 (let* ((avy-handler-function aw-dispatch-function)
311 (res (avy-read (avy-tree candidate-list aw-keys)
313 #'avy--remove-leading-chars)))
320 (funcall aw-action window)
325 (defun ace-select-window ()
328 (aw-select " Ace - Window"
329 #'aw-switch-to-window))
332 (defun ace-delete-window ()
335 (aw-select " Ace - Delete Window"
339 (defun ace-swap-window ()
342 (aw-select " Ace - Swap Window"
346 (defun ace-maximize-window ()
347 "Ace maximize window."
349 (aw-select " Ace - Maximize Window"
350 #'delete-other-windows))
353 (defun ace-window (arg)
355 Perform an action based on ARG described below.
357 By default, behaves like extended `other-window'.
359 Prefixed with one \\[universal-argument], does a swap between the
360 selected window and the current window, so that the selected
361 buffer moves to current window (and current buffer moves to
364 Prefixed with two \\[universal-argument]'s, deletes the selected
372 (4 (ace-swap-window))
373 (16 (ace-delete-window))
374 (t (ace-select-window))))
377 (defun aw-window< (wnd1 wnd2)
378 "Return true if WND1 is less than WND2.
379 This is determined by their respective window coordinates.
380 Windows are numbered top down, left to right."
381 (let ((f1 (window-frame wnd1))
382 (f2 (window-frame wnd2))
383 (e1 (window-edges wnd1))
384 (e2 (window-edges wnd2)))
385 (cond ((string< (frame-parameter f1 'window-id)
386 (frame-parameter f2 'window-id))
388 ((< (car e1) (car e2))
390 ((> (car e1) (car e2))
392 ((< (cadr e1) (cadr e2))
395 (defvar aw--window-ring (make-ring 10)
396 "Hold the window switching history.")
398 (defun aw--push-window (window)
399 "Store WINDOW to `aw--window-ring'."
400 (when (or (zerop (ring-length aw--window-ring))
402 (ring-ref aw--window-ring 0)
404 (ring-insert aw--window-ring (selected-window))))
406 (defun aw--pop-window ()
407 "Return the removed top of `aw--window-ring'."
410 (while (or (not (window-live-p
411 (setq res (ring-remove aw--window-ring 0))))
412 (equal res (selected-window))))
414 (if (= (length (aw-window-list)) 2)
417 (setq res (selected-window)))
418 (error "No previous windows stored"))))
421 (defun aw-switch-to-window (window)
422 "Switch to the window WINDOW."
423 (let ((frame (window-frame window)))
424 (when (and (frame-live-p frame)
425 (not (eq frame (selected-frame))))
426 (select-frame-set-input-focus frame))
427 (if (window-live-p window)
429 (aw--push-window (selected-window))
430 (select-window window))
431 (error "Got a dead window %S" window))))
433 (defun aw-flip-window ()
434 "Switch to the window you were previously in."
436 (aw-switch-to-window (aw--pop-window)))
438 (defun aw-delete-window (window)
439 "Delete window WINDOW."
440 (let ((frame (window-frame window)))
441 (when (and (frame-live-p frame)
442 (not (eq frame (selected-frame))))
443 (select-frame-set-input-focus (window-frame window)))
444 (if (= 1 (length (window-list)))
446 (if (window-live-p window)
447 (delete-window window)
448 (error "Got a dead window %S" window)))))
450 (defcustom aw-swap-invert nil
451 "When non-nil, the other of the two swapped windows gets the point."
454 (defun aw-swap-window (window)
455 "Swap buffers of current window and WINDOW."
456 (cl-labels ((swap-windows (window1 window2)
457 "Swap the buffers of WINDOW1 and WINDOW2."
458 (let ((buffer1 (window-buffer window1))
459 (buffer2 (window-buffer window2)))
460 (set-window-buffer window1 buffer2)
461 (set-window-buffer window2 buffer1)
462 (select-window window2))))
463 (let ((frame (window-frame window))
464 (this-window (selected-window)))
465 (when (and (frame-live-p frame)
466 (not (eq frame (selected-frame))))
467 (select-frame-set-input-focus (window-frame window)))
468 (when (and (window-live-p window)
469 (not (eq window this-window)))
470 (aw--push-window this-window)
472 (swap-windows window this-window)
473 (swap-windows this-window window))))))
475 (defun aw-split-window-vert (window)
476 "Split WINDOW vertically."
477 (select-window window)
478 (split-window-vertically))
480 (defun aw-split-window-horz (window)
481 "Split WINDOW horizontally."
482 (select-window window)
483 (split-window-horizontally))
485 (defun aw-offset (window)
486 "Return point in WINDOW that's closest to top left corner.
487 The point is writable, i.e. it's not part of space after newline."
488 (let ((h (window-hscroll window))
489 (beg (window-start window))
490 (end (window-end window))
491 (inhibit-field-text-motion t))
493 (window-buffer window)
496 (while (and (< (point) end)
497 (< (- (line-end-position)
498 (line-beginning-position))
505 (define-minor-mode ace-window-display-mode
506 "Minor mode for showing the ace window key in the mode line."
508 (if ace-window-display-mode
513 `((ace-window-display-mode
514 (:eval (window-parameter (selected-window) 'ace-window-path)))
516 'ace-window-display-mode
517 (default-value 'mode-line-format))))
518 (force-mode-line-update t)
519 (add-hook 'window-configuration-change-hook 'aw-update))
523 'ace-window-display-mode
524 (default-value 'mode-line-format)))
525 (remove-hook 'window-configuration-change-hook 'aw-update)))
528 "Update ace-window-path window parameter for all windows."
530 (avy-tree (aw-window-list) aw-keys)
532 (set-window-parameter
533 leaf 'ace-window-path
535 (apply #'string (reverse path))
536 'face 'aw-mode-line-face)))))
538 (provide 'ace-window)
540 ;;; ace-window.el ends here