;;; 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. Auto-assign functions must not assign any indices that are `equal' to an index already used (stored in `winum--assigned-indices'). The convenience macro `winum--assign-unique' will handle this automatically. 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 (or (and (winum-get-window-by-index 0) 0) (and (winum-get-window-by-index 10) 10) (user-error "No window with index 0 or 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)))) (defmacro winum--assign-unique (window var &optional after update pred) "Ensure that the index in VAR is unique, then assign it to WINDOW. If AFTER is non-nil, also ensure that VAR is unique after the assignment. UPDATE and PRED are quoted function symbols or unquoted sexps. If they are functions, then the value of VAR is passed as input. UPDATE must return a new index to be assigned to VAR, with the guarantee that repeatedly executing it will never return the same index twice. The default updating function is `1+'. PRED must be non-nil if VAR must be updated. By default, PRED checks if the variable's value is in `winum--assigned-indices'." (unless pred (setq pred `(member ,var winum--assigned-indices))) (unless update (setq update '#'1+)) (and (= (safe-length pred) 2) (memq (car pred) '(quote function)) (setq pred `(funcall ,pred ,var))) (and (= (safe-length update) 2) (memq (car update) '(quote function)) (setq update `(funcall ,update ,var))) `(progn (while ,pred (setq ,var ,update)) (winum--assign ,window ,var) ,@(and after `((setq ,var ,update))))) (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) (winum--assign-unique w index t)))) (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