diff --git a/src/workgroups-workgroup.el b/src/workgroups-workgroup.el new file mode 100644 index 0000000000000000000000000000000000000000..ec860f0a750f04a2f548e4af26f651106547818b --- /dev/null +++ b/src/workgroups-workgroup.el @@ -0,0 +1,956 @@ +;;; workgroups-workgroup.el --- workgroup functions +;;; Commentary: +;;; Code: + +(require 'ring) +(require 'workgroups-wconfig) +(require 'workgroups-minibuffer) + +;; +;; Variables +;; + +(defcustom wg-confirm-on-get-workgroup-create nil + "Non-nil means request confirmation before creating a new +workgroup when `wg-get-workgroup-create' is called with a string +that doesn't name an existing workgroup." + :type 'boolean + :group 'workgroups) + +(defun wg-flag-workgroup-modified (workgroup) + "Set WORKGROUP's and the current session's modified flags." + (when wg-flag-modified + (setf (wg-workgroup-modified workgroup) t) + (setf (wg-session-modified (wg-current-session)) t))) + + +(defun wg-current-workgroup (&optional noerror frame) + "Return the current workgroup in FRAME, or error unless NOERROR." + (or wg-current-workgroup + (wg-aif (frame-parameter frame 'wg-current-workgroup-uid) + (wg-find-workgroup-by :uid it noerror) + (unless noerror (error "No current workgroup in this frame"))))) + +(defun wg-previous-workgroup (&optional noerror frame) + "Return the previous workgroup in FRAME, or error unless NOERROR." + (wg-aif (frame-parameter frame 'wg-previous-workgroup-uid) + (wg-find-workgroup-by :uid it noerror) + (unless noerror (error "No previous workgroup in this frame")))) + +(defun wg-set-current-workgroup (workgroup &optional frame) + "Set the current workgroup to WORKGROUP. +WORKGROUP should be a workgroup or nil." + (set-frame-parameter frame 'wg-current-workgroup-uid + (when workgroup (wg-workgroup-uid workgroup)))) + +(defun wg-set-previous-workgroup (workgroup &optional frame) + "Set the previous workgroup to WORKGROUP. +WORKGROUP should be a workgroup or nil." + (set-frame-parameter frame 'wg-previous-workgroup-uid + (when workgroup (wg-workgroup-uid workgroup)))) + +(defun wg-current-workgroup-p (workgroup &optional frame) + "Return t when WORKGROUP is the current workgroup, nil otherwise." + (wg-awhen (wg-current-workgroup t frame) + (eq workgroup it))) + +(defun wg-previous-workgroup-p (workgroup &optional frame) + "Return t when WORKGROUP is the previous workgroup, nil otherwise." + (wg-awhen (wg-previous-workgroup t frame) + (eq workgroup it))) + +(defmacro wg-with-current-workgroup (workgroup &rest body) + "Execute forms in BODY with WORKGROUP temporarily current. +WORKGROUP should be any workgroup identifier accepted by +`wg-get-workgroup'. The value returned is the last form +in BODY." + (declare (indent 1)) + `(let ((wg-current-workgroup (wg-get-workgroup ,workgroup))) + ,@body)) + +(defun wg-get-workgroup (obj &optional noerror) + "Return a workgroup from OBJ. +If OBJ is a workgroup, return it. +If OBJ is a string, return the workgroup named OBJ, or error unless NOERROR. +If OBJ is nil, return the current workgroup, or error unless NOERROR." + (cond ((wg-workgroup-p obj) obj) + ((stringp obj) (wg-find-workgroup-by :name obj noerror)) + ((null obj) (wg-current-workgroup noerror)) + (t (error "Can't get workgroup from type:: %S" (type-of obj))))) + + + +;;; workgroup parameters +;; +;; Quick test: +;; (wg-workgroup-parameters (wg-current-workgroup)) +;; (wg-set-workgroup-parameter (wg-current-workgroup) 'test1 t) +;; (wg-workgroup-parameter (wg-current-workgroup) 'test1) +(defun wg-workgroup-parameter (workgroup parameter &optional default) + "Return WORKGROUP's value for PARAMETER. +If PARAMETER is not found, return DEFAULT which defaults to nil. +WORKGROUP should be accepted by `wg-get-workgroup'." + (wg-aget (wg-workgroup-parameters (wg-get-workgroup workgroup)) + parameter default)) + +(defun wg-set-workgroup-parameter (workgroup parameter value) + "Set WORKGROUP's value of PARAMETER to VALUE. +WORKGROUP should be a value accepted by `wg-get-workgroup'. +Return VALUE." + (let ((workgroup (wg-get-workgroup workgroup))) + (wg-set-parameter (wg-workgroup-parameters workgroup) parameter value) + (wg-flag-workgroup-modified workgroup) + value)) + +(defun wg-remove-workgroup-parameter (workgroup parameter) + "Remove PARAMETER from WORKGROUP's parameters." + (let ((workgroup (wg-get-workgroup workgroup))) + (wg-flag-workgroup-modified workgroup) + (wg-asetf (wg-workgroup-parameters workgroup) (wg-aremove it parameter)))) + +(defun wg-workgroup-local-value (variable &optional workgroup) + "Return the value of VARIABLE in WORKGROUP. +WORKGROUP nil defaults to the current workgroup. If there is no +current workgroup, or if VARIABLE does not have a workgroup-local +binding in WORKGROUP, resolve VARIABLE with `wg-session-local-value'." + (let ((workgroup (wg-get-workgroup workgroup t))) + (if (not workgroup) (wg-session-local-value variable) + (let* ((undefined (cl-gensym)) + (value (wg-workgroup-parameter workgroup variable undefined))) + (if (not (eq value undefined)) value + (wg-session-local-value variable)))))) + +(defalias 'wg-local-value 'wg-workgroup-local-value) + + +;;; workgroup saved wconfigs + +(defun wg-workgroup-saved-wconfig-names (workgroup) + "Return a new list of the names of all WORKGROUP's saved wconfigs." + (mapcar 'wg-wconfig-name (wg-workgroup-saved-wconfigs workgroup))) + +(defun wg-workgroup-get-saved-wconfig (workgroup wconfig-or-name) + "Return the wconfig in WORKGROUP's saved wconfigs named WCONFIG-OR-NAME. +WCONFIG-OR-NAME must be either a string or a wconfig. If +WCONFIG-OR-NAME is a string and there is no saved wconfig with +that name, return nil. If WCONFIG-OR-NAME is a wconfig, and it +is a member of WORKGROUP's saved wconfigs, return is as given. +Otherwise return nil." + (let ((wconfigs (wg-workgroup-saved-wconfigs workgroup))) + (cl-etypecase wconfig-or-name + (wg-wconfig (car (memq wconfig-or-name wconfigs))) + (string (cl-find wconfig-or-name wconfigs + :key 'wg-wconfig-name + :test 'string=))))) + +(defun wg-workgroup-save-wconfig (workgroup wconfig) + "Add WCONFIG to WORKGROUP's saved wconfigs. WCONFIG must have +a name. If there's already a wconfig with the same name in +WORKGROUP's saved wconfigs, replace it." + (let ((name (wg-wconfig-name wconfig))) + (unless name (error "Attempt to save a nameless wconfig")) + (setf (wg-workgroup-modified workgroup) t) + (wg-asetf (wg-workgroup-saved-wconfigs workgroup) + (cons wconfig (cl-remove name it + :key 'wg-wconfig-name + :test 'string=))))) + +(defun wg-workgroup-kill-saved-wconfig (workgroup wconfig-or-name) + "Delete WCONFIG-OR-NAME from WORKGROUP's saved wconfigs. +WCONFIG-OR-NAME is resolved with `wg-workgroup-get-saved-wconfig'." + (wg-when-let ((wconfig (wg-workgroup-get-saved-wconfig + workgroup wconfig-or-name))) + (wg-asetf (wg-workgroup-saved-wconfigs workgroup) (remq wconfig it) + (wg-workgroup-modified workgroup) t))) + + + +(defun wg-workgroup-base-wconfig-buf-uids (workgroup) + "Return a new list of all unique buf uids in WORKGROUP's working wconfig." + (wg-wconfig-buf-uids (wg-workgroup-base-wconfig workgroup))) + +(defun wg-workgroup-saved-wconfigs-buf-uids (workgroup) + "Return a new list of all unique buf uids in WORKGROUP's base wconfig." + (cl-reduce 'wg-string-list-union + (wg-workgroup-saved-wconfigs workgroup) + :key 'wg-wconfig-buf-uids)) + +(defun wg-workgroup-all-wconfig-buf-uids (workgroup) + "Return a new list of all unique buf uids in WORKGROUP's wconfigs." + (cl-union (wg-workgroup-base-wconfig-buf-uids workgroup) + (wg-workgroup-saved-wconfigs-buf-uids workgroup) + :test 'string=)) + +(defun wg-workgroup-all-buf-uids (workgroup) + "Return a new list of all unique buf uids in WORKGROUP." + (cl-reduce 'wg-string-list-union + (list (wg-workgroup-base-wconfig-buf-uids workgroup) + (wg-workgroup-saved-wconfigs-buf-uids workgroup)))) + + + +;;; workgroup restoration + +(defun wg-restore-workgroup (workgroup) + "Restore WORKGROUP in `selected-frame'." + (let (wg-flag-modified) + (wg-restore-wconfig-undoably (wg-workgroup-working-wconfig workgroup) t))) + + +(defun wg-workgroup-list-or-error (&optional noerror) + "Return the value of `wg-current-session's :workgroup-list slot. +Or scream unless NOERROR." + (or (wg-workgroup-list) + (unless noerror + (error "No workgroups are defined")))) + +(defun wg-find-workgroup-by (slotkey value &optional noerror) + "Return the workgroup on which ACCESSOR returns VALUE or error." + (let ((accessor (cl-ecase slotkey + (:name 'wg-workgroup-name) + (:uid 'wg-workgroup-uid)))) + (or (cl-find value (wg-workgroup-list-or-error noerror) :test 'equal :key accessor) + (unless noerror + (error "No are no workgroups with a %S of %S" + accessor value))))) + +(defun wg-first-workgroup () + "Return a first workgroup." + (car (wg-workgroup-list-or-error))) + +(defun wg-cyclic-nth-from-workgroup (workgroup &optional n) + "Return the workgroup N places from WORKGROUP in `wg-workgroup-list'." + (wg-cyclic-nth-from-elt workgroup (wg-workgroup-list-or-error) (or n 1))) + + +(defun wg-read-workgroup-name (&optional require-match) + "Read a workgroup with `wg-completing-read'." + (wg-completing-read + "Workgroup: " (wg-workgroup-names) nil require-match nil nil + (wg-awhen (wg-current-workgroup t) (wg-workgroup-name it)))) + +(defun wg-new-default-workgroup-name () + "Return a new, unique, default workgroup name." + (let ((names (wg-workgroup-names t)) (index -1) result) + (while (not result) + (let ((new-name (format "wg%s" (cl-incf index)))) + (unless (member new-name names) + (setq result new-name)))) + result)) + +(defun wg-unique-workgroup-name-p (new-name) + "Return t if NEW-NAME is unique in `wg-workgroup-list', nil otherwise." + (cl-every (lambda (existing-name) (not (equal new-name existing-name))) + (wg-workgroup-names t))) + +(defun wg-read-new-workgroup-name (&optional prompt) + "Read a non-empty name string from the minibuffer." + (let ((default (wg-new-default-workgroup-name))) + (wg-read-object + (or prompt (format "Name (default: %S): " default)) + (lambda (new) (and (stringp new) + (not (equal new "")) + (wg-unique-workgroup-name-p new))) + "Please enter a unique, non-empty name" + nil nil nil nil default))) + +(defun wg-read-workgroup-index () + "Prompt for the index of a workgroup." + (let ((max (1- (length (wg-workgroup-list-or-error))))) + (wg-read-object + (format "%s\n\nEnter [0-%d]: " (wg-workgroup-list-display) max) + (lambda (obj) (and (integerp obj) (wg-within obj 0 max t))) + (format "Please enter an integer [%d-%d]" 0 max) + nil nil t))) + +(defun wg-read-saved-wconfig-name (workgroup &optional prompt require-match) + "Read the name of a saved wconfig, completing on the names of +WORKGROUP's saved wconfigs." + (wg-completing-read + (or prompt "Saved wconfig name: ") + (wg-workgroup-saved-wconfig-names workgroup) + nil require-match)) + +(defun wg-read-saved-wconfig (workgroup) + "Read the name of and return one of WORKGROUP's saved wconfigs." + (wg-workgroup-get-saved-wconfig + workgroup (wg-read-saved-wconfig-name workgroup nil t))) + + +;;; workgroup-list reorganization commands + +(defun wg-swap-workgroups () + "Swap the previous and current workgroups." + (interactive) + (wg-swap-workgroups-in-workgroup-list + (wg-current-workgroup) (wg-previous-workgroup)) + (wg-fontified-message + (:cmd "Swapped: ") + (wg-workgroup-list-display))) + +(defun wg-offset-workgroup-left (&optional workgroup n) + "Offset WORKGROUP leftward in `wg-workgroup-list' cyclically." + (interactive (list nil current-prefix-arg)) + (wg-cyclic-offset-workgroup (wg-get-workgroup workgroup) (or n -1)) + (wg-fontified-message + (:cmd "Offset left: ") + (wg-workgroup-list-display))) + +(defun wg-offset-workgroup-right (&optional workgroup n) + "Offset WORKGROUP rightward in `wg-workgroup-list' cyclically." + (interactive (list nil current-prefix-arg)) + (wg-cyclic-offset-workgroup (wg-get-workgroup workgroup) (or n 1)) + (wg-fontified-message + (:cmd "Offset right: ") + (wg-workgroup-list-display))) + + +;;; undo/redo commands + +(defun wg-undo-wconfig-change (&optional workgroup) + "Undo a change to the current workgroup's window-configuration." + (interactive) + (let* ((workgroup (wg-get-workgroup workgroup)) + (undid? (wg-workgroup-offset-position-in-undo-list workgroup 1))) + (wg-fontified-message + (:cmd "Undo") + (:cur (if undid? "" " No more undo info"))))) + +(defun wg-redo-wconfig-change (&optional workgroup) + "Redo a change to the current workgroup's window-configuration." + (interactive) + (let* ((workgroup (wg-get-workgroup workgroup)) + (redid? (wg-workgroup-offset-position-in-undo-list workgroup -1))) + (wg-fontified-message + (:cmd "Redo") + (:cur (if redid? "" " No more redo info"))))) + +(defun wg-undo-once-all-workgroups () + "Do what the name says. Useful for instance when you +accidentally call `wg-revert-all-workgroups' and want to return +all workgroups to their un-reverted state." + (interactive) + (mapc 'wg-undo-wconfig-change (wg-workgroup-list-or-error)) + (wg-message "Undid once on all workgroups.")) + +(defun wg-redo-once-all-workgroups () + "Do what the name says. Probably useless. Included for +symetry with `wg-undo-once-all-workgroups'." + (interactive) + (mapc 'wg-redo-wconfig-change (wg-workgroup-list-or-error)) + (wg-message "Redid once on all workgroups.")) + + + +;;; window-tree commands +;; +;; TODO: These are half-hearted. Clean them up; allow specification of the +;; window-tree depth at which to operate; add complex window creation commands; +;; and add window splitting, deletion and locking commands. + +(defun wg-reverse-frame-horizontally (&optional workgroup) + "Reverse the order of all horizontally split wtrees." + (interactive) + (wg-restore-wconfig-undoably + (wg-reverse-wconfig + (wg-workgroup-working-wconfig + (wg-get-workgroup workgroup))))) + +(defun wg-reverse-frame-vertically (&optional workgroup) + "Reverse the order of all vertically split wtrees." + (interactive) + (wg-restore-wconfig-undoably + (wg-reverse-wconfig + (wg-workgroup-working-wconfig + (wg-get-workgroup workgroup)) + t))) + +(defun wg-reverse-frame-horizontally-and-vertically (&optional workgroup) + "Reverse the order of all wtrees." + (interactive) + (wg-restore-wconfig-undoably + (wg-reverse-wconfig + (wg-workgroup-working-wconfig + (wg-get-workgroup workgroup)) + 'both))) + + +;;; misc commands + +(defun wg-rename-workgroup (workgroup newname) + "Rename WORKGROUP to NEWNAME." + (interactive (list nil (wg-read-new-workgroup-name "New name: "))) + (let* ((workgroup (wg-get-workgroup workgroup)) + (oldname (wg-workgroup-name workgroup))) + (setf (wg-workgroup-name workgroup) newname) + (wg-flag-workgroup-modified workgroup) + (wg-fontified-message + (:cmd "Renamed: ") + (:cur oldname) + (:msg " to ") + (:cur (wg-workgroup-name workgroup))))) + +(defun wg-reset (&optional force) + "Reset Workgroups. +Resets all frame parameters, buffer-local vars, the current +Workgroups session object, etc." + (interactive "P") + (unless (or force wg-no-confirm-on-destructive-operation + (y-or-n-p "Really reset Workgroups? ")) + (error "Canceled")) + (wg-reset-internal) + (wg-fontified-message (:cmd "Reset: ") (:msg "Workgroups"))) + + +(defun wg-query-and-save-if-modified () + "Query for save when `wg-modified-p'." + (or (not (wg-modified-p)) + (when (y-or-n-p "Save modified workgroups? ") + (wg-save-session)))) + + +;;; workgroup creation commands + +(defun wg-create-workgroup (name &optional blank) + "Create and add a workgroup named NAME. +Optional argument BLANK non-nil (set interactively with a prefix +arg) means use a blank, one window window-config. Otherwise use +the current window-configuration. Keep in mind that even though +the current window-config may be used, other parameters of the +current workgroup are not copied to the created workgroup. For +that, use `wg-clone-workgroup'." + (interactive (list (wg-read-new-workgroup-name) current-prefix-arg)) + (wg-switch-to-workgroup (wg-make-and-add-workgroup name blank)) + (wg-fontified-message + (:cmd "Created: ") (:cur name) " " (wg-workgroup-list-display))) + +(defun wg-clone-workgroup (workgroup name) + "Create and add a clone of WORKGROUP named NAME. +Keep in mind that only WORKGROUP's top-level alist structure is +copied, so destructive operations on the keys or values of +WORKGROUP will be reflected in the clone, and vice-versa. Be +safe -- don't mutate them." + (interactive (list nil (wg-read-new-workgroup-name))) + (let* ((workgroup (wg-get-workgroup workgroup)) + (clone (wg-copy-workgroup workgroup))) + (setf (wg-workgroup-name clone) name + (wg-workgroup-uid clone) (wg-generate-uid)) + (when (wg-check-and-add-workgroup clone) + (wg-flag-workgroup-modified clone)) + (wg-set-workgroup-working-wconfig + clone (wg-workgroup-working-wconfig workgroup)) + (wg-switch-to-workgroup clone) + (wg-fontified-message + (:cmd "Cloned: ") + (:cur (wg-workgroup-name workgroup)) + (:msg " to ") + (:cur name) " " + (wg-workgroup-list-display)))) + + + +;;; workgroup switching commands +(defun wg-switch-to-workgroup (workgroup &optional noerror) + "Switch to WORKGROUP. +NOERROR means fail silently." + (interactive (list (wg-read-workgroup-name))) + ;; Mark if ECB is active + (if (wg-current-workgroup t) + (wg-set-workgroup-parameter (wg-current-workgroup t) 'ecb (and (boundp 'ecb-minor-mode) + ecb-minor-mode))) + ;;(wg-set-workgroup-parameter (wg-current-workgroup t) 'ecb-win-config (ecb-current-window-configuration)) + ;; (type-of (ecb-current-window-configuration)) + ;; (type-of (car (ecb-current-window-configuration))) + ;; (type-of (car (nthcdr 3 (ecb-current-window-configuration)))) + ;; (wg-pickelable-or-error (ecb-current-window-configuration)) + ;;(ecb-current-window-configuration) + ;;) + + (let ((workgroup (wg-get-workgroup-create workgroup)) + (current (wg-current-workgroup t))) + (when (and (eq workgroup current) (not noerror)) + (error "Already on: %s" (wg-workgroup-name current))) + (when current (push current wg-deactivation-list)) + (unwind-protect + (progn + ;; Before switching - turn off ECB + ;; https://github.com/pashinin/workgroups2/issues/34 + (if (and (boundp 'ecb-minor-mode) + (boundp 'ecb-frame) + (fboundp 'ecb-deactivate) + ecb-minor-mode + (equal ecb-frame (selected-frame))) + (let ((ecb-split-edit-window-after-start 'before-deactivation)) + (ecb-deactivate))) + + (wg-restore-workgroup workgroup) + (wg-set-previous-workgroup current) + (wg-set-current-workgroup workgroup) + + ;; Save "last-workgroup" to the session params + (if (and (wg-current-session t) + (wg-current-workgroup t)) + (wg-set-session-parameter (wg-current-session t) + 'last-workgroup + (wg-workgroup-name (wg-current-workgroup)))) + + ;; If a workgroup had ECB - turn it on + (if (and (boundp 'ecb-minor-mode) + (not ecb-minor-mode) + (fboundp 'ecb-activate) + (wg-workgroup-parameter (wg-current-workgroup t) 'ecb nil)) + (let ((ecb-split-edit-window-after-start 'before-deactivation)) + (ecb-activate))) + ;;(ecb-last-window-config-before-deactivation + ;; (wg-workgroup-parameter (wg-current-workgroup t) 'ecb-win-config nil))) + + (run-hooks 'wg-switch-to-workgroup-hook) + (wg-fontified-message + (:cmd "Switched: ") + (wg-workgroup-name (wg-current-workgroup t)) + )) + (when current (pop wg-deactivation-list))))) + +(defun wg-switch-to-workgroup-other-frame (workgroup &optional n) + "Switch to WORKGROUP in the frame N places cyclically from `selected-frame'. +Use `current-prefix-arg' for N if non-nil. Otherwise N defaults to 1." + (interactive (list (wg-read-workgroup-name) current-prefix-arg)) + (with-selected-frame (wg-cyclic-nth-from-frame (or n 1)) + (wg-switch-to-workgroup workgroup))) + +(defun wg-switch-to-workgroup-at-index (index) + "Switch to the workgroup at INDEX in `wg-workgroup-list'." + (interactive (list (or current-prefix-arg (wg-read-workgroup-index)))) + (let ((wl (wg-workgroup-list-or-error))) + (wg-switch-to-workgroup + (or (nth index wl) (error "There are only %d workgroups" (length wl)))))) + +(cl-macrolet + ((define-range-of-switch-to-workgroup-at-index (num) + `(progn + ,@(wg-docar (i (wg-range 0 num)) + `(defun ,(intern (format "wg-switch-to-workgroup-at-index-%d" i)) () + ,(format "Switch to the workgroup at index %d." i) + (interactive) + (wg-switch-to-workgroup-at-index ,i)))))) + (define-range-of-switch-to-workgroup-at-index 10)) + +(defun wg-switch-to-cyclic-nth-from-workgroup (workgroup n) + "Switch N workgroups cyclically from WORKGROUP in `wg-workgroup-list.'" + (let ((workgroup-list (wg-workgroup-list-or-error)) + (workgroup (wg-get-workgroup workgroup t))) + (wg-switch-to-workgroup + (cond ((not workgroup) (car workgroup-list)) + ((= 1 (length workgroup-list)) (error "There's only one workgroup")) + (t (wg-cyclic-nth-from-workgroup workgroup n)))))) + +(defun wg-switch-to-workgroup-left (&optional workgroup n) + "Switch to the workgroup (- N) places from WORKGROUP in `wg-workgroup-list'. +Use `current-prefix-arg' for N if non-nil. Otherwise N defaults to 1." + (interactive (list nil current-prefix-arg)) + (wg-switch-to-cyclic-nth-from-workgroup workgroup (- (or n 1)))) + +(defun wg-switch-to-workgroup-right (&optional workgroup n) + "Switch to the workgroup N places from WORKGROUP in `wg-workgroup-list'. +Use `current-prefix-arg' for N if non-nil. Otherwise N defaults to 1." + (interactive (list nil current-prefix-arg)) + (wg-switch-to-cyclic-nth-from-workgroup workgroup (or n 1))) + +(defun wg-switch-to-previous-workgroup () + "Switch to the previous workgroup." + (interactive) + (wg-switch-to-workgroup (wg-previous-workgroup))) + + + +;;; workgroup killing commands + +(defun wg-wconfig-kill-ring () + "Return `wg-wconfig-kill-ring', creating it first if necessary." + (or wg-wconfig-kill-ring + (setq wg-wconfig-kill-ring (make-ring wg-wconfig-kill-ring-max)))) + +(defun wg-add-to-wconfig-kill-ring (wconfig) + "Add WCONFIG to `wg-wconfig-kill-ring'." + (ring-insert (wg-wconfig-kill-ring) wconfig)) + +(defun wg-kill-workgroup (&optional workgroup) + "Kill WORKGROUP, saving its working-wconfig to the kill ring." + (interactive) + (let* ((workgroup (wg-get-workgroup workgroup)) + (to (or (wg-previous-workgroup t) + (wg-cyclic-nth-from-workgroup workgroup)))) + (wg-add-to-wconfig-kill-ring (wg-workgroup-working-wconfig workgroup)) + (wg-delete-workgroup workgroup) + (if (eq workgroup to) (wg-restore-wconfig (wg-make-blank-wconfig)) + (wg-switch-to-workgroup to)) + (wg-fontified-message + (:cmd "Killed: ") + (:cur (wg-workgroup-name workgroup)) " " + (wg-workgroup-list-display)))) + +(defun wg-kill-ring-save-base-wconfig (&optional workgroup) + "Save WORKGROUP's base wconfig to the kill ring." + (interactive) + (let ((workgroup (wg-get-workgroup workgroup))) + (wg-add-to-wconfig-kill-ring (wg-workgroup-base-wconfig workgroup)) + (wg-fontified-message + (:cmd "Saved: ") (:cur (wg-workgroup-name workgroup)) + (:cur "'s ") (:msg "base wconfig to the kill ring")))) + +(defun wg-kill-ring-save-working-wconfig (&optional workgroup) + "Save WORKGROUP's working-wconfig to `wg-wconfig-kill-ring'." + (interactive) + (let ((workgroup (wg-get-workgroup workgroup))) + (wg-add-to-wconfig-kill-ring (wg-workgroup-working-wconfig workgroup)) + (wg-fontified-message + (:cmd "Saved: ") (:cur (wg-workgroup-name workgroup)) + (:cur "'s ") (:msg "working-wconfig to the kill ring")))) + +(defun wg-yank-wconfig () + "Restore a wconfig from `wg-wconfig-kill-ring'. +Successive yanks restore wconfigs sequentially from the kill +ring, starting at the front." + (interactive) + (when (zerop (ring-length (wg-wconfig-kill-ring))) + (error "The kill-ring is empty")) + (let ((pos (if (not (eq real-last-command 'wg-yank-wconfig)) 0 + (1+ (or (get 'wg-yank-wconfig :position) 0))))) + (put 'wg-yank-wconfig :position pos) + (wg-restore-wconfig-undoably (ring-ref (wg-wconfig-kill-ring) pos)) + (wg-fontified-message + (:cmd "Yanked: ") + (:msg (format "%S" pos)) " " + (wg-workgroup-list-display)))) + +(defun wg-kill-workgroup-and-buffers (&optional workgroup) + "Kill WORKGROUP and the buffers in its working-wconfig." + (interactive) + (let* ((workgroup (wg-get-workgroup workgroup)) + (bufs (save-window-excursion + (wg-restore-workgroup workgroup) + (mapcar #'window-buffer (window-list))))) + (wg-kill-workgroup workgroup) + (mapc #'kill-buffer bufs) + (wg-fontified-message + (:cmd "Killed: ") + (:cur (wg-workgroup-name workgroup)) + (:msg " and its buffers ") "\n" + (wg-workgroup-list-display)))) + +(defun wg-delete-other-workgroups (&optional workgroup) + "Delete all workgroups but WORKGROUP." + (interactive) + (let ((workgroup (wg-get-workgroup workgroup))) + (unless (or wg-no-confirm-on-destructive-operation + (y-or-n-p "Really delete all other workgroups? ")) + (error "Cancelled")) + (dolist (w (wg-workgroup-list-or-error)) + (unless (eq w workgroup) + (wg-delete-workgroup w))) + (unless (wg-current-workgroup-p workgroup) + (wg-switch-to-workgroup workgroup)) + (wg-fontified-message + (:cmd "Deleted: ") + (:msg "All workgroups but ") + (:cur (wg-workgroup-name workgroup))))) + + + +;;; workgroup updating and reverting commands + +(defun wg-revert-workgroup (&optional workgroup) + "Restore WORKGROUP's window configuration to its state at the last save." + (interactive) + (let* ((workgroup (wg-get-workgroup workgroup)) + (base-wconfig (wg-workgroup-base-wconfig workgroup))) + (if (wg-current-workgroup-p workgroup) + (wg-restore-wconfig-undoably base-wconfig) + (wg-add-wconfig-to-undo-list workgroup base-wconfig)) + (wg-fontified-message + (:cmd "Reverted: ") + (:cur (wg-workgroup-name workgroup))))) + +(defun wg-revert-all-workgroups () + "Revert all workgroups to their base wconfigs. +Only workgroups' working-wconfigs in `selected-frame' are +reverted." + (interactive) + (mapc #'wg-revert-workgroup (wg-workgroup-list-or-error)) + (wg-fontified-message + (:cmd "Reverted: ") + (:msg "All"))) + + + +;;; workgroup working-wconfig and wconfig undo/redo + +(defun wg-workgroup-state-table (&optional frame) + "Return FRAME's workgroup table, creating it first if necessary." + (or (frame-parameter frame 'wg-workgroup-state-table) + (let ((wtree (make-hash-table :test 'equal))) + (set-frame-parameter frame 'wg-workgroup-state-table wtree) + wtree))) + +(defun wg-get-workgroup-state (workgroup &optional frame) + "Return WORKGROUP's state table in a FRAME." + (let ((uid (wg-workgroup-uid workgroup)) + (state-table (wg-workgroup-state-table frame))) + (or (gethash uid state-table) + (let ((wgs (wg-make-workgroup-state + :undo-pointer 0 + :undo-list + (list (or (wg-workgroup-selected-frame-wconfig workgroup) + (wg-workgroup-base-wconfig workgroup)))))) + (puthash uid wgs state-table) + wgs)))) + +(defmacro wg-with-undo (workgroup spec &rest body) + "Bind WORKGROUP's undo state to SPEC and eval BODY." + (declare (indent 2)) + (wg-dbind (state undo-pointer undo-list) spec + `(let* ((,state (wg-get-workgroup-state ,workgroup)) + (,undo-pointer (wg-workgroup-state-undo-pointer ,state)) + (,undo-list (wg-workgroup-state-undo-list ,state))) + ,@body))) + +(defun wg-flag-just-exited-minibuffer () + "Added to `minibuffer-exit-hook'." + (setq wg-just-exited-minibuffer t)) + +(defun wg-flag-window-configuration-changed () + "Set `wg-window-configuration-changed' to t. +But only if not the minibuffer was just exited. Added to +`window-configuration-change-hook'." + (if wg-just-exited-minibuffer + (setq wg-just-exited-minibuffer nil) + (setq wg-window-configuration-changed t))) + +(defun wg-unflag-undoify-window-configuration-change () + "Set `wg-undoify-window-configuration-change' to nil, exempting +from undoification any window-configuration changes caused by the +current command." + (setq wg-undoify-window-configuration-change nil)) + +(defun wg-set-workgroup-working-wconfig (workgroup wconfig) + "Set the working-wconfig of WORKGROUP to WCONFIG." + (wg-flag-workgroup-modified workgroup) + (setf (wg-workgroup-selected-frame-wconfig workgroup) wconfig) + (wg-with-undo workgroup (state undo-pointer undo-list) + (setcar (nthcdr undo-pointer undo-list) wconfig))) + +(defun wg-add-wconfig-to-undo-list (workgroup wconfig) + "Add WCONFIG to WORKGROUP's undo list, truncating its future if necessary." + (wg-with-undo workgroup (state undo-pointer undo-list) + (let ((undo-list (cons nil (nthcdr undo-pointer undo-list)))) + (wg-awhen (nthcdr wg-wconfig-undo-list-max undo-list) (setcdr it nil)) + (setf (wg-workgroup-state-undo-list state) undo-list)) + (setf (wg-workgroup-state-undo-pointer state) 0)) + (wg-set-workgroup-working-wconfig workgroup wconfig)) + +(defun wg-workgroup-working-wconfig (workgroup &optional noupdate) + "Return WORKGROUP's working-wconfig. +If WORKGROUP is the current workgroup in `selected-frame', and +NOUPDATE is nil, set its working wconfig in `selected-frame' to +`wg-current-wconfig' and return the updated wconfig. Otherwise +return WORKGROUP's current undo state." + (if (and (not noupdate) (wg-current-workgroup-p workgroup)) + (wg-set-workgroup-working-wconfig workgroup (wg-current-wconfig)) + (wg-with-undo workgroup (state undo-pointer undo-list) + (nth undo-pointer undo-list)))) + +(defun wg-update-current-workgroup-working-wconfig () + "Update `selected-frame's current workgroup's working-wconfig with `wg-current-wconfig'." + (wg-awhen (wg-current-workgroup t) + (wg-set-workgroup-working-wconfig it (wg-current-wconfig)))) + +(defun wg-restore-wconfig-undoably (wconfig &optional noundo) + "Restore WCONFIG in `selected-frame', saving undo information. +Skip undo when NOUNDO." + (when noundo (wg-unflag-undoify-window-configuration-change)) + (wg-update-current-workgroup-working-wconfig) + (wg-restore-wconfig wconfig)) + +(defun wg-workgroup-offset-position-in-undo-list (workgroup increment) + "Increment WORKGROUP's undo-pointer by INCREMENT. +Also restore the wconfig at the incremented undo-pointer if +WORKGROUP is current." + (wg-with-undo workgroup (state undo-pointer undo-list) + (let ((new-pointer (+ undo-pointer increment))) + (when (wg-within new-pointer 0 (length undo-list)) + (when (wg-current-workgroup-p workgroup) + (wg-restore-wconfig-undoably (nth new-pointer undo-list) t)) + (setf (wg-workgroup-state-undo-pointer state) new-pointer))))) + +(defun wg-undoify-window-configuration-change () + "Conditionally `wg-add-wconfig-to-undo-list'. +Added to `post-command-hook'." + (when (and + wg-window-configuration-changed ;; When the window config has changed, + wg-undoify-window-configuration-change ;; and undoification is still on for the current command + (wg-minibuffer-inactive-p)) ;; and the change didn't occur while the minibuffer is active, + (wg-when-let ((workgroup (wg-current-workgroup t))) ;; and there's a current workgroup, + ;; add the current wconfig to that workgroup's undo list: + (wg-add-wconfig-to-undo-list workgroup (wg-current-wconfig)))) + ;; Reset all flags no matter what: + (setq wg-window-configuration-changed nil + wg-undoify-window-configuration-change t + wg-already-updated-working-wconfig nil)) + +(defun wg-update-working-wconfig-hook () + "Used in before advice on all functions that trigger `window-configuration-change-hook'. +To save up to date undo info before the change." + (when (and (not wg-already-updated-working-wconfig) + (wg-minibuffer-inactive-p)) + (wg-update-current-workgroup-working-wconfig) + (setq wg-already-updated-working-wconfig t))) + + +(defun wg-workgroup-gc-buf-uids (workgroup) + "Remove buf uids from WORKGROUP that have no referent in `wg-buf-list'." + (wg-asetf (wg-workgroup-strong-buf-uids workgroup) + (cl-remove-if-not 'wg-find-buf-by-uid it) + (wg-workgroup-weak-buf-uids workgroup) + (cl-remove-if-not 'wg-find-buf-by-uid it))) + +(defun wg-gc-buf-uids () + "Remove from all workgroups those buf uids that have no referent in `wg-buf-list'." + (mapc 'wg-workgroup-gc-buf-uids (wg-workgroup-list))) + + + +(defun wg-display-internal (elt-fn list) + "Return display string built by calling ELT-FN on each element of LIST." + (let ((div (wg-add-face :div wg-list-display-decor-divider)) + (wwidth (window-width (minibuffer-window))) + (i -1) + (str)) + (setq str + (wg-fontify + (:brace wg-list-display-decor-left-brace) + (if (not list) (funcall elt-fn nil nil) + (wg-doconcat (elt list div) (funcall elt-fn elt (cl-incf i)))) + (:brace wg-list-display-decor-right-brace))) + ;; (subseq str 0 wwidth) + )) + +(defun wg-workgroup-list-display (&optional workgroup-list) + "Return the Workgroups list display string. +The string contains the names of all workgroups in `wg-workgroup-list', +decorated with faces, dividers and strings identifying the +current and previous workgroups." + (wg-display-internal 'wg-workgroup-display + (or workgroup-list (wg-workgroup-list)))) + +(defun wg-create-first-wg () + "Create a first workgroup if needed." + (if (and workgroups-mode + wg-session-load-on-start + (= (length (wg-workgroup-list)) 0)) + (wg-create-workgroup wg-first-wg-name))) + + +(defun wg-pickel-workgroup-parameters (workgroup) + "Return a copy of WORKGROUP after pickeling its parameters. +If WORKGROUP's parameters are non-nil, otherwise return +WORKGROUP." + (if (not (wg-workgroup-parameters workgroup)) workgroup + (let ((copy (wg-copy-workgroup workgroup))) + (wg-asetf (wg-workgroup-parameters copy) (wg-pickel it)) + copy))) + +(defun wg-unpickel-workgroup-parameters (workgroup) + "If WORKGROUP's parameters are non-nil, return a copy of +WORKGROUP after unpickeling its parameters. Otherwise return +WORKGROUP." + (if (not (wg-workgroup-parameters workgroup)) workgroup + (let ((copy (wg-copy-workgroup workgroup))) + (wg-asetf (wg-workgroup-parameters copy) (wg-unpickel it)) + copy))) + +;;; workgroup-list ops + +(defun wg-delete-workgroup (workgroup) + "Remove WORKGROUP from `wg-workgroup-list'. +Also delete all references to it by `wg-workgroup-state-table', +`wg-current-workgroup' and `wg-previous-workgroup'." + (dolist (frame (frame-list)) + (remhash (wg-workgroup-uid workgroup) (wg-workgroup-state-table frame)) + (when (wg-current-workgroup-p workgroup frame) + (wg-set-current-workgroup nil frame)) + (when (wg-previous-workgroup-p workgroup frame) + (wg-set-previous-workgroup nil frame))) + (setf (wg-workgroup-list) (remove workgroup (wg-workgroup-list-or-error))) + (setf (wg-session-modified (wg-current-session)) t) + workgroup) + +(defun wg-add-workgroup (workgroup &optional index) + "Add WORKGROUP to `wg-workgroup-list' at INDEX or the end. +If a workgroup with the same name exists, overwrite it." + (wg-awhen (wg-find-workgroup-by :name (wg-workgroup-name workgroup) t) + (unless index (setq index (cl-position it (wg-workgroup-list-or-error)))) + (wg-delete-workgroup it)) + (wg-asetf (wg-workgroup-list) + (wg-insert-before workgroup it (or index (length it)))) + (setf (wg-session-modified (wg-current-session)) t) + workgroup) + +(defun wg-check-and-add-workgroup (workgroup) + "Add WORKGROUP to `wg-workgroup-list'. +Ask to overwrite if a workgroup with the same name exists." + (let ((name (wg-workgroup-name workgroup)) + (uid (wg-workgroup-uid workgroup))) + (when (wg-find-workgroup-by :uid uid t) + (error "A workgroup with uid %S already exists" uid)) + (when (wg-find-workgroup-by :name name t) + (unless (or wg-no-confirm-on-destructive-operation + (y-or-n-p (format "%S exists. Overwrite? " name))) + (error "Cancelled")))) + (wg-add-workgroup workgroup)) + +(defun wg-make-and-add-workgroup (name &optional blank) + "Create a workgroup named NAME with current `window-tree'. +If BLANK - then just scratch buffer. +Add it with `wg-check-and-add-workgroup'." + (wg-check-and-add-workgroup + (wg-make-workgroup + :name name + :base-wconfig (if blank (wg-make-blank-wconfig) + (wg-current-wconfig))))) + +(defun wg-get-workgroup-create (workgroup) + "Return the workgroup specified by WORKGROUP, creating a new one if needed. +If `wg-get-workgroup' on WORKGROUP returns a workgroup, return it. +Otherwise, if WORKGROUP is a string, create a new workgroup with +that name and return it. Otherwise error." + (or (wg-get-workgroup workgroup t) + (if (stringp workgroup) + (when (or (not wg-confirm-on-get-workgroup-create) + (y-or-n-p (format "%S doesn't exist. Create it? " + workgroup))) + (wg-make-and-add-workgroup workgroup)) + ;; Call this again for its error message + (wg-get-workgroup workgroup)))) + +(defun wg-cyclic-offset-workgroup (workgroup n) + "Offset WORKGROUP's position in `wg-workgroup-list' by N." + (let ((workgroup-list (wg-workgroup-list-or-error))) + (unless (member workgroup workgroup-list) + (error "Workgroup isn't present in `wg-workgroup-list'.")) + (setf (wg-workgroup-list) (wg-cyclic-offset-elt workgroup workgroup-list n) + (wg-session-modified (wg-current-session)) t))) + +(defun wg-swap-workgroups-in-workgroup-list (workgroup1 workgroup2) + "Swap the positions of WORKGROUP1 and WORKGROUP2 in `wg-workgroup-list'." + (let ((workgroup-list (wg-workgroup-list-or-error))) + (when (eq workgroup1 workgroup2) + (error "Can't swap a workgroup with itself")) + (unless (and (memq workgroup1 workgroup-list) + (memq workgroup2 workgroup-list)) + (error "Both workgroups aren't present in `wg-workgroup-list'.")) + (setf (wg-workgroup-list) (wg-util-swap workgroup1 workgroup2 workgroup-list) + (wg-session-modified (wg-current-session)) t))) + + +(provide 'workgroups-workgroup) +;;; workgroups-workgroup.el ends here