branch: externals/transient-cycles commit a5b86dd04e84e7ff1cf8c4062843fccb92991145 Author: Sean Whitton <spwhit...@spwhitton.name> Commit: Sean Whitton <spwhit...@spwhitton.name>
* transient-cycles: Import version 1.0 --- transient-cycles.el | 598 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 598 insertions(+) diff --git a/transient-cycles.el b/transient-cycles.el new file mode 100644 index 0000000000..4e78db2261 --- /dev/null +++ b/transient-cycles.el @@ -0,0 +1,598 @@ +;;; transient-cycles.el --- Define command variants with transient cycling -*- lexical-binding: t -*- + +;; Copyright (C) 2020-2022 Free Software Foundation, Inc. + +;; Author: Sean Whitton <spwhit...@spwhitton.name> +;; Maintainer: Sean Whitton <spwhit...@spwhitton.name> +;; Package-Requires: ((emacs "27.1")) +;; Version: 1.0 +;; URL: https://git.spwhitton.name/dotfiles/tree/.emacs.d/site-lisp/transient-cycles.el +;; Keywords: buffer, window, minor-mode, convenience + +;; This file 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 file 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 file. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Many commands can be conceptualised as selecting an item from an ordered +;; list or ring. Sometimes after running such a command, you find that the +;; item selected is not the one you would have preferred, but the preferred +;; item is nearby in the list. If the command has been augmented with +;; transient cycling, then it finishes by setting a transient map with keys to +;; move backwards and forwards in the list of items, so you can select a +;; nearby item instead of the one the command selected. From the point of +;; view of commands subsequent to the deactivation of the transient map, it is +;; as though the first command actually selected the nearby item, not the one +;; it really selected. +;; +;; For example, suppose that you often use `eshell' with a prefix argument to +;; create multiple Eshell buffers, *eshell*, *eshell*<2>, *eshell*<3> and so +;; on. When you use `switch-to-buffer' to switch to one of these buffers, you +;; will typically end up in the most recently used Eshell. But sometimes what +;; you wanted was an older Eshell -- but which one? It would be convenient to +;; quickly cycle through the other Eshell buffers when you discover that +;; `switch-to-buffer' took you to the wrong one. That way, you don't need to +;; remember which numbered Eshell you were using to do what. +;; +;; In this example, we can think of `switch-to-buffer' as selecting from the +;; list of buffers in `eshell-mode'. If we augment the command with transient +;; cycling, then you can use the next and previous keys in the transient map +;; to cycle through the `eshell-mode' buffers to get to the right one. +;; Afterwards, it is as though `switch-to-buffer' had taken you directly +;; there: the buffer list is undisturbed except for the target Eshell buffer +;; having been moved to the top, and only the target Eshell buffer is pushed +;; to the window's previous buffers (see `window-prev-buffers'). +;; +;; This library provides macros to define variants of commands which have +;; transient cycling, and also some minor modes which replace some standard +;; Emacs commands with transient cycling variants the author has found useful. +;; `transient-cycles-buffer-siblings-mode' implements a slightly more complex +;; version of the transient cycling described in the above example. +;; +;; Definitions of command variants in this file only hide the fact that +;; transient cycling went on -- in the above example, how the buffer list is +;; undisturbed and how only the final buffer is pushed to the window's +;; previous buffers -- to the extent that doing so does not require saving a +;; lot of information when commencing transient cycling. + +;;; News: + +;; Ver 1.0 2022/04/10 Sean Whitton +;; Initial release. +;; Thanks to Protesilaos Stavrou for testing and docs feedback. + +;;; Code: + +(require 'ring) +(require 'subr-x) +(require 'cl-lib) + +(defgroup transient-cycles nil + "Defaults and options when defining variants of commands with +transient cycling." + :group 'convenience) + +(defcustom transient-cycles-show-cycling-keys t + "Whether to show the cycling keys in the echo area when +commencing transient cycling." + :type 'boolean + :group 'transient-cycles) + +(defcustom transient-cycles-default-cycle-backwards-key [left] + "Default key for cycling backwards in the transient maps set by +commands to which transient cycling has been added." + :type 'key-sequence + :group 'transient-cycles) + +(defcustom transient-cycles-default-cycle-forwards-key [right] + "Default key for cycling forwards in the transient maps set by +commands to which transient cycling has been added." + :type 'key-sequence + :group 'transient-cycles) + +(cl-defmacro transient-cycles-define-commands + (bindings commands cycler-generator + &key on-exit + (cycle-forwards-key transient-cycles-default-cycle-forwards-key) + (cycle-backwards-key transient-cycles-default-cycle-backwards-key) + (keymap '(current-global-map))) + "Define command variants closing over BINDINGS as specified by +COMMANDS with transient cycling as supplied by CYCLER-GENERATOR. + +BINDINGS are established by means of `let*' at the beginning of +each command variant. Thus each command variant, +CYCLER-GENERATOR and ON-EXIT all close over each of BINDINGS. +The storage is intended to last for the duration of transient +cycling, and may be used for cycling state or to save values from +before cycling began for restoration during ON-EXIT. + +Each of COMMANDS defines a command variant, and should be of one +of the following forms: + +1. (ORIGINAL ARGS [INTERACTIVE] &body BODY) + +2. ORIGINAL alone, which means + (ORIGINAL (&rest args) (apply ORIGINAL args)). + +ORIGINAL can have one of the following forms: + +a. a plain symbol + +b. a pair (KEY . ORIGINAL) where ORIGINAL is a symbol + +c. a vector [remap ORIGINAL] where ORIGINAL is a symbol, which + means ([remap ORIGINAL] . ORIGINAL). + +In each combination, ORIGINAL names the command for which a +transient cycling variant should be defined; ARGS, INTERACTIVE +and BODY are as in `lambda'; and KEY, if present, is a key +sequence to which the command should be bound in KEYMAP. If +INTERACTIVE is absent then the newly defined command receives +ORIGINAL's interactive form. + +CYCLER-GENERATOR defines a function which will be called with the +return value of each command variant, and must return a function +of one argument, which is known as the cycler. After the call to +the command variant, a transient map is established in which +CYCLE-FORWARDS-KEY invokes the cycler with the numeric value of +the prefix argument and CYCLE-BACKWARDS-KEY invokes the cycler +with the numeric value of the prefix argument multiplied by -1. + +CYCLE-FORWARDS-KEY and CYCLE-BACKWARDS-KEY are evaluated at the +time the transient map is established, so it is possible to +compute cycling keys from the binding used to invoke the command. +For example, for CYCLE-FORWARDS-KEY, you might have + + (cond ((memq last-command-event '(up down)) [down]) + ((memq last-command-event '(left right)) [right]) + (t transient-cycles-default-cycle-forwards-key)) + +ON-EXIT, if present, is wrapped in a lambda expression with no +arguments, i.e. (lambda () ON-EXIT), and passed as the third +argument to `set-transient-map'." + (macroexp-progn + (cl-loop with on-exit = (and on-exit `(lambda () ,on-exit)) + and arg = (gensym) and cycler = (gensym) and tmap = (gensym) + and kforwards = (gensym) and kbackwards = (gensym) + for command in commands + for (original args . body) + = (if (proper-list-p command) + command + (let ((f (cl-etypecase command + (symbol command) + (cons (cdr command)) + (vector (aref command 1))))) + `(,command (&rest args) (apply #',f args)))) + for (key . original*) = (cond ((symbolp original) + (cons nil original)) + ((and (vectorp original) + (eq 'remap (aref original 0))) + (cons original (aref original 1))) + (t original)) + for name + = (intern (format "transient-cycles-cmd-%s" (symbol-name original*))) + collect + `(defun ,name () + ,(format "Like `%s', but augmented with transient cycling." + (symbol-name original*)) + (interactive) + (let* (,@bindings + (,arg (call-interactively + (lambda ,args + ,@(if (and (listp (car body)) + (eq 'interactive (caar body))) + body + (cons (interactive-form original*) body)))))) + (when-let ((,cycler (funcall ,cycler-generator ,arg)) + (,tmap (make-sparse-keymap)) + (,kforwards ,cycle-forwards-key) + (,kbackwards ,cycle-backwards-key)) + ;; It might be additionally useful to bind something in the + ;; transient map to kill the current buffer and cycle once. + ;; + ;; For the forward direction we could elide this lambda and + ;; just bind the key to the cycler. But this way means we are + ;; consistent in always supplying an integer. + (define-key ,tmap ,kforwards (lambda (,arg) + (interactive "p") + (funcall ,cycler ,arg))) + (define-key ,tmap ,kbackwards (lambda (,arg) + (interactive "p") + (funcall ,cycler (* -1 ,arg)))) + (when transient-cycles-show-cycling-keys + (message "Cycle forwards with %s, backwards with %s" + (key-description ,kforwards) + (key-description ,kbackwards))) + (set-transient-map ,tmap t ,on-exit)))) + when key collect `(define-key ,keymap ,key #',name)))) +(put 'transient-cycles-define-commands 'common-lisp-indent-function + '(4 (&whole 2 &rest (&whole 1 4 &body)) &body)) + +(cl-defmacro transient-cycles-buffer-ring-cycler + (&key (start 0) + (ring '(transient-cycles-buffer-siblings-ring ret-val)) + (action '(switch-to-buffer buffer t t))) + "Yield a lambda expression to cycle RING from START using ACTION. +This macro is intended for use as the CYCLER-GENERATOR argument +to `transient-cycles-define-keys'. + +RING is a form which evaluates to a ring of buffers. It should +be written in terms of `ret-val', which at time of evaluation +will hold the return value of calling the command variant as +described in the docstring of `transient-cycles-define-keys'. +ACTION is a form in terms of `buffer', which should cycle to +`buffer' in the relevant sense." + (let ((count (gensym)) + (buffers (gensym)) + (buffers-pos (gensym))) + `(lambda (ret-val) + (when-let ((,buffers ,ring) + (,buffers-pos ,start)) + (lambda (,count) + (interactive "p") + (cl-incf ,buffers-pos ,count) + (let ((buffer (ring-ref ,buffers ,buffers-pos))) + ,action)))))) + +(defcustom transient-cycles-buffer-siblings-major-modes + '(("\\`*unsent mail" . message-mode)) + "Alist mapping regexps to major modes. +Buffers whose names match a regexp are considered to have the +associated major mode for the purpose of determining whether they +should be associated with families of clones as generated by +`transient-cycles-buffer-siblings-ring', which see." + :type '(alist :key-type regexp :value-type symbol) + :group 'transient-cycles) + +(defun transient-cycles-buffer-siblings-ring (buffer) + "Return ring of BUFFER clones and buffers sharing the clones' major mode. +BUFFER itself is the first element of the ring, followed by the +clones of BUFFER, and then buffers merely sharing the major mode +of the family of clones. + +Clonehood is determined by similarity of buffer names. Clones +produced by `clone-buffer' and `clone-indirect-buffer' will be +counted as siblings, but so will the two Eshell buffers produced +if you type \\[project-eshell] then \\[universal-argument] \\[project-eshell], +as the same naming scheme is used. This is desirable for +`transient-cycles-buffer-siblings-mode', which see. + +The singular major mode of the family of clones is determined +using heuristics, as it is expected that clones of a buffer may +have different major modes: visiting one file with more than one +major mode is one of the primary uses of indirect clones." + (let* ((clones-hash (make-hash-table)) + (root-name (buffer-name buffer)) + (root-name (if (string-match "\\`\\(.+\\)<[0-9]+>\\'" root-name) + (match-string 1 root-name) + root-name)) + (clones-regexp + (concat "\\`" (regexp-quote root-name) "\\(<[0-9]+>\\)?\\'")) + (clones-pred + (lambda (b) (string-match clones-regexp (buffer-name b)))) + (buffers (cl-remove-if-not clones-pred (buffer-list))) + (mode (or (cdr (assoc root-name + transient-cycles-buffer-siblings-major-modes + #'string-match)) + ;; If only one buffer or root clone is visiting a file, use + ;; major mode of that one buffer or root clone. The only + ;; case we want here is the root of a family of indirect + ;; clones. Thus, don't consider arbitrary clones visiting + ;; files, as this may be because the user cloned, edited + ;; down, changed major mode and then wrote to a file. + (and (= 1 (length buffers)) + (with-current-buffer (car buffers) major-mode)) + (let ((root-clone + (cl-find root-name buffers + :key #'buffer-name :test #'string=))) + (and root-clone (with-current-buffer root-clone + (and (buffer-file-name) major-mode)))) + ;; See if the name of one of the clones is a substring of + ;; its major mode, and if so, use that mode. + ;; E.g. *eww* -> `eww-mode'. Cases this heuristic will get + ;; wrong should have entries in + ;; `transient-cycles-buffer-sublings-major-modes'. + (cl-loop + with root-root-name = (regexp-quote + (string-trim root-name "\\*" "\\*")) + with case-fold-search = t + for buffer in buffers + for mode = (symbol-name + (with-current-buffer buffer major-mode)) + when (string-match root-root-name mode) return mode) + ;; Fallback. + (with-current-buffer buffer major-mode)))) + (dolist (buffer buffers) (puthash buffer t clones-hash)) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (when (and (eq mode major-mode) (not (gethash buffer clones-hash))) + (push buffer buffers)))) + (let ((ring (make-ring (length buffers))) + ;; Often BUFFER will be the most recently selected buffer and so the + ;; car of the buffer list, but not always, and we always want + ;; cycling to begin from BUFFER. + (reversed (nreverse (cons buffer (remove buffer buffers))))) + (dolist (buffer reversed ring) (ring-insert ring buffer))))) + + +;;;; Minor modes + +(defvar transient-cycles-buffer-siblings-mode-map (make-sparse-keymap) + "Keymap for `transient-cycles-buffer-siblings-mode'.") + +(defcustom transient-cycles-buffer-siblings-cycle-backwards-key [left] + "Key to cycle backwards in the transient maps set by commands +defined by `transient-cycles-buffer-siblings-mode'." + :type 'key-sequence + :group 'transient-cycles) + +(defcustom transient-cycles-buffer-siblings-cycle-forwards-key [right] + "Key to cycle forwards in the transient maps set by commands +defined by `transient-cycles-buffer-siblings-mode'." + :type 'key-sequence + :group 'transient-cycles) + +;;;###autoload +(define-minor-mode transient-cycles-buffer-siblings-mode + "Enhance buffer switching commands by adding transient cycling. + +Augments a number of standard buffer switching commands. After +typing \\[switch-to-buffer], \\[display-buffer], \\[info] and +some others, you can use the keys +`transient-cycles-buffer-siblings-cycle-backwards-key' and +`transient-cycles-buffer-siblings-cycle-forwards-key' to select a +different, relevantly similar buffer to select or display +instead. See `transient-cycles-buffer-siblings-ring' for details +of the notion of similarity employed. + +The purpose of this mode is to make it easier to handle large +numbers of similarly-named buffers without having to take the +time to manually rename them. For example, suppose while reading +Info you type \\<Info-mode-map>\\[clone-buffer] several times in +order to view several pieces of information at once. Later you +need to refer back to one of those buffers, but \\[info] will +always take you to `*info*', and if you use \\[switch-to-buffer] +it might take you several tries to select the buffer you wanted. +Thanks to this minor mode, after using either of those commands +to switch to any `Info-mode' buffer you can quickly cycle through +to the intended target." + :lighter nil :keymap transient-cycles-buffer-siblings-mode-map :global t + :group 'transient-cycles) + +(transient-cycles-define-commands (prev-buffers) + (([remap switch-to-buffer] (buffer &optional _norecord force-same-window) + (prog1 (switch-to-buffer buffer t force-same-window) + (setq prev-buffers (window-prev-buffers)))) + + ([remap switch-to-buffer-other-window] (buffer-or-name &rest _ignore) + (prog1 (switch-to-buffer-other-window buffer-or-name t) + (setq prev-buffers (window-prev-buffers)))) + + ([remap switch-to-buffer-other-tab] (buffer-or-name) + (prog1 (window-buffer (switch-to-buffer-other-tab buffer-or-name)) + (setq prev-buffers (window-prev-buffers))))) + + (transient-cycles-buffer-ring-cycler) + :on-exit (progn (switch-to-buffer (current-buffer) nil t) + (set-window-next-buffers nil nil) + (set-window-prev-buffers nil prev-buffers)) + :keymap transient-cycles-buffer-siblings-mode-map + :cycle-forwards-key transient-cycles-buffer-siblings-cycle-forwards-key + :cycle-backwards-key transient-cycles-buffer-siblings-cycle-backwards-key) + +;; Here we don't try to restore the fundamental or frame buffer lists, but it +;; would be possible to do so. See (info "(elisp) Buffer List"). +(transient-cycles-define-commands (window prev-buffers) + (([remap display-buffer] (buffer-or-name &optional action frame) + (prog1 (setq window (display-buffer buffer-or-name action frame)) + (setq prev-buffers (window-prev-buffers window)))) + + ([remap info] (&optional file-or-node buffer) + (prog2 (info file-or-node buffer) + (setq window (get-buffer-window buffer)) + (setq prev-buffers (and window (window-prev-buffers window)))))) + + (transient-cycles-buffer-ring-cycler + :ring (transient-cycles-buffer-siblings-ring (window-buffer ret-val)) + :action + (with-selected-window ret-val + (let ((display-buffer-overriding-action + '((display-buffer-same-window) (inhibit-same-window . nil)))) + (display-buffer buffer)))) + :on-exit (progn (set-window-next-buffers window nil) + (set-window-prev-buffers window prev-buffers)) + :keymap transient-cycles-buffer-siblings-mode-map + :cycle-forwards-key transient-cycles-buffer-siblings-cycle-forwards-key + :cycle-backwards-key transient-cycles-buffer-siblings-cycle-backwards-key) + +(defvar transient-cycles-window-buffers-mode-map (make-sparse-keymap) + "Keymap for `transient-cycles-window-buffers-mode'.") + +(defcustom transient-cycles-window-buffers-cycle-backwards-key [left] + "Key to cycle backwards in the transient maps set by commands +defined by `transient-cycles-window-buffers-mode'." + :type 'key-sequence + :group 'transient-cycles) + +(defcustom transient-cycles-window-buffers-cycle-forwards-key [right] + "Key to cycle forwards in the transient maps set by commands +defined by `transient-cycles-window-buffers-mode'." + :type 'key-sequence + :group 'transient-cycles) + +;;;###autoload +(define-minor-mode transient-cycles-window-buffers-mode + "Enhance window buffer switching commands by adding transient cycling. + +Augments \\[previous-buffer] and \\[next-buffer]. After typing +those commands, you can use +`transient-cycles-window-buffers-cycle-backwards-key' and +`transient-cycles-window-buffers-cycle-forwards-key' to move +forwards and backwards in a virtual list of the window's +previous, current and next buffers. When transient cycling +completes, your starting point will be stored, such that +\\[transient-cycles-window-buffers-back-and-forth] can quickly +take you back there." + :lighter nil :keymap transient-cycles-window-buffers-mode-map :global t + :group 'transient-cycles + (if transient-cycles-window-buffers-mode + ;; Clear window parameter just when the list of next buffers is cleared. + (advice-add 'set-window-buffer :after + #'transient-cycles--reset-window-recent-buffer) + (advice-remove 'set-window-buffer + #'transient-cycles--reset-window-recent-buffer))) + +(cl-symbol-macrolet + ((param (window-parameter nil 'transient-cycles--window-recent-buffer))) + (transient-cycles-define-commands (recent-buffer last-recent-buffer) + (([remap previous-buffer] (count) + (interactive "p") + (setq recent-buffer (and (window-next-buffers) count) + last-recent-buffer param) + (previous-buffer count)) + + ([remap next-buffer] (count) + (interactive "p") + ;; We consider only the window's next buffers, not the frame's next + ;; buffers as `next-buffer' does. This is because otherwise our + ;; `recent-buffer' and window parameter become invalid. + ;; + ;; `previous-buffer' and `next-buffer' use `switch-to-prev-buffer' and + ;; `switch-to-next-buffer' as subroutines, so buffers previously shown + ;; in the selected window come up first + (if (window-next-buffers) + (progn (setq recent-buffer (* -1 count) + last-recent-buffer param) + (next-buffer count)) + (user-error "No next buffer")))) + + (lambda (_ignore) + (lambda (count) + (if (cl-plusp count) + (if (window-next-buffers) + (progn (when recent-buffer (cl-decf recent-buffer count)) + (next-buffer count)) + (message "No next buffer")) + (setq count (* -1 count)) + (when recent-buffer (cl-incf recent-buffer count)) + (previous-buffer count)))) + ;; If `recent-buffer' is zero then we are back where we started. + ;; in. In that case, restore the old parameter value so that + ;; `transient-cycles-window-buffers-back-and-forth' does something useful. + :on-exit (setq param (if (and recent-buffer (zerop recent-buffer)) + last-recent-buffer + recent-buffer)) + :keymap transient-cycles-window-buffers-mode-map + :cycle-forwards-key transient-cycles-window-buffers-cycle-forwards-key + :cycle-backwards-key transient-cycles-window-buffers-cycle-backwards-key)) + +(defun transient-cycles--reset-window-recent-buffer (&rest _ignore) + (set-window-parameter nil 'transient-cycles--window-recent-buffer nil)) + +(defun transient-cycles-window-buffers-back-and-forth () + "Switch to the buffer most recently accessed using the bindings +established by `transient-cycles-window-buffers-mode', on the +condition that no other commands have set this window's buffer +since then. Otherwise, call `previous-buffer'." + (interactive) + (cl-symbol-macrolet + ((param (window-parameter nil 'transient-cycles--window-recent-buffer))) + (cond (param + (let ((new (and (window-next-buffers) (* -1 param)))) + (cond ((cl-plusp param) (next-buffer param)) + ((cl-minusp param) (previous-buffer (* -1 param)))) + (setq param new))) + ((window-next-buffers) + (let ((count (length (window-next-buffers)))) + (next-buffer count) + (setq param (* -1 count)))) + (t (previous-buffer))))) + +(defvar transient-cycles-tab-bar-mode-map (make-sparse-keymap) + "Keymap for `transient-cycles-tab-bar-mode'.") + +;;;###autoload +(define-minor-mode transient-cycles-tab-bar-mode + "Enhance tab switching commands by adding transient cycling. + +Augments \\[tab-previous], \\[tab-next] and +\\[tab-bar-switch-to-recent-tab]. After running those commands, +you can use `transient-cycles-tab-bar-cycle-backwards-key' and +`transient-cycles-tab-bar-cycle-forwards-key' to move forwards +and backwards in the list of tabs. When transient cycling +completes, tab access times will be as though you had moved +directly from the first tab to the final tab. That means that +\\[tab-bar-switch-to-recent-tab] may be used to switch back and +forth between the first tab and the final tab." + :lighter nil :keymap transient-cycles-tab-bar-mode-map :global t + :group 'transient-cycles) + +(defcustom transient-cycles-tab-bar-cycle-backwards-key [left] + "Key to cycle backwards in the transient maps set by commands +defined by `transient-cycles-tab-bar-mode'." + :type 'key-sequence + :group 'transient-cycles) + +(defcustom transient-cycles-tab-bar-cycle-forwards-key [right] + "Key to cycle forwards in the transient maps set by commands +defined by `transient-cycles-tab-bar-mode'." + :type 'key-sequence + :group 'transient-cycles) + +(transient-cycles-define-commands (recent-tab-old-time) + (([remap tab-previous] (count) + (setq recent-tab-old-time (transient-cycles--nth-tab-time (* -1 count))) + (tab-previous count)) + + ;; `tab-bar-switch-to-recent-tab' does not have a binding by default but + ;; establish a remapping so that the user can easily access the transient + ;; cycling variant simply by adding a binding for the original command. + ([remap tab-bar-switch-to-recent-tab] (count) + (setq recent-tab-old-time + (cl-loop for tab in (funcall tab-bar-tabs-function) + unless (eq 'current-tab (car tab)) + minimize (cdr (assq 'time tab)))) + (tab-bar-switch-to-recent-tab count)) + + ([remap tab-next] (count) + (setq recent-tab-old-time (transient-cycles--nth-tab-time count)) + (tab-next count))) + + (lambda (_ignore) + (lambda (count) + ;; We are moving away from the current tab, so restore its time as if + ;; we had never selected it, and store the time of the tab we're + ;; moving to in case we need to do this again. + (let ((next-tab-old-time (transient-cycles--nth-tab-time count))) + (tab-bar-switch-to-next-tab count) + (cl-loop with max + for tab in (funcall tab-bar-tabs-function) + for tab-time = (assq 'time tab) + when (and (not (eq 'current-tab (car tab))) + (or (not max) (> (cdr tab-time) (cdr max)))) + do (setq max tab-time) + finally (setcdr max recent-tab-old-time)) + (setq recent-tab-old-time next-tab-old-time)))) + :keymap transient-cycles-tab-bar-mode-map + :cycle-forwards-key transient-cycles-tab-bar-cycle-forwards-key + :cycle-backwards-key transient-cycles-tab-bar-cycle-backwards-key) + +(defun transient-cycles--nth-tab-time (n) + (let* ((tabs (funcall tab-bar-tabs-function)) + (current-index (cl-position 'current-tab tabs :key #'car)) + (new-index (mod (+ n current-index) (length tabs)))) + (alist-get 'time (nth new-index tabs)))) + +(provide 'transient-cycles) + +;;; transient-cycles.el ends here