;;; winum.el --- Navigate windows and frames using numbers.
;;
;; Copyright (c) 2006-2015 Nikolaj Schumacher
;; Copyright (c) 2016 Thomas Chauvot de Beauchêne
;;
;; This program 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 of the License, or
;; (at your option) any later version.
;;
;; This program 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 this program. If not, see .
;;
;; Author: Thomas de Beauchêne
;; Version: 2.2.0
;; Keywords: convenience, frames, windows, multi-screen
;; URL: http://github.com/deb0ch/winum.el
;; Created: 2016
;; Compatibility: GNU Emacs 24.x
;; Package-requires: ((cl-lib "0.5") (dash "2.13.0"))
;;
;; This file is NOT part of GNU Emacs.
;;
;;; Commentary:
;;
;; Window numbers for Emacs: Navigate your windows and frames using numbers.
;;
;; This package is an extended and actively maintained version of the
;; https://github.com/nschum/window-numbering.el package by Nikolaj Schumacher,
;; with some ideas and code taken from https://github.com/abo-abo/ace-window.
;;
;; This version brings, among other things, support for number sets across multiple
;; frames, giving the user a smoother experience of multi-screen Emacs.
;;
;;; Code:
;;
;; FIXME: The mode-line's window number is not always up to date in all frames.
;;
(eval-when-compile (require 'cl-lib))
(require 'dash)
;; Configuration variables -----------------------------------------------------
(defgroup winum nil
"Navigate and manage windows using numbers."
:group 'convenience)
(defcustom winum-scope 'global
"Frames affected by a number set."
:group 'winum
:type '(choice
(const :tag "frame local" frame-local)
(const :tag "visible frames" visible)
(const :tag "global" global)))
(defcustom winum-reverse-frame-list nil
"If t, order frames by reverse order of creation.
Has effect only when `winum-scope' is not 'frame-local."
:group 'winum
:type 'boolean)
(defcustom winum-minibuffer-auto-assign 0
"If non-nil, `winum-mode' automatically assigns this index to the minibuffer."
:group 'winum
:type 'sexp)
(defcustom winum-assign-func nil
"Function called for each window by `winum-mode'.
This is called before automatic assignment begins. The function should
return an index to have it assigned to the current-window, nil otherwise.
Example: always assign *Calculator* the number 9 and *NeoTree* the number 0:
(defun my-winum-assign-func ()
(cond
((equal (buffer-name) \"*Calculator*\")
9)
((string-match-p (buffer-name) \".*\\*NeoTree\\*.*\")
0)
(t
nil)))
(setq winum-assign-func 'my-winum-assign-func)"
:group 'winum
:type 'function)
(make-obsolete-variable 'winum-assign-func 'winum-assign-functions "2.0.0")
(defcustom winum-assign-functions nil
"List of functions called for each window by `winum-mode'.
These functions allow for deterministic assignment of indices to windows. Each
function is called for every window. A function should return the index to be
assigned to a window or nil. The *first* function to output a value for a
given window will determine this window's number.
If the list is empty or if every functions returns nil for a given window winum
will proceed to automatic assignment.
Since this list is meant to allow custom window assignment for *mutiple*
packages at once it should never be directly set, only added to and removed
from.
Example: always assign *Calculator* the number 9, *Flycheck-errors* the number 8
and *NeoTree* the number 0:
(defun winum-assign-9-to-calculator-8-to-flycheck-errors ()
(cond
((equal (buffer-name) \"*Calculator*\") 9)
((equal (buffer-name) \"*Flycheck errors*\") 8)))
(defun winum-assign-0-to-neotree ()
(when (string-match-p (buffer-name) \".*\\*NeoTree\\*.*\") 10))
(add-to-list
'winum-assign-functions #'winum-assign-9-to-calculator-8-to-flycheck-errors)
(add-to-list
'winum-assign-functions #'winum-assign-0-to-neotree)"
:group 'winum
:type '(repeat function))
(defcustom winum-auto-assign-function #'winum--auto-assign
"The function called to auto-assign indices to windows.
This function is called after `winum-assign-functions' to automatically assign
indices to the remaining windows. It must take in a list of windows and call
the function `winum--assign' to assign an index to each, while avoiding
assigning any indices already taken (stored in `winum--assigned-indices').
Members of this list should be tested for using `equal'.
The default auto-assign function is `winum--auto-assign', which assigns strictly
increasing integers to each window."
:group 'winum
:type 'function)
(defcustom winum-auto-setup-mode-line t
"When nil, `winum-mode' will not display window numbers in the mode-line.
You might want this to be nil if you use a package that already manages window
numbers in the mode-line."
:group 'winum
:type 'boolean)
(defcustom winum-mode-line-position 1
"The position in the mode-line `winum-mode' displays the number."
:group 'winum
:type 'integer)
(defcustom winum-mode-line-p #'integerp
"A predicate that determines when to display a window index on the mode-line.
By default, only integers are displayed."
:group 'winum
:type 'function)
(defcustom winum-format " %s "
"Format string defining how the window number looks like in the mode-line.
This string is passed to the `format' function along with the index."
:group 'winum
:type 'string)
(defcustom winum-ignored-buffers '(" *which-key*")
"List of buffers to ignore when assigning indices."
:group 'winum
:type '(repeat string))
(defcustom winum-ignored-buffers-regexp '()
"List of regexps for buffer names to ignore when assigning indices.
See Info node `(emacs) Regexps' or Info node `(elisp) Regular Expressions'"
:group 'winum
:type '(repeat string)
:risky t)
(defface winum-face '()
"Face used for the index in the mode-line."
:group 'winum)
(defvar winum-base-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "`") 'winum-select-window-by-index)
(define-key map (kbd "²") 'winum-select-window-by-index)
(define-key map (kbd "0") 'winum-select-window-0-or-10)
(define-key map (kbd "1") 'winum-select-window-1)
(define-key map (kbd "2") 'winum-select-window-2)
(define-key map (kbd "3") 'winum-select-window-3)
(define-key map (kbd "4") 'winum-select-window-4)
(define-key map (kbd "5") 'winum-select-window-5)
(define-key map (kbd "6") 'winum-select-window-6)
(define-key map (kbd "7") 'winum-select-window-7)
(define-key map (kbd "8") 'winum-select-window-8)
(define-key map (kbd "9") 'winum-select-window-9)
map)
"Keymap to be used under the prefix provided by `winum-keymap-prefix'.")
(defvar winum-mode-map (let ((map (make-sparse-keymap)))
(define-key map (kbd "C-x w") winum-base-map)
map)
"Keymap used for `winum-mode'.")
;; Internal variables ----------------------------------------------------------
(defvar winum--max-frames 16
"Maximum number of frames that can be numbered.")
(defvar winum--assigned-indices nil
"List of indices that have already been assigned by `winum-assign-functions'.")
(defvar winum--window-table nil
"Hash table with indices as keys and windows as values.
Used internally by winum to get a window provided an index.")
(defvar winum--index-table nil
"Hash table with windows as keys and indices as values.
Used internally by winum to get an index provided a window.")
(defvar winum--frames-table nil
"Table linking windows to indices and indices to windows for each frame.
Used only when `winum-scope' is 'frame-local to keep track of
separate window index sets in every frame.
It is a hash table using Emacs frames as keys and cons of the form
\(`winum--window-table' . `winum--index-table')
as values.
To get a window given an index, use the `car' of a value.
To get an index given a window, use the `cdr' of a value.
Such a structure allows for per-frame bidirectional fast access.")
(defvar winum--mode-line-segment
'(:eval (let ((index (winum-get-index)))
(when (funcall winum-mode-line-p index)
(propertize (format winum-format index) 'face 'winum-face))))
"What is pushed into `mode-line-format' when setting it up automatically.")
(defvar winum--last-used-scope winum-scope
"Tracks the last used `winum-scope'.
Needed to detect scope changes at runtime.")
;; Interactive functions -------------------------------------------------------
;;;###autoload
(define-minor-mode winum-mode
"A minor mode that allows for managing windows based on window numbers."
:lighter nil
:global t
(if winum-mode
(winum--init)
(winum--deinit)))
;;;###autoload
(defun winum-select-window-0-or-10 (&optional arg)
"Jump to window 0, or window 10 if 0 is not assigned.
If prefix ARG is given, delete the window instead of selecting it."
(interactive "P")
(let ((n (if (winum-get-window-by-index 0) 0 10)))
(if arg
(winum-delete-window-by-index n)
(winum-select-window-by-index n))))
;;;###autoload
(defun winum-select-window-0 (&optional arg)
"Jump to window 0.
If prefix ARG is given, delete the window instead of selecting it."
(interactive "P")
(if arg
(winum-delete-window-by-index 0)
(winum-select-window-by-index 0)))
;;;###autoload
(defun winum-select-window-1 (&optional arg)
"Jump to window 1.
If prefix ARG is given, delete the window instead of selecting it."
(interactive "P")
(if arg
(winum-delete-window-by-index 1)
(winum-select-window-by-index 1)))
;;;###autoload
(defun winum-select-window-2 (&optional arg)
"Jump to window 2.
If prefix ARG is given, delete the window instead of selecting it."
(interactive "P")
(if arg
(winum-delete-window-by-index 2)
(winum-select-window-by-index 2)))
;;;###autoload
(defun winum-select-window-3 (&optional arg)
"Jump to window 3.
If prefix ARG is given, delete the window instead of selecting it."
(interactive "P")
(if arg
(winum-delete-window-by-index 3)
(winum-select-window-by-index 3)))
;;;###autoload
(defun winum-select-window-4 (&optional arg)
"Jump to window 4.
If prefix ARG is given, delete the window instead of selecting it."
(interactive "P")
(if arg
(winum-delete-window-by-index 4)
(winum-select-window-by-index 4)))
;;;###autoload
(defun winum-select-window-5 (&optional arg)
"Jump to window 5.
If prefix ARG is given, delete the window instead of selecting it."
(interactive "P")
(if arg
(winum-delete-window-by-index 5)
(winum-select-window-by-index 5)))
;;;###autoload
(defun winum-select-window-6 (&optional arg)
"Jump to window 6.
If prefix ARG is given, delete the window instead of selecting it."
(interactive "P")
(if arg
(winum-delete-window-by-index 6)
(winum-select-window-by-index 6)))
;;;###autoload
(defun winum-select-window-7 (&optional arg)
"Jump to window 7.
If prefix ARG is given, delete the window instead of selecting it."
(interactive "P")
(if arg
(winum-delete-window-by-index 7)
(winum-select-window-by-index 7)))
;;;###autoload
(defun winum-select-window-8 (&optional arg)
"Jump to window 8.
If prefix ARG is given, delete the window instead of selecting it."
(interactive "P")
(if arg
(winum-delete-window-by-index 8)
(winum-select-window-by-index 8)))
;;;###autoload
(defun winum-select-window-9 (&optional arg)
"Jump to window 9.
If prefix ARG is given, delete the window instead of selecting it."
(interactive "P")
(if arg
(winum-delete-window-by-index 9)
(winum-select-window-by-index 9)))
;;;###autoload
(defun winum-select-window-by-index (&optional arg)
"Select window whose index is specified by ARG.
There are several ways to provide the value:
- if called from elisp with an argument, use it.
- if called from elisp without an argument, use current window.
- if called interactively with a numeric prefix argument, use it.
- if called interactively with the default prefix argument, use current window.
- if called interactively and no argument is provided, read from minibuffer."
(interactive "P")
(let* ((i (if (called-interactively-p 'any)
(cond
((integerp arg) arg)
(arg (winum-get-index))
(t (read--expression "Window: ")))
(or arg (winum-get-index))))
(w (winum-get-window-by-index i)))
(if w
(winum--switch-to-window w)
(user-error "No window with index %S" i))))
;;;###autoload
(defun winum-delete-window-by-index (&optional arg)
"Delete window whose index is specified by ARG.
There are several ways to provide the value:
- if called from elisp with an argument, use it.
- if called from elisp without an argument, use current window.
- if called interactively with a numeric prefix argument, use it.
- if called interactively with the default prefix argument, use current window.
- if called interactively and no argument is provided, read from minibuffer."
(interactive "P")
(let* ((i (if (called-interactively-p 'any)
(cond
((integerp arg) arg)
(arg (winum-get-index))
(t (read--expression "Window: ")))
(or arg (winum-get-index))))
(w (winum-get-window-by-index i)))
(if w
(delete-window w)
(user-error "No window with index %S" i))))
;; Public API ------------------------------------------------------------------
;;;###autoload
(defun winum-set-keymap-prefix (prefix)
"Set key bindings prefix for `winum-keymap' based on `winum-base-map'.
This function overrides the value of `winum-keymap', so you
should call it before customization of `winum-keymap' and/or
after customization of `winum-base-map'.
PREFIX must be a key sequence, like the ones returned by `kbd'."
(setq winum-keymap (when prefix (let ((map (make-sparse-keymap)))
(define-key map prefix winum-base-map)
map)))
(setcdr (assoc 'winum-mode minor-mode-map-alist)
winum-keymap))
;;;###autoload
(defun winum-get-window-by-index (index)
"Return window INDEX if exists, nil otherwise."
(let ((window-table (winum--get-window-table)))
(gethash index window-table)))
;;;###autoload
(defun winum-get-index (&optional window)
"Get the index of WINDOW or the current window."
(let ((w (or window (selected-window))))
(gethash w (winum--get-index-table))))
;; For backwards compatibility
;;;###autoload
(defun winum-get-number-string (&optional window)
"Get the current or specified window's current number as a propertized string.
WINDOW: if specified, the window of which we want to know the number.
If not specified, the number of the currently selected window is
returned."
(let* ((n (winum-get-index window))
(s (if (funcall winum-mode-line-p n)
(format "%s" n)
"")))
(propertize s 'face 'winum-face)))
;; Internal functions ----------------------------------------------------------
(defun winum--init ()
"Initialize winum-mode."
(if (eq winum-scope 'frame-local)
(setq winum--frames-table (make-hash-table :size winum--max-frames))
(setq winum--window-table (make-hash-table :test 'equal)
winum--index-table (make-hash-table :test 'equal)))
(when winum-auto-setup-mode-line
(winum--install-mode-line))
(add-hook 'minibuffer-setup-hook 'winum--update)
(add-hook 'window-configuration-change-hook 'winum--update)
(dolist (frame (frame-list))
(select-frame frame)
(winum--update)))
(defun winum--deinit ()
"Actions performed when turning off winum-mode."
(when winum-auto-setup-mode-line
(winum--clear-mode-line))
(remove-hook 'minibuffer-setup-hook 'winum--update)
(remove-hook 'window-configuration-change-hook 'winum--update)
(setq winum--frames-table nil))
(defun winum--install-mode-line (&optional position)
"Install the window index from `winum-mode' to the mode-line.
POSITION: position in the mode-line."
(let ((mode-line (default-value 'mode-line-format))
res)
(dotimes (i (min (or position winum-mode-line-position 1)
(length mode-line)))
(push (pop mode-line) res))
(unless (equal (car mode-line) winum--mode-line-segment)
(push winum--mode-line-segment res))
(while mode-line
(push (pop mode-line) res))
(let ((nres (nreverse res)))
(setq mode-line-format nres)
(setq-default mode-line-format nres)))
(force-mode-line-update t))
(defun winum--clear-mode-line ()
"Remove the window index of `winum-mode' from the mode-line."
(let ((mode-line (default-value 'mode-line-format))
res)
(while mode-line
(let ((item (pop mode-line)))
(unless (equal item winum--mode-line-segment)
(push item res))))
(let ((nres (nreverse res)))
(setq mode-line-format nres)
(setq-default mode-line-format nres)))
(force-mode-line-update t))
(defun winum--update ()
"Update window indices."
(let ((windows (winum--window-list)))
(setq winum--assigned-indices nil)
(clrhash (winum--get-window-table))
(clrhash (winum--get-index-table))
(when winum-assign-functions
(--each (-copy windows) (when (winum--try-to-find-custom-index it)
(setq windows (delq it windows)))))
(when (and winum-minibuffer-auto-assign
(active-minibuffer-window)
(not (winum-get-window-by-index winum-minibuffer-auto-assign)))
(winum--assign (active-minibuffer-window) winum-minibuffer-auto-assign)
(push winum-minibuffer-auto-assign winum--assigned-indices))
;; Auto-assign remaining windows
(when windows
(funcall winum-auto-assign-function windows))))
(defun winum--auto-assign (windows)
"The default auto-assign function for assigning indices to windows.
Takes in the list of windows WINDOWS and uses `winum--assign' to assign
increasing integers to it."
(let ((index 1))
(dolist (w windows)
(while (member index winum--assigned-indices)
(setq index (1+ index)))
(winum--assign w index)
(setq index (1+ index)))))
(defun winum--try-to-find-custom-index (window)
"Try to find and assign a custom index for WINDOW.
Do so by trying every function in `winum-assign-functions' and assign the
*first* non nil value.
When multiple functions assign an index to a window log a warning and use the
first index anyway."
(with-selected-window window
(with-current-buffer (window-buffer window)
(when-let* ((inds (->> winum-assign-functions
(--map (cons it (funcall it)))
(--remove (null (cdr it)))))
(ind (-> inds (cl-first) (cdr))))
(when (> (length inds) 1)
(message "Winum conflict - window %s was assigned an index by multiple custom assign functions: '%s'"
window (--map (format "%s -> %S" (car it) (cdr it)) inds)))
(winum--assign window ind)
(push ind winum--assigned-indices)))))
(defun winum--assign (window index)
"Assign to window WINDOW the index INDEX.
Returns the assigned index, or nil on error."
(if (gethash index (winum--get-window-table))
(progn
(message "Index %S already assigned to %s, can't assign to %s"
index (gethash index (winum--get-window-table)) window)
nil)
(puthash index window (winum--get-window-table))
(puthash window index (winum--get-index-table))
index))
(defun winum--window-list ()
"Return a list of interesting windows."
(cl-remove-if
#'winum--ignore-window-p
(cl-case winum-scope
(global
(cl-mapcan 'winum--list-windows-in-frame
(if winum-reverse-frame-list
(frame-list)
(nreverse (frame-list)))))
(visible
(cl-mapcan 'winum--list-windows-in-frame
(if winum-reverse-frame-list
(visible-frame-list)
(nreverse (visible-frame-list)))))
(frame-local
(winum--list-windows-in-frame))
(t
(user-error "Invalid `winum-scope': %S" winum-scope)))))
(defun winum--ignore-window-p (window)
"Non-nil if WINDOW should be ignored for indexing."
(let ((f (window-frame window)))
(or (not (and (frame-live-p f)
(frame-visible-p f)))
(string= "initial_terminal" (terminal-name f))
(member (buffer-name (window-buffer window)) winum-ignored-buffers)
(cl-some
(lambda (regex) (string-match regex (buffer-name (window-buffer window))))
winum-ignored-buffers-regexp))))
(defun winum--list-windows-in-frame (&optional f)
"List windows in frame F using natural Emacs ordering."
(window-list f 0 (frame-first-window f)))
(defun winum--get-window-table ()
"Return the window table used to get a window given an index.
This hashtable is not stored the same way depending on the value of
`winum-scope'."
(winum--check-for-scope-change)
(winum--check-frames-table)
(if (eq winum-scope 'frame-local)
(car (gethash (selected-frame) winum--frames-table))
winum--window-table))
(defun winum--get-index-table ()
"Return the index hashtable used to get an index given a window.
This hashtable is not stored the same way depending on the value of
`winum-scope'."
(winum--check-for-scope-change)
(winum--check-frames-table)
(if (eq winum-scope 'frame-local)
(cdr (gethash (selected-frame) winum--frames-table))
winum--index-table))
(defun winum--check-frames-table ()
"Make sure `winum--frames-table' exists and is correctly equipped.
Verifies 2 things (when `winum-scope' is frame local):
* When `winum-scope' is frame-local for the first time it may be necessary to
instantiate `winum--frames-table'.
* A table entry for the current frame must be made when the frame has just
been created."
(when (eq winum-scope 'frame-local)
(unless winum--frames-table
(setq winum--frames-table (make-hash-table :size winum--max-frames)))
(unless (gethash (selected-frame) winum--frames-table)
(puthash (selected-frame) (cons
(make-hash-table :test 'equal)
(make-hash-table :test 'equal))
winum--frames-table)
(winum--update))))
(defun winum--switch-to-window (window)
"Switch to the window WINDOW and switch input focus if on a different frame."
(let ((frame (window-frame window)))
(when (and (frame-live-p frame)
(not (eq frame (selected-frame))))
(select-frame-set-input-focus frame))
(if (window-live-p window)
(select-window window)
(error "Got a dead window %S" window))))
(defun winum--check-for-scope-change ()
"Check whether the `winum-scope' has been changed.
If a change is detected run `winum--init' to reinitialize all
internal data structures according to the new scope."
(unless (eq winum-scope winum--last-used-scope)
(setq winum--last-used-scope winum-scope)
(winum--init)))
(defun winum--remove-deleted-frame-from-frames-table (frame)
"Remove FRAME from `winum--frames-table' after it was deleted."
(when winum--frames-table
(remhash frame winum--frames-table)))
(add-hook 'delete-frame-functions #'winum--remove-deleted-frame-from-frames-table)
(provide 'winum)
;;; winum.el ends here