diff --git a/modules/feature/workspaces/autoload.el b/modules/feature/workspaces/autoload.el
new file mode 100644
index 000000000..e6b5c25ec
--- /dev/null
+++ b/modules/feature/workspaces/autoload.el
@@ -0,0 +1,273 @@
+;;; feature/workspaces/autoload.el
+
+;;;###autoload
+(defun +workspace-cleanup ()
+ "Remove unsavable windows and buffers before we save the window
+configuration."
+ (let (doom-buffer-inhibit-refresh)
+ (doom/popup-close-all)
+ (when (and (featurep 'neotree) (neo-global--window-exists-p))
+ (neotree-hide))))
+
+;;;###autoload
+(defun +workspace-save (session-file)
+ "Save the current workspace as SESSION-FILE. Ensure a workgroup exists to be
+saved."
+ (+workspace-cleanup)
+ (unless (wg-workgroup-list)
+ (wg-create-workgroup wg-first-wg-name))
+ (wg-save-session-as session-file))
+
+;;;###autoload
+(defun +workspace-load (session-file)
+ "Load a workspace from SESSION-FILE."
+ (+workspace-cleanup)
+ (when (not (and session-file (file-exists-p session-file)))
+ (user-error "No session found"))
+ (wg-open-session session-file))
+
+;;;###autoload
+(defun +workspace-new (&optional session-name overwrite-p)
+ "Create a new workspace, named SESSION-NAME (optional) and return it (without
+switching to it). Otherwise, it's named #N, where N is the number of current
+workgroups + 1."
+ (unless session-name
+ (setq session-name (format "#%s" (1+ (length (wg-session-workgroup-list (wg-current-session t)))))))
+ (awhen (and overwrite-p (wg-get-workgroup session-name t))
+ (wg-delete-workgroup new-wg))
+ (let ((new-wg (wg-make-and-add-workgroup session-name t)))
+ (add-to-list '+workspace-names (wg-workgroup-uid new-wg))
+ new-wg))
+
+;;;###autoload
+(defun +workspace-rename (new-name)
+ "Rename the current workspace to NEW-NAME."
+ (unless new-name
+ (user-error "You didn't enter in a name"))
+ (let ((wg (wg-current-workgroup)))
+ (wg-rename-workgroup new-name wg)
+ (add-to-list '+workspace-names (wg-workgroup-uid wg))
+ wg))
+
+;;;###autoload
+(defun +workspace-delete (&optional session-name)
+ "Delete the current workspace."
+ (let* ((current-wg (wg-current-workgroup t))
+ (wg (if session-name (wg-get-workgroup session-name t) current-wg)))
+ (unless wg
+ (user-error "No workgroup found with that name: %s" session-name))
+ (setq +workspace-names (delete (wg-workgroup-uid wg) +workspace-names))
+ (if (eq wg current-wg)
+ (wg-kill-workgroup)
+ (wg-delete-workgroup wg))))
+
+;;;###autoload (autoload '+workspace:save "feature/workspaces/autoload" nil t)
+;;;###autoload (autoload '+workspace:load "feature/workspaces/autoload" nil t)
+;;;###autoload (autoload '+workspace:new "feature/workspaces/autoload" nil t)
+;;;###autoload (autoload '+workspace:rename "feature/workspaces/autoload" nil t)
+;;;###autoload (autoload '+workspace:delete "feature/workspaces/autoload" nil t)
+;;;###autoload (autoload '+workspace:jump-to "feature/workspaces/autoload" nil t)
+;;;###autoload (autoload '+workspace:switch-left "feature/workspaces/autoload" nil t)
+;;;###autoload (autoload '+workspace:switch-right "feature/workspaces/autoload" nil t)
+
+(after! evil
+ (evil-define-command +workspace:save (&optional bang name)
+ (interactive "")
+ (+workspace-save
+ (cond (name (concat wg-workgroup-directory session-name))
+ (bang (concat wg-workgroup-directory (f-filename (doom-project-root))))
+ (t wg-session-file))))
+
+ (evil-define-command +workspace:load (&optional bang name)
+ (interactive "")
+ (+workspace-load
+ (cond (name (concat wg-workgroup-directory session-name))
+ (bang
+ (concat wg-workgroup-directory (f-filename (doom-project-root))))
+ (t wg-session-file)))
+ (+workspace/display t))
+
+ (evil-define-command +workspace:new (bang name)
+ "Create a new workgroup named NAME. If BANG, overwrite any workgroup named
+NAME. If NAME is omitted, autogenerate a name."
+ (interactive "")
+ (let* ((wg (+workspace-new name bang))
+ (wg-name (wg-workgroup-name wg)))
+ (wg-switch-to-workgroup wg)
+ (workspace--display-inline (wg-previous-workgroup t)
+ (format "Created %s" wg-name)
+ 'success)))
+
+ (evil-define-command +workspace:rename (&optional bang new-name)
+ "Rename the current workgroup to NEW-NAME. If BANG and this workgroup has a
+fixed name, un-fix it."
+ (interactive "")
+ (let* ((wg (wg-current-workgroup))
+ (wg-uid (wg-workgroup-uid wg))
+ (old-name (wg-workgroup-name wg)))
+ (if bang
+ (progn
+ (setq +workspace-names (delete wg-uid +workspace-names))
+ (workspace--display-inline t (format "Unfixed '%s'" old-name) 'success))
+ (+workspace-rename new-name)
+ (workspace--display-inline nil (format "Renamed '%s'->'%s'" old-name new-name) 'success))))
+
+ (evil-define-command +workspace:delete (&optional bang name)
+ "Delete the workgroup specified by NAME. If NAME is omitted, delete the
+current workgroup. If BANG, prompts the user for which workgroup to delete."
+ (interactive "")
+ (when bang
+ (setq name (wg-read-workgroup-name))
+ (unless name
+ (user-error "You didn't select a workgroup.")))
+ (let ((wg-name (ignore-errors (wg-workgroup-name (wg-current-workgroup t)))))
+ (+workspace-delete name)
+ (workspace--display-inline nil (format "Deleted %s" wg-name) 'success)))
+
+ (evil-define-command +workspace:jump-to (&optional search)
+ (interactive "")
+ (if search
+ (progn) ;; TODO Fuzzy matching
+ (awhen (wg-read-workgroup-name)
+ (wg-switch-to-workgroup it))))
+
+ (evil-define-command +workspace:switch-left (&optional count)
+ "Switch to the previous workspace on the right. If COUNT, switch to the workspace
+at that index counting from the end."
+ (interactive "")
+ (if count
+ (+workspace/activate-at (- 0 count))
+ (+workspace--switch-in-direction 'left)))
+
+ (evil-define-command +workspace:switch-right (&optional count)
+ "Switch to the next workspace on the right. If COUNT, switch to the workspace
+at that index."
+ (interactive "")
+ (if count
+ (+workspace/activate-at (max 0 (1- count)))
+ (+workspace--switch-in-direction 'right))))
+
+;;;###autoload
+(defun +workspace/kill-others ()
+ "Kill all other workspaces, besides the current one."
+ (interactive)
+ (let (workgroup (wg-current-workgroup))
+ (dolist (w (wg-session-workgroup-list (wg-current-session t)))
+ (unless (wg-current-workgroup-p w)
+ (wg-kill-workgroup w)))))
+
+(defun workspace--display-inline (&optional suppress-update message message-face)
+ (message "%s%s" (+workspace/display suppress-update t)
+ (propertize message 'face message-face)))
+
+;;;###autoload
+(defun +workspace/display (&optional suppress-update return-p message)
+ "Display all the open workspaces in the minibuffer, like tabs."
+ (interactive)
+ (awhen (wg-current-session t)
+ (unless (eq suppress-update t)
+ (+workgroup--update-names (if (wg-workgroup-p suppress-update) suppress-update)))
+ (let ((output (wg-display-internal
+ (lambda (workgroup index)
+ (if (not workgroup) wg-nowg-string
+ (wg-element-display
+ workgroup
+ (format " [%d] %s " (1+ index) (wg-workgroup-name workgroup))
+ 'wg-current-workgroup-p)))
+ (wg-session-workgroup-list it))))
+ (if return-p
+ output
+ (message "%s%s" output (or message ""))))))
+
+(defun +workgroup--update-names (&optional wg)
+ (let ((wg (or wg (wg-current-workgroup))))
+ (unless (member (wg-workgroup-uid wg) +workspace-names)
+ (ignore-errors
+ (let ((old-name (wg-workgroup-name wg))
+ (new-name (f-filename (doom-project-root))))
+ (unless (string= new-name old-name)
+ (wg-rename-workgroup new-name wg)))))))
+
+(defun +workspace--switch-in-direction (direction &optional count)
+ (interactive "")
+ (assert (memq direction '(left right)))
+ (condition-case err
+ (progn
+ (if count
+ (wg-switch-to-workgroup-at-index (1- count))
+ (funcall (intern (format "wg-switch-to-workgroup-%s" direction))))
+ (+workspace/display t))
+ (error (+workspace/display t nil (format "Nope! %s" (cadr err))))))
+
+;;;###autoload
+(defun +workspace-switch-last ()
+ "Switch to the last workspace."
+ (interactive)
+ (let ((count (length (wg-session-workgroup-list (wg-current-session t)))))
+ (+workspace/activate-at (max 0 (1- count)))))
+
+;;;###autoload
+(defun +workspace/activate-at (index)
+ "Switch to a workspace at a given INDEX. A negative number will start from the
+end of the workspace list."
+ (interactive)
+ (+workgroup--update-names)
+ (let ((wg-list (wg-workgroup-list-or-error)))
+ (when (< index 0)
+ (setq index (- (length wg-list) (abs index))))
+ (let ((wg (nth index wg-list))
+ msg)
+ (if wg
+ (unless (eq wg (wg-current-workgroup t))
+ (wg-switch-to-workgroup-at-index index))
+ (setq msg (format "No tab #%s" (1+ index))))
+ (+workspace/display t nil msg))))
+
+;;;###autoload
+(defun +workspace/undo ()
+ (interactive)
+ (call-interactively (if (wg-current-workgroup t) 'wg-undo-wconfig-change 'winner-undo)))
+
+;;;###autoload
+(defun +workspace/redo ()
+ (interactive)
+ (call-interactively (if (wg-current-workgroup t) 'wg-redo-wconfig-change 'winner-redo)))
+
+;;;###autoload
+(defun +workspace/new-frame ()
+ "Create a new frame, and create a new, blank workgroup within it. Also ensure
+nlinum behaves in the process."
+ (interactive)
+ (let ((nlinum-p (and (featurep 'nlinum)
+ (memq 'nlinum--setup-window window-configuration-change-hook))))
+ ;; Disable nlinum to fix elusive "invalid face linum" bug
+ (remove-hook 'window-configuration-change-hook 'nlinum--setup-window t)
+ (let ((frame (new-frame))
+ (frame-name (format "*new-%s*" (length +workspace-frames))))
+ (with-selected-frame frame
+ (wg-create-workgroup frame-name t)
+ (add-to-list '+workspace-frames (cons frame frame-name))))
+ (when nlinum-p
+ (add-hook 'window-configuration-change-hook 'nlinum--setup-window nil t))))
+
+;;;###autoload
+(defun +workspace/close-frame ()
+ (interactive)
+ (let ((frame (assq (selected-frame) +workspace-frames)))
+ (if frame
+ (progn (wg-delete-workgroup (wg-get-workgroup (cdr frame)))
+ (delete-frame (car frame)))
+ (delete-frame))))
+
+;;;###autoload
+(defun +workspace/or-window-close ()
+ (interactive)
+ (if (doom-popup-p)
+ (doom/popup-close)
+ (when (doom/kill-real-buffer)
+ (if (and (one-window-p t)
+ (> (length (wg-workgroup-list)) 1))
+ (if (string= (wg-workgroup-name (wg-current-workgroup)) wg-first-wg-name)
+ (evil-window-delete)
+ (+workspace:delete))
+ (evil-window-delete)))))
diff --git a/modules/feature/workspaces/config.el b/modules/feature/workspaces/config.el
new file mode 100644
index 000000000..ef03d5d17
--- /dev/null
+++ b/modules/feature/workspaces/config.el
@@ -0,0 +1,71 @@
+;;; feature/workspaces/config.el
+
+(defvar +workspace-frames '()
+ "A list of all the frames opened as separate workgroups. See
+defuns/defuns-workgroups.el.")
+
+(defvar +workspace-names '()
+ "Keeps track of the fixed names for workgroups (set with :tabrename), so that
+these workgroups won't be auto-renamed.")
+
+(defvar +workspaces-dir (concat doom-cache-dir "workgroups/")
+ "Path to workspaces.")
+
+
+(use-package! workgroups2 :demand t
+ :init
+ (setq wg-workgroup-directory +workspaces-dir
+ wg-session-file (concat wg-workgroup-directory "last")
+ wg-first-wg-name "*untitled*"
+ wg-session-load-on-start nil
+ wg-mode-line-display-on nil
+ wg-mess-with-buffer-list nil
+ wg-emacs-exit-save-behavior 'save ; Options: 'save 'ask nil
+ wg-workgroups-mode-exit-save-behavior 'save
+ wg-log-level 0
+
+ ;; NOTE: Some of these make workgroup-restoration unstable
+ wg-restore-mark t
+ wg-restore-frame-position t
+ wg-restore-remote-buffers nil
+ wg-restore-scroll-bars nil
+ wg-restore-fringes nil
+ wg-restore-margins nil
+ wg-restore-point-max t ; Throws silent errors if non-nil
+
+ wg-list-display-decor-divider " "
+ wg-list-display-decor-left-brace ""
+ wg-list-display-decor-right-brace "| "
+ wg-list-display-decor-current-left ""
+ wg-list-display-decor-current-right ""
+ wg-list-display-decor-previous-left ""
+ wg-list-display-decor-previous-right "")
+
+ :config
+ (eval-when-compile
+ (unless (f-exists-p +workspaces-dir)
+ (make-directory +workspaces-dir t)))
+
+ ;; Remember fixed workgroup names between sessions
+ (push '+workspace-names savehist-additional-variables)
+
+ ;; `wg-mode-line-display-on' wasn't enough
+ (advice-add 'wg-change-modeline :override 'ignore)
+ ;; Don't remember popup and neotree windows
+ (add-hook 'kill-emacs-hook '+workspace-cleanup)
+
+ (after! projectile
+ ;; Create a new workspace on project switch
+ ;; FIXME Possibly bug?
+ (defun doom*workspace-projectile-switch-project ()
+ (let ((project-root (doom-project-root)))
+ (doom:workspace-new nil (file-name-nondirectory (directory-file-name project-root)) t)
+ (doom-reload-scratch-buffer project-root)
+ (when (featurep 'neotree)
+ (neotree-projectile-action))))
+ (setq projectile-switch-project-action 'doom*workspace-projectile-switch-project))
+
+ (workgroups-mode +1)
+ ;; Ensure there is always a workgroup active
+ (wg-create-workgroup wg-first-wg-name))
+
diff --git a/modules/feature/workspaces/packages.el b/modules/feature/workspaces/packages.el
new file mode 100644
index 000000000..835748527
--- /dev/null
+++ b/modules/feature/workspaces/packages.el
@@ -0,0 +1,5 @@
+;; -*- no-byte-compile: t; -*-
+;;; feature/workspaces/packages.el
+
+(package! workgroups2)
+