maybe have each persp have its own save file, and when autosaving, save each persp? maybe have a function to delete a persp from the main autosave file? - prompt available perspectives from main autosave file, after selection, delete each from file.
Activities
:custom
;; only show tab bar if more than 3 activities open
(tab-bar-show . 3)
:init
(activities-mode)
(activities-tabs-mode)
;; prevent edebug default bindings from interfering
;; (setq edebug-inhibit-emacs-lisp-mode-bindings t)
;; create map
(defun +activities-define-existing ()
(interactive)
(let ((current-prefix-arg '(4)))
(call-interactively #'activities-define)))
(defvar activities-mode-map
(let ((map (make-sparse-keymap)))
;; Define keys in bulk
(dolist
(binding
'(;; create new activity name.
("N" . activities-new)
;; define new activity's default with current frame state.
;; (prefix) define pre-existing activity' default with current frame state.
("d" . +activities-define-existing)
;; resume suspended activity
;; (prefix) resume activity with default state.
("." . +activities-resume-custom)
("s" . +activities-resume-custom)
;; save and close activity.
;; ("k" . activities-suspend)
;; reset to default state and close activity.
("c" . activities-kill)
;; switch to an opened activity
;; ("s" . activities-switch)
;; permanently delete activity
("k" . activities-discard)
;; switch to a buffer in the current activity.
("b" . activities-switch-buffer)
;; revert activity to default state.
("g" . activities-revert)
;; list activities in vtable buffer
("l" . activities-list)
;; rename activity
("R" . activities-rename)
;; next tab
("n" . tab-next)
;; previous tab
("p" . tab-previous)))
(define-key map (kbd (car binding)) (cdr binding)))
;; set up autoloads
(let ((cmds (mapcar #'cdr (cdr map))))
(dolist (c cmds)
(unless (fboundp c)
(autoload c "activities" nil t))))
;; return map
map))
;; bind map
(global-set-key (kbd "C-c .") activities-mode-map)
(global-set-key (kbd "C-c x") activities-mode-map)
> consult integration
(defun activities-local-buffer-p (buffer)
"Returns non-nil if BUFFER is present in `activities-current'."
(when (activities-current)
(memq buffer
(activities-tabs--tab-parameter
'activities-buffer-list
(activities-tabs--tab (activities-current))))))
(defvar activities-consult-source
`(:name "Activity"
:narrow ?a
:category buffer
:face consult-buffer
:history buffer-name-history
:state ,#'consult--buffer-state
:default t
:enabled ,#'activities-current
:items ,(lambda () (consult--buffer-query
:predicate #'activities-local-buffer-p
:sort 'visibility
:as #'buffer-name)))
"Activities local buffers candidate source for `consult-buffer'.")
(defvar activities-rest-consult-source
`(:name "Rest"
:narrow ?r
:category buffer
:face consult-buffer
:history buffer-name-history
:state ,#'consult--buffer-state
:enabled ,#'activities-current
:items ,(lambda () (consult--buffer-query
:predicate (lambda (buf)
(not (activities-local-buffer-p buf)))
:as #'buffer-name)))
"Other buffers candidate source for `consult-buffer'.")
(with-eval-after-load 'consult
(consult-customize consult--source-buffer :hidden nil :default nil
:name "Buffers" :narrow ?b
:enabled (lambda () (not (activities-current))))
(add-to-list 'consult-buffer-sources activities-rest-consult-source)
(add-to-list 'consult-buffer-sources activities-consult-source))
‣ exclude consult-buffer previews from activity-buffer-list
(with-eval-after-load 'consult
(defun +consult-buffer--frame-buffers-around (orig-fn &rest args)
(let ((_log t)
(result nil))
(if-let* ((_predicates (and activities-mode
activities-tabs-mode
tab-bar-mode
(activities-current)))
(current-tab (tab-bar--current-tab))
(tab-index (tab-bar--current-tab-index))
(init-tab-buffers (alist-get 'activities-buffer-list current-tab)))
;; if-let then form
(unwind-protect
(setq result (apply orig-fn args))
;; re-fetch current tab after consult-buffer
(let* ((updated-tab (tab-bar--current-tab))
(post-tab-buffers
(seq-filter #'buffer-live-p
(alist-get 'activities-buffer-list updated-tab)))
(current-buffer (current-buffer))
;; remove new temp buffers from post-tab-buffers
(filtered-new
(seq-filter (lambda (buf)
(and buf
(or (member buf init-tab-buffers)
(eq buf current-buffer)
(equal (buffer-name buf)
" *Minibuf-1*"))))
post-tab-buffers)))
;; debugging
(when _log
(message "LOG: activities: ignore consult-buffer previews: %S"
(mapcar #'buffer-name
(seq-remove (lambda (x) (member x filtered-new))
post-tab-buffers))))
;; update the tab's activities-buffer-list
(let ((tabs (funcall tab-bar-tabs-function)))
(setf (alist-get 'activities-buffer-list (nth tab-index tabs))
filtered-new))
result))
;; if-let else form
(apply orig-fn args))))
(advice-add #'consult-buffer :around #'+consult-buffer--frame-buffers-around))
‣ Frame delete n restore (disabled)
;; ;; helper variable
;; (defvar +activities--last-name nil)
;; ;; save activities before deleting frame
;; (add-to-list 'delete-frame-functions
;; (lambda (frame)
;; (setq +activities--last-name (activities-current))
;; (activities-save-all)))
;; ;; save activities before killing emacs
;; (add-hook 'kill-emacs-hook #'activities-save-all)
;; ;; resume last activity when creating frame
;; (add-hook 'after-make-frame-functions
;; (lambda (frame)
;; (when +activities--last-name
;; (with-selected-frame frame
;; (activities-resume +activities--last-name)))))
‣ smart resuming (exclude running in frames, suspend all other)
;; TODO: show this to https://github.com/alphapapa/activities.el/issues/22
(cl-defun +activities-resume-custom (activity &key resetp)
"Wrapper around `activities-resume'.
When prompted, it excludes activities that are active in other frames.
After evaluating, it suspends all non-current activities."
(interactive
(list (activities-completing-read
:activities (+activities--exclude-other-frames)
:prompt "Resume activity" :default nil)
:resetp current-prefix-arg))
(activities-suspend (activities-current))
(let ((result (apply #'activities-resume
activity
(when resetp (list :resetp resetp)))))
;; after apply
(+activities--suspend-not-cur)
result))
;; helper function
(defun +activities--exclude-other-frames ()
"Return loadable activities, excluding already active in other frames."
(let (;; activities loadable from file
(activities-loadable (-map #'car-safe activities-activities))
;; activities active in any frame except current
(activities-active-in-other
(->>
(frame-list)
(--remove (equal it (selected-frame)))
(-mapcat
(lambda (frame)
(with-selected-frame frame
(let ((active-names
(->>
activities-activities
(-filter (-compose #'activities-activity-active-p
#'cdr))
(-map #'car-safe))))
(prog1 active-names
(message "LOG: active-names: %S" active-names)
(unless (or (<= 1 (length active-names))
(not active-names))
(warn "Expected no more than 1 activity in frame %s: %s"
frame active-names)))))))
(-non-nil)
(-uniq))))
;; from loadable, remove active-in-other
(message "LOG: activities-loadable: %S" activities-loadable)
(message "LOG: activities-active-in-other: %S" activities-active-in-other)
(->> activities-loadable
(--remove (-contains? activities-active-in-other
it))
(--map (cons it (activities-named it))))))
;; helper function
(cl-defun +activities--suspend-not-cur (&rest _args)
"Suspend all non-current activities."
(when-let*
((fn-get-active-lst
(lambda () (->> activities-activities
(-filter (-compose #'activities-activity-active-p #'cdr))
(-map #'car-safe))))
(active-lst (funcall fn-get-active-lst))
(current (activities-activity-name (activities-current)))
(not-current-lst
(->> active-lst (--remove (equal current it)))))
;; suspend all non-current activities
(--each not-current-lst
(activities-suspend (activities-named it)))
;; assert expected length and current-value
(setq active-lst (funcall fn-get-active-lst))
(unless (<= 1 (length active-lst))
(warn "expected 1 or 0 length for active-lst: %s"
active-lst)
(when (and (= 1 (length active-lst))
(equal current (car active-lst)))
(warn "expected 1 elem of %s, got %S"
current (car active-lst))))))
‣ misc
)
% Persp-mode
(leaf persp-mode :disabled t
:bind-keymap
("C-c w w" . persp-key-map)
("C-c ." . persp-key-map)
("C-c (" . persp-key-map)
:bind (persp-key-map
("." . my-persp-load-name-from-latest)
("D" . my-persp-delete-name-from-latest))
:setq
;; animation
(wg-morph-on . nil)
;; ?
(persp-autokill-buffer-on-remove . 'kill-weak)
;; dont autoresume at startup
(persp-auto-resume-time . -1)
;; save on shutdown
(persp-auto-save-opt . 2)
:hook
(elpaca-after-init-hook . (lambda () (persp-mode 1)))
:commands
persp-consult-source
:config
;; dont save persp-nil to file
(set-persp-parameter 'dont-save-to-file t nil)
;; consult-buffer integration
(defvar persp-consult-source
(list :name "Persp Buffers"
:narrow ?.
:category 'buffer
:state #'consult--buffer-state
:history 'buffer-name-history
:default t
:items
(lambda ()
(let ((current-persp (get-current-persp)))
(consult--buffer-query
:sort 'visibility
:predicate (lambda (buf)
(and current-persp
(persp-contain-buffer-p buf)))
:as 'buffer-name)))))
(defvar persp-rest-consult-source
(list :name "Other Buffers"
:narrow ?s
:category 'buffer
:state #'consult--buffer-state
:history 'buffer-name-history
:default t
:items
(lambda ()
(let ((current-persp (get-current-persp)))
(consult--buffer-query
:sort 'visibility
:predicate (lambda (buf)
(if current-persp
(not (persp-contain-buffer-p buf))
t))
:as 'buffer-name)))))
(with-eval-after-load 'consult
(consult-customize consult--source-buffer :hidden t :default nil)
(add-to-list 'consult-buffer-sources persp-rest-consult-source)
(add-to-list 'consult-buffer-sources persp-consult-source))
;; helper functions
(defun my/persp--get-names-from-savelist (&optional fname savelist)
(let* ((available (persp-list-persp-names-in-file fname savelist))
(loaded (persp-names-current-frame-fast-ordered))
(unloaded (seq-remove (lambda (p) (member p loaded)) available)))
(list available loaded unloaded)))
(defun my/persp--prompt-for-persp-name (lst)
(when lst
(persp-read-persp
"to load" nil nil t t nil lst t 'push)))
;; TODO: relocate
(defmacro +message-if-debug (format-string &rest args)
(when debug-on-error
`(message ,format-string ,@args)))
;; load from file
(cl-defun my-persp-load-name-from-latest
(&optional (fname persp-auto-save-fname)
(phash *persp-hash*)
(savelist (persp-savelist-from-savefile fname))
name)
"Load and switch to a perspective via name from the latest backup file."
(interactive)
;; prompt for name from available
(when savelist
(cl-destructuring-bind (available loaded unloaded)
(my/persp--get-names-from-savelist fname savelist)
(when unloaded
(unless name
(setq name (my/persp--prompt-for-persp-name unloaded)))
(+message-if-debug "DEBUG: > %s\n> %s\n> %s\n> %s"
fname
phash
(regexp-opt (list name))
savelist)
(persp-load-state-from-file fname phash
(regexp-opt (list name))
t savelist)
;; (persp-frame-switch name)
)))
;; (when name
;; (let ((names-regexp (regexp-opt (list name))))
;; (persp-load-state-from-file fname phash names-regexp t savelist))
;; ;; switch to new loaded persp
;; (persp-frame-switch name))
)
;; don't overwrite backup file with current; merge.
(advice-add 'persp-save-state-to-file :around
(lambda (orig-fun &rest args)
;; We need to modify the fourth optional parameter
;; Default arguments structure:
;; (fname phash respect-persp-file-parameter keep-others-in-non-parametric-file)
(let ((fname (or (nth 0 args) persp-auto-save-fname))
(phash (or (nth 1 args) *persp-hash*))
(respect-param (or (nth 2 args) persp-auto-save-persps-to-their-file))
;; Always set the fourth parameter to 'yes regardless of what was passed
(keep-others 'yes))
;; Call the original function with modified arguments
(funcall orig-fun fname phash respect-param keep-others))))
;; delete persp from file
(defun my-persp-delete-name-from-latest ()
(interactive)
(let* ((fname persp-auto-save-fname)
(savelist (persp-savelist-from-savefile fname))
(available-names (persp-list-persp-names-in-file fname savelist))
(names (persp-read-persp
"to delete" 'reverse nil t nil nil available-names t 'push))
(filtered-savelist (cl-remove-if
(lambda (expr)
(and (listp expr)
(eq (car expr) 'def-persp)
(seq-contains-p names (cadr expr))))
savelist)))
(if (y-or-n-p (format "Delete %s?" names))
(persp-savelist-to-file filtered-savelist fname))))
)
;; enable persp-mode-project-bridge mode
;; (when nil
;; (with-eval-after-load "persp-mode"
;; (defvar persp-mode-projectile-bridge-before-switch-selected-window-buffer nil)
;; ;; (setq persp-add-buffer-on-find-file 'if-not-autopersp)
;; (persp-def-auto-persp
;; "projectile"
;; :parameters '((dont-save-to-file . t)
;; (persp-mode-projectile-bridge . t))
;; :hooks '(projectile-before-switch-project-hook
;; projectile-after-switch-project-hook
;; projectile-find-file-hook
;; find-file-hook)
;; :dyn-env '((after-switch-to-buffer-adv-suspend t))
;; :switch 'frame
;; :predicate
;; #'(lambda (buffer &optional state)
;; (if (eq 'projectile-before-switch-project-hook
;; (alist-get 'hook state))
;; state
;; (and
;; projectile-mode
;; (buffer-live-p buffer)
;; (buffer-file-name buffer)
;; ;; (not git-commit-mode)
;; (projectile-project-p)
;; (or state t))))
;; :get-name
;; #'(lambda (state)
;; (if (eq 'projectile-before-switch-project-hook
;; (alist-get 'hook state))
;; state
;; (push (cons 'persp-name
;; (concat "[p] "
;; (with-current-buffer (alist-get 'buffer state)
;; (projectile-project-name))))
;; state)
;; state))
;; :on-match
;; #'(lambda (state)
;; (let ((hook (alist-get 'hook state))
;; (persp (alist-get 'persp state))
;; (buffer (alist-get 'buffer state)))
;; (pcase hook
;; (projectile-before-switch-project-hook
;; (let ((win (if (minibuffer-window-active-p (selected-window))
;; (minibuffer-selected-window)
;; (selected-window))))
;; (when (window-live-p win)
;; (setq persp-mode-projectile-bridge-before-switch-selected-window-buffer
;; (window-buffer win)))))
;; (projectile-after-switch-project-hook
;; (when (buffer-live-p
;; persp-mode-projectile-bridge-before-switch-selected-window-buffer)
;; (let ((win (selected-window)))
;; (unless (eq (window-buffer win)
;; persp-mode-projectile-bridge-before-switch-selected-window-buffer)
;; (set-window-buffer
;; win persp-mode-projectile-bridge-before-switch-selected-window-buffer)))))
;; (find-file-hook
;; (setcdr (assq :switch state) nil)))
;; (if (pcase hook
;; (projectile-before-switch-project-hook nil)
;; (t t))
;; (persp--auto-persp-default-on-match state)
;; (setcdr (assq :after-match state) nil)))
;; state)
;; :after-match
;; #'(lambda (state)
;; (when (eq 'find-file-hook (alist-get 'hook state))
;; (run-at-time 0.5 nil
;; #'(lambda (buf persp)
;; (when (and (eq persp (get-current-persp))
;; (not (eq buf (window-buffer (selected-window)))))
;; ;; (switch-to-buffer buf)
;; (persp-add-buffer buf persp t nil)))
;; (alist-get 'buffer state)
;; (get-current-persp)))
;; (persp--auto-persp-default-after-match state)))
;; ;; (add-hook 'persp-after-load-state-functions
;; ;; #'(lambda (&rest args) (persp-auto-persps-pickup-buffers)) t)
;; ))
;; Shows groups for all perspectives. But can't show same buffer in multiple groups.
;; (with-eval-after-load "ibuffer"
;; (require 'ibuf-ext)
;; (define-ibuffer-filter persp
;; "Toggle current view to buffers of current perspective."
;; (:description "persp-mode"
;; :reader (persp-prompt nil nil (safe-persp-name (get-frame-persp)) t))
;; (find buf (safe-persp-buffers (persp-get-by-name qualifier))))
;; (defun persp-add-ibuffer-group ()
;; (let ((perspslist (mapcar #'(lambda (pn)
;; (list pn (cons 'persp pn)))
;; (nconc
;; (cl-delete persp-nil-name
;; (persp-names-current-frame-fast-ordered)
;; :test 'string=)
;; (list persp-nil-name)))))
;; (setq ibuffer-saved-filter-groups
;; (cl-delete "persp-mode" ibuffer-saved-filter-groups
;; :test 'string= :key 'car))
;; (push
;; (cons "persp-mode" perspslist)
;; ibuffer-saved-filter-groups)))
;; (defun persp-ibuffer-visit-buffer ()
;; (interactive)
;; (let ((buf (ibuffer-current-buffer t))
;; (persp-name (get-text-property
;; (line-beginning-position) 'ibuffer-filter-group)))
;; (persp-switch persp-name)
;; (switch-to-buffer buf)))
;; (define-key ibuffer-mode-map (kbd "RET") 'persp-ibuffer-visit-buffer)
;; (add-hook 'ibuffer-mode-hook
;; #'(lambda ()
;; (persp-add-ibuffer-group)
;; (ibuffer-switch-to-saved-filter-groups "persp-mode"))))
% Perspective
(leaf perspective :disabled t
:init
(persp-mode)
:custom
`(persp-mode-prefix-key . ,(kbd "C-c ."))
:bind
("C-c ." . perspective-map)
(perspective-map
("S" . persp-state-save)
("M-s" . persp-state-save)
("C-s" . nil))
:hook
(kill-emacs-hook . persp-state-save)
:config
;; default backup file
(setq persp-state-default-file
(file-name-concat persp-save-dir "persp-auto-save"))
;; prev/next buffers
(setq switch-to-prev-buffer-skip
(lambda (win buff bury-or-kill)
(not (persp-is-current-buffer buff))))
;; consult-buffer
(with-eval-after-load 'consult
(consult-customize consult--source-buffer :hidden nil :default nil)
(add-to-list 'consult-buffer-sources persp-consult-source))
;; save activities before deleting frame
(add-to-list 'delete-frame-functions
(lambda (frame)
(persp-save-state)))
;; save activities before killing emacs
(add-hook 'kill-emacs-hook #'persp-save-state)
)
(provide '+workspaces)
Last updated: August 22, 2025