branch: externals/bufferlo
commit 362de728ab661c7921bd2d6669e77eef749363a1
Author: shipmints <shipmi...@gmail.com>
Commit: shipmints <shipmi...@gmail.com>

    WIP.
---
 bufferlo.el | 289 ++++++++++++++++++++++++++++++++++++++++++------------------
 1 file changed, 203 insertions(+), 86 deletions(-)

diff --git a/bufferlo.el b/bufferlo.el
index 6623a7f7f0..7fdceb07fe 100644
--- a/bufferlo.el
+++ b/bufferlo.el
@@ -98,7 +98,7 @@ Matching buffers are hidden even if displayed in the current 
frame or tab."
 This is a list of regular expressions that match buffer names."
   :type '(repeat string))
 
-(defcustom bufferlo-bookmark-buffers-exclude-filters ; WIP: +++
+(defcustom bufferlo-bookmark-buffers-exclude-filters ; TODO: +++
   (list
    (rx "*Messages*")
    (rx "*scratch*")
@@ -115,6 +115,7 @@ This is a list of regular expressions that match buffer 
names."
    (rx "*helpful*")
    (rx "*helpful " (1+ anything) "*")
    (rx "*which-key*")
+   (rx "*timer-list*")
    (rx "*cvs*")
    (rx "*esh command on file*"))
   "Buffers that should be excluded in Bufferlo bookmarks.
@@ -134,31 +135,71 @@ and its buffers."
   "If non-nil, confirm before deleting the frame and killing its buffers."
   :type 'boolean)
 
-(defcustom bufferlo-bookmarks-save-frame-policy 'all
-  "Bufferlo auto save bookmarks frame policy. Can be 'current to
-save bookmarks on the current frame only, 'other to save
-bookmarks on non-current frames, or 'all to save bookmarks across
+(defcustom bufferlo-bookmark-frame-load-policy 'prompt
+  "Behavior when a frame bookmark is loaded into an
+already-bookmarked frame. \\='prompt asks you to pick a policy.
+\\='disallow prevents accidental overlays on existing bookmarked
+frames, with the exception that a bookmarked frame may be
+reloaded to restore its state. \\='current replaces the frame
+content using the existing frame bookmark name. \\='replace replaces
+the new content and adopts the new bookmark name. \\='merge adds the
+new tabs to the existing frame retaining the existing bookmark
+name. This policy is d useful when
+\\=`bufferlo-bookmark-frame-load-make-frame\\=' is not enabled or frame
+loading is not overridden with a prefix argument that suppresses
+making a new frame."
+  :type '(radio (const :tag "Prompt" prompt)
+                (const :tag "Disallow" disallow)
+                (const :tag "Current bookmark name" current)
+                (const :tag "Replace bookmark name" replace)
+                (const :tag "Merge" merge)))
+
+(defcustom bufferlo-bookmark-frame-duplicate-policy 'prompt
+  "Behavior controlling duplicate active frame bookmarks. One
+typically does not want to save the same bookmark with content
+that may differ among frames. \\='prompt asks you to pick a policy.
+\\='allow will allow duplicates. \\='raise will locate the frame with
+the existing bookmark and raise its frame."
+  :type '(radio (const :tag "Prompt" prompt)
+                (const :tag "Allow" allow)
+                (const :tag "Raise" raise)))
+
+(defcustom bufferlo-bookmark-tab-load-with-bookmarked-frame-policy 'clear-warn
+  "Behavior when a bookmarked tab is loaded into an
+already-bookmarked frame. \\='clear will silently clear the tab
+bookmark which is natural reified frame bookmark behavior.
+\\='clear-warn warns about the tab losing its bookmark. \\='allow will
+retain the tab bookmark to enable it to be saved or
+updated--note that tab will be added to the frame bookmark when
+that gets saved and the tab will lose its own bookmark
+association when the frame bookmark is loaded."
+  :type '(radio (const :tag "Clear (silently)" clear)
+                (const :tag "Clear (with message)" clear-warn)
+                (const :tag "Allow" allow)))
+
+(defcustom bufferlo-bookmarks-auto-save-frame-policy 'all
+  "Bufferlo auto save bookmarks frame policy. \\='current
+saves bookmarks on the current frame only. \\='other saves
+bookmarks on non-current frames. \\='all saves bookmarks across
 all frames."
   :type '(radio (const :tag "Current frame" current)
                 (const :tag "Other frames" other)
                 (const :tag "All frames" all)))
 
-(defcustom bufferlo-bookmarks-save-predicate-functions nil ; TODO: +++ set to 
#'bufferlo-bookmarks-save-p-default?
+(defcustom bufferlo-bookmarks-save-predicate-functions nil ; TODO: +++ set to 
#'bufferlo-bookmarks-save-all-p?
   "Functions to call for each active bufferlo bookmark to determine
 if the bookmark should be automatically saved by the auto-save
 timer. Functions are passed the bufferlo bookmark name and
 invoked until the first positive result."
   :type 'hook)
 
-(defcustom bufferlo-bookmarks-save-at-emacs-exit nil
-  "If non-nil, save bufferlo bookmarks when Emacs exits."
-  :type 'boolean)
-
-(defcustom bufferlo-bookmarks-save-at-emacs-exit-policy 'pred
-  "Bufferlo auto save bookmarks at Emacs exit policy. Set to 'all to
-save all active bufferlo bookmarks. Set to 'pred to honor the
-auto-save predicates in `bufferlo-bookmarks-save-predicate-functions'."
-  :type '(radio (const :tag "Filter bookmarks with predicates" pred)
+(defcustom bufferlo-bookmarks-save-at-emacs-exit 'nosave
+  "Bufferlo save bookmarks at Emacs exit policy. \\'=nosave does not
+save any bookmarks. \\='all saves all active bufferlo bookmarks.
+\\='pred honors auto-save predicates in
+\\=`bufferlo-bookmarks-save-predicate-functions\\='."
+  :type '(radio (const :tag "Do not save at exit" nosave)
+                (const :tag "Predicate-filtered bookmarks" pred)
                 (const :tag "All bookmarks" all)))
 
 (defcustom bufferlo-ibuffer-bind-local-buffer-filter t
@@ -233,28 +274,44 @@ frame bookmark is a collection of tab bookmarks."
 
 (defvar bufferlo--clear-buffer-lists-active nil)
 
-(defvar bufferlo--bookmarks-save-timer nil
-  "Timer to save bufferlo bookmarks on 
`bufferlo-bookmarks-save-idle-interval'.")
+(defvar bufferlo--bookmarks-auto-save-timer nil
+  "Timer to save bufferlo bookmarks on 
`bufferlo-bookmarks-auto-save-idle-interval'.")
 
-(defun bufferlo--bookmarks-save-timer-maybe-cancel ()
-  (when (timerp bufferlo--bookmarks-save-timer)
-    (cancel-timer bufferlo--bookmarks-save-timer))
-  (setq bufferlo--bookmarks-save-timer nil))
+(defun bufferlo--bookmarks-auto-save-timer-maybe-cancel ()
+  (when (timerp bufferlo--bookmarks-auto-save-timer)
+    (cancel-timer bufferlo--bookmarks-auto-save-timer))
+  (setq bufferlo--bookmarks-auto-save-timer nil))
 
-(defun bufferlo--bookmarks-save-timer-maybe-start ()
-  (bufferlo--bookmarks-save-timer-maybe-cancel)
-  (when (> bufferlo-bookmarks-save-idle-interval 0)
-    (setq bufferlo--bookmarks-save-timer
-          (run-with-idle-timer bufferlo-bookmarks-save-idle-interval t 
#'bufferlo-bookmarks-save))))
+(defvar bufferlo-bookmarks-auto-save-idle-interval) ; byte compiler
+(defun bufferlo--bookmarks-auto-save-timer-maybe-start ()
+  (bufferlo--bookmarks-auto-save-timer-maybe-cancel)
+  (when (> bufferlo-bookmarks-auto-save-idle-interval 0)
+    (setq bufferlo--bookmarks-auto-save-timer
+          (run-with-idle-timer bufferlo-bookmarks-auto-save-idle-interval t 
#'bufferlo-bookmarks-save))))
 
 ;; NOTE: must come after the above timer variable and function definitions
-(defcustom bufferlo-bookmarks-save-idle-interval 30
+(defcustom bufferlo-bookmarks-auto-save-idle-interval 0
   "Save bufferlo bookmarks when Emacs has been idle this many seconds.
 Set to 0 to disable timer."
   :type 'natnum
   :set (lambda (sym val)
-         (setq sym val)
-         (bufferlo--bookmarks-save-timer-maybe-start)))
+         (set-default sym val)
+         (bufferlo--bookmarks-auto-save-timer-maybe-start)))
+
+(defun bufferlo-mode-line-format () ; TODO: needs refinement
+  "Bufferlo mode-line format."
+  (when bufferlo-mode
+    (let ((fbm (frame-parameter nil 'bufferlo-bookmark-frame-name))
+          (tbm (alist-get 'bufferlo-bookmark-tab-name 
(tab-bar--current-tab-find))))
+      (concat " 🐃"
+              (if fbm (concat "f=" fbm))
+              "."
+              (if tbm (concat "t=" tbm))))))
+
+(defcustom bufferlo-mode-line-lighter '(:eval (bufferlo-mode-line-format))
+  "Bufferlo mode line definition."
+  :type 'sexp
+  :risky t)
 
 ;;;###autoload
 (define-minor-mode bufferlo-mode
@@ -262,7 +319,7 @@ Set to 0 to disable timer."
   :global t
   :require 'bufferlo
   :init-value nil
-  :lighter " 🐃"
+  :lighter bufferlo-mode-line-lighter
   :keymap nil
   (if bufferlo-mode
       (progn
@@ -290,9 +347,9 @@ Set to 0 to disable timer."
         (advice-add #'tab-bar-select-tab :around 
#'bufferlo--clear-buffer-lists-activate)
         (advice-add #'tab-bar--tab :after #'bufferlo--clear-buffer-lists)
         ;; Set up bookmarks save timer
-        (bufferlo--bookmarks-save-timer-maybe-start)
+        (bufferlo--bookmarks-auto-save-timer-maybe-start)
         ;; kill-emacs-hook save bookmarks option
-        (when bufferlo-bookmarks-save-at-emacs-exit
+        (when (not (eq bufferlo-bookmarks-save-at-emacs-exit 'nosave))
           (add-hook 'kill-emacs-hook 
#'bufferlo--bookmarks-save-at-emacs-exit)))
     ;; Prefer local buffers
     (dolist (frame (frame-list))
@@ -317,7 +374,7 @@ Set to 0 to disable timer."
     (advice-remove #'tab-bar-select-tab 
#'bufferlo--clear-buffer-lists-activate)
     (advice-remove #'tab-bar--tab #'bufferlo--clear-buffer-lists)
     ;; Cancel bookmarks save timer
-    (bufferlo--bookmarks-save-timer-maybe-cancel)
+    (bufferlo--bookmarks-auto-save-timer-maybe-cancel)
     ;; kill-emacs-hook save bookmarks option
     (remove-hook 'kill-emacs-hook #'bufferlo--bookmarks-save-at-emacs-exit)))
 
@@ -494,12 +551,11 @@ function.  WINDOW and WRITABLE are passed to the 
function."
           (append ws (list (list 'bufferlo-buffer-list names)))
         ws))))
 
-(defun bufferlo--window-state-put (state &optional window ignore)
+(defun bufferlo--window-state-put (state &optional window _ignore)
   "Restore the frame's buffer list from the window state.
 Used as advice after `window-state-put'.  STATE is the window state.
 WINDOW is the window in question.  IGNORE is not used and exists for
 compatibility with the adviced function."
-  (ignore ignore)
   ;; We have to make sure that the window is live at this point.
   ;; `frameset-restore' may pass a window with a non-existing buffer
   ;; to `window-state-put', which in turn will delete that window
@@ -1169,8 +1225,9 @@ The argument BOOKMARK is the to-be restored tab bookmark 
created via
 `bufferlo--bookmark-tab-get'.  The optional argument NO-MESSAGE inhibits
 the message after successfully restoring the bookmark."
   (let* ((ws (copy-tree (alist-get 'window bookmark)))
-         (dummy (generate-new-buffer " *bufferlo dummy buffer*")) ; TODO: 
needs unwind-protect if we error?
+         (dummy (generate-new-buffer " *bufferlo dummy buffer*")) ; TODO: 
needs unwind-protect?
          (bookmark-name (if (null is-fbm-tab) (bookmark-name-from-full-record 
bookmark) nil))
+         (msg)
          (renamed
           (mapcar
            (lambda (bm)
@@ -1198,18 +1255,26 @@ the message after successfully restoring the bookmark."
          (bl (mapcar #'get-buffer bl)))
     (kill-buffer dummy)
     (bufferlo--ws-replace-buffer-names ws renamed)
-    (window-state-put ws (frame-root-window))
+    (window-state-put ws (frame-root-window) 'safe)
     (set-frame-parameter nil 'buffer-list bl)
     (set-frame-parameter nil 'buried-buffer-list nil)
-    (message "bufferlo--bookmark-tab-handler: bookmark-name=%s" bookmark-name) 
; +++
-    (setf (alist-get 'bufferlo-bookmark-tab-name
-                     (cdr (bufferlo--current-tab)))
-          bookmark-name)
+    (if (frame-parameter nil 'bufferlo-bookmark-frame-name)
+        (pcase bufferlo-bookmark-tab-load-with-bookmarked-frame-policy
+          ('clear) ; do nothing
+          ('clear-warn
+           (setq msg (concat msg "; cleared tab bookmark")))
+          ('allow
+           (setf (alist-get 'bufferlo-bookmark-tab-name
+                            (cdr (bufferlo--current-tab)))
+                 bookmark-name)))
+      (setf (alist-get 'bufferlo-bookmark-tab-name
+                       (cdr (bufferlo--current-tab)))
+            bookmark-name))
     (unless no-message
-      (message "Restored bufferlo tab bookmark%s"
-               (if bookmark-name (format ": %s" bookmark-name) "")))))
+      (message "Restored bufferlo tab bookmark%s%s"
+               (if bookmark-name (format ": %s" bookmark-name) "") (if msg msg 
"")))))
 
-(put #'bufferlo--bookmark-tab-handler 'bookmark-handler-type "BflTab") ; short 
name here as bookmark-bmenu-list hard codes width of 8 chars
+(put #'bufferlo--bookmark-tab-handler 'bookmark-handler-type "B-Tab") ; short 
name here as bookmark-bmenu-list hard codes width of 8 chars
 
 (defun bufferlo--bookmark-frame-get (&optional name frame)
   "Get the bufferlo frame bookmark.
@@ -1238,34 +1303,84 @@ FRAME specifies the frame; the default value of nil 
selects the current frame."
 The argument BOOKMARK is the to-be restored frame bookmark created via
 `bufferlo--bookmark-frame-get'.  The optional argument NO-MESSAGE inhibits
 the message after successfully restoring the bookmark."
-  (let ((bookmark-name (bookmark-name-from-full-record bookmark)))
-    (when (and
-           bufferlo-bookmark-frame-load-make-frame
-           (not current-prefix-arg) ; user make-frame suppression
-           (not pop-up-frames)) ; make-frame implied by functions like 
`bookmark-jump-other-frame'
-      (make-frame))
-    (if (>= emacs-major-version 28)
-        (tab-bar-tabs-set nil)
-      (set-frame-parameter nil 'tabs nil))
-    (let ((first t)
-          (tab-bar-new-tab-choice t))
-      (mapc
-       (lambda (tbm)
-         (if first
-             (setq first nil)
-           (tab-bar-new-tab-to))
-         (bufferlo--bookmark-tab-handler tbm t 'is-fbm-tab)
-         (when-let (tab-name (alist-get 'tab-name tbm))
-           (tab-bar-rename-tab tab-name)))
-       (alist-get 'tabs bookmark)))
-    (tab-bar-select-tab (alist-get 'current bookmark))
-    (when bookmark-name
-      (set-frame-parameter nil 'bufferlo-bookmark-frame-name bookmark-name))
-    (unless no-message
-      (message "Restored bufferlo frame bookmark%s"
-               (if bookmark-name (format ": %s" bookmark-name) "")))))
-
-(put #'bufferlo--bookmark-frame-handler 'bookmark-handler-type "BflFrame") ; 
short name here as bookmark-bmenu-list hard codes width of 8 chars
+  (catch :noload
+    (let ((bookmark-name (bookmark-name-from-full-record bookmark))
+          (msg))
+      (when-let ((active-bookmark (assoc bookmark-name 
(bufferlo-active-bookmarks)))
+                 (duplicate-policy bufferlo-bookmark-frame-duplicate-policy))
+        (when (eq duplicate-policy 'prompt)
+          (pcase (let ((read-answer-short t))
+                   (read-answer "Frame bookmark already loaded "
+                                '(("allow" ?a "Allow duplicate")
+                                  ("raise" ?r "Raise the frame with the 
existing bookmark")
+                                  ("help" ?h "Help")
+                                  ("quit" ?q "Quit with no changes"))))
+            ("allow" (setq duplicate-policy 'allow))
+            ("raise" (setq duplicate-policy 'raise))
+            (_ (throw :noload t))))
+        (pcase duplicate-policy
+          ('allow)
+          ('raise
+           (raise-frame (alist-get 'frame active-bookmark))
+           (throw :noload t))))
+      (when (and
+             bufferlo-bookmark-frame-load-make-frame
+             (not current-prefix-arg) ; user make-frame suppression
+             (not pop-up-frames)) ; make-frame implied by functions like 
`bookmark-jump-other-frame'
+        (make-frame))
+      (let ((fbm (frame-parameter nil 'bufferlo-bookmark-frame-name))
+            (load-policy bufferlo-bookmark-frame-load-policy))
+        (if (not (null fbm))
+            (progn
+              (when (eq load-policy 'prompt)
+                (pcase (let ((read-answer-short t))
+                         (read-answer "Frame already bookmarked. Choose a 
bookmark for this frame: "
+                                      '(("current" ?c "Use the existing 
bookmark")
+                                        ("replace" ?r "Replace the bookmark 
with the selected bookmark")
+                                        ("merge" ?m "Merge the new tab content 
with the existing bookmark")
+                                        ("help" ?h "Help")
+                                        ("quit" ?q "Quit with no changes"))))
+                  ("current" (setq load-policy 'current))
+                  ("replace" (setq load-policy 'replace))
+                  ("merge" (setq load-policy 'merge))
+                  (_ (throw :noload t))))
+              (pcase load-policy
+                ('disallow
+                 (when (not (equal fbm bookmark-name)) ; allow reloads of 
existing bookmark
+                   (unless no-message (message "Frame already bookmarked as 
%s; %s not loaded." fbm bookmark-name))
+                   (throw :noload t)))
+                ('current
+                 (setq msg (concat msg (format "; merged with existing 
bookmark %s." fbm))))
+                ('replace
+                 (setq msg (concat msg (format "; replaced bookmark %s." fbm)))
+                 (setq fbm bookmark-name))
+                ('merge
+                 (setq msg (concat msg (format "; merged bookmark %s." 
bookmark-name))))))
+          (setq fbm bookmark-name)) ; not already bookmarked
+        (unless (eq load-policy 'merge)
+          (if (>= emacs-major-version 28)
+              (tab-bar-tabs-set nil)
+            (set-frame-parameter nil 'tabs nil)))
+        (let ((first (if (eq load-policy 'merge) nil t))
+              (tab-bar-new-tab-choice t))
+          (mapc
+           (lambda (tbm)
+             (if first
+                 (setq first nil)
+               (tab-bar-new-tab-to))
+             (bufferlo--bookmark-tab-handler tbm t 'is-fbm-tab)
+             (when-let (tab-name (alist-get 'tab-name tbm))
+               (tab-bar-rename-tab tab-name)))
+           (alist-get 'tabs bookmark)))
+        (tab-bar-select-tab (alist-get 'current bookmark))
+        (when fbm
+          (set-frame-parameter nil 'bufferlo-bookmark-frame-name fbm)))
+      (unless no-message
+        (message "Restored bufferlo frame bookmark%s%s"
+                 (if bookmark-name (format ": %s" bookmark-name) "")
+                 (if msg msg ""))))))
+
+(put #'bufferlo--bookmark-frame-handler 'bookmark-handler-type "B-Frame") ; 
short name here as bookmark-bmenu-list hard codes width of 8 chars
 
 (defun bufferlo--bookmark-get-names (&rest handlers)
   "Get the names of all existing bookmarks for HANDLERS."
@@ -1316,10 +1431,7 @@ NAME is the bookmark's name."
           nil nil nil 'bufferlo-bookmark-tab-history)))
   (bufferlo--warn)
   (let ((bookmark-fringe-mark nil))
-    (bookmark-jump name #'ignore))
-  (setf (alist-get 'bufferlo-bookmark-tab-name
-                   (cdr (bufferlo--current-tab)))
-        name))
+    (bookmark-jump name #'ignore)))
 
 (defun bufferlo-bookmark-tab-save-current ()
   "Save the current tab to its associated bookmark.
@@ -1418,24 +1530,29 @@ associated bookmark exists."
           (push (cons 'tbm bookmark-name) bookmarks))))
     bookmarks))
 
-(defun bufferlo-active-bookmarks (&optional frames)
+(defun bufferlo-active-bookmarks (&optional frames type)
+  "Produces an alist of the form
+ (bookmark-name . (('type . type) ('frame . frame) ('tab . tab)))
+for the specified FRAMES, filtered by TYPE"
   (let ((bookmarks))
     (dolist (frame (or frames (frame-list)))
       (when-let ((fbm (frame-parameter frame 'bufferlo-bookmark-frame-name)))
-        (push (cons 'fbm fbm) bookmarks))
+        (when (or (null type) (eq type 'fbm))
+          (push (cons fbm (list (cons 'type 'fbm) (cons 'frame frame))) 
bookmarks)))
       (dolist (tab (funcall tab-bar-tabs-function frame))
         (when-let ((tbm (alist-get 'bufferlo-bookmark-tab-name tab)))
-          (push (cons 'tbm tbm) bookmarks))))
+          (when (or (null type) (eq type 'tbm))
+            (push (cons tbm (list (cons 'type 'tbm) (cons 'frame frame) (cons 
'tab tab))) bookmarks)))))
     bookmarks))
 
-(defun bufferlo-bookmarks-save-p-default (_bookmark-name)
+(defun bufferlo-bookmarks-save-all-p (_bookmark-name)
   t)
 
 (defun bufferlo-bookmarks-save ()
   (let ((bookmarks-saved nil)
         (start-time (current-time)))
     (let ((bookmark-save-flag nil)
-          (frames (pcase bufferlo-bookmarks-save-frame-policy
+          (frames (pcase bufferlo-bookmarks-auto-save-frame-policy
                     ('current
                      (list (selected-frame)))
                     ('other
@@ -1443,8 +1560,8 @@ associated bookmark exists."
                     (_
                      (frame-list)))))
       (dolist (bookmark (bufferlo-active-bookmarks frames))
-        (let ((bookmark-type (car bookmark))
-              (bookmark-name (cdr bookmark)))
+        (let ((bookmark-name (car bookmark))
+              (bookmark-type (alist-get 'type bookmark)))
           (when (run-hook-with-args-until-success 
'bufferlo-bookmarks-save-predicate-functions bookmark-name)
             (when (eq bookmark-type 'fbm)
               ;; BUG: fbm's not yet enforced to be unique among frames, so we 
may save the same bookmark more than once
@@ -1461,10 +1578,10 @@ associated bookmark exists."
                (float-time (time-subtract (current-time) start-time))))))
 
 (defun bufferlo--bookmarks-save-at-emacs-exit ()
-  (bufferlo--bookmarks-save-timer-maybe-cancel)
+  (bufferlo--bookmarks-auto-save-timer-maybe-cancel)
   (let ((bufferlo-bookmarks-save-predicate-functions
-         (if (eq bufferlo-bookmarks-save-at-emacs-exit-policy 'all)
-             (list #'bufferlo-bookmarks-save-p-default)
+         (if (eq bufferlo-bookmarks-save-at-emacs-exit 'all)
+             (list #'bufferlo-bookmarks-save-all-p)
            bufferlo-bookmarks-save-predicate-functions)))
     (bufferlo-bookmarks-save)))
 

Reply via email to