This package allows the user to manage multiple projects in a single Emacs instance. It uses the built-in desktop.el package to save and restore collections (i.e. user-defined groups of window configurations, frames and buffers) over Emacs restarts. It also gives you commands to work with these projects (e.g. create them, switch to a buffer within the project, cycle the window configurations (“tabs”) belonging to the project, etc.). Many more features are provided, see below.
The name is a play on “per project”, since this package manages frames, buffers and tabs per project.
All demonstrations use the full default configuration, the doom one theme, a modified version of echo-bar and consult, vertico, marginalia etc.
These demonstrations are supposed to give you a small glimpse into the features offered by perject. There are many more, which are mentioned in text below. If you like what you see, give the package a try!
demo.mp4
In the first demo, you can see that the bottom right shows the current collection (“elisp”) and the current project (“perject”) within this collection.
After calling consult-buffer
to switch buffer, only the buffers that belong to the current project are offered as candidates.
By pressing SPC I force Emacs to show all buffers, not just those belonging to the project.
I then switch to a different project within a different collection, namely “math | category” (collection “math”, project “category”).
When calling consult-buffer
within this context, the offered buffers have changed.
I switch to another project “fractals” within the same collection and of course the candidates when switching buffers have again changed.
ibuffer-demo.mp4
Here I call ibuffer, remove a buffer from the current project and then add two buffers to the current project.
tabs-demo.mp4
I demonstrate the basic functionality of perject-tab. By looking at the bottom right, we can see that the current project “elisp | perject” (collection “elisp”, project “perject”) has three tabs (“window configurations”) and we are currently viewing the first. I then switch to the second tab and after a short while back to the first. You can see that the window configuration changes accordingly.
Back at the first tab, we switch to a different buffer (consult.el) and after switching back and forth, we see that the first tab did not update its value. This is because its current state is “dynamic” (see below). After changing the state to “mutable” and performing the buffer switch again, we see that the tab’s value now updates when switching to a different tab.
This package gives Emacs a notion of “current project” and this added context makes working with Emacs more convenient.
For example, when switching buffers using consult-buffer
, Emacs will offer the buffers belonging to the current project.
After all, within e.g. the context of the project “haskell” it makes no sense to show the Emacs configuration buffer,
whereas within a project “org” used for organizational purposes, the haskell files are completely irrelevant.
This is somewhat analogous to the buffer-local variable default-directory, which saves the current directory of a buffer,
allowing Emacs to start in that directory when calling M-x find-file
.
In the same way, perject assigns a context (project) to a frame, which makes working on the current task more convenient.
Here we outline the terminology used throughout this document. How to make use of these various concepts is outlined below.
A project (in the perject sense) groups frames, buffers and tabs (with perject-tab) that serve a common purpose.
Which of those belong to a particular project is determined by the user. They are preserved between Emacs sessions using the built-in package desktop.el.
Furthermore, hooks are configured (see the variable perject-auto-add-hooks
) to automatically assign the current buffer to the current project in certain situations (e.g. when opening a file).
This ensures that the user rarely has to manually add a buffer to a project.
While a buffer can belong to multiple projects, a frame can only belong to a single project (or collection or neither). Similarly, every tab (see below) belongs to exactly one project.
A collection consists of any number of projects and every project belongs to precisely one collection. Collections are saved and restored using desktop.el. Every collection is saved in a single desktop file. As such, their one and only purpose is to group projects into appropriate blocks, allowing quick loading of and switching between related projects. In particular, collections do not have a list of associated buffers or tabs. However, a frame may be associated with a collection, though there is usually no real benefit in this.
For example, a collection “elisp” might consist of various Emacs lisp projects; a collection “uni” might have one project per university course or a collection “work” could have one project per client.
Of course, it is up to the user to define meaningful collections to suit the workflow. Note that there is also the option to completely ignore the concept of collections by using only a single collection and adding all projects to that collection.
We call a collection active if it is currently loaded in Emacs. That means that it was just created or it was previously loaded from its desktop file.
In contrast, an inactive collection is one that is not loaded but has a corresponding desktop file (within an appropriately named subdirectory of perject-directory
).
With the optional module perject-tab, a project may also contain window configurations (which we call tabs for brevity). These are then saved and restored upon exiting and restarting Emacs and the user may quickly switch between them.
A new collection can be created using perject-open
. The same command is also used to restore previously opened collections from the corresponding desktop files.
To close a collection, run the command perject-close
.
A project can also be reloaded using perject-reload
. This means that any changes to the collection are discarded and its state is reverted it to that of the previous save.
For convenience, these commands are combined into the command perject-open-close-or-reload
(see its documentation).
After opening a collection, the user may create a new project using perject-switch
.
The command is also used to switch to an existing project of the current collection.
After creating a project, you may want to create a new frame for it (perject-create-new-frame
) or add various buffers to the project.
The latter is achieved using the command perject-add-buffer-to-project
.
In case you want to add multiple buffers to the same project, it might be more convenient to use ibuffer and perject-ibuffer-add-to-project
.
It is also possible to open a collection in a new Emacs process using perject-open-in-new-instance
.
Within a collection a user can cycle between the various projects using perject-next-project
and perject-previous-project
.
One can also cycle between the different collections via perject-next-collection
and perject-previous-collection
.
When exiting, Emacs will save the active collections as determined by the variable perject-save-on-exit
but the user may always manually save one or multiple collections using perject-save
.
Projects and collections can also be renamed (perject-rename
), deleted (perject-delete
) and sorted (perject-sort
).
The command perject-print-buffer-projects
lists the projects to which the current buffer belongs.
The perject-consult package integrates perject with the excellent consult package.
More precisely, it adds two new sources to the consult-buffer
command
(which is an enhanced version of the built-in switch-to-buffer
command).
With the default configuration (see below), calling this command within a frame that currently belongs to a project will restrict
the buffer list to those buffers belonging to the current collection.
By pressing SPC
at the beginning of the prompt (which initiates narrowing), all buffers become available.
The user may also restrict the candidates to the buffers belonging to the current collection
(meaning that there exists some project within the collection to which the buffer belongs) by pressing c
at the beginning of the prompt.
The perject-ibuffer package intgrates perject with the built-in ibuffer package.
More precisely, it adds two new filters, namely ibuffer-filter-by-project
and ibuffer-filter-by-collection
,
which allow restricting the ibuffer list to only those buffers belonging to a particular project (or collection).
It also provides commands to add (perject-ibuffer-add-to-project
) or remove (perject-ibuffer-remove-from-project
) the marked buffers within ibuffer to the current project (or a selected project).
Additionally, the command perject-ibuffer-print-buffer-projects
prints the projects to which the buffer at point (within ibuffer) belongs.
Perject-tab allows the user to save and restore the window configurations belonging to a project.
This uses the built-in library tab-bar.el.
Every project has a list of tabs (window configurations), which can be cycled using perject-tab-next
and perject-tab-previous
.
You can also switch to the perject-tab-switch
.
Create a new tab with perject-tab-create
and delete it using perject-tab-delete
.
When cycling, it might be convenient to reorder the tabs in certain situations.
To that end, the commands perject-tab-decrement-index
and perject-tab-increment-index
are provided.
The index of the current and previous tab are saved, so that the user may easily toggle between the current and previous tab using perject-tab-recent
.
When switching from one project to another, the window configuration will switch to the current tab of the current project.
Whether a tab is updated when switching to a different one is determined by its state. By default, there are three states:
- immutable: An immutable tab is never updated.
- mutable: A mutable tab is always updated.
- dynamic: A dynamic tab is updated only if the new window configuration has the same window layout. This test ignores details such as the values of point and scrolling positions.
You can cycle the state of the current tab using perject-tab-cycle-state
and
custom states can be defined by customizing the variable perject-tab-states
.
You can always set the current tab to the current window configuration by calling perject-tab-set
and reset the current window configuration to that specified by the current tab using perject-tab-reset
.
Perject provides a mode line indicator, which can be customized (and disabled) via the variable perject-mode-line-format
.
It is shown in mode-line-misc-info
, which by default is displayed for every buffer.
It displays the project and collection name (and some information about the tabs if perject-tab-mode
is enabled).
Because displaying this information (which is independent of the current buffer) clutters the screen with redudant information, I suggest using something like echo-bar and configure it to display mode-line-misc-info
.
In that way, the information is only displayed once at the bottom of the screen and not for every buffer in the frame.
See the demonstrations above for how this looks (when used together with a package like echo-bar).
There is also an extra indicator for the mode line perject-mode-line-current
,
that can be added to mode-line-format
like so:
(setq-default mode-line-format
'("%e" mode-line-front-space
(:propertize
("" mode-line-mule-info mode-line-client mode-line-modified mode-line-remote perject-mode-line-current)
display
(min-width
(5.0)))
mode-line-frame-identification mode-line-buffer-identification " " mode-line-position
(vc-mode vc-mode)
" " mode-line-modes mode-line-misc-info mode-line-end-spaces))
Perject adds a new command line option to Emacs.
After passing the argument --perject
, the user may list the collections (comma separated) that should be loaded after Emacs has initialized.
For example, when starting Emacs with –perject “org,elisp”, the collections “org” and “elisp” (and all of their projects) will be restored
after opening Emacs.
Similarly, running Emacs with –perject “” prevents perject from automatically opening any collections on startup.
Note that other features built into Emacs like bookmarks, registers etc. are shared for all projects. However, it should not be hard to implement those facilities if desired.
This package requires at least version 27.1 of GNU Emacs and depends on the following packages:
- desktop.el (built-in)
- seq.el (built-in)
- cl-lib.el (built-in)
- transient.el (built-in since Emacs 28.1)
- tab-bar.el (built-in)
- dash.el
The package is available on MELPA, so after installing MELPA, you can directly install perject via package-install
.
The package manager will take care of installing the dependencies.
Here we outline a possible configuration for perject, which should be put into the user’s Emacs configuration file. perject does not define any keybindings (except via transient), so this must be done in the user’s configuration. Of course, the user might want to change the keybindings defined below. This configuration uses the excellent use-package, which will be built into Emacs from version 29 onwards.
(use-package perject
:after savehist
:init
;; Make perject load the collections that were previously open.
;; This requires configuring `savehist' (see next code block).
(setq perject-load-at-startup 'previous)
(perject-mode 1)
:bind
(:map perject-mode-map
("s-S" . perject-switch)
("s-a" . perject-next-project)
("s-A" . perject-previous-project)
("s-z" . perject-next-collection)
("s-Z" . perject-previous-collection)
("C-x 5 2" . perject-create-new-frame)
("<C-insert>" . perject-add-buffer-to-project)
("<C-delete>" . perject-remove-buffer-from-project)
("<C-home>" . perject-open-close-or-reload)
("<C-end>" . perject-rename)
("<C-S-end>" . perject-sort)
("<C-prior>" . perject-save)
("<C-next>" . perject-delete)))
A mode line entry displaying the current collection, project and tabs (when using perject-tab.el) is enabled by default.
The extra mode line entry perject-mode-line-current
can be added to the mode line (see above).
If perject-load-at-startup
is set to ‘previous, then you need to use the built-in savehist package
in order to save and restore its value like so:
(use-package savehist
:config
(savehist-mode 1)
;; Required if `perject-load-at-startup' is set to 'previous.
(add-to-list 'savehist-additional-variables 'perject--previous-collections))
Note that savehist can furthermore be used to restore global variables that do not have a different value per project.
When using desktop.el with the default configuration, certain global variables are saved to the desktop file.
Because every collection corresponds to one desktop file, keeping these settings would mean that the the value of these global variables
is determined by the collection most recently loaded. In other words, the previous value of these global variables (which might have changed
while using Emacs) is overwritten with that saved in the desktop file whenever a new collection is loaded.
Therefore, perject does not restore these global variables.
Instead, you can use savehist for that purpose by adding the following lines to the previous :config
block:
(add-to-list 'savehist-additional-variables 'tag-file-name)
(add-to-list 'savehist-additional-variables 'tags-table-list)
(add-to-list 'savehist-additional-variables 'search-ring)
(add-to-list 'savehist-additional-variables 'regexp-search-ring)
(add-to-list 'savehist-additional-variables 'register-alist)
(add-to-list 'savehist-additional-variables 'file-name-history)
The variable perject-global-vars-to-save
exists for saving global variables that should depend on the current project.
Optionally load perject-tab
and bind some keys.
(use-package perject-tab
:after perject
:init
(perject-tab-mode 1)
:bind
(:map perject-tab-mode-map
("s-s" . perject-tab-recent)
("s-D" . perject-tab-previous)
("s-d" . perject-tab-next)
("s-f" . perject-tab-set)
("s-F" . perject-tab-cycle-state)
("s-x" . perject-tab-create)
("s-X" . perject-tab-delete)
("s-c" . perject-tab-reset)
("s-v" . perject-tab-increment-index)
("s-V" . perject-tab-decrement-index)))
Before adding the following snippet, ensure that you have a (use-package consult ...)
block within your configuration file.
The following code loads perject-consult
and modifies the command consult-buffer
.
It will by default only display the buffers belonging to the current project. You can also manually narrow to that view with j.
By narrowing with SPC all buffers become available and by narrowing with c only the buffers belonging to the current collection (i.e. to some project of the current collection) are shown.
(use-package perject-consult
:after (perject consult)
:config
;; Hide the list of all buffers by default and set narrowing to all buffers to space.
(consult-customize consult--source-buffer :hidden t :narrow 32)
(consult-customize consult--source-hidden-buffer :narrow ?h)
(add-to-list 'consult-buffer-sources 'perject-consult--source-collection-buffer)
(add-to-list 'consult-buffer-sources 'perject-consult--source-project-buffer))
Load perject-ibuffer
and make ibuffer restrict the buffer list to the buffers of the current project by default.
Run M-x ibuffer-filter-disable
in ibuffer to temporarily remove this filter.
The following snippet also binds some keys.
(use-package perject-ibuffer
:after perject
:init
;; By default restrict ibuffer to the buffers of the current project.
(add-hook 'ibuffer-hook #'perject-ibuffer-enable-filter-by-project)
:bind
(:map ibuffer-mode-map
("<insert>" . perject-ibuffer-add-to-project)
("<delete>" . perject-ibuffer-remove-from-project)
("<next>" . perject-ibuffer-print-buffer-projects)
("/ y" . ibuffer-filter-by-collection)
("/ u" . ibuffer-filter-by-project)))
The variables are extensively documented and should be self-explanatory.
Use M-x customize-group perject
and M-x customize-group perject-tab
to view them.
You can customize the faces used by perject via M-x customize-group perject-faces
.
We mention a couple of special customization options.
This variable is used to systematically add buffers to the current project.
It is a list of hooks and whenever one of the hooks is run, the current buffer is added to the current project.
Therefore, manually adding a buffer to a project (with perject-add-buffer-to-project
) is only rarely required.
There are many hooks that a user may or may not want to add to this variable.
By default, the list contains find-file-hook
, clone-indirect-buffer-hook
and some mode hooks.
While there is no hook that is run after an arbitrary buffer is created (see here),
one could experiment with buffer-list-update-hook
or after-change-major-mode-hook
.
The hook window-selection-change-functions
is a special case since they are called with a frame as its only argument.
It can be used to add a buffer to a project whenever it is shown in a frame of that project.
In that case, one has to also remove the hook before opening a collection (and add it again afterwards), because otherwise the hooks might add the restored buffers to an unwanted project.
For this, use the code:
(defun perject-add-visible-buffers (frame)
"Add the buffers that are visible in the frame FRAME to the current project."
(dolist (buf (cl-remove-duplicates (mapcar #'window-buffer (window-list nil 0))))
(with-current-buffer buf (perject--auto-add-buffer))))
(add-hook 'window-selection-change-functions #'perject-add-visible-buffers)
(add-hook 'perject-before-open-hook
(lambda (&rest _)
(remove-hook 'window-selection-change-functions #'perject-add-visible-buffers)))
(add-hook 'perject-before-open-hook
(lambda (&rest _)
(add-hook 'window-selection-change-functions #'perject-add-visible-buffers)))
This variable controls which buffers are automatically associated with projects.
When a hook in perject-auto-add-hooks
runs, this function is called in order
to decide to which projects the current buffer should be added to.
It is called with two arguments. The first argument is the current buffer. The
second is a cons cell with car a collection name and cdr a project name. This
might be nil or the project name could be nil.
The function should return a list of projects to which the buffer should be
added. By returning nil (the empty list) the buffer is not added to any project.
For example, suppose one has the project “org” within a collection of the same name and one wants
help-mode
and info-mode
buffers to always be added to that project and to no other ones.
The following code implements this behavior:
(defun perject-auto-add-function (buffer project)
"Decide if buffer BUFFER should be added to the project PROJECT.
Returns a list of project names to which BUFFER should be added (might be
empty)."
(if (memq (buffer-local-value 'major-mode buffer) '(help-mode info-mode))
(list (cons "org" "org"))
(list project)))
(setq perject-auto-add-function #'perject-auto-add-function)
A list of global variables to be saved and restored by perject for every collection.
This is a generalization of the variable desktop-globals-to-save
.
A list of buffer-local variables to be saved and restored by perject for every collection.
This is a generalization of the variables desktop-locals-to-save
and desktop-var-serdes-funs
.
This variable determines whether perject raises and focuses a frame in certain situations. In those cases, the function select-frame-set-input-focus is used to raise and focus a frame in perject-open and at startup. However, depending on the window manager, the raising and focusing of the frame might or might not work properly. Therefore, I introduced this variable so that the user can tweak the behavior. For example, one could set the variable to nil and optionally add a custom function to perject-after-open-hook and perject-after-init-hook to perform the frame focusing.
Every collection corresponds to one desktop file which saves the buffers, frames and tabs of that collection (i.e. of all its projects).
Of course, restoring buffers and frames takes time and thus increases the startup time of Emacs.
To decrease the penalty, it is recommended to only load few collections at startup (see perject-load-at-startup
) and load the other ones “on demand” using perject-open
.
This package uses desktop.el
to save and restore the collections.
As such, it can be seen as an enhancement of that package.
With perject, there should never be a reason to directly use the desktop.el library directly
and doing so is not supported. In particular, this applies to all of the desktop-* functions.
To avoid unexpected behavior, the user should additionally keep all desktop-* variables at their default value. Exceptions are the following variables:
desktop-buffers-not-to-save
,desktop-files-not-to-save
,desktop-modes-not-to-save
.
Furthermore, the user should check carefully the use of desktop hooks
and might prefer using perject-desktop-save-hook
and perject-desktop-after-load-hook
(but then the functions are called with one argument).
This package has been tested with Emacs 28 and 29 on Linux. Officially supported is version 27.1 or newer of GNU Emacs on Linux, run in a graphical user interface (not inside a terminal). The package has not been tested on Windows or MacOS and as such, I cannot give any guarantees for these operating systems. When you have issues in that regard, feel free to open an Issue and I will try to assist you in debugging the issue, even though I do not have access to one of those operating systems.
I currently do not know how this package behaves when Emacs is run inside a terminal or when used inside exwm.
Note that whether focusing frames works properly is dependent on the window manager used.
See the variable perject-raise-and-focus-frame
.
The built-in project.el package might seem quite similar to perject.
However, this is not the case. The main difference is what is considered a “project”.
In project.el, every project has a “root directory” in which the files belonging to the project are supposed to live.
In contrast, there is no such assumption in perject.
Projects are completely user-defined and it does not matter where a file is actually located on the file system (also see above “Terminology”).
As such, one can say that the two packages focus on different kinds of projects.
Project.el focuses on projects that are identified “programatically” (usually by being within the same directory, e.g. a git respository),
whereas perject has a more “ad-hoc”, generalized notion of projects.
Note that because projects are saved over restarts and some commands automatically add a buffer to a project
(see perject-auto-add-hooks
), it is only rarely necessary for the user to manually add a buffer to a particular project.
Therefore, one can use project.el alongside perject, in particular since it defines commands like project-find-regexp
which are not provided by perject.
To sum up, if your projects are always given by a collection of files within a root directory, then project.el will probably suffice for your needs; potentially enhanced by some package like tabspaces or project-tab-groups. If however you want a more generalized notion of projects that can be grouped into collections, are preserved over restarts and “naturally” grow as you open and close files, then perject might just be the package for you.
Here we compare the functionality and implementation of perject with that of related packages (which are not based on project.el):
- perspective: This package is quite similar to perject.
However, there are some key differences. First of all, perspective has no notion of collections, so there is no obvious way to group related projects together.
Furthermore, in perject, every collection then is saved to a desktop file and you can load certain collections at startup and manually open additional ones when you need them.
This makes you more flexible, because you can manually load collections as required instead of loading them at startup. To my knowledge, this is not possible in perspective.
You can also decide which collections to save and load in perject, whereas in perspective, you always save or load all projects.
Another important point is that perject uses the built-in desktop.el, but perspective does not.
Second, in perspective, every frame has a distinct list of projects. In contrast, you can switch to any project from within any frame in perject.
perject also provides some heuristics to automatically add buffers to projects (see
perject-auto-add-hooks
). Because of this, at least in my usage of perject, I rarely have to manually callperject-add-buffer-to-project
. Another difference between the two packages are tabs (“window configurations”). perject uses the built-in tab-bar.el under the hood and every project has a distinct list of window configurations, whereas in perspective you only have a single window configuration per project. perject offers some additional features not existent in perspective like a notion of state of tabs (which determine whether a tab is updated or not when switching away from it, see third demo), a command line option and the ability to reload collections. On the other hand, perspective allows having one scratch buffer per project. perject has no such feature. Finally, perspective is more mature. - bufferlo: This package defines a buffer list per frame, which can be restored with desktop.el.
Therefore, this package is also quite close to perject in terms of functionality, but instead of the notion of projects and collections
you just have frames. Functionality like
perject-auto-add-hooks
, tab support etc. are not offered by this package. - burly: This package allows saving and restoring frames and window configurations in Emacs, similar to the built-in package desktop.el. It also adds the ability to “bookmark” these features. It has no notion of projects or collections.
The development of the package began roughly two years ago, when it originally started as a thin wrapper around desktop.el. Since then I have significantly improved the functionality and scope of the package to become the Emacs project management package I always wanted. I can confidently say that the package works without any bugs for my day to day usage. However, due to the sheer size and customization options of this package I am also quite certain that there are still some bugs.
I appreciate your comments and issues, though I may not be able to answer everything due to time constraints.