;;; winum.el --- Navigate windows and frames using numbers. ;; ;; Copyright (c) 2006-2015 Nikolaj Schumacher ;; Copyright (c) 2016-2019 Thomas Chauvot de Beauchêne ;; Copyright (c) 2024 Kiana Sheibani ;; ;; 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: Kiana Sheibani ;; Version: 3.0.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: (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