branch: externals/bufferlo commit 9a0e98bde2a0ca2832d9978b9c6ed220cb3185ad Author: shipmints <shipmi...@gmail.com> Commit: shipmints <shipmi...@gmail.com>
Sessions. Menu bar. Mode line menu. Tab and frame bookmark save policies. Misc. --- bufferlo.el | 915 ++++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 677 insertions(+), 238 deletions(-) diff --git a/bufferlo.el b/bufferlo.el index 04f5d03d01..a959c46d35 100644 --- a/bufferlo.el +++ b/bufferlo.el @@ -59,6 +59,19 @@ "Manage frame/tab-local buffer lists." :group 'convenience) +(defcustom bufferlo-menu-bar-show t + "Show the bufferlo menu on the menu bar." + :type 'boolean) + +(defcustom bufferlo-menu-bar-list-buffers 'both + "Show simple and/or `ibuffer' buffer list menu items. +Set to \\='both to show both. +Set to \\='simple to show simple only. +Set to \\='ibuffer to show `ibuffer' only." + :type '(radio (const :tag "Show both simple and `ibuffer'" both) + (const :tag "Show simple only" simple) + (const :tag "Show `ibuffer' only" ibuffer))) + (defcustom bufferlo-prefer-local-buffers t "Use the frame `buffer-predicate' to prefer local buffers. Without this option, buffers from across all frames are @@ -255,9 +268,13 @@ frame." (const :tag "Clear (with message)" clear-warn) (const :tag "Raise" raise))) -(defcustom bufferlo-bookmark-tab-load-into-bookmarked-frame-policy 'prompt +(defcustom bufferlo-bookmark-tab-in-bookmarked-frame-policy 'prompt "Control when a tab bookmark is loaded into an already-bookmarked frame. +This also warns about setting a new frame bookmark on a frame +that has tab bookmarks, and vice versa setting a tab bookmark on +a bookmarked frame. + \\='clear will silently clear the tab bookmark which is natural reified frame bookmark behavior. @@ -365,7 +382,7 @@ If non-nil, a new frame is created for restored tabs." The local buffer filter is bound to \"/ l\" and the orphan filter to \"/ L\"." :type 'boolean) -(defcustom bufferlo-ibuffer-bind-keys nil +(defcustom bufferlo-ibuffer-bind-keys t "If non-nil, bind ibuffer convenience keys for bufferlo functions." :type 'boolean) @@ -476,23 +493,102 @@ Set to 0 to disable the timer. Units are whole integer seconds." (const :tag "Saved only" saved) (const :tag "Not-saved only" notsaved))) +(defcustom bufferlo-session-restore-geometry-policy 'all + "Bufferlo frame restoration geometry policy. + +\\='all restores both frame and tab bookmark frame geometries. + +\\='frames restores only frame bookmark geometry. + +\\='tab-frame restores only tab bookmark logical frame geometry." + :type '(radio (const :tag "All" all) + (const :tag "Frames" frames) + (const :tag "Tabs" tab-frames))) + +(defcustom bufferlo-session-restore-tabs-reuse-init-frame nil + "Restore first session tabs frame to the current frame. +This affects the first frame of session tab bookmarks. Subsequent +frames of tab bookmarks are restored to their own frames." + :type '(radio (const :tag "Reuse" reuse) + (const :tag "Reuse & reset geometry" reuse-reset-geometry) + (const :tag "New frame" nil))) + +(defcustom bufferlo-frameset-restore-geometry 'bufferlo + "Frameset restore geometry handling control. + +\\='native uses Emacs built-in geometry handling. + +\\='bufferlo uses bufferlo's geometry handling. + +Set to nil to ignore geometry handling." + :type '(radio (const :tag "Emacs" native) + (const :tag "Bufferlo" bufferlo) + (const :tag "Ignore" nil))) + +(defcustom bufferlo-frameset-save-filter nil + "Extra Emacs frame parameters to filter before saving a `frameset'. +Use this if you define custom frame parameters, or you use +packages that do, and you want to avoid storing such parameters +in bufferlo framesets." + :type '(repeat symbol)) + +(defcustom bufferlo-frameset-restore-filter nil + "Extra Emacs frame parameters to filter before restoring a `frameset'. +Use this if you define custom frame parameters, or you use +packages that do, and you want to ensure they are filtered in +advance of restoring bufferlo framesets." + :type '(repeat symbol)) + +(defcustom bufferlo-frame-geometry-function #'bufferlo-frame-geometry-default + "Function to produce a bufferlo-frame-geometry alist. +It defaults to `bufferlo-frame-geometry-default'. + +The function takes one parameter, FRAME, for which geometry is to +be ascertained. See `bufferlo-frame-geometry-default' for +the returned alist form. + +Replace this function with your own if the default produces +suboptimal results for your platform." + :type 'function) + (defcustom bufferlo-mode-line-lighter-prefix " Bfl" "Bufferlo mode-line lighter prefix." :type 'string) (defvar bufferlo-mode) ; byte compiler +(defvar bufferlo-mode-line-menu) ; byte compiler (defun bufferlo-mode-line-format () "Bufferlo mode-line format to display the current active frame or tab bookmark." (when bufferlo-mode - (let ((fbm (frame-parameter nil 'bufferlo-bookmark-frame-name)) - (tbm (alist-get 'bufferlo-bookmark-tab-name (tab-bar--current-tab-find (frame-parameter nil 'tabs)))) - (maybe-space (if (display-graphic-p) "" " "))) ; tty rendering can be off for Ⓕ Ⓣ - (concat bufferlo-mode-line-lighter-prefix - "[" - (if fbm (concat "Ⓕ" maybe-space fbm)) ; the space accommodates tty rendering - (if (and fbm tbm) " ") - (if tbm (concat "Ⓣ" maybe-space tbm)) ; the space accommodates tty rendering - "]")))) + (let* ((fbm (frame-parameter nil 'bufferlo-bookmark-frame-name)) + (tbm (alist-get 'bufferlo-bookmark-tab-name (tab-bar--current-tab-find (frame-parameter nil 'tabs)))) + (bm (or fbm tbm "")) + (maybe-space (if (display-graphic-p) "" " "))) ; tty rendering can be off for Ⓕ Ⓣ + `(:propertize + ,(concat bufferlo-mode-line-lighter-prefix + "[" + (if fbm (concat "Ⓕ" maybe-space fbm)) ; the space accommodates tty rendering + (if (and fbm tbm) " ") + (if tbm (concat "Ⓣ" maybe-space tbm)) ; the space accommodates tty rendering + "]") + mouse-face mode-line-highlight + help-echo + ,(lambda (&rest _) + (concat + (format "Active bufferlo bookmark: %s\n" bm) + "mouse-1: Display minor mode menu\n" + "mouse-2: Show help for minor mode")) + keymap + ,(let ((map (make-sparse-keymap))) + (define-key map [mode-line down-mouse-1] + bufferlo-mode-line-menu) + (define-key map [mode-line down-mouse-3] + bufferlo-mode-line-menu) + (define-key map [mode-line mouse-2] + (lambda () + (interactive) + (describe-function 'bufferlo-mode))) + map))))) (defcustom bufferlo-mode-line-lighter '(:eval (bufferlo-mode-line-format)) "Bufferlo mode line definition." @@ -521,6 +617,9 @@ Set to 0 to disable the timer. Units are whole integer seconds." (message "bufferlo--command-line-noload=%s" bufferlo--command-line-noload) (message "command-line-args=%s" command-line-args))) +(defvar bufferlo-mode-map (make-sparse-keymap) + "`bufferlo-mode' keymap.") + ;;;###autoload (define-minor-mode bufferlo-mode "Manage frame/tab-local buffers." @@ -528,7 +627,7 @@ Set to 0 to disable the timer. Units are whole integer seconds." :require 'bufferlo :init-value nil :lighter bufferlo-mode-line-lighter - :keymap nil + :keymap bufferlo-mode-map (if bufferlo-mode (progn (bufferlo--parse-command-line) ; parse user-provided settings first @@ -603,6 +702,117 @@ Set to 0 to disable the timer. Units are whole integer seconds." (advice-remove 'bookmark-rename #'bufferlo--bookmark-rename-advice) (advice-remove 'bookmark-delete #'bufferlo--bookmark-delete-advice))) +(defun bufferlo--current-bookmark-name () + "Current bufferlo bookmark name, where frame beats tab." + (if-let ((fbm (frame-parameter nil 'bufferlo-bookmark-frame-name))) + fbm + (if-let ((tbm (alist-get 'bufferlo-bookmark-tab-name + (cdr (bufferlo--current-tab))))) + tbm + nil))) + +(defvar bufferlo-menu-item-raise + '("Raise" + :help "Raise an open bufferlo bookmark" + :active (> (length (bufferlo--active-bookmarks)) 0) + :filter (lambda (&optional _) + (let* ((abms (bufferlo--active-bookmarks)) + (abm-names (mapcar #'car abms)) + (current-bm-name (bufferlo--current-bookmark-name))) + (mapcar (lambda (abm-name) + (vector abm-name `(bufferlo--bookmark-raise-by-name ,abm-name) + :style 'radio + :selected (equal abm-name current-bm-name))) + abm-names))))) + +(easy-menu-define bufferlo-mode-menu bufferlo-mode-map + "`bufferlo-mode' menu." + `("Bufferlo" :visible (and bufferlo-mode bufferlo-menu-bar-show) + ["Buffer Management" :active nil] + ["Local Buffers..." bufferlo-list-buffers :help "Display a list of local buffers" :visible (memq bufferlo-menu-bar-list-buffers '(both simple))] + ["Orphan Buffers..." bufferlo-list-orphan-buffers :help "Display a list of orphan buffers" :visible (memq bufferlo-menu-bar-list-buffers '(both simple))] + ["Local Buffers (ibuffer)..." bufferlo-ibuffer :help "Invoke `ibuffer' filtered for local buffers" :visible (memq bufferlo-menu-bar-list-buffers '(both ibuffer))] + ["Orphan Buffers (ibuffer)..." bufferlo-ibuffer-orphans :help "Invoke `ibuffer' filtered for orphan buffers" :visible (memq bufferlo-menu-bar-list-buffers '(both ibuffer))] + ["Clear Buffer Locals" bufferlo-clear :help "Clear the frame/tab's buffer list, except for the current buffer"] + ["Remove Buffer from Locals..." bufferlo-remove :help "Remove buffer from the frame/tab's buffer list"] + ["Remove Non Exclusives" bufferlo-remove-non-exclusive-buffers :help "Remove all buffers from the local buffer list that are not exclusive to it"] + ["Bury and Remove from Locals" bufferlo-bury :help "Bury and remove the buffer specified by BUFFER-OR-NAME from the local list"] + ["Kill Local Buffers..." bufferlo-kill-buffers :help "Kill the buffers of the frame/tab-local buffer list"] + ["Kill Orphan Buffers..." bufferlo-kill-orphan-buffers :help "Kill all buffers that are not in any local list of a frame or tab"] + ("Find/Switch" + ["Find..." bufferlo-find-buffer :help "Switch to the frame/tab containing buffer in its local list"] + ["Find & Switch..." bufferlo-find-buffer-switch :help "Switch to the frame/tab containing buffer and select the buffer"] + ["Display..." bufferlo-switch-to-buffer :help "Display the selected buffer in the selected window"] + ) + ("*scratch*" + ["*scratch*..." bufferlo-switch-to-scratch-buffer :help "Switch to the scratch buffer"] + ["Local *scratch*..." bufferlo-switch-to-local-scratch-buffer :help "Switch to the local scratch buffer"] + ["Toggle *scratch*..." bufferlo-toggle-local-scratch-buffer :help "Switch to the local scratch buffer or bury it if it is already selected"] + ) + ["Isolate Project" bufferlo-isolate-project :help "Isolate a project in the frame or tab" :active (project-current)] + "--" + ["Bookmarks" :active nil] + ["Load..." bufferlo-bms-load :help "Load specified bookmarks"] + ["Save..." bufferlo-bms-save :help "Save specified bookmarks"] + ["Close/Kill..." bufferlo-bms-close :help "Close/kill specified bookmarks"] + ["Save Current" bufferlo-bm-save :help "Save the current tab bookmark"] + ["Reload Current" bufferlo-bm-load :help "Reload a tab bookmark replacing existing state"] + ["Close/Kill Current..." bufferlo-bm-close :help "Close the current tab bookmark and kill its buffers"] + ["Raise..." bufferlo-bookmark-raise :help "Raise an active bufferlo bookmark"] + ,bufferlo-menu-item-raise ; sub-menu of actives to select that also lives in the mode line + ["Clear Actives..." bufferlo-clear-active-bookmarks :help "Clear active bookmarks"] + ["Clear Active (if duped)" bufferlo-maybe-clear-active-bookmark :help "Clear active bookmark if already in use elsewhere"] + "--" + ;; ["Tab Bookmarks" :active nil] + ("Tab Bookmarks" + ["Create..." bufferlo-bm-tab-save :help "Create a new tab bookmark"] + ["Load..." bufferlo-bm-tab-load :help "Load a tab bookmark"] + ["Save Current" bufferlo-bm-tab-save-curr :help "Save the current tab bookmark"] + ["Reload Current" bufferlo-bm-tab-load-curr :help "Reload a tab bookmark replacing existing state"] + ["Close/Kill Current" bufferlo-bm-tab-close-curr :help "Close the current tab bookmark and kill its buffers"] + ) + ;; "--" + ;; ["Frame Bookmarks" :active nil] + ("Frame Bookmarks" + ["Create..." bufferlo-bm-frame-save :help "Create a new frame bookmark"] + ["Load..." bufferlo-bm-frame-load :help "Load a frame bookmark"] + ["Merge..." bufferlo-bm-frame-load-merge :help "Merge a frame bookmark tabs into the current frame"] + ["Save Current" bufferlo-bm-frame-save-curr :help "Save the current frame bookmark"] + ["Reload Current" bufferlo-bm-frame-load-curr :help "Reload a frame bookmark replacing existing state"] + ["Close/Kill Current" bufferlo-bm-frame-close-curr :help "Close the current frame bookmark and kill its buffers"] + ) + ;; "--" + ;; ["Session Bookmarks" :active nil] + ("Session Bookmarks" + ["Create..." bufferlo-sess-save :help "Create a new session bookmark"] + ["Load..." bufferlo-sess-load :help "Load a session bookmark"] + ["Close/Kill..." bufferlo-sess-close :help "Close the specified session bookmarks (kills frames, tabs, buffers)"] + ["Clear..." bufferlo-sess-clear :help "Clear the specified session bookmark (does not kill frames, tabs, buffers)"] + ) + ;; "--" + ;; ["Bookmark Management" :active nil] + ("Bookmark Management" + ["Emacs Bookmarks..." bookmark-bmenu-list :help "Emacs bookmarks"] + ["Rename Bookmark..." (lambda () + (interactive) + (let ((last-nonmenu-event "")) ; (listp nil) returns t so we hack it to be nil + (call-interactively #'bookmark-rename))) + :help "Rename a bookmark"] + ["Delete Bookmark..." (lambda () + (interactive) + (let ((last-nonmenu-event "")) ; (listp nil) returns t so we hack it to be nil + (call-interactively #'bookmark-delete))) + :help "Delete a bookmark"] + ) + "--" + ;; customize + ["Customize Bufferlo" (lambda () (interactive) (customize-group "bufferlo"))])) + +(easy-menu-define bufferlo-mode-line-menu nil + "`bufferlo-mode' mode-line menu." + `("Bufferlo" + ,bufferlo-menu-item-raise)) + (defun bufferlo-local-buffer-p (buffer &optional frame tabnum include-hidden) "Return non-nil if BUFFER is in the list of local buffers. A non-nil value of FRAME selects a specific frame instead of the current one. @@ -1049,6 +1259,7 @@ argument INTERNAL-TOO is non-nil." (when bufferlo-delete-frame-kill-buffers-prompt (setq kill (y-or-n-p "Kill frame and its buffers? "))) (when kill + (raise-frame frame) ; if called in a batch, raise frame in case of prompts for buffers that need saving (bufferlo-kill-buffers nil frame 'all internal-too) ;; TODO: Emacs 30 frame-deletable-p ;; account for top-level, non-child frames @@ -1076,7 +1287,8 @@ The optional arguments KILLALL and INTERNAL-TOO are passed to (setq kill (y-or-n-p "Kill tab and its buffers? "))) (when kill (bufferlo-kill-buffers killall nil nil internal-too) - (tab-bar-close-tab)))) + (let ((tab-bar-close-last-tab-choice 'delete-frame)) + (ignore-errors (tab-bar-close-tab)))))) ; catch errors in case this is the last tab on the last frame (defun bufferlo-isolate-project (&optional file-buffers-only) "Isolate a project in the frame or tab. @@ -1471,6 +1683,13 @@ In contrast to `bufferlo-anywhere-mode', this does not adhere to (advice-add #'call-interactively :around #'bufferlo--interactive-advice)) (add-hook 'post-command-hook postfun))) +(defun bufferlo--bookmark-jump (bookmark) + "Guarded `bookmark-jump' for BOOKMARK." + (condition-case err + (let ((bookmark-fringe-mark nil)) + (bookmark-jump bookmark #'ignore)) + (message (delay-warning 'bufferlo (format "Error %S when jumping to bookmark %S" err bookmark))))) + (defun bufferlo--bookmark-get-for-buffer (buffer) "Get `buffer-name' and bookmark for BUFFER." (with-current-buffer buffer @@ -1553,7 +1772,7 @@ this bookmark is embedded in a frame bookmark." (when (eq duplicate-policy 'prompt) (pcase (let ((read-answer-short t)) (with-local-quit - (read-answer "Tab bookmark already active: Allow, Clear bookmark after loading, Raise existing " + (read-answer "Tab bookmark name already active: Allow, Clear bookmark after loading, Raise existing " '(("allow" ?a "Allow duplicate") ("clear" ?c "Clear the bookmark after loading") ("raise" ?r "Raise the existing tab bookmark") @@ -1627,7 +1846,7 @@ this bookmark is embedded in a frame bookmark." (when (and (not embedded-tab) bookmark-name (frame-parameter nil 'bufferlo-bookmark-frame-name)) - (let ((clear-policy bufferlo-bookmark-tab-load-into-bookmarked-frame-policy)) + (let ((clear-policy bufferlo-bookmark-tab-in-bookmarked-frame-policy)) (when (eq clear-policy 'prompt) (pcase (let ((read-answer-short t)) (with-local-quit @@ -1692,7 +1911,7 @@ the message after successfully restoring the bookmark." (when (eq duplicate-policy 'prompt) (pcase (let ((read-answer-short t)) (with-local-quit - (read-answer "Frame bookmark already active: Allow, Clear bookmark after loading, Raise existing " + (read-answer "Frame bookmark name already active: Allow, Clear bookmark after loading, Raise existing " '(("allow" ?a "Allow duplicate") ("clear" ?c "Clear the bookmark after loading") ("raise" ?r "Raise the frame with the existing bookmark") @@ -1710,7 +1929,7 @@ the message after successfully restoring the bookmark." bufferlo-bookmark-frame-load-make-frame (not (consp current-prefix-arg)) ; user make-frame suppression (not pop-up-frames)) ; make-frame implied by functions like `bookmark-jump-other-frame' - (with-current-buffer (messages-buffer) ; least expensive (fundamental-mode) + (with-temp-buffer (setq new-frame (make-frame)))) (let ((fbm (frame-parameter nil 'bufferlo-bookmark-frame-name)) (load-policy bufferlo-bookmark-frame-load-policy)) @@ -1792,31 +2011,23 @@ the message after successfully restoring the bookmark." (bookmark-prop-set bookmark-name-or-record 'location (or location "")) bookmark-name-or-record) -(defcustom bufferlo-frameset-restore-geometry 'bufferlo - "Frameset restore geometry handling control. - -\\='native uses Emacs built-in geometry handling. - -\\='bufferlo uses bufferlo's geometry handling. - -Set to nil to ignore geometry handling." - :type '(radio (const :tag "Emacs" native) - (const :tag "Bufferlo" bufferlo) - (const :tag "Ignore" nil))) - -(defcustom bufferlo-frameset-save-filter nil - "Extra Emacs frame parameters to filter before saving a `frameset'. -Use this if you define custom frame parameters, or you use -packages that do, and you want to avoid storing such parameters -in bufferlo framesets." - :type '(repeat symbol)) - -(defcustom bufferlo-frameset-restore-filter nil - "Extra Emacs frame parameters to filter before restoring a `frameset'. -Use this if you define custom frame parameters, or you use -packages that do, and you want to ensure they are filtered in -advance of restoring bufferlo framesets." - :type '(repeat symbol)) +(defun bufferlo--bookmark-completing-read (prompt candidates) + "Common bufferlo bookmark completing read. +PROMPT is the prompt text ending with a space. +CANDIDATES are the prompt options to select." + (let* ((comps + (completion-all-completions + (completing-read prompt + (lambda (str pred flag) + (pcase flag + ('metadata + '(metadata (category . bookmark))) + (_ + (all-completions str candidates pred))))) + candidates nil nil)) + (base-size (cdr (last comps)))) + (when base-size (setcdr (last comps) nil)) + (setq comps (seq-uniq (mapcar (lambda (x) (substring-no-properties x)) comps))))) (defvar bufferlo--frameset-save-filter ; filter out vs. frameset-persistent-filter-alist '( @@ -1907,18 +2118,6 @@ advance of restoring bufferlo framesets." width )) -(defcustom bufferlo-frame-geometry-function #'bufferlo-frame-geometry-default - "Function to produce a bufferlo-frame-geometry alist. -It defaults to `bufferlo-frame-geometry-default'. - -The function takes one parameter, FRAME, for which geometry is to -be ascertained. See `bufferlo-frame-geometry-default' for -the returned alist form. - -Replace this function with your own if the default produces -suboptimal results for your platform." - :type 'function) - (defun bufferlo-frame-geometry-default (frame) "Produce an alist for FRAME's geometry. The alist is of the form: @@ -1931,14 +2130,30 @@ The alist is of the form: (width . ,(frame-text-width frame)) (height .,(frame-text-height frame)))) -(defun bufferlo--bookmark-session-make (active-bookmark-names frameset) +(defvar bufferlo--active-sessions nil + "Global active bufferlo sessions. +This is an alist of the form: + ((session-name ('bufferlo--bookmark-names . name-list))).") + +(defun bufferlo--bookmark-session-make (active-bookmark-names tabsets frameset) "Make a bufferlo session bookmark. -FRAMESET is a filtered frame set with bufferlo properties -containing ACTIVE-BOOKMARK-NAMES to define the session." + +ACTIVE-BOOKMARK-NAMES defines the bookmarks for the stored +session. + +TABSETS is a list of tab bookmark names organized in sub-lists +representing logical container frames. + +FRAMESET is a bufferlo-filtered `frameset'." (let ((bookmark-record (bookmark-make-record-default t t 0))) ; (&optional no-file no-context posn) - (bookmark-prop-set bookmark-record 'bufferlo-bookmark-names (if (consp active-bookmark-names) active-bookmark-names (list active-bookmark-names))) - (bookmark-prop-set bookmark-record 'bufferlo-frameset (prin1-to-string frameset)) - (bookmark-prop-set bookmark-record 'handler #'bufferlo--bookmark-session-handler) + (bookmark-prop-set bookmark-record + 'bufferlo-bookmark-names (if (consp active-bookmark-names) active-bookmark-names (list active-bookmark-names))) + (bookmark-prop-set bookmark-record + 'bufferlo-tabsets (prin1-to-string tabsets)) + (bookmark-prop-set bookmark-record + 'bufferlo-frameset (prin1-to-string frameset)) + (bookmark-prop-set bookmark-record + 'handler #'bufferlo--bookmark-session-handler) bookmark-record)) (defun bufferlo--bookmark-session-handler (bookmark-record &optional no-message) @@ -1950,52 +2165,91 @@ the message after successfully restoring the bookmark." (bufferlo-bookmark-names (bookmark-prop-get bookmark-record 'bufferlo-bookmark-names)) (abm-names (mapcar #'car (bufferlo--active-bookmarks))) (active-bookmark-names (seq-intersection bufferlo-bookmark-names abm-names))) - (if (> (length active-bookmark-names) 0) - (message "Close or clear active bufferlo bookmarks: %s" active-bookmark-names) - (let* ((frameset-str (bookmark-prop-get bookmark-record 'bufferlo-frameset)) - (frameset)) - (if (not (readablep frameset-str)) - (message "Bufferlo session bookmark %s: unreadable frameset" bookmark-name) - (setq frameset (car (read-from-string frameset-str))) - (if (not (frameset-valid-p frameset)) - (message "Bufferlo session bookmark %s: invalid frameset" bookmark-name) - (when (ignore-errors - (with-temp-buffer - (let ((default-frame-alist) - (inhibit-redisplay t)) - (frameset-restore frameset - :filters - (when (memq bufferlo-frameset-restore-geometry '(bufferlo nil)) - (let ((filtered-alist (copy-tree frameset-persistent-filter-alist))) - (mapc (lambda (sym) (setf (alist-get sym filtered-alist) :never)) - (seq-union bufferlo--frameset-restore-filter bufferlo-frameset-restore-filter)) - filtered-alist)) - :reuse-frames nil - :force-display t - :force-onscreen (display-graphic-p) - :cleanup-frames nil))) - t) - (dolist (frame (frame-list)) - (with-selected-frame frame - (when (frame-parameter nil 'bufferlo--frame-to-restore) - (lower-frame) ; attempt to reduce visual flashing - (when-let (fbm-name (frame-parameter nil 'bufferlo-bookmark-frame-name)) - (let ((bufferlo-bookmark-frame-load-make-frame nil) - (bufferlo-bookmark-frame-duplicate-policy 'allow) - (bufferlo-bookmark-frame-load-policy 'replace-frame-adopt-loaded-bookmark) - (bufferlo--bookmark-handler-no-message t)) - (bookmark-jump fbm-name #'ignore)) - (when (and - (display-graphic-p frame) - (eq bufferlo-frameset-restore-geometry 'bufferlo)) - (let-alist (frame-parameter nil 'bufferlo--frame-geometry) - (set-frame-position nil .left .top) - (set-frame-size nil .width .height 'pixelwise))) - (set-frame-parameter nil 'bufferlo--frame-to-restore nil)) - (raise-frame)))))) - (unless (or no-message bufferlo--bookmark-handler-no-message) - (message "Restored bufferlo session bookmark %s %s" - bookmark-name bufferlo-bookmark-names))))))) + (if (assoc bookmark-name bufferlo--active-sessions) + (message "Bufferlo session %s is already active" bookmark-name) + (if (> (length active-bookmark-names) 0) + (message "Close or clear active bufferlo bookmarks: %s" active-bookmark-names) + (let* ((tabsets-str (bookmark-prop-get bookmark-record 'bufferlo-tabsets)) + (tabsets)) + (if (not (readablep tabsets-str)) + (message "Bufferlo session bookmark %s: unreadable tabsets" bookmark-name) + (setq tabsets (car (read-from-string tabsets-str))) + (let ((first-tab-frame t)) + (dolist (tab-group tabsets) + (when (or (not first-tab-frame) + (and first-tab-frame (not bufferlo-session-restore-tabs-reuse-init-frame))) + (with-temp-buffer + (select-frame (make-frame)))) + ;; (lower-frame) ; attempt to reduce visual flashing + (when-let ((fg (alist-get 'bufferlo--frame-geometry tab-group))) + (when (and + (display-graphic-p) + (memq bufferlo-session-restore-geometry-policy '(all tab-frames)) + (or (not first-tab-frame) + (and first-tab-frame (eq bufferlo-session-restore-tabs-reuse-init-frame 'reuse-reset-geometry)))) + (let-alist fg + (set-frame-position nil .left .top) + (set-frame-size nil .width .height 'pixelwise)))) + (when-let ((tbm-names (alist-get 'bufferlo--tbms tab-group))) + (let ((bufferlo-bookmark-tab-replace-policy 'replace) ; we handle making tabs in this loop + (tab-bar-new-tab-choice t) + (first-tab (or + (not first-tab-frame) + (and first-tab-frame (not bufferlo-session-restore-tabs-reuse-init-frame))))) + (dolist (tbm-name tbm-names) + (unless first-tab + (tab-bar-new-tab-to)) + (bufferlo--bookmark-jump tbm-name) + (setq first-tab nil)))) + (setq first-tab-frame nil)) + (raise-frame)))) + (let* ((frameset-str (bookmark-prop-get bookmark-record 'bufferlo-frameset)) + (frameset)) + (if (not (readablep frameset-str)) + (message "Bufferlo session bookmark %s: unreadable frameset" bookmark-name) + (setq frameset (car (read-from-string frameset-str))) + (if (and frameset (not (frameset-valid-p frameset))) + (message "Bufferlo session bookmark %s: invalid frameset" bookmark-name) + (when (ignore-errors + (with-temp-buffer + (let ((default-frame-alist) + (inhibit-redisplay t)) + (frameset-restore frameset + :filters + (when (memq bufferlo-frameset-restore-geometry '(bufferlo nil)) + (let ((filtered-alist (copy-tree frameset-persistent-filter-alist))) + (mapc (lambda (sym) (setf (alist-get sym filtered-alist) :never)) + (seq-union bufferlo--frameset-restore-filter bufferlo-frameset-restore-filter)) + filtered-alist)) + :reuse-frames nil + :force-display t + :force-onscreen (display-graphic-p) + :cleanup-frames nil))) + t) + (dolist (frame (frame-list)) + (with-selected-frame frame + (when (frame-parameter nil 'bufferlo--frame-to-restore) + ;; (lower-frame) ; attempt to reduce visual flashing + (when-let (fbm-name (frame-parameter nil 'bufferlo-bookmark-frame-name)) + (let ((bufferlo-bookmark-frame-load-make-frame nil) + (bufferlo-bookmark-frame-duplicate-policy 'allow) + (bufferlo-bookmark-frame-load-policy 'replace-frame-adopt-loaded-bookmark) + (bufferlo--bookmark-handler-no-message t)) + (bufferlo--bookmark-jump fbm-name)) + (when (and + (display-graphic-p frame) + (memq bufferlo-session-restore-geometry-policy '(all frames))) + (let-alist (frame-parameter nil 'bufferlo--frame-geometry) + (set-frame-position nil .left .top) + (set-frame-size nil .width .height 'pixelwise))) + (set-frame-parameter nil 'bufferlo--frame-to-restore nil)) + (raise-frame)))))) + (push + `(,bookmark-name (bufferlo-bookmark-names . ,bufferlo-bookmark-names)) + bufferlo--active-sessions) + (unless (or no-message bufferlo--bookmark-handler-no-message) + (message "Restored bufferlo session bookmark %s %s" + bookmark-name bufferlo-bookmark-names)))))))) (put #'bufferlo--bookmark-session-handler 'bookmark-handler-type "B-Sess") ; short name here as bookmark-bmenu-list hard codes width of 8 chars @@ -2004,64 +2258,84 @@ the message after successfully restoring the bookmark." Store the session in BOOKMARK-NAME for the named bookmarks in ACTIVE-BOOKMARK-NAMES represented in ACTIVE-BOOKMARKS. +Frame bookmarks are stored with their geometry for optional +restoration. + +Tab bookmarks are stored in groups associated with their current +frame. New frames will be created to hold tab bookmarks in the +same grouping. Order may not be preserved. Tab frame geometry is +stored for optional restoration. + If NO-OVERWRITE is non-nil, record the new bookmark without throwing away the old one. NO-MESSAGE inhibits the save status message." (let* ((abms (seq-filter (lambda (x) (member (car x) active-bookmark-names)) active-bookmarks)) - (abm-frames (mapcar (lambda (x) (alist-get 'frame (cadr x))) abms))) + (tbms (seq-filter + (lambda (x) (eq (alist-get 'type (cadr x)) 'tbm)) + abms)) + (tbm-frame-groups (seq-group-by + (lambda (x) (alist-get 'frame (cadr x))) + tbms)) + (fbms (seq-filter + (lambda (x) (eq (alist-get 'type (cadr x)) 'fbm)) + abms)) + (fbm-frames (mapcar (lambda (x) (alist-get 'frame (cadr x))) fbms))) (if (= (length abms) 0) - (message "Specify at least one active bufferlo frame bookmark") - ;; Set a flag we can use to identify restored frames (this is - ;; removed in the handler during frame restoration). Save - ;; frame geometries for more accurate restoration than - ;; frameset-restore provides. - (dolist (frame abm-frames) - (set-frame-parameter frame 'bufferlo--frame-to-restore t) - (set-frame-parameter frame 'bufferlo--frame-geometry (funcall bufferlo-frame-geometry-function frame))) - (let ((frameset (frameset-save - abm-frames - :app 'bufferlo - :name bookmark-name - :predicate (lambda (x) (not (frame-parameter x 'parent-frame))) - :filters - (let ((filtered-alist (copy-tree frameset-persistent-filter-alist))) - (mapc (lambda (sym) (setf (alist-get sym filtered-alist) :never)) - (seq-union bufferlo--frameset-save-filter bufferlo-frameset-save-filter)) - filtered-alist)))) + (message "Specify at least one active bufferlo bookmark") + (let ((tabsets) + (frameset)) + (dolist (group tbm-frame-groups) + (let ((tbm-frame (car group)) + (tbm-names (mapcar #'car (cdr group)))) + (push `((bufferlo--frame-geometry . ,(funcall bufferlo-frame-geometry-function tbm-frame)) + (bufferlo--tbms . ,tbm-names)) + tabsets))) + (when fbm-frames + ;; Set a flag we can use to identify restored frames (this is + ;; removed in the handler during frame restoration). Save + ;; frame geometries for more accurate restoration than + ;; frameset-restore provides. + (dolist (frame fbm-frames) + (set-frame-parameter frame 'bufferlo--frame-to-restore t) + (set-frame-parameter frame 'bufferlo--frame-geometry (funcall bufferlo-frame-geometry-function frame))) + (setq frameset + (frameset-save + fbm-frames + :app 'bufferlo + :name bookmark-name + :predicate (lambda (x) (not (frame-parameter x 'parent-frame))) + :filters + (let ((filtered-alist (copy-tree frameset-persistent-filter-alist))) + (mapc (lambda (sym) (setf (alist-get sym filtered-alist) :never)) + (seq-union bufferlo--frameset-save-filter bufferlo-frameset-save-filter)) + filtered-alist)))) (bookmark-store bookmark-name (bufferlo--bookmark-set-location - (bufferlo--bookmark-session-make active-bookmark-names frameset)) + (bufferlo--bookmark-session-make active-bookmark-names tabsets frameset)) no-overwrite))))) (defun bufferlo-session-save-interactive (bookmark-name &optional no-overwrite) "Save a bufferlo session bookmark for the specified active bookmarks. The session will be stored under BOOKMARK-NAME. -Only frame bookmarks can be associated with a session. + +Tab bookmarks are grouped based on their shared frame along with +the frame's geometry. + +Frame bookmarks represent themselves. + If NO-OVERWRITE is non-nil, record the new bookmark without throwing away the old one." (interactive (list (completing-read - "Save bufferlo session bookmark: " + "Save bufferlo session bookmark as: " (bufferlo--bookmark-get-names #'bufferlo--bookmark-session-handler) nil nil nil 'bufferlo-bookmark-session-history nil))) (bufferlo--warn) - (let* ((abms (bufferlo--active-bookmarks nil 'fbm)) + (let* ((abms (bufferlo--active-bookmarks)) (abm-names (mapcar #'car abms)) - (comps - (completion-all-completions - (completing-read (format "Add bookmark(s) to %s: " bookmark-name) - (lambda (str pred flag) - (pcase flag - ('metadata - '(metadata (category . bookmark))) - (_ - (all-completions str abm-names pred))))) - abm-names nil nil)) - (base-size (cdr (last comps)))) - (when base-size (setcdr (last comps) nil)) - (setq comps (seq-uniq (mapcar (lambda (x) (substring-no-properties x)) comps))) + (comps (bufferlo--bookmark-completing-read (format "Add bookmark(s) to %s: " bookmark-name) abm-names))) (bufferlo--session-save bookmark-name comps abms no-overwrite))) (defun bufferlo-session-load-interactive () @@ -2070,24 +2344,40 @@ throwing away the old one." (let ((current-prefix-arg '(64))) ; emulate C-u C-u C-u (call-interactively 'bufferlo-bookmarks-load-interactive))) -(defun bufferlo-session-close-interactive () - "TODO: WIP: need to decide a few things. - -Should we consider a session bookmark an active entity or just -leave it as an inactive frameset/bookmark container? - -If active, we could keep track of which sessions are \"open\" in -a global. We could enforce a single session at a time or more -than one. The global would contain the session bookmark name and -the constituent bookmarks. - -There is no infrastructure to track if the constituent bookmarks -are themselves still open but could be done since we know all -active bufferlo bookmarks. +(defun bufferlo--session-clear (names) + "Clear active session NAMES." + (mapc (lambda (x) + (setq bufferlo--active-sessions + (assoc-delete-all x bufferlo--active-sessions))) + names)) + +(defun bufferlo-session-clear-interactive () + "Clear the specified sessions. +This does not close its associated bookmarks or kill their +buffers." + (interactive) + (let* ((candidates (mapcar #'car bufferlo--active-sessions)) + (comps (bufferlo--bookmark-completing-read "Select sessions to clear: " candidates))) + (bufferlo--session-clear comps))) -Same with renaming. A bufferlo bookmark associated with a session -does not rename embedded session bookmark names." - ) +(defun bufferlo-session-close-interactive () + "Close the specified sessions. +This closes their associated bookmarks and kills their buffers." + (interactive) + (let* ((candidates (mapcar #'car bufferlo--active-sessions)) + (comps (bufferlo--bookmark-completing-read "Select sessions to close/kill: " candidates))) + (let* ((abms (bufferlo--active-bookmarks)) + (abm-names (mapcar #'car abms)) + (abm-names-to-close)) + (dolist (sess-name comps) + (setq abm-names-to-close + (append abm-names-to-close + (seq-intersection + (alist-get 'bufferlo-bookmark-names (assoc sess-name bufferlo--active-sessions)) + abm-names)))) + (setq abm-names-to-close (seq-uniq abm-names-to-close)) + (bufferlo--close-active-bookmarks abm-names-to-close abms) + (bufferlo--session-clear comps)))) (defvar bufferlo--bookmark-handlers (list @@ -2122,7 +2412,12 @@ This function persists the current tab's state: The resulting bookmark stores the window configuration and the local buffer list of the current tab. In addition, it saves the bookmark state (not the contents) of the bookmarkable buffers in the tab's local -buffer list." +buffer list. + +Use `bufferlo-bookmark-tab-in-bookmarked-frame-policy' to +influence how this function handles setting a tab bookmark in the +presence of a frame bookmark. Using both together is allowed, but +is not recommended." (interactive (list (completing-read "Save bufferlo tab bookmark: " @@ -2130,12 +2425,60 @@ buffer list." nil nil nil 'bufferlo-bookmark-tab-history (alist-get 'bufferlo-bookmark-tab-name (bufferlo--current-tab))))) (bufferlo--warn) - (bookmark-store name (bufferlo--bookmark-set-location (bufferlo--bookmark-tab-make)) no-overwrite) - (setf (alist-get 'bufferlo-bookmark-tab-name - (cdr (bufferlo--current-tab))) - name) - (unless no-message - (message "Saved bufferlo tab bookmark: %s" name))) + (catch :nosave + (let ((msg)) + (when-let ((abm (assoc name (bufferlo--active-bookmarks))) + (duplicate-policy bufferlo-bookmark-tab-duplicate-policy)) + (when (eq duplicate-policy 'prompt) + (pcase (let ((read-answer-short t)) + (with-local-quit + (read-answer "Tab bookmark name already active: Allow, Clear existing, Raise existing, Quit " + '(("allow" ?a "Allow duplicate") + ("clear" ?c "Clear the active matching tab bookmarks, preferring new") + ("raise" ?r "Raise the existing tab bookmark") + ("help" ?h "Help") + ("quit" ?q "Quit with no changes"))))) + ("allow" (setq duplicate-policy 'allow)) + ("clear" (setq duplicate-policy 'clear)) + ("raise" (setq duplicate-policy 'raise)) + (_ (throw :nosave t)))) + (pcase duplicate-policy + ('allow) + ('clear + (bufferlo--clear-tab-bookmarks-by-name name)) + ('clear-warn + (bufferlo--clear-tab-bookmarks-by-name name) + (setq msg (concat msg "; cleared duplicate active tab bookmarks"))) + ('raise + (bufferlo--bookmark-raise abm) + (throw :nosave t)))) + (when (frame-parameter nil 'bufferlo-bookmark-frame-name) + (let ((clear-policy bufferlo-bookmark-tab-in-bookmarked-frame-policy)) + (when (eq clear-policy 'prompt) + (pcase (let ((read-answer-short t)) + (with-local-quit + (read-answer "Frame already bookmarked: Allow tab bookmark, Clear frame bookmark, Quit to cancel " + '(("allow" ?a "Allow tab bookmark, retain frame bookmark") + ("clear" ?c "Clear frame bookmark, set tab bookmark") + ("help" ?h "Help") + ("quit" ?q "Quit--retain the frame bookmark"))))) + ("allow" (setq clear-policy 'allow)) + ("clear" (setq clear-policy 'clear)) + (_ (setq clear-policy nil)))) ; quit case + (pcase clear-policy + ('allow) + ('clear + (set-frame-parameter nil 'bufferlo-bookmark-frame-name nil)) + ('clear-warn + (set-frame-parameter nil 'bufferlo-bookmark-frame-name nil) + (setq msg (concat msg "; cleared frame bookmark"))) + (_ )))) + (bookmark-store name (bufferlo--bookmark-set-location (bufferlo--bookmark-tab-make)) no-overwrite) + (setf (alist-get 'bufferlo-bookmark-tab-name + (cdr (bufferlo--current-tab))) + name) + (unless no-message + (message "Saved bufferlo tab bookmark: %s%s" name (if msg msg "")))))) (defun bufferlo-bookmark-tab-load (name) "Load a tab bookmark. @@ -2152,8 +2495,7 @@ Specify a prefix argument to force reusing the current tab." nil nil nil 'bufferlo-bookmark-tab-history (alist-get 'bufferlo-bookmark-tab-name (bufferlo--current-tab))))) (bufferlo--warn) - (let ((bookmark-fringe-mark nil)) - (bookmark-jump name #'ignore))) + (bufferlo--bookmark-jump name)) (defun bufferlo-bookmark-tab-save-current () "Save the current tab to its associated bookmark. @@ -2186,15 +2528,35 @@ This reuses the current tab even if (bufferlo-bookmark-tab-load bm)) (call-interactively #'bufferlo-bookmark-tab-load))) +(defun bufferlo--clear-tab-bookmarks-by-name (bookmark-name) + "Clear BOOKMARK-NAME frame bookmarks across all frames and their tabs." + (dolist (frame (frame-list)) + (dolist (tab (funcall tab-bar-tabs-function frame)) + (when-let ((tbm (alist-get 'bufferlo-bookmark-tab-name tab))) + (when (equal tbm bookmark-name) + (assq-delete-all 'bufferlo-bookmark-tab-name tab)))))) + +(defun bufferlo--clear-frame-bookmarks-by-name (bookmark-name) + "Clear BOOKMARK-NAME frame bookmarks across all frames." + (dolist (frame (frame-list)) + (when (equal bookmark-name (frame-parameter frame 'bufferlo-bookmark-frame-name)) + (set-frame-parameter frame 'bufferlo-bookmark-frame-name nil)))) + (defun bufferlo-bookmark-frame-save (name &optional no-overwrite no-message) "Save the current frame as a bookmark. NAME is the bookmark's name. If NO-OVERWRITE is non-nil, record the new bookmark without throwing away the old one. If NO-MESSAGE is non-nil, inhibit the save status message. - This function persists the current frame's state (the \"session\"): + +This function persists the current frame's state (the \"session\"): The resulting bookmark stores the window configurations and the local buffer lists of all tabs in the frame. In addition, it saves the bookmark -state (not the contents) of the bookmarkable buffers for each tab." +state (not the contents) of the bookmarkable buffers for each tab. + +Use `bufferlo-bookmark-tab-in-bookmarked-frame-policy' to +influence how this function handles setting a frame bookmark in +the presence of bookmarked tabs. Using both together is allowed, +but is not recommended." (interactive (list (completing-read "Save bufferlo frame bookmark: " @@ -2202,10 +2564,59 @@ state (not the contents) of the bookmarkable buffers for each tab." nil nil nil 'bufferlo-bookmark-frame-history (frame-parameter nil 'bufferlo-bookmark-frame-name)))) (bufferlo--warn) - (bookmark-store name (bufferlo--bookmark-set-location (bufferlo--bookmark-frame-make)) no-overwrite) - (set-frame-parameter nil 'bufferlo-bookmark-frame-name name) - (unless no-message - (message "Saved bufferlo frame bookmark: %s" name))) + (catch :nosave + (let ((msg)) + (when-let ((abm (assoc name (bufferlo--active-bookmarks))) + (duplicate-policy bufferlo-bookmark-frame-duplicate-policy)) + (when (eq duplicate-policy 'prompt) + (pcase (let ((read-answer-short t)) + (with-local-quit + (read-answer "Frame bookmark name already active: Allow, Clear existing, Raise existing, Quit " + '(("allow" ?a "Allow duplicate") + ("clear" ?c "Clear the active matching frame bookmarks, preferring new") + ("raise" ?r "Raise the existing frame bookmark") + ("help" ?h "Help") + ("quit" ?q "Quit with no changes"))))) + ("allow" (setq duplicate-policy 'allow)) + ("clear" (setq duplicate-policy 'clear)) + ("raise" (setq duplicate-policy 'raise)) + (_ (throw :nosave t)))) + (pcase duplicate-policy + ('allow) + ('clear + (bufferlo--clear-frame-bookmarks-by-name name)) + ('clear-warn + (bufferlo--clear-frame-bookmarks-by-name name) + (setq msg (concat msg "; cleared duplicate active frame bookmarks"))) + ('raise + (bufferlo--bookmark-raise abm) + (throw :nosave t)))) + (when (> (length (bufferlo--active-bookmarks (list (selected-frame)) 'tbm)) 0) + (let ((clear-policy bufferlo-bookmark-tab-in-bookmarked-frame-policy)) + (when (eq clear-policy 'prompt) + (pcase (let ((read-answer-short t)) + (with-local-quit + (read-answer "Tabs in this frame have bookmarks: Allow tab bookmarks, Clear tab bookmarks " + '(("allow" ?a "Allow tab bookmarks") + ("clear" ?c "Clear tab bookmarks") + ("help" ?h "Help") + ("quit" ?q "Quit"))))) + ("allow" (setq clear-policy 'allow)) + ("clear" (setq clear-policy 'clear)) + (_ (throw :nosave t)))) + (pcase clear-policy + ('clear + (let ((current-prefix-arg '(4))) ; emulate C-u + (bufferlo-clear-active-bookmarks (list (selected-frame))))) + ('clear-warn + (let ((current-prefix-arg '(4))) ; emulate C-u + (bufferlo-clear-active-bookmarks (list (selected-frame)))) + (setq msg (concat msg "; cleared tab bookmarks"))) + ('allow)))) + (bookmark-store name (bufferlo--bookmark-set-location (bufferlo--bookmark-frame-make)) no-overwrite) + (set-frame-parameter nil 'bufferlo-bookmark-frame-name name) + (unless no-message + (message "Saved bufferlo frame bookmark: %s%s" name (if msg msg "")))))) (defun bufferlo-bookmark-frame-load (name) "Load a frame bookmark. @@ -2219,8 +2630,7 @@ Replace the current frame's state if nil nil nil 'bufferlo-bookmark-frame-history (frame-parameter nil 'bufferlo-bookmark-frame-name)))) (bufferlo--warn) - (let ((bookmark-fringe-mark nil)) - (bookmark-jump name #'ignore))) + (bufferlo--bookmark-jump name)) (defun bufferlo-bookmark-frame-save-current () "Save the current frame to its associated bookmark. @@ -2463,19 +2873,7 @@ current or new frame according to (interactive) (let* ((abms (bufferlo--active-bookmarks)) (abm-names (mapcar #'car abms)) - (comps - (completion-all-completions - (completing-read "Close bookmark(s) without saving: " - (lambda (str pred flag) - (pcase flag - ('metadata - '(metadata (category . bookmark))) - (_ - (all-completions str abm-names pred))))) - abm-names nil nil)) - (base-size (cdr (last comps)))) - (when base-size (setcdr (last comps) nil)) - (setq comps (seq-uniq (mapcar (lambda (x) (substring-no-properties x)) comps))) + (comps (bufferlo--bookmark-completing-read "Close bookmark(s) without saving: " abm-names))) (bufferlo--close-active-bookmarks comps abms))) (defun bufferlo-bookmarks-save-interactive () @@ -2483,19 +2881,7 @@ current or new frame according to (interactive) (let* ((abms (bufferlo--active-bookmarks)) (abm-names (mapcar #'car abms)) - (comps - (completion-all-completions - (completing-read "Save bookmark(s): " - (lambda (str pred flag) - (pcase flag - ('metadata - '(metadata (category . bookmark))) - (_ - (all-completions str abm-names pred))))) - abm-names nil nil)) - (base-size (cdr (last comps)))) - (when base-size (setcdr (last comps) nil)) - (setq comps (seq-uniq (mapcar (lambda (x) (substring-no-properties x)) comps))) + (comps (bufferlo--bookmark-completing-read "Save bookmark(s): " abm-names))) (bufferlo--bookmarks-save comps abms))) (defun bufferlo-bookmarks-load-interactive () @@ -2511,21 +2897,9 @@ bookmarks, double for bookmarks, triple for session bookmarks." ((and (consp current-prefix-arg) (eq (prefix-numeric-value current-prefix-arg) 16)) (list #'bufferlo--bookmark-tab-handler)) ((and (consp current-prefix-arg) (eq (prefix-numeric-value current-prefix-arg) 64)) (list #'bufferlo--bookmark-session-handler)) (t bufferlo--bookmark-handlers)))) - (comps - (completion-all-completions - (completing-read "Load bookmark(s): " - (lambda (str pred flag) - (pcase flag - ('metadata - '(metadata (category . bookmark))) - (_ - (all-completions str bookmark-names pred))))) - bookmark-names nil nil)) - (base-size (cdr (last comps)))) - (when base-size (setcdr (last comps) nil)) - (setq comps (seq-uniq (mapcar (lambda (x) (substring-no-properties x)) comps))) + (comps (bufferlo--bookmark-completing-read "Load bookmark(s): " bookmark-names))) (dolist (bookmark-name comps) - (bookmark-jump bookmark-name #'ignore)))) + (bufferlo--bookmark-jump bookmark-name)))) (defun bufferlo-maybe-clear-active-bookmark (&optional force) "Clear the current frame and/or tab bufferlo bookmark. @@ -2551,7 +2925,7 @@ Specify a prefix argument to imply FORCE." (cdr (bufferlo--current-tab))) nil)))) -(defun bufferlo-clear-active-bookmarks () +(defun bufferlo-clear-active-bookmarks (&optional frames) "Clear all active bufferlo frame and tab bookmarks. This leaves all content untouched and does not impact stored bookmarks. @@ -2562,11 +2936,14 @@ This is useful when you have accumulated a complex working set of frames, tabs, buffers and want to save new bookmarks without disturbing existing bookmarks, or where auto-saving is enabled and you want to avoid overwriting stored bookmarks, perhaps with -transient work." +transient work. + +FRAMES is an optional list of frames on which to clear bookmarks +which defaults to all frames, if not specified." (interactive) (when (or (consp current-prefix-arg) - (y-or-n-p "Clear all active bufferlo bookmarks? ")) - (dolist (frame (frame-list)) + (y-or-n-p "Clear active bufferlo bookmarks? ")) + (dolist (frame (or frames (frame-list))) (set-frame-parameter frame 'bufferlo-bookmark-frame-name nil) (dolist (tab (funcall tab-bar-tabs-function frame)) (setf (alist-get 'bufferlo-bookmark-tab-name tab) nil))))) @@ -2586,16 +2963,22 @@ transient work." (orig-tab-name (alist-get 'name (bufferlo--current-tab)))) ; can't rely on index, it might disappear (dolist (abm tbms) (let ((abm-frame (alist-get 'frame (cadr abm))) + (orig-frame (selected-frame)) (abm-tab-number (alist-get 'tab-number (cadr abm)))) (with-selected-frame abm-frame + (raise-frame) ; if called in a batch, raise frame in case of prompts for buffers that need saving (tab-bar-select-tab abm-tab-number) - (let ((bufferlo-close-tab-kill-buffers-save-bookmark-prompt nil) + (let ((bufferlo-kill-buffers-prompt nil) + (bufferlo-close-tab-kill-buffers-save-bookmark-prompt nil) (bufferlo-close-tab-kill-buffers-prompt nil)) - (bufferlo-tab-close-kill-buffers))))) + (bufferlo-tab-close-kill-buffers))) + (when (frame-live-p orig-frame) + (raise-frame orig-frame)))) (dolist (abm fbms) (let ((abm-frame (alist-get 'frame (cadr abm)))) (with-selected-frame abm-frame - (let ((bufferlo-delete-frame-kill-buffers-save-bookmark-prompt nil) + (let ((bufferlo-kill-buffers-prompt nil) + (bufferlo-delete-frame-kill-buffers-save-bookmark-prompt nil) (bufferlo-delete-frame-kill-buffers-prompt nil)) (bufferlo-delete-frame-kill-buffers))))) ;; Frame and/or tab could now be gone. @@ -2646,6 +3029,12 @@ A prefix argument inhibits the prompt and bypasses saving." (tab-bar-select-tab (alist-get 'tab-number (cadr abm))))))) +(defun bufferlo--bookmark-raise-by-name (abm-name &optional abms) + "Raise bookmark's frame/tab by ABM-NAME in ABMS." + (setq abms (or abms (bufferlo--active-bookmarks))) + (when-let ((abm (assoc abm-name abms))) + (bufferlo--bookmark-raise abm))) + (defun bufferlo-bookmark-raise () "Raise the selected bookmarked frame or tab. Note: If there are duplicated bookmarks, the first one found is @@ -2653,23 +3042,67 @@ raised." (interactive) (let* ((abms (bufferlo--active-bookmarks)) (abm-names (mapcar #'car abms)) - (comps - (completion-all-completions - (completing-read "Select a bookmark to raise: " - (lambda (str pred flag) - (pcase flag - ('metadata - '(metadata (category . bookmark))) - (_ - (all-completions str abm-names pred))))) - abm-names nil nil)) - (base-size (cdr (last comps)))) - (when base-size (setcdr (last comps) nil)) - (setq comps (seq-uniq (mapcar (lambda (x) (substring-no-properties x)) comps))) + (comps (bufferlo--bookmark-completing-read "Select a bookmark to raise: " abm-names))) (if (not (= (length comps) 1)) (message "Please select a single bookmark to raise") - (when-let* ((abm (assoc (car comps) abms))) - (bufferlo--bookmark-raise abm))))) + (bufferlo--bookmark-raise-by-name (car comps) abms)))) + +;; DWIM convenience functions + +(defun bufferlo-bookmark-save-curr () + "DWIM save current bufferlo bookmark. +Save the current bufferlo frame bookmark or tab bookmark, +prioritizing frame bookmarks over tab bookmarks, should both +exist. + +Unlike, `bufferlo-bookmark-frame-save-current' and +`bufferlo-bookmark-tab-save-current', this does not prompt to +save a new bookmark." + (interactive) + (bufferlo--warn) + (if-let (bm (frame-parameter nil 'bufferlo-bookmark-frame-name)) + (bufferlo-bookmark-frame-save bm) + (if-let (bm (alist-get 'bufferlo-bookmark-tab-name + (cdr (bufferlo--current-tab)))) + (bufferlo-bookmark-tab-save bm) + (message "No active bufferlo frame or tab bookmark to save.")))) + +(defun bufferlo-bookmark-load-curr () + "DWIM reload current bufferlo bookmark. +Load the current bufferlo frame bookmark or tab bookmark, +prioritizing frame bookmarks over tab bookmarks, should both +exist. + +Unlike, `bufferlo-bookmark-frame-load-current' and +`bufferlo-bookmark-tab-load-current', this does not prompt to +load a new bookmark." + (interactive) + (bufferlo--warn) + (if-let (bm (frame-parameter nil 'bufferlo-bookmark-frame-name)) + (let ((bufferlo-bookmark-frame-load-make-frame nil) ; reload reuses the current frame + (bufferlo-bookmark-frame-load-policy 'replace-frame-retain-current-bookmark) + (bufferlo-bookmark-frame-duplicate-policy 'allow)) ; not technically a duplicate + (bufferlo-bookmark-frame-load bm)) + (if-let (bm (alist-get 'bufferlo-bookmark-tab-name + (cdr (bufferlo--current-tab)))) + (let ((bufferlo-bookmark-tab-replace-policy 'replace) ; reload reuses current tab + (bufferlo-bookmark-tab-duplicate-policy 'allow)) ; not technically a duplicate + (bufferlo-bookmark-tab-load bm)) + (message "No active bufferlo frame or tab bookmark to load.")))) + +(defun bufferlo-bookmark-close-curr () + "DWIM close current bufferlo bookmark and kill its buffers. +Close the current bufferlo frame bookmark or tab bookmark, +prioritizing frame bookmarks over tab bookmarks, should both +exist." + (interactive) + (bufferlo--warn) + (if-let (bm (frame-parameter nil 'bufferlo-bookmark-frame-name)) + (bufferlo-delete-frame-kill-buffers) + (if-let (bm (alist-get 'bufferlo-bookmark-tab-name + (cdr (bufferlo--current-tab)))) + (bufferlo-tab-close-kill-buffers) + (message "No active bufferlo frame or tab bookmark to close.")))) ;;; bookmark advisories @@ -2713,18 +3146,24 @@ OLDFN BOOKMARK-NAME BATCH" (defalias 'bufferlo-bms-save 'bufferlo-bookmarks-save-interactive) (defalias 'bufferlo-bms-close 'bufferlo-bookmarks-close-interactive) (defalias 'bufferlo-bm-raise 'bufferlo-bookmark-raise) +(defalias 'bufferlo-bm-save 'bufferlo-bookmark-save-curr) +(defalias 'bufferlo-bm-load 'bufferlo-bookmark-load-curr) +(defalias 'bufferlo-bm-close 'bufferlo-bookmark-close-curr) (defalias 'bufferlo-bm-tab-save 'bufferlo-bookmark-tab-save) (defalias 'bufferlo-bm-tab-save-curr 'bufferlo-bookmark-tab-save-current) (defalias 'bufferlo-bm-tab-load 'bufferlo-bookmark-tab-load) (defalias 'bufferlo-bm-tab-load-curr 'bufferlo-bookmark-tab-load-current) +(defalias 'bufferlo-bm-tab-close-curr 'bufferlo-tab-close-kill-buffers) (defalias 'bufferlo-bm-frame-save 'bufferlo-bookmark-frame-save) (defalias 'bufferlo-bm-frame-save-curr 'bufferlo-bookmark-frame-save-current) (defalias 'bufferlo-bm-frame-load 'bufferlo-bookmark-frame-load) (defalias 'bufferlo-bm-frame-load-curr 'bufferlo-bookmark-frame-load-current) (defalias 'bufferlo-bm-frame-load-merge 'bufferlo-bookmark-frame-load-merge) +(defalias 'bufferlo-bm-frame-close-curr 'bufferlo-delete-frame-kill-buffers) (defalias 'bufferlo-sess-save 'bufferlo-session-save-interactive) (defalias 'bufferlo-sess-load 'bufferlo-session-load-interactive) (defalias 'bufferlo-sess-close 'bufferlo-session-close-interactive) +(defalias 'bufferlo-sess-clear 'bufferlo-session-clear-interactive) (provide 'bufferlo)