Showing preview only (240K chars total). Download the full file or copy to clipboard to get everything.
Repository: jamescherti/easysession.el
Branch: main
Commit: 69eede7a3be3
Files: 17
Total size: 231.0 KB
Directory structure:
gitextract_xt80m82j/
├── .dir-locals.el
├── .github/
│ ├── .nosearch
│ ├── FUNDING.yml
│ └── workflows/
│ ├── ci.yml
│ └── melpazoid.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG.md
├── Cask
├── LICENSE
├── Makefile
├── README.md
├── easysession.el
├── extensions/
│ ├── easysession-magit.el
│ └── easysession-scratch.el
└── tests/
├── .nosearch
└── test-easysession.el
================================================
FILE CONTENTS
================================================
================================================
FILE: .dir-locals.el
================================================
;; pre-commit-elisp .dir-locals.el | https://github.com/jamescherti/pre-commit-elisp
((nil . ((pre-commit-elisp-load-path . ("."))
(pre-commit-elisp-error-on-compile-warning . t))))
================================================
FILE: .github/.nosearch
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: jamescherti
================================================
FILE: .github/workflows/ci.yml
================================================
---
#
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This file is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
#
name: CI
on: # yamllint disable-line rule:truthy
push:
branches:
- main
- develop
pull_request:
branches:
- main
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
emacs-version:
- 26.3
- 27.1
- 28.1
- 29.1
- 29.4
- 30.1
python-version:
- 3.11
steps:
# Cask
- uses: actions/checkout@v2
- uses: purcell/setup-emacs@master
with:
version: ${{ matrix.emacs-version }}
- uses: actions/cache@v4
id: cache-cask-packages
with:
path: .cask
key: cache-cask-packages-000
- uses: actions/cache@v4
id: cache-cask-executable
with:
path: ~/.cask
key: cache-cask-executable-000
- uses: cask/setup-cask@master
if: steps.cache-cask-executable.outputs.cache-hit != 'true'
with:
version: snapshot
- run: echo "$HOME/.cask/bin" >> $GITHUB_PATH
# Tests
- name: Compile
run: make compile
env:
CASK_PATH: $HOME/.cask/bin
- name: Package-lint
run: make package-lint
env:
CASK_PATH: $HOME/.cask/bin
- name: Unit-tests
run: make test
env:
CASK_PATH: $HOME/.cask/bin
================================================
FILE: .github/workflows/melpazoid.yml
================================================
---
# melpazoid <https://github.com/riscy/melpazoid> build checks.
# If your package is on GitHub, enable melpazoid's checks by copying this file
# to .github/workflows/melpazoid.yml and modifying RECIPE and EXIST_OK below.
name: melpazoid
on: # yamllint disable-line rule:truthy
push:
branches:
- main
- develop
pull_request:
branches:
- main
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install
run: |
python -m pip install --upgrade pip
sudo apt-get install emacs && emacs --version
git clone https://github.com/riscy/melpazoid.git ~/melpazoid
pip install ~/melpazoid
cat ~/melpazoid/melpazoid/melpazoid.el
- name: Run
env:
LOCAL_REPO: "${{ github.workspace }}"
RECIPE: (easysession :fetcher github :repo "jamescherti/easysession.el" :branch "${{ github.ref_name }}" :files ("easysession.el"))
# Set EXIST_OK to false (or remove it) if the package isn't on MELPA
EXIST_OK: true
run: echo $GITHUB_REF && make -C ~/melpazoid
================================================
FILE: .gitignore
================================================
flycheck_*
*.elc
================================================
FILE: .pre-commit-config.yaml
================================================
---
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/jamescherti/pre-commit-elisp
rev: v1.0.9
hooks:
- id: elisp-check-parens
- id: elisp-check-byte-compile
exclude: '(^|/)\.dir-locals\.el$'
# TODO fix regexp
# exclude: '\.dir-locals\.el|test-easysession\.el)'
================================================
FILE: CHANGELOG.md
================================================
# easysession - Changelog
**URL:** https://github.com/jamescherti/easysession.el
**Author:** [James Cherti](https://www.jamescherti.com/)
## WIP
* Change all defvar options to defcustom
* Move the `easysession-save-mode-lighter-session-name-spec` sexp into the `easysession--lighter-session-name-format` function
* Fix an issue where narrowing was not correctly restored. The `easysession--restore-buffer-state` function now unconditionally clears any existing narrowing before applying saved boundaries. This ensures that buffers remaining open across session switches correctly reflect the saved state, even if the narrowing has changed or should be removed.
* `easysession-reset`, `easysession-save-session-and-close-frames`, and `easysession-load`: Set `inhibit-redisplay` to `t` to prevent visual flickering caused by window splitting and resizing. Modifying the frameset and windows requires no user input, making it safe to freeze the display.
## 1.2.1
* Update the auto-save timer dynamically when `easysession-save-interval` changes by centralizing the timer logic into `easysession--update-timer`. This ensures the timer is correctly cancelled and restarted based on the current mode state. A `:set` function was added to `easysession-save-interval` so that modifying the variable immediately updates the running timer without requiring a restart of the mode.
* Check that the session path is not an existing file before attempting to create the session directory.
* Add the `easysession-kill-all-buffers` function to kill all buffers except for internal system buffers and modified files.
* Ensure errors in `easysession--auto-save` are ignored since this `easysession--auto-save' is part of `kill-emacs-query-functions`. Returning nil would prevent Emacs from exiting.
* Fix `redisplay-skip-fontification-on-input` support: Ensure buffers are fully fontified, preventing deferred or partial syntax highlighting.
* Simplify the implementation of `easysession-load` to reduce code complexity. Extract the font lock restoration logic into a new dedicated helper function, `easysession--ensure-font-lock`. Add pre-validation for load handlers to ensure they are bound functions before execution.
* Update `easysession-delete` to pre-validate that all selected session files exist before prompting the user or performing any destructive actions. Add logic to automatically unload the current session if it is among the sessions being deleted.
* `easysession-save`: Force window state deserialization in `tab-bar-mode`, correcting an issue where background tabs displayed outdated names after buffers were renamed by packages like uniquify.
* Add the `easysession-font-lock-fontify` variable, which ensures the buffer has been fontified.
* Add the `easysession-refresh-tab-bar` variable to force a state and name refresh of all tabs.
* Add new defaults to `easysession-exclude-from-find-file-hook`: `save-place-find-file-hook` and `auto-revert-find-file-function`. This prevents cursor jumping and unnecessary file tracking during restoration.
* Change the following state variables during session loading:
* `auto-insert`: Set to nil to prevent interactive boilerplate prompts if a saved file is missing or empty.
* `inhibit-message`: Set to t to suppress "Reading file..." logs in the echo area, ensuring a clean and flicker-free startup.
* Disable persistence of margins to prevent layout artifacts when switching displays or toggling modes like olivetti, perfect-margin, or visual-fill-column.
* Add support for the Elpaca package manager in `easysession-setup`. The function now dynamically detects Elpaca and uses `elpaca-after-init-hook` instead of `emacs-startup-hook`, ensuring that sessions are restored only after all asynchronous package installations have finished.
* Auto save session: Inhibit interaction to prevent background session saves from blocking the user interface or causing Emacs to hang. This ensures that background tasks remain intrusive, allowing the editor to stay responsive even if a save handler attempts to initiate a user prompt.
* Improve session persistence in daemon mode by calling `easysession-save` before the last active client frame closes. To support this, the `easysession--frame-list` function was modified to accurately track real user sessions by explicitly excluding the initial daemon terminal, child frames, tooltips, and hidden graphical frames, while correctly retaining minimized GUI frames and all terminal connections.
* Add support for saving and restoring explicitly set frame names with `M-x set-frame-name`. (Implement `easysession--frameset-filter-name-if-explicit` to conditionally save the `name` frame parameter and update `easysession--load-frameset` to pass custom filter alists to `frameset-restore`, ensuring custom rules are respected during session loading.)
* Ensure that iconified frames are treated as visible.
## 1.2.0
* `easysession-save`: Serialization now reliably produces `(QUOTE . SEXP)` pairs, preserving the exact structure of saved values. This ensures correct quoting of complex Emacs Lisp objects, including structs (`#s(...)`) and other otherwise unreadable or unloadable entities (`#>`), preventing errors when restoring sessions.
* Make easysession-save more memory efficient.
* Bug fix: Prevent `easysession-save` from adding indirect buffers to the list of base buffers.
* Refactored `easysession-load` to use direct buffer-stream deserialization rather than string-based evaluation, reducing memory overhead and computational pressure when restoring large session files.
* Add easysession-setup-load-predicate: A predicate to determine whether the `easysession-load` function load should occur.
* Convert warnings to errors in `easysession-load`, `easysession-delete` for missing sessions.
* Add interactive session selection prompts to `easysession-load` and `easysession-save`. Make `easysession-save-as` obsolete in favor of `easysession-save`.
* Fix frameset restoration in Emacs daemon mode: Avoid cleaning up the initial daemon frame during frameset restoration by disabling frame cleanup when running under a daemon with a single frame.
* Save the current session when a client frame is deleted in daemon mode, ensuring the session is preserved before the last frame closes and enabling complete restoration when a new frame is subsequently created.
* Added the `easysession-visible-buffer-list` function, allowing session persistence and restoration to be limited to buffers that are currently visible in windows or associated with visible tab-bar tabs.
* Enhanced session buffer restoration logic (`easysession--handler-load-file-editing-buffers` and `easysession--handler-load-indirect-buffers`) to improve robustness and structural clarity.
* Added two interactive confirmations to `easysession-switch-to`:
* Prompt the user before creating a new session, requiring explicit approval to switch to a session that does not yet exist.
* When reloading the current session, prompt the user to confirm saving it before reloading it.
* Contributions by Emre Yolcu (@emreyolcu on GitHub):
* Added Magit integration through the `easysession-magit` extension (`extensions/easysession-magit.el`). This introduces `easysession-magit-mode`, allowing Magit status, log, and diff buffers to be persisted and restored across sessions. Log and diff buffers retain their arguments, including selected revisions and ranges, ensuring continuity when working with Git repositories.
* Introduced a mode registration system for persisting and restoring custom major modes across EasySession sessions. Users can register and unregister modes using `easysession-add-managed-major-mode` and `easysession-remove-managed-major-mode`.
* Add support for deleting multiple sessions at once via `easysession-delete`.
* Do not run `jit-lock-fontify-now` when `redisplay-skip-fontification-on-input` is non-nil.
* Support the `desktop-dont-save` frame parameter.
* Contribution by Herman He (@hermanhel on GitHub): Added support for persisting and restoring the narrowing state of both base and indirect buffers.
* Track successful session loads and prevent auto-save from writing if the session has not been loaded.
* Add the following to the session file: comment, file-format-version, and mtime.
* Add `easysession-unload`, which saves the active session and then clears all in-memory session state.
* When `easysession-switch-to-exclude-current` is non-nil, ensure the session is excluded from the prompt only after it has been loaded.
* Add the `easysession-edit` command, enabling direct editing of the currently loaded session.
* Add `easysession-save-in-progress` to indicate when a session is actively being saved.
* Fix conflict between easysession buffer renaming and uniquify. Avoid an ordering issue where easysession renames buffers while uniquify is managing their names, which could result in inconsistent uniquify-managed state or unexpected buffer names.
* Enhance `easysession-reset` safety by targeting only non-file buffers and unmodified file-visiting buffers.
* Add `easysession-save-pretty-print` variable to control pretty-printing of session data when saving. When non-nil, session files are written in a human-readable format.
* Introduce `easysession-setup` and `easysession-setup-load-session` to simplify EasySession configuration in both normal and daemon modes.
* Ensure `easysession-save-mode` cancels the timer before creating a new one.
* Ensure the mode-line indicator updates immediately.
* Load indirect buffers for all buffers, not only file-visiting and Dired buffers.
* Add the `easysession-save-and-close-all-frames` function to save the session and close all frames without terminating the Emacs daemon.
## 1.1.7
* Fix #48: Prevent clearing `file-name-handler-alist` during execution
## 1.1.6
* Update the `easysession-load` function to set the session name when the session file is absent
* Revise docstrings and refresh `README.md` for improved clarity and completeness
## 1.1.5
* Add the `easysession-scratch` extension to enable persisting and restoring the scratch buffer in session files
* Ensure the session name is set only after the session has been successfully loaded
* Improve geometry restoration
* Enable restoring TTY sessions to GUI
* Refine mode line and enhance error message clarity
* Close #39: Add a command to switch sessions and restore window geometry
* Fix #45: Update modeline and current session only if a session is loaded
* Resolve warning: Assignment to the free variable `trusted-content`
* Update handler functions to improve reliability
* Add pre-commit hooks for Emacs Lisp to enforce code quality
* Improve `easysession-save-handler-dolist-buffers` macro for buffer handling
* Add macro declarations: `(declare (indent 0) (debug t))`
* Rename `defcustom` to `easysession-switch-to-save-session`
* Introduce `defcustom`: `easysession-switch-to-inhibit-save`
* Introduce new functions: `easysession-set-current-session-name` and `easysession-reset`
* Revise docstrings and update `README.md` for clarity and completeness
* Remove the `.images/` directory to clean up unused files
* Ensure `easysession-frameset-restore-geometry` resets automatically after `easysession-load`
* Modify `easysession-save-as` to save a session without switching to it
* Remove `&optional` from `easysession-rename` for clarity
## 1.1.4
* Add macros to simplify the definition of load/save handlers, enabling users to create custom handlers and extend EasySession more easily
* Optimize EasySession functions to improve the performance of session loading and saving operations.
* Contribution by Artem Tseranu: Allow excluding the current session when switching sessions
* Rename session prompts to include the current session name
* Update `.github` files, docstrings, and GitHub Actions
* Improve session saving by making `write-region` more efficient and reliable for file output.
* Enhance how the session file is read from disk
* Fix `easysession-save-as` `(interactive)` form and update rename session to include the session name in the prompt
* Add a `.nosearch` file to the `tests/` directory
* Add `CHANGELOG.md`
## 1.1.3
* Enhance and refactor the source code
* Improve session loading and saving
* Enhance unit tests
* Replace the `f` package with built-in functions to reduce dependencies
* Fix: Adjust settings to ensure EasySession restores all frames
* Fix: Prevent the Dirvish package from breaking the session file
* Fix: Refactor frameset filtering into a dedicated function
* Fix: `lsp-ui-doc`: Failed to evaluate session information
* Add option to exclude specific functions from `find-file-hook` when restoring a file
* Replace `(dired-current-directory)` with `default-directory`
* Add variable: `easysession-frameset-restore-geometry`
* Ensure `easysession-load` sets `easysession--load-error` to `t` on failure
* Remove direct `require` and replace it with `default-directory`
* Add function: `easysession-path`
* Rename functions and variables such (`easysession--is-loading`, `easysession-get-session-name`, `easysession-get-session-file-path`)
* Enhance functions to get session file path and session name
## 1.1.2
* Ensure access to frames during the Emacs shutdown process
* Enable `tab-bar-mode` if the loaded session includes a `tab-bar`
* Removed reliance on `kill-emacs-hook` for cleanup, as frames were no longer accessible at that stage.
* Add a check to save handlers
* Add an error message when the handler type is not a function
## 1.1.1
* Refactor the load indirect buffers function: simplify logic and improve readability
* Enhance restoring frameset (customizable and disabled when running as a daemon)
* Contribution by Enzo Gurgel: Display the session name in the mode lighter.
* Make easysession functions utilize the `session-name` argument
* Make unit tests delete the test session
* Make `easysession-save-as` ask the user to enter a session when `session-name` is not specified
* Make `easysession-delete` return `t` when the session is successfully deleted
* Remove docstring warning
* Add autoload to interactive functions
* Add easysession-frameset-restore-force-display, easysession-frameset-restore-force-onscreen, additional frameset-restore options, easysession-frameset-restore-cleanup-frames, easysession-frameset-restore-reuse-frames
* Change the default value of `easysession-frameset-restore-force-onscreen` to `(display-graphic-p)`
* Ensure handlers return a non-nil value
## 1.1.0
* Implement handlers allowing EasySession to become extensible. It allows users to add their own handlers that can include entries in the session file.
* Fix: Fontify buffers when `redisplay-skip-fontification-on-input` is non-nil
* Create unit tests for EasySession using GitHub Actions and Cask
* Add predicate that determines if the session is saved automatically
* Update docstrings
* Renames `easysession-set-current-session` to `easysession--set-current-session`.
* Change lighter to EasySes
* Ignore `jit-lock-fontify-now` errors
## 1.0.5
* Fix issue that prevented EasySession from saving/loading frame geometry
* Use `bound-and-true-p` to check if `easysession--load-geometry` is set to `t`
* Add a defcustom containing a function to retrieve buffers for persistence and restoration
* Fix warnings
* Add `:nowarn` to `find-file-noselect`
## 1.0.4
* Optimize loading file editing buffers and indirect buffers
* Improve detection of indirect buffers
* Improve error handling and messages
* Enhance `easysession-set-current-session` session name check
* Ensure `easysession--handler-load-base-buffers` loads base buffers
* Fix: Ensure indirect buffer names match frameset buffer names
* Fix: Add the `easysession-quiet` defcustom
* Simplify the `easysession--handler-load-base-buffers` function
* Add `easysession-before-save-hook` and `easysession-after-save-hook`
* Add `:group` to `easysession-save-interval` and rename `easysession-timer` to `easysession--timer`
* Make easysession use `frameset-persistent-filter-alist` instead of `frameset-filter-alist`
* Make `easysession--init-frame-parameters-filters` parameter mandatory
* Fix `frameset-filter-alist` bug and improve session reading
* Rename variables (`easysession--is-valid-session-name` and `easysession-after-new-session-created-hook`)
## 1.0.3
* Handle frameset and buffer errors when loading a session, and update README.md
* Add `auto-save-interval` and `easysession--load-geometry`
* Correct `easysession-save-interval` variable name
* Add the `easysession--is-loading-p` variable
* Remove `interactive` from `easysession-load-including-geometry`
* Fix warnings
## 1.0.2
* New feature: save/load geometry
* Add the `easysession-rename` function
* Fix the `make-directory` error when a file's parent directory is missing
* Fix issue with `frameset-restore` caused by a change in the file format
* Fix error: Cannot open load file: No such file or directory: f
* Add new options, variables, and functions: `easysession-persist-geometry`, `easysession--get-geometry-frameset-filter-alist`, `frameset--text-pixel-width`, `frameset--text-pixel-height`, and `easysession-load-including-geometry`
* Fix warning: `called-interactively-p` called with 0 arguments but requires 1
* Fix warning: unused lexical argument `session-name`
## 1.0.1
* Fix border color and border width issues
* Add support for indirect buffers
* Rename variables and update comments
* Add list of geometry parameters
* Fix warnings
* Optimization: Decrease the frequency of calls to `easysession--init-frame-parameters-filters` to improve efficiency.
## 1.0.0
* Initial version of EasySession
* Add docstrings
* Add `easysession-mode`
* Add additional hooks
* Remove the `emacs-startup-hook` function
* Fix warning: the function `dired-current-directory` is not known to be defined
================================================
FILE: Cask
================================================
(source gnu)
(source melpa)
(package-file "easysession.el")
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
================================================
FILE: Makefile
================================================
#
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This file is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
#
export EMACS ?= $(shell command -v emacs 2>/dev/null)
CASK_DIR := $(shell cask package-directory)
INIT_PACKAGES="(progn \
(require 'package) \
(push '(\"melpa-stable\" . \"https://stable.melpa.org/packages/\") package-archives) \
(customize-set-variable 'package-archive-priorities \
'((\"gnu\" . 99) (\"nongnu\" . 80) \
(\"melpa-stable\" . 70) (\"melpa\" . 0))) \
(package-initialize) \
(package-refresh-contents) \
(dolist (pkg '(package-lint)) \
(unless (assoc pkg package-archive-contents) \
(package-refresh-contents)) \
(unless (package-installed-p pkg) \
(package-install pkg))))"
$(CASK_DIR): Cask
cask install
@touch $(CASK_DIR)
.PHONY: cask
cask: $(CASK_DIR)
.PHONY: compile
compile: cask
cask emacs -batch -L . \
--eval "(setq byte-compile-error-on-warn t)" \
-f batch-byte-compile $$(cask files); \
(ret=$$? ; cask clean-elc && exit $$ret)
.PHONY: package-lint
package-lint:
cask emacs -Q --eval ${INIT_PACKAGES} -batch -f package-lint-batch-and-exit easysession.el
.PHONY: test
test:
if test -d tests; then cask emacs --batch -L . -L tests -l tests/test-easysession.el -f ert-run-tests-batch-and-exit; else true; fi
================================================
FILE: README.md
================================================
# easysession.el: Easily persist and restore Emacs sessions (windows, tab-bar, file buffers, scratch, Dired, narrowing, indirect buffers/clones, Magit buffers, scratch...); a robust desktop.el replacement

[](https://melpa.org/#/easysession)
[](https://stable.melpa.org/#/easysession)


The **easysession** Emacs package provides a comprehensive session management for Emacs. It is capable of persisting and restoring file-visiting buffers, indirect buffers (clones), buffer narrowing, Dired buffers, window configurations, the built-in tab-bar (including tabs, their buffers, and associated windows), as well as entire Emacs frames (frame name, size, position, etc.).
With **easysession**, your Emacs setup is restored automatically when you restart. All files, Dired buffers, and window layouts come back as they were, so you can continue working right where you left off. While editing, you can also switch to another session, switch back, rename sessions, or delete them, giving you full control over multiple work environments.
Easysession also supports extensions, enabling the restoration of Magit buffers and the scratch buffer. Custom extensions can also be created to extend its functionality.
<p align="center">
<img src="https://jamescherti.com/misc/easysession-m.png" width="40%" />
</p>
If this package enhances your workflow, please show your support by **⭐ starring EasySession on GitHub** to help more users discover its benefits.
**Key features include:**
* Quickly switch between sessions while editing with or without disrupting the frame geometry.
* Capture the full Emacs workspace state: file buffers, indirect buffers and clones, buffer narrowing, Dired buffers, window layouts and splits, the built-in tab-bar with its tabs and buffers, and Emacs frames with optional position and size restoration.
* Built from the ground up with an emphasis on speed, minimalism, and predictable behavior, even in large or long-running Emacs setups.
* Supports both standard Emacs sessions and Emacs running in daemon mode.
* Never lose context with automatic session persistence. (Enable `easysession-save-mode` to save the active session at regular intervals defined by `easysession-save-interval` and again on Emacs exit.)
* Comprehensive command set for session management: switch sessions instantly with `easysession-switch-to`, save with `easysession-save`, delete with `easysession-delete`, and rename with `easysession-rename`.
* Highly extensible architecture that allows custom handlers for non-file buffers, making it possible to restore complex or project-specific buffers exactly as needed.
* Fine-grained control over file restoration by selectively excluding individual functions from `find-file-hook` during session loading via `easysession-exclude-from-find-file-hook`.
* Clear visibility of the active session through modeline integration or a lighter.
* Built-in predicate to determine whether the current session qualifies for automatic saving.
* Save and unload the currently loaded session using `easysession-unload`.
* Exact restoration of **narrowed regions** in both base and indirect buffers, ensuring each buffer reopens with the same visible scope as when it was saved.
* Optional **scratch buffer persistence** via the **extensions/easysession-scratch.el** extension, preserving notes and experiments across restarts.
* Optional **Magit state restoration** via the **extensions/easysession-magit.el** extension, keeping version control workflows intact.
<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
## Table of Contents
- [easysession.el: Easily persist and restore Emacs sessions (windows, tab-bar, file buffers, scratch, Dired, narrowing, indirect buffers/clones, Magit buffers, scratch...); a robust desktop.el replacement](#easysessionel-easily-persist-and-restore-emacs-sessions-windows-tab-bar-file-buffers-scratch-dired-narrowing-indirect-buffersclones-magit-buffers-scratch-a-robust-desktopel-replacement)
- [Installation](#installation)
- [Extensions](#extensions)
- [Extension: easysession-scratch (Persist and restore the scratch buffer)](#extension-easysession-scratch-persist-and-restore-the-scratch-buffer)
- [Extension: easysession-magit (Persist and restore Magit buffers)](#extension-easysession-magit-persist-and-restore-magit-buffers)
- [Usage](#usage)
- [Testimonials from users](#testimonials-from-users)
- [Customization](#customization)
- [How to only persist and restore visible buffers](#how-to-only-persist-and-restore-visible-buffers)
- [How to persist and restore global variables?](#how-to-persist-and-restore-global-variables)
- [How to make the current session name appear in the mode-line?](#how-to-make-the-current-session-name-appear-in-the-mode-line)
- [How to create an empty session setup](#how-to-create-an-empty-session-setup)
- [How to prevent EasySession from saving when switching sessions](#how-to-prevent-easysession-from-saving-when-switching-sessions)
- [How to configure easysession-save-mode to automatically save only the "main" session and let me manually save others?](#how-to-configure-easysession-save-mode-to-automatically-save-only-the-main-session-and-let-me-manually-save-others)
- [Passing the session name to Emacs via an environment variable](#passing-the-session-name-to-emacs-via-an-environment-variable)
- [How to make EasySession kill all buffers, frames, and windows before loading a session?](#how-to-make-easysession-kill-all-buffers-frames-and-windows-before-loading-a-session)
- [How to create custom load and save handlers for non-file-visiting buffers](#how-to-create-custom-load-and-save-handlers-for-non-file-visiting-buffers)
- [How to start afresh after loading too many buffers](#how-to-start-afresh-after-loading-too-many-buffers)
- [How to kill all buffers when changing a session?](#how-to-kill-all-buffers-when-changing-a-session)
- [How to save the session and close frames without quitting `emacs --daemon`](#how-to-save-the-session-and-close-frames-without-quitting-emacs---daemon)
- [How to persist and restore text scale?](#how-to-persist-and-restore-text-scale)
- [How does the author use easysession?](#how-does-the-author-use-easysession)
- [How to reduce the number of buffers in my session, regularly](#how-to-reduce-the-number-of-buffers-in-my-session-regularly)
- [What does 'EasySession supports restoring indirect buffers' mean?](#what-does-easysession-supports-restoring-indirect-buffers-mean)
- [What does EasySession offer that desktop.el doesn't?](#what-does-easysession-offer-that-desktopel-doesnt)
- [Why not just improve and submit patches to desktop.el?](#why-not-just-improve-and-submit-patches-to-desktopel)
- [Why not use one of the other third-party session packages?](#why-not-use-one-of-the-other-third-party-session-packages)
- [License](#license)
- [Links](#links)
<!-- markdown-toc end -->
## Installation
To install **easysession** from MELPA:
1. If you haven't already done so, [add MELPA repository to your Emacs configuration](https://melpa.org/#/getting-started).
2. Add the following code to your Emacs init file to install **easysession** from MELPA:
``` emacs-lisp
(use-package easysession
;; ':demand t' ensures the package is loaded immediately upon startup
:demand t
:config
;; Key mappings
(global-set-key (kbd "C-c sl") #'easysession-switch-to) ; Load session
(global-set-key (kbd "C-c ss") #'easysession-save) ; Save session
(global-set-key (kbd "C-c sL") #'easysession-switch-to-and-restore-geometry)
(global-set-key (kbd "C-c sr") #'easysession-rename)
(global-set-key (kbd "C-c sR") #'easysession-reset)
(global-set-key (kbd "C-c su") #'easysession-unload)
(global-set-key (kbd "C-c sd") #'easysession-delete)
;; Save every 10 minutes
(setq easysession-save-interval (* 10 60))
;; Save the current session when using `easysession-switch-to'
(setq easysession-switch-to-save-session t)
;; Do not exclude the current session when switching sessions
(setq easysession-switch-to-exclude-current nil)
;; Display the active session name in the mode-line lighter.
;; (setq easysession-save-mode-lighter-show-session-name t)
;; Optionally, the session name can be shown in the modeline info area:
;; (setq easysession-mode-line-misc-info t)
;; non-nil: Make `easysession-setup' load the session automatically.
;; (nil: session is not loaded automatically; the user can load it manually.)
(setq easysession-setup-load-session t)
;; The `easysession-setup' function adds hooks:
;; - To enable automatic session loading during `emacs-startup-hook', or
;; `server-after-make-frame-hook' when running in daemon mode.
;; - To save the session at regular intervals, and when Emacs exits.
(easysession-setup))
```
## Extensions
### Extension: easysession-scratch (Persist and restore the scratch buffer)
This extension makes EasySession persist and restore the scratch buffer.
To enable `easysession-scratch-mode`, add the following to your configuration:
```elisp
(with-eval-after-load 'easysession
(require 'easysession-scratch)
(easysession-scratch-mode 1))
```
### Extension: easysession-magit (Persist and restore Magit buffers)
This extension enables EasySession to persist and restore Magit buffers.
To activate `easysession-magit-mode`, add the following to your Emacs configuration:
```elisp
(with-eval-after-load 'easysession
(require 'easysession-magit)
(easysession-magit-mode 1))
```
## Usage
It is recommended to use the following functions:
- `easysession-switch-to` to switch to another session,
- `easysession-save` to save the current session as the current name or another name.
### Testimonials from users
- [spartanOrk](https://www.reddit.com/r/emacs/comments/1r47s44/comment/o5eg7uc/): "I use it and I love it and thank you ❤️"
- [RaxelPepi on Reddit](https://www.reddit.com/r/emacs/comments/1r47s44/comment/o5dpsbv/): "Ty for the hard work, this program is crucial to my Emacs and the least I can do is say thanks!"
- [emreyolcu on GitHub](https://github.com/jamescherti/easysession.el/pull/50#issuecomment-3752409317): "Thanks for developing EasySession! I've looked for a long time for a session management package that did everything I wanted, and EasySession is close to perfect for me."
- [tdavey on Reddit](https://www.reddit.com/r/emacs/comments/1lalerg/comment/mxxv7xc/): "Let me simply say that I love this package. It was easy to learn; the docs are very good. It is actively maintained. The author is indefatigable. Easysession works superbly with tab-bar-mode and tab-line-mode, both of which are essential to my workflow. The fact that it can restore indirect buffer clones is huge."
- [UnitaryInverse on Reddit](https://www.reddit.com/r/emacs/comments/1jah0e4/comment/mho5kqj/): "I have started using easysession more and more on my Spacemacs setup and it great! I can have a “lab notes” setup, a coding/simulation setup (I’m a physicist), a course planning setup for the courses I teach, and a personal setup all in one. Each one with custom windows setup so I spend SO MUCH less time splitting and moving windows. What a great package."
- [ghostlou1043 on GitHub](https://github.com/jamescherti/easysession.el/issues/52#issuecomment-3830343008): "Thank you for writing such a useful Emacs package. I think many people will use daemons more because of this package."
- [Hungariantoast on Reddit](https://www.reddit.com/r/emacs/comments/1i93ly5/comment/m980q04/): "I have a single raylib-experiments repository that I have been writing a bunch of separate, miniature gamedev projects in. This package has made the process of creating, managing, and restoring each of those little coding sessions such a breeze. Thanks for writing it."
- [ghoseb on GitHub](https://github.com/jamescherti/easysession.el/issues/21): "Thanks a lot for your amazing packages! Easysession works great 🎉"
- [hapst3r on GitHub](https://github.com/jamescherti/easysession.el/issues/58): "...thank you so much for this package. I am in the process of setting it up and I can foresee a huge productivity boost once I will have it setup."
- Mijail Guillemard (Email): "Thanks a lot for easysession.el, it is definitely more useful than other desktop*.el packages. The workflow I now have with Emacs has drastically improved with easysession."
- [tdavey on Reddit](https://www.reddit.com/r/emacs/comments/1r47s44/comment/o639qyo/):
```
Easysession is essential to my workflow. I rely heavily on tab-bar mode and
tab-line mode to organize my work, e.g., one tab-bar tab per project. My typical
Emacs session includes ~20 tab-bar tabs and ~70 buffers.
Upon restarting Emacs, Easysession restores everything, and I mean everything.
Many of my buffers are indirect clones. Easysession restores them. And many of
these clones are narrowed, for zooming in on a section of code or an Org-mode
tree. Easysession restores the narrowed state too.
This is huge. I know of no other desktop package for Emacs that restores
indirect buffers AND their narrowed state.
Easysession can also restore earmuff buffers, like Magit status, the Org Agenda,
and *Packages*, as long your init files initialize them first. I load
easysession at the end of my Emacs start-up to make sure that Easysession will
put everything I need in their designated tabs, including the special earmuff
buffers
Mr. Cherti, thanks so much for this package and its continuing development. In
my opinion it should replace the native desktop.el and be included in Emacs
itself...
```
## Customization
### How to only persist and restore visible buffers
By default, all file-visiting buffers, Dired buffers, and indirect buffers are persisted and restored as part of a session.
To restrict session persistence and restoration to buffers that are actually visible, configure `easysession-buffer-list-function` to use the `easysession-visible-buffer-list` function:
```emacs-lisp
;; Restrict session persistence and restoration to buffers that are visible
;; A buffer is included if it satisfies any of the following:
;; - It is currently displayed in a visible window.
;; - It is associated with a visible tab in tab-bar-mode, if enabled.
(setq easysession-buffer-list-function 'easysession-visible-buffer-list)
```
With this configuration, only buffers that are currently visible in a window or associated with a visible tab-bar tab are included in the session state.
### How to persist and restore global variables?
To persist and restore global variables in Emacs, you can use the built-in `savehist` Emacs package. This package is designed to save and restore minibuffer histories, but it can also be configured to save other global variables:
``` emacs-lisp
(use-package savehist
:ensure nil
:hook
(after-init . savehist-mode)
:config
(add-to-list 'savehist-additional-variables 'kill-ring)
(add-to-list 'savehist-additional-variables 'mark-ring)
(add-to-list 'savehist-additional-variables 'search-ring)
(add-to-list 'savehist-additional-variables 'regexp-search-ring))
```
(Each element added to `savehist-additional-variables` is a variable that will be persisted across Emacs sessions that use `savehist`.)
The **easysession** package can leverage `savehist` save the restore the current session name:
```emacs-lisp
(add-to-list 'savehist-additional-variables 'easysession--current-session-name)
```
### How to make the current session name appear in the mode-line?
You can display the current session name in the mode line by setting the following variable to t:
```emacs-lisp
(setq easysession-mode-line-misc-info t)
```
### How to create an empty session setup
To set up a minimal environment when easysession creates a new session, you can define a function that closes all other tabs, deletes all other windows, and switches to the scratch buffer. The following Emacs Lisp code demonstrates how to achieve this:
``` emacs-lisp
(add-hook 'easysession-new-session-hook #'easysession-reset)
```
NOTE: The `easysession-new-session-hook` functions are called when the user switches to a non-existent session using the `easysession-switch-to` function.
### How to prevent EasySession from saving when switching sessions
By default, the `easysession-switch-to` function saves the current session before switching to another session.
This behavior can be modified by setting the variable `easysession-switch-to-save-session` to nil, which prevents the session from being saved automatically when switching.
Here is how to disable saving before switching:
```elisp
;; Do not save the current session when switching to another session
(setq easysession-switch-to-save-session nil)
```
Here is how to enable saving before switching (default behavior):
```elisp
;; Save the current session when switching to another session
(setq easysession-switch-to-save-session t)
```
### How to configure easysession-save-mode to automatically save only the "main" session and let me manually save others?
To set up `easysession-save-mode` to automatically save only the "main" session and allow you to manually save other sessions, add the following code to your configuration:
```emacs-lisp
(defun my-easysession-only-main-saved ()
"Only save the main session."
(when (string= "main" (easysession-get-current-session-name))
t))
(setq easysession-save-mode-predicate 'my-easysession-only-main-saved)
```
### Passing the session name to Emacs via an environment variable
To pass a session name to Emacs through an environment variable, for instance:
```shell
EMACS_SESSION_NAME="my-session-name" emacs
```
The corresponding Elisp code to restore the session is:
```elisp
(add-hook 'emacs-startup-hook
#'(lambda ()
(let* ((env-session-name (getenv "EMACS_SESSION_NAME"))
(session-name (if (string-empty-p env-session-name)
"main"
env-session-name)))
(easysession-set-current-session-name session-name)
(easysession-load-including-geometry)))
102)
```
This Elisp code adds a function to the `emacs-startup-hook` that automatically restores a session. It retrieves the value of the `EMACS_SESSION_NAME` environment variable and falls back to `"main"` if the variable is unset or empty. Before switching sessions, it sets `easysession-frameset-restore-geometry` to `t` to ensure that the frame layout is also restored.
### How to make EasySession kill all buffers, frames, and windows before loading a session?
Here is how to configure EasySession to kill all buffers, frames, and windows before loading a session:
``` emacs-lisp
;; Make EasySession kill all buffers, frames, and windows before loading a
;; session
(add-hook 'easysession-before-load-hook #'easysession-reset)
```
Optionally, the `easysession-reset` function can be configured to automatically save all buffers without prompting the user:
```elisp
;; Automatically save all buffers without prompting the user
(add-hook 'easysession-before-reset-hook #'(lambda()
(save-some-buffers t)))
```
### How to create custom load and save handlers for non-file-visiting buffers
**Note:** The code below is provided for illustrative purposes to show how to create a custom EasySession extension. To persist and restore the scratch buffer, use the `easysession-scratch` extension (`extensions/easysession-scratch.el`) instead of the example below.
EasySession is customizable. Users can implement their own handlers to manage non-file-visiting buffers, enabling the creation of custom functions for restoring buffers.
Here is a simple example to persist and restore the scratch buffer:
```elisp
(easysession-define-handler
"scratch"
;; Load
#'(lambda (session-data)
"Load SESSION-DATA."
(dolist (item session-data)
(let ((buffer-name (car item)))
(when (string= buffer-name "*scratch*")
(let* ((buffer (get-scratch-buffer-create))
(buffer-data (cdr item))
(buffer-string (when buffer-data
(assoc-default 'buffer-string buffer-data))))
(when (and buffer buffer-string)
(with-current-buffer buffer
(erase-buffer)
(insert buffer-string))))))))
;; Save
#'(lambda(buffers)
"Save the BUFFERS buffer."
(easysession-save-handler-dolist-buffers
buffers
(let ((buffer-name (buffer-name)))
(when (string= buffer-name "*scratch*")
(cons buffer-name
(list
(cons 'buffer-string
(buffer-substring-no-properties (point-min)
(point-max))))))))))
```
The code above enables EasySession to go beyond the default handlers, which support regular and indirect buffers, by also persisting and restoring the `*scratch*` buffer.
### How to start afresh after loading too many buffers
To reset EasySession by killing all buffers, frames, and windows, effectively simulating a fresh Emacs start, use `M-x easysession-reset`.
### How to kill all buffers when changing a session?
By default, EasySession keeps existing buffers open when you switch sessions. If you prefer to start with a clean environment for each session, you can configure the package to kill all buffers before loading the new context.
To do this, register a function in `easysession-before-load-hook` that invokes `easysession-kill-all-buffers`:
```elisp
(defun my-easysession-kill-all-buffers ()
"Kill all buffers before switching sessions."
(easysession-kill-all-buffers))
(add-hook 'easysession-before-load-hook #'my-easysession-kill-all-buffers)
```
Even though `easysession-kill-all-buffers` does not terminate internal or system buffers, it still removes all open user buffers. If any of these buffers are part of the new session being loaded, they will need to be reopened and reinitialized. This can lead to unnecessary disk I/O, slower session loading, and loss of in-memory buffer state.
A safer and more efficient alternative is to use the [buffer-terminator](https://github.com/jamescherti/buffer-terminator.el) Emacs package. It allows precise control over which buffers should be closed when switching sessions, while leaving others intact if they are likely to be reused.
### How to save the session and close frames without quitting `emacs --daemon`
**Note:** This is intended for environments using `emacs --daemon` or `emacs --fg-daemon`, where the Emacs process persists independently of client frames.
EasySession operates effectively when Emacs runs in daemon mode. The `easysession-save-session-and-close-frames` function implements a controlled routine to simulate a termination without stopping the Emacs daemon:
```elisp
(defun my-easysession-save-buffers-kill-emacs ()
"Handle quitting Emacs with daemon-aware frame management."
(interactive)
(if (daemonp)
(easysession-save-sesssion-and-close-frames)
(save-buffers-kill-emacs)))
(global-set-key (kbd "C-q") #'my-easysession-save-buffers-kill-emacs)
(when (daemonp)
(global-set-key (kbd "C-x C-c") #'my-easysession-save-buffers-kill-emacs))
```
The `easysession-save-sesssion-and-close-frames` function persists modified buffers, saves the EasySession state, and deletes all active frames. (The Emacs daemon's internal terminal frame is preserved to ensure the daemon remains resident.)
From the perspective of EasySession, this is functionally equivalent to an application shutdown: the session is fully saved and unloaded. When a new frame is later initialized by the Emacs daemon, EasySession restores the state as if the process had been freshly started.
### How to persist and restore text scale?
The [persist-text-scale](https://github.com/jamescherti/persist-text-scale.el) Emacs package provides `persist-text-scale-mode`, which ensures that all adjustments made with `text-scale-increase` and `text-scale-decrease` are persisted and restored across sessions. As a result, the text size in each buffer remains consistent, even after restarting Emacs. This package also facilitates grouping buffers into categories, allowing buffers within the same category to share a consistent text scale. This ensures uniform font sizes when adjusting text scaling.
### How does the author use easysession?
The author uses easysession by setting up each session to represent a distinct project or a specific "view" on a particular project, including various tabs (built-in tab-bar), window splits, dired buffers, and file buffers. This organization allows for the creation of dedicated environments for different tasks or aspects of a project, such as development, debugging, specific issue, and documentation. The author switches between projects and views of the same projects multiple times a day, and easysession helps significantly by allowing quick transitions between them.
### How to reduce the number of buffers in my session, regularly
If your Emacs session tends to accumulate buffers over time, and you would like Emacs to automatically clean up unused and inactive ones, the author recommends trying the [buffer-terminator](https://github.com/jamescherti/buffer-terminator.el) package. This package safely and automatically kills inactive buffers, helping maintain a cleaner workspace and potentially improving Emacs performance by reducing the number of active modes, timers, and background processes associated with open buffers.
### What does 'EasySession supports restoring indirect buffers' mean?
Try the following:
1. Open a file,
2. Open an indirect buffer in another window with `M-x clone-indirect-buffer-other-window`,
3. Creating a new tab using `M-x tab-new`,
4. Open a second indirect buffer in the new tab with `M-x clone-indirect-buffer-other-window`.
EasySession can persist and restore all original and indirect buffers exactly as they were, maintaining their buffer names and their status as either original or indirect, including buffers located in different tabs.
### What does EasySession offer that desktop.el doesn't?
Easysession.el provides a reliable and modern alternative to desktop.el.
While desktop.el is a foundational session management tool for Emacs, it has several limitations:
- It primarily saves Emacs' state on exit and restores it on startup, making it difficult to switch between different session files during an editing session.
- desktop.el does not restores buffer narrowing, which is the restriction of a buffer to display and edit only a specific portion of its contents.
- The desktop.el package does not allow the user to easily choose whether to load sessions with or without modifying the Emacs frame geometry. This last feature is important in easysession because it allows switching between sessions without the annoyance of changing the window position or size.
- The desktop.el package saves and restores major modes and important global variables, which can prevent some packages from initializing correctly. For example, the `vdiff` package may stop working after comparing two files and reloading Emacs and the desktop.el session. This issue has also occurred with a few other packages.
- The desktop.el package can be bulky and slow in operation.
- The desktop.el package lacks support for saving and restoring indirect buffers (clones). Indirect buffers are secondary buffers that share the same content as an existing buffer but can have different point positions, narrowing, folds, and other buffer-local settings. This allows users to view and edit the same file or text content in multiple ways simultaneously without duplicating the actual data. There are third-party packages, such as desktop+, that extend desktop.el to restore indirect buffers. However, packages like desktop+ are still based on desktop.el and can cause the issues described above.
- Although desktop.el can operate in daemon mode, users have occasionally encountered issues such as sessions failing to save automatically when the last client frame is closed, and unpredictable behavior when multiple frames are opened or closed. EasySession addresses these challenges by ensuring reliable session saving and restoration across all frames and client connections, delivering consistent and dependable session management in both interactive and daemon workflows.
- In `desktop.el`, explicitly set frame names are typically lost because the default configuration filters out the `name` parameter to prevent freezing titles that should update dynamically. EasySession addresses this by implementing a conditional filtering mechanism. Rather than ignoring frame names unconditionally, it uses a custom filter function to inspect the `explicit-name` parameter of each frame. When a frame has been manually named using a command like `set-frame-name`, Emacs marks that name as explicit. EasySession detects this flag during the save process and ensures that the custom name is preserved in the session file.
In contrast, easysession offers enhanced functionality:
- It supports saving and loading various buffer types, including indirect buffers (clones), and buffer narrowing.
- It allows users to load or save different sessions while actively editing, without the need to restart Emacs.
- EasySession provides full support for both standard Emacs sessions and Emacs running in daemon mode.
- It excels in speed and efficiency, enabling seamless session management within Emacs.
### Why not just improve and submit patches to desktop.el?
It is preferable for EasySession to remain a third-party plugin, as this provides more flexibility for implementing new features. EasySession relies on the same built-in functions as desktop.el (e.g., frameset) but includes additional features that enhance the experience of persisting and restoring sessions. EasySession is also customizable, allowing users to implement their own handlers to persist and restore new types of non-file-visiting buffers.
### Why not use one of the other third-party session packages?
Several packages exist for session management, including `minimal-session-saver`, `save-visited-files`, `sesman`, and `psession`. However, these packages have notable limitations:
- None of them can restore indirect buffers (clones). Indirect buffers, which can be created using `clone-indirect-buffer`, are secondary buffers that share the same content as an existing buffer but can have different point positions, narrowing, folds, and other buffer-local settings. This allows users to view and edit the same file or text content in multiple ways simultaneously without duplicating the actual data.
- Neither restores buffer narrowing, which is the restriction of a buffer to display and edit only a specific portion of its contents.
- The minimal-session-saver and save-visited-files packages are no longer maintained and cannot restore the frameset and the tab-bar.
- Sesman is designed to implement some IDE features in Emacs.
- Psession cannot switch between sessions quickly, with or without modifying the the Emacs frame geometry. This last feature is important in easysession because it allows switching between sessions without the annoyance of changing the window position or size.
Easysession can persist and restore file editing buffers, indirect buffers/clones, Dired buffers, buffer narrowing, the tab-bar, and the Emacs frames (with or without the Emacs frames geometry). It is similar to Vim or Neovim sessions because it loads and restores your editing environment, including buffers, windows, tabs, and other settings, allowing you to resume work exactly where you left off.
Other packages focus more on managing activities rather than full session management, such as activities.el. Here is how activities.el and EasySession differ:
- EasySession is designed for loading, saving, and switching entire sessions, while Activities focuses on managing "activities" and allows for multiple activities within a single session.
- EasySession supports restoring indirect buffers that were created with `M-x clone-indirect-buffer-other-window`, whereas Activities does not. However, since Activities relies on Emacs bookmarks to save and restore buffers, the behavior depends entirely on the buffer's major mode bookmark handler. For example, when used with `org-bookmark-heading`, Org-mode indirect buffers are properly saved and restored.
- EasySession allows you to choose whether to restore the geometry (position, width, and height) of your frames.
- EasySession relies on Emacs built-in functions for saving and restoring frames and tab-bar tabs (the built-in `frameset` package). Activities uses the built-in bookmark system to save and restore buffers and tabs.
- Both EasySession and Activities are customizable. In EasySession, users can define custom handlers to manage non-file-backed buffers, allowing the creation of specialized functions for restoring them. In Activities, bookmarks can be used to achieve similar customizations.
- EasySession persists and restores all frames and tabs. Activities, by design, operates differently: Its scope is limited to a single frame (without referencing tabs) or to a single tab when `tab-bar-mode` is active; it does not span multiple frames or tabs. Each buffer is managed through its major mode's bookmark handler, which handles details such as indirect buffers and narrowing.
- Activities is fundamentally limited to bookmarkable buffers by design, whereas EasySession is architected for extensibility and can reliably support arbitrary buffer types. (e.g., EasySession supports Magit buffers through the easysession-magit extension.)
## License
The easysession Emacs package has been written by [James Cherti](https://www.jamescherti.com/) and is distributed under terms of the GNU General Public License version 3, or, at your choice, any later version.
Copyright (C) 2024-2026 [James Cherti](https://www.jamescherti.com)
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program.
## Links
- [easysession.el @GitHub](https://github.com/jamescherti/easysession.el)
- [easysession.el @MELPA](https://melpa.org/#/easysession)
- There is also a Vim version of Easysession: [vim-easysession](https://github.com/jamescherti/vim-easysession)
Other Emacs packages by the same author:
- [minimal-emacs.d](https://github.com/jamescherti/minimal-emacs.d): This repository hosts a minimal Emacs configuration designed to serve as a foundation for your vanilla Emacs setup and provide a solid base for an enhanced Emacs experience.
- [compile-angel.el](https://github.com/jamescherti/compile-angel.el): **Speed up Emacs!** This package guarantees that all .el files are both byte-compiled and native-compiled, which significantly speeds up Emacs.
- [outline-indent.el](https://github.com/jamescherti/outline-indent.el): An Emacs package that provides a minor mode that enables code folding and outlining based on indentation levels for various indentation-based text files, such as YAML, Python, and other indented text files.
- [vim-tab-bar.el](https://github.com/jamescherti/vim-tab-bar.el): Make the Emacs tab-bar Look Like Vim’s Tab Bar.
- [elispcomp](https://github.com/jamescherti/elispcomp): A command line tool that allows compiling Elisp code directly from the terminal or from a shell script. It facilitates the generation of optimized .elc (byte-compiled) and .eln (native-compiled) files.
- [tomorrow-night-deepblue-theme.el](https://github.com/jamescherti/tomorrow-night-deepblue-theme.el): The Tomorrow Night Deepblue Emacs theme is a beautiful deep blue variant of the Tomorrow Night theme, which is renowned for its elegant color palette that is pleasing to the eyes. It features a deep blue background color that creates a calming atmosphere. The theme is also a great choice for those who miss the blue themes that were trendy a few years ago.
- [Ultyas](https://github.com/jamescherti/ultyas/): A command-line tool designed to simplify the process of converting code snippets from UltiSnips to YASnippet format.
- [dir-config.el](https://github.com/jamescherti/dir-config.el): Automatically find and evaluate .dir-config.el Elisp files to configure directory-specific settings.
- [flymake-bashate.el](https://github.com/jamescherti/flymake-bashate.el): A package that provides a Flymake backend for the bashate Bash script style checker.
- [flymake-ansible-lint.el](https://github.com/jamescherti/flymake-ansible-lint.el): An Emacs package that offers a Flymake backend for ansible-lint.
- [inhibit-mouse.el](https://github.com/jamescherti/inhibit-mouse.el): A package that disables mouse input in Emacs, offering a simpler and faster alternative to the disable-mouse package.
- [quick-sdcv.el](https://github.com/jamescherti/quick-sdcv.el): This package enables Emacs to function as an offline dictionary by using the sdcv command-line tool directly within Emacs.
- [enhanced-evil-paredit.el](https://github.com/jamescherti/enhanced-evil-paredit.el): An Emacs package that prevents parenthesis imbalance when using *evil-mode* with *paredit*. It intercepts *evil-mode* commands such as delete, change, and paste, blocking their execution if they would break the parenthetical structure.
- [stripspace.el](https://github.com/jamescherti/stripspace.el): Ensure Emacs Automatically removes trailing whitespace before saving a buffer, with an option to preserve the cursor column.
- [persist-text-scale.el](https://github.com/jamescherti/persist-text-scale.el): Ensure that all adjustments made with text-scale-increase and text-scale-decrease are persisted and restored across sessions.
- [pathaction.el](https://github.com/jamescherti/pathaction.el): Execute the pathaction command-line tool from Emacs. The pathaction command-line tool enables the execution of specific commands on targeted files or directories. Its key advantage lies in its flexibility, allowing users to handle various types of files simply by passing the file or directory as an argument to the pathaction tool. The tool uses a .pathaction.yaml rule-set file to determine which command to execute. Additionally, Jinja2 templating can be employed in the rule-set file to further customize the commands.
- [kirigami.el](https://github.com/jamescherti/kirigami.el): The *kirigami* Emacs package offers a unified interface for opening and closing folds across a diverse set of major and minor modes in Emacs, including `outline-mode`, `outline-minor-mode`, `outline-indent-minor-mode`, `org-mode`, `markdown-mode`, `vdiff-mode`, `vdiff-3way-mode`, `hs-minor-mode`, `hide-ifdef-mode`, `origami-mode`, `yafolding-mode`, `folding-mode`, and `treesit-fold-mode`. With Kirigami, folding key bindings only need to be configured **once**. After that, the same keys work consistently across all supported major and minor modes, providing a unified and predictable folding experience.
- [buffer-guardian.el](https://github.com/jamescherti/buffer-guardian.el): Automatically saves Emacs buffers without requiring manual intervention. By default, it triggers a save when the user switches to another buffer, switches to another window or frame, Emacs loses focus, or the minibuffer is opened. Beyond standard file buffers, *buffer-guardian* also manages specialized editing buffers such as *org-src* and *edit-indirect*. Additional features, disabled by default, include periodic or idle-time saving of all buffers, automatic exclusion of remote, nonexistent, or large files, and support for custom exclusion rules via regular expressions or predicate functions.
================================================
FILE: easysession.el
================================================
;;; easysession.el --- Persist and restore your sessions (desktop.el alternative) -*- lexical-binding: t; -*-
;; Copyright (C) 2024-2026 James Cherti | https://www.jamescherti.com/contact/
;; Author: James Cherti <https://www.jamescherti.com/contact/>
;; Version: 1.2.1
;; URL: https://github.com/jamescherti/easysession.el
;; Keywords: convenience
;; Package-Requires: ((emacs "26.1"))
;; SPDX-License-Identifier: GPL-3.0-or-later
;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.
;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; The easysession Emacs package is a session manager for Emacs that can persist
;; and restore file editing buffers, indirect buffers (clones), Dired buffers,
;; the tab-bar, and Emacs frames (including or excluding the geometry: frame
;; size, width, and height). It offers a convenient and effortless way to manage
;; Emacs editing sessions and utilizes built-in Emacs functions to persist and
;; restore frames.
;;
;; With Easysession.el, you can effortlessly switch between sessions,
;; ensuring a consistent and uninterrupted editing experience.
;;
;; Key features include:
;; - Quickly switch between sessions while editing with or without disrupting
;; the frame geometry.
;; - Capture the full Emacs workspace state: file buffers, indirect buffers and
;; clones, buffer narrowing, Dired buffers, window layouts and splits, the
;; built-in tab-bar with its tabs and buffers, and Emacs frames with optional
;; position and size restoration.
;; - Built from the ground up with an emphasis on speed, minimalism, and
;; predictable behavior, even in large or long-running Emacs setups.
;; - Never lose context with automatic session persistence. (Enable
;; `easysession-save-mode' to save the active session at regular intervals
;; defined by `easysession-save-interval' and again on Emacs exit.)
;; - Comprehensive command set for session management: switch sessions instantly
;; with `easysession-switch-to', save with `easysession-save', delete with
;; `easysession-delete', and rename with `easysession-rename'.
;; - Highly extensible architecture that allows custom handlers for non-file
;; buffers, making it possible to restore complex or project-specific buffers
;; exactly as needed.
;; - Fine-grained control over file restoration by selectively excluding
;; individual functions from `find-file-hook' during session loading via
;; `easysession-exclude-from-find-file-hook'.
;; - Clear visibility of the active session through modeline integration or a
;; lighter.
;; - Built-in predicate to determine whether the current session qualifies for
;; automatic saving.
;; - Optional scratch buffer persistence via the `easysession-scratch'
;; extension, preserving notes and experiments across restarts.
;; - Optional Magit state restoration via the `easysession-magit' extension,
;; keeping version control workflows intact.
;; - Exact restoration of narrowed regions in both base and indirect buffers,
;; ensuring each buffer reopens with the same visible scope as when it was
;; saved.
;;
;; Installation from MELPA:
;; ------------------------
;; (use-package easysession
;; ;; ':demand t' ensures the package is loaded immediately upon startup
;; :demand t
;;
;; :config
;; ;; Key mappings
;; (global-set-key (kbd "C-c sl") #'easysession-switch-to) ; Load session
;; (global-set-key (kbd "C-c ss") #'easysession-save) ; Save session
;; (global-set-key (kbd "C-c sL") #'easysession-switch-to-and-restore-geometry)
;; (global-set-key (kbd "C-c sr") #'easysession-rename)
;; (global-set-key (kbd "C-c sR") #'easysession-reset)
;; (global-set-key (kbd "C-c su") #'easysession-unload)
;; (global-set-key (kbd "C-c sd") #'easysession-delete)
;;
;; ;; Save every 10 minutes
;; (setq easysession-save-interval (* 10 60))
;;
;; ;; Save the current session when using `easysession-switch-to'
;; (setq easysession-switch-to-save-session t)
;;
;; ;; Do not exclude the current session when switching sessions
;; (setq easysession-switch-to-exclude-current nil)
;;
;; ;; Display the active session name in the mode-line lighter.
;; ;; (setq easysession-save-mode-lighter-show-session-name t)
;;
;; ;; Optionally, the session name can be shown in the modeline info area:
;; ;; (setq easysession-mode-line-misc-info t)
;; ;; non-nil: Make `easysession-setup' load the session automatically.
;; ;; (nil: session is not loaded automatically; the user can load it manually.)
;; (setq easysession-setup-load-session t)
;;
;; ;; The `easysession-setup' function adds hooks:
;; ;; - To enable automatic session loading during `emacs-startup-hook', or
;; ;; `server-after-make-frame-hook' when running in daemon mode.
;; ;; - To save the session at regular intervals, and when Emacs exits.
;; (easysession-setup))
;;
;; Usage:
;; ------
;; It is recommended to use the following functions:
;; - (easysession-switch-to) to switch to another session or (easysession-load)
;; to reload the current one,
;; - (easysession-save) to save the current session as the current name or
;; another name.
;;
;; Links:
;; ------
;; - More information about easysession (Frequently asked questions, usage...):
;; https://github.com/jamescherti/easysession.el
;;; Code:
(require 'frameset)
(require 'cl-lib)
(require 'seq)
;;; Variables
(defgroup easysession nil
"Customization options for EasySession."
:group 'easysession
:prefix "easysession-")
(defcustom easysession-directory (expand-file-name "easysession"
user-emacs-directory)
"Directory where the session files are stored."
:type 'directory
:group 'easysession)
(defcustom easysession-before-load-hook nil
"Hooks to run before the session is loaded.
Each element should be a function to be called with no arguments."
:type 'hook
:group 'easysession)
(defcustom easysession-after-load-hook nil
"Hooks to run after the session is loaded.
Each element should be a function to be called with no arguments."
:type 'hook
:group 'easysession)
(defcustom easysession-before-save-hook nil
"Hooks to run before the session is saved.
Each element should be a function to be called with no arguments."
:type 'hook
:group 'easysession)
(defcustom easysession-after-save-hook nil
"Hooks to run after the session is saved.
Each element should be a function to be called with no arguments."
:type 'hook
:group 'easysession)
(defcustom easysession-new-session-hook nil
"Hooks to run after a new session is created.
Each element should be a function to be called with no arguments.
This can be used to customize behavior, such as emptying a session
after a new one is created."
:type 'hook
:group 'easysession)
(defcustom easysession-before-reset-hook nil
"Hooks to run before the `easysession-reset' function.
Each element should be a function to be called with no arguments."
:type 'hook
:group 'easysession)
(defcustom easysession-after-reset-hook nil
"Hooks to run after the `easysession-reset' function.
Each element should be a function to be called with no arguments."
:type 'hook
:group 'easysession)
(defcustom easysession-quiet nil
"If non-nil, suppress all messages and only show errors and warnings.
This includes messages such as \='Session Deleted\=', \='Session Loaded\=',
\='Session Saved\=', etc."
:type 'boolean
:group 'easysession)
(defvar easysession--timer nil)
(defun easysession--update-timer ()
"Update the auto-save timer based on `easysession-save-interval'."
;; Always cancel the existing timer to prevent duplicates/leaks
(when (timerp easysession--timer)
(cancel-timer easysession--timer)
(setq easysession--timer nil))
;; Only start the timer if the mode is actually enabled
(when (and (bound-and-true-p easysession-save-mode)
(boundp 'easysession-save-interval)
easysession-save-interval)
(setq easysession--timer (run-with-timer easysession-save-interval
easysession-save-interval
#'easysession--auto-save))))
(defcustom easysession-save-interval nil
"The interval between automatic session saves.
If set to nil, it disables timer-based autosaving. Automatic session saves are
activated when `easysession-save-mode' is enabled."
:type '(choice (const :tag "Disabled" nil)
(integer :tag "Seconds"))
:group 'easysession
:set (lambda (sym val)
(set-default sym val)
(when (bound-and-true-p easysession-save-mode)
(easysession--update-timer))))
;; Mode line
(defface easysession-mode-line-session-name-face
'((t :inherit font-lock-constant-face :weight bold))
"Face used in the mode-line to indicate the current session.")
(defun easysession--update-modeline-misc-info (value)
"Update the mode line with VALUE."
(setq mode-line-misc-info (assq-delete-all 'easysession-mode-line-misc-info
mode-line-misc-info))
(add-to-list 'mode-line-misc-info `(easysession-mode-line-misc-info
,value)))
(defcustom easysession-mode-line-misc-info-format
'(:eval (easysession--mode-line-session-name-format))
"Mode-line format used to display the session name."
:type 'sexp
:group 'easysession
:set (lambda (symbol value)
(set symbol value)
(easysession--update-modeline-misc-info value)))
(put 'easysession-mode-line-misc-info-format 'risky-local-variable t)
(defcustom easysession-mode-line-misc-info nil
"If non-nil, add `easysession` to `mode-line-misc-info'. If nil, remove it."
:type 'boolean
:group 'easysession
:set (lambda (symbol value)
(set symbol value)
(if value
(easysession--update-modeline-misc-info
easysession-mode-line-misc-info-format)
(setq mode-line-misc-info
(assq-delete-all 'easysession-mode-line-misc-info
mode-line-misc-info)))))
(defcustom easysession-mode-line-misc-info-prefix " [EasySession:"
"Prefix string displayed before the session name in the mode line."
:type 'string
:group 'easysession)
(defcustom easysession-mode-line-misc-info-suffix "] "
"Suffix string displayed after the session name in the mode line."
:type 'string
:group 'easysession)
(defcustom easysession-switch-to-save-session t
"Non-nil means save the current session when using `easysession-switch-to'."
:type 'boolean
:group 'easysession)
(defcustom easysession-switch-to-exclude-current nil
"Non-nil to exclude the current session when switching sessions.
This setting affects the interactive prompt used by the following functions:
- `easysession-load'
- `easysession-switch-to'
- `easysession-switch-to-and-restore-geometry'
This can be useful to prevent accidental re-selection of the session already in
use, especially when cycling through or interactively selecting among available
sessions."
:type 'boolean
:group 'easysession)
;; Lighter
(defcustom easysession-save-mode-lighter " EasySessionSv"
"Default lighter string for `easysession-save-mode'."
:type 'string
:group 'easysession)
(defcustom easysession-save-mode-lighter-show-session-name nil
"If non-nil, display the session name in the lighter."
:type 'boolean
:group 'easysession)
(defcustom easysession-save-mode-lighter-session-name-spec
'((:eval (easysession--lighter-session-name-format)))
"Mode line lighter specification for displaying the current session name.
This is only displayed when a session is active."
:type 'sexp
:group 'easysession)
(put 'easysession-save-mode-lighter-session-name-spec 'risky-local-variable t)
;; Other
(defcustom easysession-buffer-list-function #'buffer-list
"Function used to retrieve the buffers for persistence and restoration.
This holds a function that returns a list of buffers to be saved and restored
during session management. By default, it is set to `buffer-list', which
includes all buffers. You can customize this variable to use a different
function, such as one that filters buffers based on visibility or other
criteria."
:type 'function
:group 'easysession)
(defun easysession--default-auto-save-predicate ()
"Default predicate function for `easysession-save-predicate`.
This function always returns non-nil, ensuring the session is saved."
t)
(defcustom easysession-save-mode-predicate
#'easysession--default-auto-save-predicate
"Predicate that determines if the session is saved automatically.
This function is called with no arguments and should return non-nil if
`easysession-save-mode' should save the session automatically. The default
predicate always returns non-nil, ensuring all sessions are saved
automatically."
:type 'function
:group 'easysession)
(define-obsolete-variable-alias
'easysession-restore-frames
'easysession-enable-frameset-restore
"1.1.2"
"Use `easysession-enable-frameset-restore' instead.")
(defcustom easysession-enable-frameset-restore t
"Non-nil to restore frames.
When non-nil, frames will be restored alongside buffers when a session is
loaded.
If set to nil, only the buffers will be restored, and frame restoration will be
skipped.
See related options:
- `easysession-frameset-restore-reuse-frames'
- `easysession-frameset-restore-force-display'
- `easysession-frameset-restore-force-onscreen'
- `easysession-frameset-restore-cleanup-frames'"
:type 'boolean
:group 'easysession)
(defcustom easysession-frameset-restore-reuse-frames t
"Specifies the policy for reusing frames when restoring:
t All existing frames can be reused.
nil No existing frames can be reused.
match Only frames with matching frame IDs can be reused.
PRED A predicate function that receives a live frame as an argument
and returns non-nil to allow reusing it, or nil otherwise.
For more details, see the `frameset-restore' docstring."
:type '(choice (const :tag "Reuse all frames" t)
(const :tag "Reuse no frames" nil)
(const :tag "Reuse frames with matching IDs" match)
(function :tag "Predicate function"))
:group 'easysession)
(defcustom easysession-frameset-restore-force-display t
"Specifies how frames are restored with respect to display:
t Frames are restored on the current display.
nil Frames are restored, if possible, on their original displays.
delete Frames in other displays are deleted instead of being restored.
PRED A function called with two arguments: the parameter alist and
the window state (in that order). It must return t, nil, or
delete, as described above, but affecting only the frame
created from that parameter alist.
For more details, see the `frameset-restore' docstring."
:type '(choice (const :tag "Restore on current display" t)
(const :tag "Restore on original displays" nil)
(const :tag "Delete frames in other displays" delete)
(function :tag "Function to determine frame restoration"))
:group 'easysession)
(defcustom easysession-frameset-restore-force-onscreen (display-graphic-p)
"Specifies how frames are handled when they are offscreen:
t Only frames that are completely offscreen are forced onscreen.
nil No frames are forced back onscreen.
all Any frame that is fully or partially offscreen is forced onscreen.
PRED A function called with three arguments:
- The live frame just restored.
- A list (LEFT TOP WIDTH HEIGHT) describing the frame.
- A list (LEFT TOP WIDTH HEIGHT) describing the work area.
It must return non-nil to force the frame onscreen, or nil otherwise.
For more details, see the `frameset-restore' docstring."
:type '(choice
(const :tag "Force onscreen only fully offscreen frames" t)
(const :tag "Do not force any frames onscreen" nil)
(const
:tag "Force onscreen any frame fully or partially offscreen" all)
(function :tag "Function to determine onscreen status"))
:group 'easysession)
(defcustom easysession-frameset-restore-cleanup-frames t
"Specifies the policy for cleaning up the frame list after restoring.
t Delete all frames that were not created or restored.
nil Retain all frames.
FUNC A function called with two arguments:
- FRAME, a live frame.
- ACTION, which can be one of:
:rejected Frame existed but was not a candidate for reuse.
:ignored Frame existed, was a candidate, but was not reused.
:reused Frame existed, was a candidate, and was reused.
:created Frame did not exist, was created and restored upon.
The return value is ignored.
For more details, see the `frameset-restore' docstring."
:type '(choice (const :tag "Delete all unneeded frames" t)
(const :tag "Retain all frames" nil)
(function :tag "Function to determine cleanup actions"))
:group 'easysession)
(defcustom easysession-edit-read-only nil
"Non-nil means session buffers opened with `easysession-edit` are read-only."
:type 'boolean
:group 'easysession)
(defcustom easysession-exclude-from-find-file-hook
'(recentf-track-opened-file
save-place-find-file-hook
auto-revert-find-file-function)
"List of hooks to be excluded from `find-file-hook'.
When EasySession restores a file editing buffer using `find-file-noselect', the
functions in this list are skipped and not executed by `find-file-hook'. This
provides control over which hooks should be bypassed during the file restoration
process, ensuring that certain actions (e.g., tracking opened files) are not
triggered in this context."
:type '(repeat symbol)
:group 'easysession)
(defvar easysession-frameset-restore-geometry nil
"If non-nil, `easysession-load' restores frame position and size.
Do not modify this variable directly; use `easysession-load-including-geometry'
instead.
Set this variable to t only if you want `easysession-load' or
`easysession-switch-to' to always restore frame geometry.
By default, this variable is nil, meaning `easysession-load' does not restore
geometry.")
(defvar easysession-load-in-progress nil
"Session name (string) if a session is currently being loaded.
This is an internal variable that is meant to be read-only. Do not modify it.
This variable is used to indicate whether a session loading process is in
progress.")
(defvar easysession-save-in-progress nil
"Session name (string) if a session is currently being saved.
This is an internal variable that is meant to be read-only. Do not modify it.
This variable is used to indicate whether a session saving process is in
progress.")
(defcustom easysession-confirm-new-session t
"Non-nil prompts the user for confirmation when creating a new session."
:type 'boolean
:group 'easysession)
(defcustom easysession-save-pretty-print nil
"Non-nil means session data is pretty-printed when written to disk.
When it is nil, session data is saved in a more compact form that is harder for
humans to read but takes less space.
This option only changes how the session file looks, not what information is
stored."
:type 'boolean
:group 'easysession)
(defcustom easysession-setup-add-hook-depth 102
"Priority depth used when `easysession-setup' adds `easysession' hooks.
Higher values ensure that `easysession' hooks run after most other startup or
frame hooks.
The default value of 102 ensures that the session loads after all other
packages. Setting the depth to 102 is useful for users of minimal-emacs.d, where
certain optimizations restore `file-name-handler-alist' at depth 101 during
`emacs-startup-hook'.
The `easysession-setup-add-hook-depth' variable must be set before calling the
`easysession-setup' function."
:type 'integer
:group 'easysession)
(defcustom easysession-setup-load-session t
"Non-nil means `easysession-setup' automatically loads the session.
Nil means the session is not loaded automatically; the user can load it
manually.
The `easysession-setup-load-session' variable must be set before calling the
`easysession-setup' function."
:type 'boolean
:group 'easysession)
(defcustom easysession-setup-load-session-including-geometry t
"Non-nil means the `easysession-setup' session restores frame geometry.
If nil, the first session is loaded without restoring frame sizes or positions.
The `easysession-setup-load-session-including-geometry' variable must be set
before calling the `easysession-setup' function."
:type 'boolean
:group 'easysession)
(defcustom easysession-setup-load-predicate nil
"Predicate to determine whether `easysession-setup' loads the session.
When nil, the session loads without additional checks. If assigned a function,
the function is called without arguments; the session restoration proceeds only
if the return value is non-nil.
This variable allows restricting session restoration to specific environments,
such as graphical frames.
The `easysession-setup-load-predicate' variable must be set
before calling the `easysession-setup' function."
:type '(choice (const :tag "Always load" nil)
(function :tag "Predicate function"))
:group 'easysession)
(defcustom easysession-refresh-tab-bar nil
"EXPERIMENTAL FEATURE. Non-nil to force a state and name refresh of all tabs.
This is an experimental feature. When non-nil, EasySession cycles through all
tabs on all frames before saving the session to ensure that tab names match the
actual buffer names.
Persisting a session with outdated tab names prevents those buffers from
matching the restored tabs. This mismatch occurs when a buffer is renamed by
`uniquify' or another package that does not notify the `tab-bar' of the change.
By default, the `tab-bar' only updates a tab name after the user visits it."
:type 'boolean
:group 'easysession)
(defcustom easysession-fontify nil
"When non-nil, force fontification of restored buffers.
This variable addresses an issue where `font-lock-mode' fails to fontify
buffers during session restoration when `redisplay-skip-fontification-on-input'
is non-nil. Without this, text remains unfontified until the user provides
input, such as pressing a key."
:type 'boolean
:group 'easysession)
;;; Internal variables
(defvar easysession--auto-saving nil
"Internal flag bound to t while an auto-save is in progress.")
(defvar easysession-debug nil)
;; Overrides `frameset-filter-alist' while preserving its keys,
;; but replaces their values with the ones specified in the following alist:
(defvar easysession--overwrite-frameset-filter-alist
'(;; Already excluded by frameset-persistent-filter-alist
(background-color . :never)
(buffer-list . :never)
(buffer-predicate . :never)
(buried-buffer-list . :never)
(delete-before . :never)
(foreground-color . :never)
(parent-frame . :never)
(mouse-wheel-frame . :never)
(window-system . :never)
(parent-id . :never)
(window-id . :never)
(name . easysession--frameset-filter-name-if-explicit)
;; Font
(font . :never)
(font-backend . :never)
(GUI:font . :never)
(font-parameter . :never)
(margins . :never)
;; Don't save the 'client' parameter to avoid that a subsequent
;; `save-buffers-kill-terminal' in a non-client session barks at
;; the user (Emacs Bug#29067).
(client . :never)
;; Geometry
(GUI:bottom . :never)
(GUI:fullscreen . :never)
(GUI:height . :never)
(GUI:left . :never)
(GUI:right . :never)
(GUI:top . :never)
(GUI:width . :never)
(bottom . :never)
(fullscreen . :never)
(height . :never)
(left . :never)
(right . :never)
(top . :never)
(width . :never)
;; Window-manager mutable flags: leave commented out
;; Uncommenting these ensures that the Window Manager (WM) decides how to
;; place the new frame (decorated, sticky, etc.) based on current rules,
;; rather than restoring old state which might confuse the WM.
(skip-taskbar . :never)
(sticky . :never)
(shaded . :never)
(undecorated . :never)
(override-redirect . :never)
;; Window-manager: Ensures frames are positioned only after the window
;; manager maps them, helping avoid small shifts in geometry.
;; (Do not restore wait-for-wm. Let the user configure it.)
(wait-for-wm . :never)
;; UI Chrome (Scroll bars, Tool bars, Menu bars)
;; To prevent the session from overriding your init.el settings (e.g., if
;; you disabled scrollbars in your config, loading a session shouldn't bring
;; them back).
(vertical-scroll-bars . :never)
(horizontal-scroll-bars . :never)
(scroll-bar-width . :never)
(scroll-bar-height . :never)
(tool-bar-position . :never)
(tool-bar-lines . :never)
(menu-bar-lines . :never)
;; (no-special-glyphs . :never)
;; Layout & Borders (Fringes, Dividers)
;; These define the "inner geometry" of the frame. If you change your font
;; size or fringe settings in your config, you don't want old session data
;; to force the old sizes back.
(left-fringe . :never)
(right-fringe . :never)
(line-spacing . :never)
(internal-border-width . :never)
(child-frame-border-width . :never) ; Geometry for child frames (popups)
(border-width . :never)
(bottom-divider-width . :never)
(right-divider-width . :never)
;; Pixel Precision (Affect the geometry, but let the user configure them)
(frameset--text-pixel-height . :never)
(frameset--text-pixel-width . :never)
;; Other exclusions
(scroll-bar-background . :never)
(scroll-bar-foreground . :never)
(background-mode . :never) ; Affects theme/light/dark mode
(border-color . :never)
(cursor-color . :never)
(mouse-color . :never)
;; On macOS, this controls whether the window title bar looks dark or light.
(ns-appearance . :never)
;; These control whether a frame automatically moves to the bottom or top of
;; the window stack when it loses/gains focus. This is a window manager
;; interaction preference. Restoring this overrides your OS/WM preferences.
(auto-lower . :never) ; control focus behavior
(auto-raise . :never) ; control focus behavior
;; This is a visual preference that belongs in your init.el. You don't want
;; a session saved 3 months ago to revert your cursor style.
(cursor-type . :never)
;; This describes the physical display (e.g., color, grayscale, mono). This
;; is determined by the hardware you are currently running on.
(display-type . :never)
;; This saves the system environment variables (like DISPLAY, XAUTHORITY,
;; SSH_AUTH_SOCK) active at the time of the save.
(environment . :never)
;; TODO
;; This sets the icon displayed by the OS.
(icon-type . :never)
;; These are graphics performance settings (fixing tearing/flickering). They
;; depend on the current machine and OS graphics stack. You definitely do
;; not want to restore these from an old file.
(inhibit-double-buffering . :never)
;; This controls whether the frame has its own minibuffer or shares one.
(minibuffer . :never)
;; These are often used for "utility" windows (like a dashboard or tooltip).
;; If you restore a regular editing frame that accidentally got tagged with
;; this, you will have a "ghost" window.
(no-accept-focus . :never)
(no-focus-on-map . :never)
;; This is the OS-level window ID. It is unique to every single window
;; instance. It is impossible to "restore" this as the OS assigns new IDs to
;; new windows.
(outer-window-id . :never)
;; This parameter controls the X Synchronization Extension (XSync) protocol
;; to prevent visual tearing during resizing; it is a hardware-dependent
;; runtime negotiation between Emacs and the Window Manager that should be
;; auto-detected on startup, not forced by an old session file.
(use-frame-synchronization . :never)
;; Hardware color correction. This belongs to the monitor profile/OS, not
;; the text editor session.
(screen-gamma . :never)
;; While you can generate titles dynamically, saving the title is often
;; harmless. If you manually rename frames to keep track of different
;; projects, you'll lose those names if you don't persist this.
;;
;; Argument for :never: If init.el uses frame-title-format to dynamically
;; show the current buffer and file path, restoring a static string from a
;; session file might temporarily override your dynamic title until the next
;; refresh.
;; (title . :never)
;; This controls if a frame is "always on top" (above) or "always on bottom"
;; (below). If your session relies on a specific window layout (e.g., a
;; reference window always floating above code), you need this parameter to
;; preserve that relationship.
;; (z-group . :never)
;; Fixes #24: Restoring a saved session does not restore all frames It was
;; caused by: (visibility . :never).
;;
;; Keep this commented, because it is CRITICAL: this must be restored. It
;; tells Emacs if the frame was minimized (iconified) or visible. If you
;; exclude this, you might restore frames that are invisible or in a
;; confused state.
;; (visibility . :never)
)
"EasySession overrides to prevent restoration of frame geometry.")
(defun easysession--filter-out-frameset-filters (list-keys)
"Remove geometry.
LIST-KEYS is the list of keys (e.g., GUI:left, bottom, height...)
from `easysession--overwrite-frameset-filter-alist`."
(seq-remove (lambda (entry)
(memq (car entry) list-keys))
easysession--overwrite-frameset-filter-alist))
(defvar easysession--overwrite-frameset-filter-include-geometry-alist
(easysession--filter-out-frameset-filters
'(; Same as `frameset-persistent-filter-alist'
GUI:bottom
GUI:fullscreen
GUI:height
GUI:left
GUI:right
GUI:top
GUI:width
bottom
fullscreen
height
left
right
top
width
;; Pixel perfect width and height
frameset--text-pixel-height
frameset--text-pixel-width
;; TODO experimental
;; vertical-scroll-bars
;; horizontal-scroll-bars
;; scroll-bar-width
;; scroll-bar-height
;; menu-bar-lines
;; tool-bar-lines
;; tool-bar-position
;; line-spacing
;; no-special-glyphs
;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Layout-Parameters.html
;; The frame is hidden from the taskbar or panel.
skip-taskbar
;; The frame is pinned to all virtual desktops/workspaces.
sticky
;; The frame is rolled up, usually showing only the title bar.
shaded
;; Decoration effect the total frame height
undecorated
;; border-width controls the thickness of the frame’s outer border (on all
;; sides) in pixels.
border-width
;; Internal border width is the padding between the text area and the outer
;; frame edge.
internal-border-width
;; The width in pixels of the frame’s internal border (see Frame Geometry)
;; if the given frame is a child frame (see Child Frames). If this is nil,
;; the value specified by the internal-border-width parameter is used
;; instead.
child-frame-border-width
;; Define the width (in pixels) of the left and right fringes, the narrow
;; areas next to the text area where indicators like git-gutter or
;; display-line-numbers appear. Restoring them ensures the text area width
;; and overall frame width remain consistent. Changes to these values can
;; shift text horizontally and slightly alter total frame width.
left-fringe
right-fringe
;; Specify the thickness of dividers separating the text area from the
;; bottom or right edges (or between window splits). Restoring them
;; preserves the exact pixel size of the text area and prevents small
;; offsets in frame height or width. Ignoring these can cause frames to
;; appear slightly larger or smaller than when saved.
bottom-divider-width
right-divider-width))
"EasySession overrides enabling restoration of frame geometry.
This alist is derived from `frameset-persistent-filter-alist' with explicit
inclusion of geometry-related frame parameters, including character-based,
pixel-based, and window-manager-sensitive layout attributes.")
(defvar easysession-file-version 3
"Version number of easysession file format.")
(defvar easysession--current-session-name nil
"Current session.")
(defvar easysession--session-loaded nil
"Non-nil indicates whether loading the current session has failed.
This variable is non-nil if an error occurred while attempting to load
the current session, otherwise it remains nil.")
(defvar easysession--load-handlers '()
"A list of functions used to load session data.
Each function in this list is responsible for loading a specific type of
buffer (e.g., file editing buffers, indirect buffers) from the session
information. These functions are applied sequentially to restore the session
state based on the saved session data.")
(defvar easysession--save-handlers '()
"A list of functions used to save session data.
Each function in this list is responsible for saving a specific type of
buffer (e.g., file editing buffers, indirect buffers) from the current
session. These functions are applied sequentially to capture the state of
the session, which can later be restored by the corresponding load handlers.")
(defvar easysession-visible-buffer-list-include-names '()
"List of buffer names that are always included in the session.
Each entry must be a string matching `buffer-name'. Buffers whose
names appear in this list are persisted and restored regardless
of their visibility.")
(defvar easysession--builtin-load-handlers
'(easysession--handler-load-file-editing-buffers
easysession--handler-load-managed-major-modes
easysession--handler-load-indirect-buffers)
"Internal variable.")
(defvar easysession--builtin-save-handlers
'(easysession--handler-save-file-editing-buffers
easysession--handler-save-managed-major-modes
easysession--handler-save-indirect-buffers)
"Internal variable.")
(defvar easysession--managed-major-modes nil
"Alist of (MODE . PROPS) for managed major modes.")
(defvar uniquify-buffer-name-style)
;;; Internal functions
(defun easysession--frameset-filter-name-if-explicit (_current
_filtered
parameters
_saving)
"Allow saving the frame name or title only if it was explicitly set.
The filter checks PARAMETERS for the `explicit-name' flag, which is
set by `set-frame-name'. This prevents EasySession from persisting
automatically generated buffer names as static titles.
The _CURRENT, _FILTERED, and _SAVING arguments are required by the
`frameset-filter-alist' interface but are currently unused."
(and (listp parameters)
(let ((explicit (assq 'explicit-name parameters)))
(and explicit (cdr explicit)
;; Return t
t))))
(defun easysession--message (&rest args)
"Display a message with '[easysession]' prepended.
The message is formatted with the provided arguments ARGS."
(unless easysession-quiet
(apply #'message (concat "[easysession] " (car args)) (cdr args))))
(defun easysession--warning (&rest args)
"Display a warning message with '[easysession] Warning: ' prepended.
The message is formatted with the provided arguments ARGS."
(apply #'message (concat "[easysession] Warning: " (car args)) (cdr args)))
(defun easysession--ensure-session-name-valid (session-name)
"Validate the provided SESSION-NAME.
If the SESSION-NAME is invalid, an error is raised with a message indicating
the invalid name.
Return the SESSION-NAME if it is valid.
Raise an error if the session name is invalid."
(when (or (not session-name)
(string= session-name "")
(let ((case-fold-search nil))
(or (string-match-p "\\\\" session-name)
(string-match-p "/" session-name)))
(string= session-name "..")
(string= session-name "."))
(user-error "[easysession] Invalid session name: %s" session-name))
session-name)
(defun easysession-set-current-session-name (&optional session-name)
"Set the current session name to SESSION-NAME.
Return t if the session name is successfully set."
(easysession--ensure-session-name-valid session-name)
(setq easysession--current-session-name session-name)
t)
(defun easysession--set-current-session (&optional session-name)
"Backward compatibility. SESSION-NAME is the session name."
(easysession-set-current-session-name session-name))
(defun easysession--init-frame-parameters-filters (overwrite-alist)
"Return an adjusted version of `frameset-filter-alist'.
This function produces a frameset filter alist based on the current
`frameset-filter-alist', applying any overrides specified in OVERWRITE-ALIST.
OVERWRITE-ALIST should be an alist where each element has the
form (FRAME-PARAM . VALUE), similar to
`easysession--overwrite-frameset-filter-alist'. Each pair in OVERWRITE-ALIST
replaces or adds the corresponding parameter in the resulting alist.
The returned alist can be used to control which frame parameters are
saved by EasySession."
(let ((result (copy-alist frameset-filter-alist)))
(dolist (pair overwrite-alist)
(setq result (assq-delete-all (car pair) result))
(push pair result))
result))
(defun easysession--get-all-names (&optional exclude-current)
"Return a list of all session names.
If EXCLUDE-CURRENT is non-nil, exclude the current session name from the list."
(if (file-directory-p easysession-directory)
(seq-filter (lambda (session-name)
(not (or (string-equal session-name ".")
(string-equal session-name "..")
(and exclude-current
easysession--current-session-name
(string= session-name
easysession--current-session-name)))))
(directory-files easysession-directory nil nil t))
'()))
(defun easysession--prompt-session-name (prompt &optional session-name
exclude-current initial-input)
"Read a session name from the minibuffer using PROMPT.
SESSION-NAME specifies the default selection.
When EXCLUDE-CURRENT is non-nil, the active session name is omitted from the
completion candidates.
When INITIAL-INPUT is non-nil, it is inserted into the minibuffer as the initial
contents.
Return the selected session name as a string."
(completing-read (concat "[easysession] " prompt)
(easysession--get-all-names exclude-current)
nil nil initial-input nil session-name))
(defun easysession--prompt-multiple-session-names (prompt
&optional session-name
exclude-current
initial-input)
"Read a session name from the minibuffer using PROMPT.
SESSION-NAME specifies the default selection.
When EXCLUDE-CURRENT is non-nil, the active session name is omitted from the
completion candidates.
When INITIAL-INPUT is non-nil, it is inserted into the minibuffer as the initial
contents.
Return the selected session name as a string."
(completing-read-multiple (concat "[easysession] " prompt)
(easysession--get-all-names exclude-current)
nil t initial-input nil session-name))
(defun easysession--buffer-narrowing-bounds (buffer)
"Return narrowing boundaries of BUFFER if it is narrowed.
If BUFFER is narrowed, return a cons cell (POINT-MIN . POINT-MAX) representing
the active narrowing region. If BUFFER is not narrowed, return nil."
(with-current-buffer buffer
(when (buffer-narrowed-p)
(cons (point-min) (point-max)))))
(defun easysession--get-indirect-buffer-info (indirect-buffer)
"Get information about the indirect buffer INDIRECT-BUFFER.
This function retrieves details about the indirect buffer INDIRECT-BUFFER and
its base buffer. It returns a list of cons cells containing the names of both
buffers, and narrowing bounds.
Return a list of cons cells: ((indirect-buffer-name . name-of-indirect-buffer)
(base-buffer-name . name-of-base-buffer)
(narrowing-bounds . (a, b))
Return nil if BUF is not an indirect buffer or if the base buffer cannot be
determined."
(when (buffer-live-p indirect-buffer)
(let ((base-buffer (buffer-base-buffer indirect-buffer)))
(when (and base-buffer ; Indirect buffer?
(buffer-live-p base-buffer))
(let ((base-buffer-name (buffer-name base-buffer))
(indirect-buffer-name (buffer-name indirect-buffer)))
(when (and base-buffer-name
indirect-buffer-name)
`((indirect-buffer-name . ,indirect-buffer-name)
(base-buffer-name . ,base-buffer-name)
(narrowing-bounds . ,(easysession--buffer-narrowing-bounds
indirect-buffer)))))))))
(defun easysession--get-managed-major-mode-buffer-info (buffer)
"Retrieve the persistent state for BUFFER if the major mode is managed.
Returns an alist containing `buffer-name', `major-mode', and `default-directory'
if the buffer's major mode derives from a key in
`easysession--managed-major-modes'.
If the configuration includes a `:save' function, it is invoked safely to obtain
custom state data, which is appended to the result under the `data' key.
Returns nil if BUFFER is not live or if no matching entry exists."
(when (buffer-live-p buffer)
(with-current-buffer buffer
(let ((entry (cl-find-if (lambda (entry)
(derived-mode-p (car entry)))
easysession--managed-major-modes)))
(when entry
(let* ((state `((buffer-name . ,(buffer-name))
(major-mode . ,(car entry))
(default-directory . ,default-directory)))
(save-fn (plist-get (cdr entry) :save))
(data (when (functionp save-fn)
(ignore-errors
(funcall save-fn)))))
(when data
(nconc state (list (cons 'data data))))
state))))))
(defun easysession-get-session-name ()
"Return the name of the current session.
Return nil when no session has been loaded before."
easysession--current-session-name)
(defalias 'easysession-get-current-session-name
'easysession-get-session-name
"Renamed to `easysession-get-session-name'.")
(make-obsolete 'easysession-get-current-session-name
'easysession-get-session-name
"1.1.3")
(defun easysession-get-session-file-path (&optional session-name)
"Return the absolute path to the session file for SESSION-NAME.
If SESSION-NAME is nil, use the currently loaded session.
Return nil if no session is loaded."
(unless session-name
(setq session-name easysession--current-session-name))
(unless session-name
(user-error "%s%s"
"[easysession] No session is active. "
"Load a session with `easysession-switch-to'"))
(when session-name
(easysession--ensure-session-name-valid session-name)
(expand-file-name session-name easysession-directory)))
;; (defvar easysession--frame-parameters-filters nil)
;; (defvar easysession--frame-parameters-filters-including-geo nil)
(defun easysession--save-frameset (session-name
&optional save-geometry)
"Create and return a frameset for the current Emacs session.
SESSION-NAME identifies the session associated with the saved frameset. When
SAVE-GEOMETRY is non-nil, frame geometry parameters are included; otherwise,
geometry-related parameters are excluded.
The frameset is generated only when at least one live frame exists. Return nil
when no frames are available for persistence."
;; TODO implement this
;; (setq easysession--frame-parameters-filters
;; (easysession--init-frame-parameters-filters
;; easysession--overwrite-frameset-filter-alist))
;; (setq easysession--frame-parameters-filters-including-geo
;; (easysession--init-frame-parameters-filters
;; easysession--overwrite-frameset-filter-include-geometry-alist))
(let ((modified-filter-alist
(if save-geometry
;; Include geometry
(easysession--init-frame-parameters-filters
easysession--overwrite-frameset-filter-include-geometry-alist)
;; Exclude geometry
(easysession--init-frame-parameters-filters
easysession--overwrite-frameset-filter-alist))))
;; Auto save when there is at least one frame
(when (frame-list)
(frameset-save nil
:app `(easysession . ,easysession-file-version)
:name session-name
:predicate #'easysession--check-dont-save
:filters modified-filter-alist))))
(defun easysession--can-restore-frameset-p ()
"True if calling `easysession--load-frameset' will actually restore it."
(and easysession-enable-frameset-restore
;; Skip restoring frames when the current frame is the daemon's initial
;; frame.
(not (and (daemonp)
(not (frame-parameter nil 'client))))
t))
(defun easysession--load-frameset (session-data &optional load-geometry)
"Load the frameset from the SESSION-DATA argument.
When LOAD-GEOMETRY is non-nil, load the frame geometry."
(when (easysession--can-restore-frameset-p)
(let* ((key (if load-geometry
"frameset-geo"
"frameset"))
(data (when (assoc key session-data)
(assoc-default key session-data)))
(modified-filter-alist
(if load-geometry
;; Include geometry
(easysession--init-frame-parameters-filters
easysession--overwrite-frameset-filter-include-geometry-alist)
;; Exclude geometry
(easysession--init-frame-parameters-filters
easysession--overwrite-frameset-filter-alist))))
(when (and (not data) load-geometry)
(setq data (when (assoc "frameset" session-data)
(assoc-default "frameset" session-data))))
(when data
(frameset-restore
data
:filters modified-filter-alist
:reuse-frames easysession-frameset-restore-reuse-frames
:cleanup-frames
(if (eq easysession-frameset-restore-cleanup-frames t)
(lambda (frame action)
(when (and (memq action '(:rejected :ignored))
;; Avoid cleaning up the initial daemon frame during
;; frameset restoration by disabling frame cleanup
;; when running under a daemon with a single frame.
(not (and (daemonp)
(equal (terminal-name (frame-terminal frame))
"initial_terminal"))))
(delete-frame frame)))
easysession-frameset-restore-cleanup-frames)
:force-display easysession-frameset-restore-force-display
:force-onscreen easysession-frameset-restore-force-onscreen)
(when (fboundp 'tab-bar-mode)
(when (seq-some
(lambda (frame)
(menu-bar-positive-p
(frame-parameter frame 'tab-bar-lines)))
(frame-list))
(tab-bar-mode 1)))))))
(defun easysession--ensure-buffer-name (buffer name)
"Ensure that BUFFER name is NAME."
(when (not (string= (buffer-name buffer) name))
(with-current-buffer buffer
(rename-buffer name t))))
(defun easysession--check-dont-save (frame)
"Check if FRAME is a real frame and should be saved.
Also checks if `easysession-dont-save' is set to t."
;; Exclude frames with a parent frame. One common use of the parent-frame
;; parameter is in the context of child frames, often used by packages like
;; posframe, which create transient, overlay, or tooltip-like frames. These
;; child frames are associated with a parent frame to maintain a logical and
;; spatial relationship.
(and (not (frame-parameter frame 'parent-frame))
(not (frame-parameter frame 'easysession-dont-save))
(not (frame-parameter frame 'desktop-dont-save))
;; Avoid saving the initial daemon frame
(not (and (daemonp)
(equal (terminal-name (frame-terminal frame))
"initial_terminal")))))
(defun easysession--auto-save ()
"Automatically save the current session when permitted.
This function is invoked by `easysession-save-mode'. It evaluates
`easysession-save-mode-predicate' and saves the current session when a session
is loaded, a session name is defined, and at least one frame exists.
The function always returns non-nil so that it does not inhibit Emacs
termination when used from `kill-emacs-query-functions'."
;; Auto save when there is at least one frame and a session has been loaded
(condition-case err
(when (and easysession--current-session-name
easysession--session-loaded
(or (not easysession-save-mode-predicate)
(funcall easysession-save-mode-predicate))
(> (length (easysession--frame-list)) 0))
(let ((easysession--auto-saving t))
(easysession-save)))
(error
(easysession--warning "Auto-save failed: %s"
(error-message-string err))))
;; Always return t, since this `easysession--auto-save' is part of
;; `kill-emacs-query-functions'. Returning nil would prevent Emacs from
;; exiting.
t)
(defun easysession--lighter-session-name-format ()
"Return the mode line lighter format for the current session."
(if easysession--current-session-name
(list (format "%s[" easysession-save-mode-lighter)
(propertize easysession--current-session-name
'face 'easysession-mode-line-session-name-face)
(unless easysession--session-loaded
" <NOT LOADED>")
"]")
easysession-save-mode-lighter))
(defun easysession--mode-line-session-name-format ()
"Return a mode-line construct for the currently loaded session.
The session name is displayed only when a session is actively loaded."
(if (bound-and-true-p easysession--current-session-name)
(list easysession-mode-line-misc-info-prefix
(propertize
easysession--current-session-name
'face 'easysession-mode-line-session-name-face
'help-echo (format "Current session: %s"
easysession--current-session-name)
'mouse-face 'mode-line-highlight)
(unless easysession--session-loaded
" <NOT LOADED>")
easysession-mode-line-misc-info-suffix)))
(defun easysession--get-scratch-buffer-create ()
"Return the *scratch* buffer, creating a new one if needed."
(if (fboundp 'get-scratch-buffer-create)
(funcall 'get-scratch-buffer-create)
(or (get-buffer "*scratch*")
(let ((scratch (get-buffer-create "*scratch*")))
(with-current-buffer scratch
(when initial-scratch-message
(insert (substitute-command-keys initial-scratch-message))
(set-buffer-modified-p nil))
(funcall initial-major-mode)
(when (eq initial-major-mode 'lisp-interaction-mode)
(with-no-warnings
(setq-local trusted-content :all))))
scratch))))
(defun easysession--buffer-is-visible (buffer)
"Return non-nil if BUFFER is currently visible in the Emacs session.
A buffer is considered visible if it is:
- Displayed in any visible window (`get-buffer-window').
- Associated with a visible tab in `tab-bar-mode' (if enabled).
Returns nil if the buffer is not displayed in a window or tab."
(or
;; Windows
(get-buffer-window buffer 0)
;; Tab-bar windows
(and (bound-and-true-p tab-bar-mode)
(fboundp 'tab-bar-get-buffer-tab)
(tab-bar-get-buffer-tab buffer t nil))))
(defun easysession--cl-list* (&rest args)
"Return a list from ARGS using `cl-list*', or nil if ARGS is empty.
This is a safe wrapper around `cl-list*' that avoids errors when called without
any arguments. Each element of ARGS becomes part of the resulting list, with the
last argument as the final element of the list."
(and args (apply #'cl-list* args)))
;; (defvar easysession--internal-delay-hook nil
;; "Hooks that run after all buffers have loaded; intended for internal use.")
(defun easysession--serialize-to-quoted-sexp (value)
"Convert VALUE to a pair (QUOTE . SEXP); (eval SEXP) gives VALUE.
SEXP is an sexp that when evaluated yields VALUE.
QUOTE may be `may' (value may be quoted),
`must' (value must be quoted), or nil (value must not be quoted)."
(cond
((or (numberp value) (null value) (eq t value) (keywordp value))
(cons 'may value))
((stringp value)
;; Remove any unreadable text properties
(if (condition-case nil (read (format "%S" value)) (error nil))
(cons 'may value)
(let ((copy (copy-sequence value)))
(set-text-properties 0 (length copy) nil copy)
(cons 'may copy))))
((symbolp value)
(cons 'must value))
((vectorp value)
(let* ((pass1 (mapcar #'easysession--serialize-to-quoted-sexp value))
(special (assq nil pass1)))
(if special
(cons nil `(vector
,@(mapcar (lambda (el)
(if (eq (car el) 'must)
`',(cdr el) (cdr el)))
pass1)))
(cons 'may `[,@(mapcar #'cdr pass1)]))))
((and (recordp value) (symbolp (aref value 0)))
(let* ((pass1 (let ((res ()))
(dotimes (i (length value))
(push (easysession--serialize-to-quoted-sexp
(aref value i)) res))
(nreverse res)))
(special (assq nil pass1)))
(if special
(cons nil `(record
,@(mapcar (lambda (el)
(if (eq (car el) 'must)
`',(cdr el) (cdr el)))
pass1)))
(cons 'may (apply #'record (mapcar #'cdr pass1))))))
((consp value)
(let ((p value)
newlist
use-list*)
(while (consp p)
(let ((q.sexp (easysession--serialize-to-quoted-sexp (car p))))
(push q.sexp newlist))
(setq p (cdr p)))
(when p
(let ((last (easysession--serialize-to-quoted-sexp p)))
(setq use-list* t)
(push last newlist)))
(if (assq nil newlist)
(cons nil
`(,(if use-list* 'easysession--cl-list* 'list)
,@(mapcar (lambda (el)
(if (eq (car el) 'must)
`',(cdr el) (cdr el)))
(nreverse newlist))))
(cons 'must
`(,@(mapcar #'cdr
(nreverse (if use-list* (cdr newlist) newlist)))
. ,(if use-list* (cdar newlist)))))))
((subrp value)
(cons nil `(symbol-function
',(intern-soft (substring (prin1-to-string value) 7 -1)))))
;; NOTE: Causes issues when opening `org-agenda', then
;; `window-toggle-side-windows', then `easysession-save'
;; ((markerp value)
;; (let ((pos (marker-position value))
;; (buf (buffer-name (marker-buffer value))))
;; (cons nil
;; `(let ((mk (make-marker)))
;; (add-hook 'easysession--internal-delay-hook
;; (lambda ()
;; (set-marker mk ,pos (get-buffer ,buf))))
;; mk))))
(t
(cons 'may "Unprintable entity"))))
(defvar easysession--daemon-session-loaded nil
"Non-nil if an EasySession session has already been loaded in daemon mode.
This variable prevents multiple session loads when Emacs is running as a daemon.
It is set to t after the first successful session load and should not be
manually modified under normal operation.")
(defun easysession--persist-session-on-frame-delete-maybe (frame)
"Save the current session when a client frame is deleted in daemon mode.
FRAME designates the frame scheduled for deletion.
This ensures the session is saved before the last client frame is closed in
daemon mode, allowing correct restoration when a new frame is created."
(when (and easysession--current-session-name
easysession--session-loaded
(daemonp)
(frame-live-p frame)
;; Since easysession--frame-list filters out the initial daemon
;; terminal and utility frames, a length of 1 means the frame
;; currently being deleted is the last active client frame.
(= (length (easysession--frame-list)) 1))
(easysession-unload)))
(defun easysession--setup-load-session ()
"Load an EasySession session.
After loading in daemon mode, `easysession--daemon-session-loaded' is set to t
to prevent multiple loads during the same daemon session."
(when (or (not easysession-setup-load-predicate)
(funcall easysession-setup-load-predicate))
(if (daemonp)
(unless easysession--daemon-session-loaded
(if easysession-setup-load-session-including-geometry
(easysession-load-including-geometry)
(easysession-load))
(setq easysession--daemon-session-loaded t))
(if easysession-setup-load-session-including-geometry
(easysession-load-including-geometry)
(easysession-load)))))
(defun easysession--frame-list ()
"Return a list of user frames, excluding utility and child frames."
(seq-filter
(lambda (frame)
(and
;; The daemon runs in the background on the "initial_terminal". This frame
;; has no display and is never interacted with by the user. Excluding it
;; ensures we only count actual emacsclient connections.
(not (equal (terminal-name (frame-terminal frame)) "initial_terminal"))
;; Packages like Corfu, Posframe, or Company create child frames to
;; display floating popups or menus. These are attached to a main user
;; frame and should not be counted as independent sessions.
(not (frame-parent frame))
;; Tooltips can sometimes be implemented as separate native frames
;; depending on the OS and Emacs build. We ignore them to prevent a
;; transient hover effect from interfering with the frame count.
(not (frame-parameter frame 'tooltip))
;; This visibility check handles specific edge cases: `frame-visible-p'
;; returns t (visible), 'icon (minimized), or nil (hidden).
;;
;; 1. It keeps minimized GUI frames because 'icon evaluates as true in
;; Elisp.
;; 2. It excludes completely hidden GUI frames, which are sometimes kept
;; invisible in the background as caches by certain packages.
;; 3. It unconditionally keeps terminal (TUI) frames via the second
;; condition. Terminal frames can report as invisible when the terminal
;; emulator is suspended or backgrounded, but they still represent real
;; connections.
(or (frame-visible-p frame)
(not (display-graphic-p frame)))))
(frame-list)))
(defun easysession--refresh-tabs-all-frames ()
"Cycle through all tabs on all frames to force a state and name refresh.
If the refresh is triggered by an auto-save, it executes even if the
minibuffer is currently active.
When Emacs buffers are renamed automatically by packages like uniquify,
background tabs in `tab-bar-mode' often retain the old buffer names because they
store window configurations as static data.
This creates a confusing interface where the visible tab titles fail to match
the actual active buffers. Cycling through all tabs across every frame forces
Emacs to deserialize the window states and update its internal tracking
information. Consequently, the workspace always displays accurate tab names,
which prevents navigation errors and ensures the visual layout reflects the
exact state of your open files."
(when (and (or easysession--auto-saving (not (active-minibuffer-window)))
(fboundp 'tab-bar--current-tab-index)
(fboundp 'tab-bar-select-tab)
(boundp 'tab-bar-tabs-function)
(bound-and-true-p tab-bar-mode))
(save-current-buffer
(let ((inhibit-redisplay t)
(inhibit-message t)
;; Core window and buffer sandbox
(buffer-list-update-hook nil)
(window-buffer-change-functions nil)
(window-configuration-change-hook nil)
(window-state-change-functions nil)
(window-size-change-functions nil)
(window-selection-change-functions nil)
(window-state-change-hook nil)
(tab-bar-tab-post-select-functions nil)
;; Tab-bar specific background overrides
(read-minibuffer-restore-windows nil)
(tab-bar-history-mode nil)
(tab-bar-select-restore-windows nil)
(window-restore-killed-buffer-windows nil)
(tab-bar-select-restore-context nil))
(ignore read-minibuffer-restore-windows)
(ignore tab-bar-history-mode)
(ignore tab-bar-select-restore-windows)
(ignore window-restore-killed-buffer-windows)
(ignore tab-bar-select-restore-context)
(ignore buffer-list-update-hook)
(ignore window-buffer-change-functions)
(ignore window-configuration-change-hook)
(ignore window-state-change-functions)
(ignore window-size-change-functions)
(ignore window-selection-change-functions)
(ignore window-state-change-hook)
(ignore tab-bar-tab-post-select-functions)
(dolist (frame (frame-list))
(with-selected-frame frame
(save-window-excursion
(let* ((tabs (funcall tab-bar-tabs-function frame))
(original-index (tab-bar--current-tab-index tabs frame))
(tab-count (length tabs)))
(when (> tab-count 1)
(unwind-protect
(dotimes (index tab-count)
(unless (eq index original-index)
(tab-bar-select-tab (1+ index))))
(when original-index
(tab-bar-select-tab (1+ original-index)))))))))))))
;;; Internal functions: handlers
(defun easysession--restore-buffer-state (buffer buffer-info)
"Restore the state of BUFFER from previously recorded session DATA.
BUFFER is the target buffer to restore. BUFFER-INFO is an alist containing saved
buffer attributes, currently including `narrowing-bounds` which specifies the
start and end of the narrowed region.
This function currently restores the narrowing region in the buffer so that it
reflects the same visible portion as when the session was saved.
In the future, this function may be extended to restore additional buffer
attributes such as point position, mark, local variables, or other editor state
information."
(with-current-buffer buffer
(widen)
(let* ((narrowing-bounds (alist-get 'narrowing-bounds buffer-info)))
(when (and narrowing-bounds (consp narrowing-bounds))
(let* ((start (car narrowing-bounds))
(end (cdr narrowing-bounds)))
(when (and (numberp start)
(numberp end))
(narrow-to-region start end)))))))
(defun easysession--handler-load-file-editing-buffers (session-data)
"Load base buffers from SESSION-DATA.
SESSION-DATA may encode buffer entries using either a legacy or a current
representation:
- In the legacy representation, each buffer entry is a cons cell of the
form (BUFFER-NAME . BUFFER-PATH). In this case, the buffer name is taken from
the car and the file path from the cdr, and no additional buffer state is
available.
- In the current representation, each buffer entry is an alist. This format is
identified when the car of the entry is itself a cons. The alist provides
structured fields such as `buffer-name' and `buffer-path', and may optionally
include `narrowing-bounds', allowing additional buffer state to be restored.
The loader detects the representation dynamically and restores buffers
accordingly, ensuring backward compatibility with legacy session files."
(dolist (buffer-info (or (assoc-default "path-buffers" session-data)
(assoc-default "buffers" session-data)))
(let* ((uniquify-buffer-name-style nil)
(new-format-p (and (consp buffer-info)
(consp (car buffer-info))))
(buffer-name (if new-format-p
(alist-get 'buffer-name buffer-info)
(car buffer-info)))
(buffer-path (if new-format-p
(alist-get 'buffer-path buffer-info)
(cdr buffer-info))))
(when buffer-path
(let ((original-buffer (get-file-buffer buffer-path))
buffer)
(if (buffer-live-p original-buffer)
(setq buffer (or (buffer-base-buffer original-buffer)
original-buffer))
(let ((new-buffer (let ((find-file-hook
(seq-difference
find-file-hook
easysession-exclude-from-find-file-hook))
(inhibit-message t)
;; Bind `auto-insert' to nil during buffer
;; restoration. This prevents
;; `auto-insert-mode' from halting the
;; background session load with interactive
;; prompts (or silently inserting
;; boilerplate text) if a file from the
;; saved session was deleted or truncated
;; between sessions. It ensures session
;; loading remains fast, non-interactive,
;; and does not unintentionally mark
;; restored buffers as modified.
(auto-insert nil))
(ignore auto-insert) ; Silence warning
(condition-case err
(find-file-noselect buffer-path t)
(error
(easysession--warning
"Failed to restore the buffer '%s': %s"
buffer-name
(error-message-string err))
nil)))))
;; We are going to be using the base buffer to make sure that the
;; buffer that was returned by `find-file-noselect' is a base
;; buffer and not a clone
(setq buffer (or (buffer-base-buffer new-buffer) new-buffer))))
(unless (buffer-base-buffer buffer)
(if (not (buffer-live-p buffer))
(easysession--warning "Failed to restore the buffer '%s': %s"
buffer-name buffer-path)
;; Ensure that buffer name is buffer-name
(easysession--ensure-buffer-name buffer buffer-name)
;; Restore buffer narrowing if present
(when new-format-p
(easysession--restore-buffer-state buffer
buffer-info)))))))))
(defun easysession--handler-load-indirect-buffers (session-data)
"Load indirect buffers from the SESSION-DATA variable."
(dolist (item (assoc-default "indirect-buffers" session-data))
(let ((indirect-buffer-name (alist-get 'indirect-buffer-name item))
(base-buffer-name (alist-get 'base-buffer-name item))
(uniquify-buffer-name-style nil))
(when (and indirect-buffer-name
base-buffer-name)
(let ((base-buffer (get-buffer base-buffer-name))
(indirect-buffer (get-buffer indirect-buffer-name)))
(when (and (not (buffer-live-p indirect-buffer))
(buffer-live-p base-buffer))
(condition-case err
(progn
(setq indirect-buffer (with-current-buffer base-buffer
(clone-indirect-buffer
indirect-buffer-name nil)))
(if (not (buffer-live-p indirect-buffer))
(easysession--warning
"Failed to restore the indirect buffer/clone: %s"
indirect-buffer-name)
;; Restore indirect buffer
(easysession--ensure-buffer-name indirect-buffer
indirect-buffer-name)
;; Restore buffer narrowing if present
(easysession--restore-buffer-state indirect-buffer item)))
(error
(easysession--warning
"Failed to restore indirect buffer/clone '%s': %s"
indirect-buffer-name (error-message-string err))))))))))
(defun easysession--get-base-buffer-info (buffer)
"Return base buffer metadata for BUFFER.
If BUFFER is a live base buffer associated with a path, return an alist with the
buffer name, buffer path, and narrowing information.
If BUFFER is not a base buffer or has no associated path, return nil."
(unless (buffer-base-buffer buffer)
(with-current-buffer buffer
(let ((path (if (derived-mode-p 'dired-mode)
default-directory
(buffer-file-name)))
(uniquify-base-name (and (fboundp 'uniquify-buffer-base-name)
(uniquify-buffer-base-name))))
(when path
;; File visiting buffer and base buffers (not carbon copies)
`((buffer-name . ,(buffer-name))
(uniquify-base-name . ,uniquify-base-name)
(buffer-path . ,path)
(narrowing-bounds . ,(easysession--buffer-narrowing-bounds
buffer))))))))
(defun easysession--handler-save-file-editing-buffers (buffers)
"Collect and categorize file editing buffers from the provided list.
BUFFERS is the list of buffers to process. This function identifies buffers
that are associated with files (file editing buffers) and those that are
not. It returns an alist with the following structure."
(let ((file-editing-buffers '())
(remaining-buffers '()))
(dolist (buf buffers)
(when (buffer-live-p buf)
(let ((base-buffer-info (easysession--get-base-buffer-info buf)))
(if base-buffer-info
(push base-buffer-info file-editing-buffers)
(push buf remaining-buffers)))))
`((key . "path-buffers")
(value . ,file-editing-buffers)
(remaining-buffers . ,remaining-buffers))))
(defun easysession--handler-save-indirect-buffers (buffers)
"Collect and categorize indirect buffers from the provided list.
BUFFERS is the list of buffers to process. This function identifies indirect
buffers and separates them from other buffers."
(let ((indirect-buffers '())
(remaining-buffers '()))
(dolist (buf buffers)
(let ((indirect-buffer-info (easysession--get-indirect-buffer-info buf)))
(if indirect-buffer-info
(push indirect-buffer-info indirect-buffers)
(push buf remaining-buffers))))
`((key . "indirect-buffers")
(value . ,indirect-buffers)
(remaining-buffers . ,remaining-buffers))))
(defun easysession--handler-load-managed-major-modes (session-data)
"Load managed major mode buffers from SESSION-DATA safely.
Failures restoring individual buffers are logged but do not stop other buffers."
(let ((managed-mode-buffers (assoc-default "managed-major-modes"
session-data)))
(when managed-mode-buffers
(dolist (item managed-mode-buffers)
(let ((buffer-name (alist-get 'buffer-name item))
(mode (alist-get 'major-mode item)))
(when (and buffer-name mode)
(let ((props (cdr (assq mode easysession--managed-major-modes))))
(when (and props (not (get-buffer buffer-name)))
(let ((restore-fn (plist-get props :restore))
(validate-fn (plist-get props :validate)))
(when (and restore-fn
(or (null validate-fn) (funcall validate-fn item)))
(let ((default-directory
(or (alist-get 'default-directory item)
default-directory)))
(condition-case err
(funcall restore-fn item)
(error
(easysession--warning
"Failed to restore %s buffer '%s': %s"
mode buffer-name (error-message-string err)))))))))))))))
(defun easysession--handler-save-managed-major-modes (buffers)
"Collect and categorize managed mode buffers from the provided list.
BUFFERS is the list of buffers to process. This function identifies buffers
with managed modes and separates them from other buffers."
(let ((managed-mode-buffers '())
(remaining-buffers '()))
(dolist (buf buffers)
(let ((managed-mode-buffer-info
(easysession--get-managed-major-mode-buffer-info buf)))
(if managed-mode-buffer-info
(push managed-mode-buffer-info managed-mode-buffers)
(push buf remaining-buffers))))
`((key . "managed-major-modes")
(value . ,managed-mode-buffers)
(remaining-buffers . ,remaining-buffers))))
(defun easysession-add-load-handler (handler-fn)
"Add a load handler.
The handler is only added if it's not already present and if HANDLER-FN is a
symbol representing an existing function. HANDLER-FN is the function to load
session data."
(unless (and (symbolp handler-fn)
(fboundp handler-fn))
(error "[easysession] HANDLER-FN must be a symbol representing a function"))
(unless (memq handler-fn easysession--load-handlers)
(setq easysession--load-handlers
(append easysession--load-handlers (list handler-fn)))))
(defun easysession-add-save-handler (handler-fn)
"Add a save handler.
HANDLER-FN is the function to save session data.
The HANDLER-FN handler is only added if it's not already present."
(unless (and (symbolp handler-fn)
(fboundp handler-fn))
(error "[easysession] HANDLER-FN must be a symbol representing a function"))
(unless (memq handler-fn easysession--save-handlers)
;; (push handler-fn easysession--save-handlers)
(setq easysession--save-handlers
(append easysession--save-handlers (list handler-fn)))))
(defun easysession-remove-load-handler (handler-fn)
"Remove a load handler.
HANDLER-FN is the function to load session data.
The HANDLER-FN handler is only added if it's not already present."
(unless (and (symbolp handler-fn)
(fboundp handler-fn))
(error "[easysession] HANDLER-FN must be a symbol representing a function"))
(setq easysession--load-handlers
(delq handler-fn easysession--load-handlers)))
(defun easysession-remove-save-handler (handler-fn)
"Remove a save handler.
HANDLER-FN is the function to be removed."
(unless (and (symbolp handler-fn)
(fboundp handler-fn))
(error "[easysession] HANDLER-FN must be a symbol representing a function"))
(setq easysession--save-handlers (delete handler-fn
easysession--save-handlers)))
(defun easysession-get-save-handlers ()
"Return a list of all built-in and user-defined save handlers."
(append easysession--save-handlers
easysession--builtin-save-handlers))
(defun easysession-get-load-handlers ()
"Return a list of all built-in and user-defined load handlers."
(append easysession--load-handlers
easysession--builtin-load-handlers))
(defun easysession-add-managed-major-mode (mode &rest props)
"Add a managed major mode.
MODE must be a non-nil symbol representing a major mode.
PROPS is a keyword-value property list. Supported keys are:
:restore (Required) A function called during session loading. It
receives an alist containing `buffer-name', `major-mode',
`default-directory', and a `data' key for mode-specific
state.
:save (Optional) A function that returns an alist of mode-specific
data to be persisted.
:validate (Optional) A function used to verify the integrity of
restored data.
If MODE is already managed, the new properties replace the existing
registration."
(unless (and (symbolp mode) mode)
(error "[easysession] MODE must be a non-nil symbol"))
(unless (plist-get props :restore)
(error "[easysession] :restore function is required for mode %S" mode))
(setq easysession--managed-major-modes
(cons (cons mode props)
(assq-delete-all mode easysession--managed-major-modes))))
(defun easysession-remove-managed-major-mode (mode)
"Unregister MODE from the collection of managed major modes.
MODE must be a symbol representing a valid major mode previously registered via
`easysession-add-managed-major-mode'. Upon execution, the associated restoration
logic is purged from the internal registry, returning the specified major mode
to an unmanaged state.
If MODE is not present in the registry, the operation terminates silently
without altering the current configuration."
(unless (and (symbolp mode) mode)
(error "[easysession] MODE must be a non-nil symbol"))
(setq easysession--managed-major-modes
(assq-delete-all mode easysession--managed-major-modes)))
(defmacro easysession-define-load-handler (key handler-func)
"Add a load handler for a specific session KEY.
KEY is the identifier used in the EasySession file. Avoid reserved keys like:
buffers, indirect-buffers, frameset, and frameset-geo. Prefix with an
underscore to be safe.
HANDLER-FUNC is a callable that is invoked with session data when the key is
found."
(declare (indent 0) (debug t))
`(progn
(defun
,(intern (concat "easysession--handler-load-" key)) (session-data)
,(format "Load handler for restoring: %s."
key)
(let ((handler-data (assoc-default ,key session-data)))
(when handler-data
(funcall ,handler-func handler-data))))
(easysession-add-load-handler
',(intern (concat "easysession--handler-load-" key)))))
(defmacro easysession-define-generic-save-handler (key &rest body)
"Add a save handler to EasySession.
KEY is the identifier for this session data within EasySession. Prefix with an
underscore to be safe. Avoid using reserved keys such as: buffers,
indirect-buffers, frameset, and frameset-geo.
BODY is executed."
(declare (indent 0) (debug t))
`(progn
(defun ,(intern (concat "easysession--" key "-save-handler")) (buffers)
,(format "Save handler for: %s." key)
(when buffers
t) ;; Remove warnings
,@body)
(easysession-add-save-handler
',(intern (concat "easysession--" key "-save-handler")))))
(defmacro easysession-define-save-handler (key handler-func)
"Add a save handler to EasySession.
KEY is the identifier for this session data within EasySession. Prefix with an
underscore to be safe. Avoid using reserved keys such as: buffers,
indirect-buffers, frameset, and frameset-geo.
HANDLER-FUNC is a callable that processes each buffer and returns its session
data."
(declare (indent 0) (debug t))
`(easysession-define-generic-save-handler
,key
(let ((result (funcall ,handler-func buffers)))
(when result
(push (cons 'key ,key) result)))))
(defmacro easysession-define-handler (key load-handler-func save-handler-func)
"Add both load and save handlers for a given KEY.
KEY is the session identifier. Avoid reserved keys.
LOAD-HANDLER-FUNC and SAVE-HANDLER-FUNC are functions for handling session
data."
(declare (indent 0) (debug t))
`(progn
(easysession-define-load-handler ,key ,load-handler-func)
(easysession-define-save-handler ,key ,save-handler-func)))
(defmacro easysession-save-handler-dolist-buffers (buffers &rest body)
"Iterate over BUFFERS, execute BODY inside each buffer's context.
Classify buffers based on BODY's result.
Returns a list:
((buffers . SAVED-BUFFERS)
(remaining-buffers . REMAINING-BUFFERS))"
(declare (indent 0) (debug t))
(let ((saved-buffers (make-symbol "saved-buffers"))
(remaining-buffers (make-symbol "remaining-buffers"))
(buffer (make-symbol "buffer"))
(buffer-data (make-symbol "buffer-data")))
`(let ((,saved-buffers nil)
(,remaining-buffers nil)
(,buffer-data nil)
(,buffer nil))
(dolist (,buffer ,buffers)
(with-current-buffer ,buffer
(let ((,buffer-data (progn ,@body)))
(if ,buffer-data
(push ,buffer-data ,saved-buffers)
(push ,buffer ,remaining-buffers)))))
(list
(cons 'buffers ,saved-buffers)
(cons 'remaining-buffers ,remaining-buffers)))))
(defmacro easysession-undefine-load-handler (key)
"Remove the load handler associated with KEY from EasySession."
(declare (indent 0) (debug t))
`(let ((fn ',(intern (concat "easysession--handler-load-" key))))
(when (fboundp fn)
(unintern fn nil))
(setq easysession--load-handlers
(delq fn easysession--load-handlers))))
(defmacro easysession-undefine-save-handler (key)
"Remove the save handler associated with KEY from EasySession."
(declare (indent 0) (debug t))
`(let ((fn ',(intern (concat "easysession--" key "-save-handler"))))
(when (fboundp fn)
(unintern fn nil))
(setq easysession--save-handlers
(delq fn easysession--save-handlers))))
(defmacro easysession-undefine-handler (key)
"Remove both load and save handlers associated with KEY from EasySession."
(declare (indent 0) (debug t))
`(progn
(easysession-undefine-load-handler ,key)
(easysession-undefine-save-handler ,key)))
;;; Autoloaded functions
;;;###autoload
(defun easysession-save-session-and-close-frames ()
"Save the session and close all frames without stopping the Emacs daemon.
Useful in daemon mode, this simulates quitting Emacs: buffers are saved, the
EasySession state is saved, and all frames except the initial terminal frame are
closed.
From the perspective of EasySession, this is functionally equivalent to an
application shutdown: the session is fully saved and unloaded. When a new frame
is later initialized by the Emacs daemon, EasySession restores the state as if
the process had been freshly started."
(interactive)
(when (yes-or-no-p "[easysession] Save session and close all frames? ")
(save-some-buffers)
(easysession-unload)
;; Freeze display before tearing down the GUI
(let ((inhibit-redisplay t))
;; Close all frames
(dolist (frame (frame-list))
(when (and (frame-live-p frame)
(or (not (daemonp))
(not (string-equal (terminal-name (frame-terminal frame))
"initial_terminal"))))
(ignore-errors
(delete-frame frame t)))))))
;;;###autoload
(defun easysession-setup ()
"Initialize `easysession' for session persistence.
If Emacs is running as a daemon, add `easysession-load-including-geometry' to
`server-after-make-frame-hook' so that session restoration occurs for each new
frame. Otherwise, add it to `emacs-startup-hook' (or `elpaca-after-init-hook`
if Elpaca is used) to restore the session at startup.
Also enable `easysession-save-mode' on startup to automatically save sessions.
Hook priorities are controlled by `easysession-setup-add-hook-depth'.
This function prepares `easysession' for automatic loading and saving of frames,
buffers, and session data."
(easysession--update-modeline-misc-info
easysession-mode-line-misc-info-format)
;; Dynamically select the correct startup hook based on the package manager
(let ((startup-hook (if (boundp 'elpaca-after-init-hook)
'elpaca-after-init-hook
'emacs-startup-hook)))
(when easysession-setup-load-session
(if (daemonp)
;; Daemon mode
(progn
(when (seq-some (lambda (frame)
(frame-parameter frame 'client))
(frame-list))
(easysession--setup-load-session))
(add-hook 'server-after-make-frame-hook
#'easysession--setup-load-session
easysession-setup-add-hook-depth))
;; Graphical mode
(add-hook startup-hook
#'easysession--setup-load-session
easysession-setup-add-hook-depth)))
;; Save the current session every `easysession-save-interval'
(add-hook startup-hook #'easysession-save-mode
easysession-setup-add-hook-depth)))
;;;###autoload
(defun easysession-visible-buffer-list ()
"Return a list of all buffers considered visible in the current session.
A buffer is included if it satisfies any of the following:
- It is the *scratch* buffer (included as a special case).
- It is currently displayed in a visible window.
- It is associated with a visible tab in `tab-bar-mode', if enabled.
The returned list contains live buffers only."
(let ((visible-buffers '()))
(dolist (buffer (buffer-list))
(when (and (buffer-live-p buffer)
(or
;; Exceptions
(member (buffer-name buffer)
easysession-visible-buffer-list-include-names)
;; Buffers and indirect buffers
(let ((base-buffer (buffer-base-buffer buffer)))
(cond
;; Indirect buffers
(base-buffer
(and
(buffer-live-p base-buffer)
(or
;; Is the indirect buffer visible?
(easysession--buffer-is-visible buffer)
;; Is the base buffer visible?
(easysession--buffer-is-visible base-buffer))))
;; Normal buffers
(t
(easysession--buffer-is-visible buffer))))))
(push buffer visible-buffers)))
visible-buffers))
;;;###autoload
(defun easysession-kill-all-buffers ()
"Kill all buffers, except special buffers."
(mapc (lambda (buffer)
(when (buffer-live-p buffer)
(let* ((name (buffer-name buffer))
(base (buffer-base-buffer buffer))
(file (buffer-file-name (if base
base
buffer))))
(when (and
;; Special Buffer Checks
(not (or (string-prefix-p " " name)
(and (string-prefix-p "*" name)
(string-suffix-p "*" name))
(with-current-buffer buffer
(derived-mode-p 'special-mode))
(minibufferp buffer)))
;; Safety Check (Don't kill modified files)
(or (not file)
(not (buffer-modified-p buffer))))
(kill-buffer buffer)))))
(buffer-list)))
;;;###autoload
(defun easysession-reset ()
"Kill all buffers and close all frames, tabs, and windows."
(interactive)
;; Hooks
(run-hooks 'easysession-before-reset-hook)
(let ((inhibit-redisplay t))
;; Delete frames
(delete-other-frames)
;; Close tabs
(when (and (bound-and-true-p tab-bar-mode)
(fboundp 'tab-bar-close-other-tabs))
(tab-bar-close-other-tabs))
;; Close windows
(delete-other-windows)
;; Switch to the scratch buffer
(switch-to-buffer (easysession--get-scratch-buffer-create) nil t))
;; Kill all buffers
(easysession-kill-all-buffers)
;; Hooks
(run-hooks 'easysession-after-reset-hook))
;;;###autoload
(defun easysession-rename (new-session-name)
"Rename the current session.
NEW-SESSION-NAME is the session name."
(interactive
(list
(progn
(unless easysession--current-session-name
(user-error
"[easysession] No session is active. Load a session with `easysession-switch-to'"))
(easysession--prompt-session-name
(format "Rename session '%s' to: "
easysession--current-session-name)
nil
nil
easysession--current-session-name))))
(unless easysession--current-session-name
(user-error
"[easysession] No session is active. Load a session with `easysession-switch-to'"))
(unless new-session-name
(user-error "[easysession] You need to specify the new session name"))
(let* ((old-path (easysession-get-session-file-path
easysession--current-session-name))
(new-path (easysession-get-session-file-path new-session-name)))
(unless (file-regular-p old-path)
(user-error "[easysession] No such file or directory: %s" old-path))
(rename-file old-path new-path)
(setq easysession--current-session-name new-session-name)))
;;;###autoload
(defun easysession-delete (session-names)
"Delete one or more sessions.
SESSION-NAMES is a string or a list of session names."
(interactive (list
(easysession--prompt-multiple-session-names
"Delete session(s): "
nil nil nil)))
(setq session-names
(delete-dups
(copy-sequence
(cond
((null session-names) nil)
((stringp session-names) (list session-names))
(t session-names)))))
(unless session-names
(user-error "[easysession] No sessions selected"))
(let ((session-files (mapcar
(lambda (name)
(let ((session-file
(easysession-get-session-file-path name)))
(cons name
(when (file-exists-p session-file)
session-file))))
session-names)))
(dolist (entry session-files)
(let ((session-name (car entry))
(session-file (cdr entry)))
(unless session-file
(user-error
"[easysession] The session '%s' cannot be deleted because it doesn't exist"
session-name))))
(when (and (called-interactively-p 'any)
(> (length session-names) 1)
(not
(yes-or-no-p
(format "[easysession] Delete the sessions %s? "
(string-join session-names ", ")))))
(user-error "[easysession] Deletion aborted"))
(dolist (entry session-files)
(let ((session-name (car entry)))
(when (and easysession--session-loaded
(string= session-name easysession--current-session-name))
;; We're deleting the current session
(when (yes-or-no-p (format "Delete the current session %s? "
session-name))
;; TODO Add option to reset if the current session is deleted
(easysession-unload)))))
(dolist (entry session-files)
(let* ((file (cdr entry))
(buffer (find-buffer-visiting file)))
(when buffer
(kill-buffer buffer))
(delete-file file nil)))
(when (called-interactively-p 'any)
(easysession--message
"Deleted session%s: %s"
(if (> (length session-names) 1) "s" "")
(string-join session-names ", ")))))
(defun easysession--ensure-font-lock ()
"Make sure the buffer has been fontified."
;; Fixes the issue preventing `font-lock-mode' from fontifying
;; restored buffers, causing the text to remain unfontified
;; until the user presses a key.
(when (bound-and-true-p redisplay-skip-fontification-on-input)
(let ((session-buf (current-buffer)))
(run-with-idle-timer
0 nil
(lambda ()
(when (buffer-live-p session-buf)
(with-current-buffer session-buf
(when (and (bound-and-true-p font-lock-mode)
;; For maximum safety during a session
;; load, check `font-lock-set-defaults'.
;; This variable guarantees that the
;; font-lock machinery has actually
;; finished configuring its keywords and
;; syntax tables for the current buffer.
(bound-and-true-p font-lock-set-defaults))
(condition-case err
(cond
((and (fboundp 'font-lock-flush)
(fboundp 'font-lock-ensure))
(font-lock-flush)
(ignore-errors
(font-lock-ensure)))
((fboundp 'jit-lock-fontify-now)
(jit-lock-fontify-now)))
(error
(when (bound-and-true-p easysession-debug)
(easysession--warning
"easysession-load font lock: %s"
(error-message-string err)))))))))))))
;;;###autoload
(defun easysession-load (&optional session-name)
"Load a session.
If SESSION-NAME is non-nil, that session is loaded. Otherwise, the function
loads the current session if set, or defaults to the \"main\" session."
(interactive
(list (easysession--prompt-session-name
"Load session: "
(unless easysession-switch-to-exclude-current
(or easysession--current-session-name
""))
(and easysession-switch-to-exclude-current
easysession--session-loaded))))
(setq easysession-load-in-progress nil)
(unwind-protect
(progn
(let* ((session-name (or session-name
easysession--current-session-name
;; The default session loaded when none is
;; specified is 'main'.
"main"))
(load-handlers (easysession-get-load-handlers))
(session-file
(let ((file-name (easysession-get-session-file-path
session-name)))
(when (file-exists-p file-name)
file-name))))
;; Pre-validate handlers before proceeding
(dolist (handler load-handlers)
(when (and handler
(not (and (symbolp handler)
(fboundp handler))))
(error
"[easysession] The following load handler is not a defined function: %s"
handler)))
(setq easysession-load-in-progress session-name)
(setq easysession--session-loaded nil)
(cond
;; The session file does not exist. This is a new session.
((not session-file)
;; TODO: Use `easysession-new-session-hook' hook?
(easysession-set-current-session-name session-name)
(setq easysession--session-loaded t)
(run-hooks 'easysession-new-session-hook))
;; The session exists
(t
(let ((session-data
(let ((coding-system-for-read 'utf-8-emacs)
(file-coding-system-alist nil))
(with-temp-buffer
(insert-file-contents session-file)
(goto-char (point-min))
(condition-case err
(read (current-buffer))
(error
(error "[easysession] easysession-load error: %s: %s"
session-file
(error-message-string err))))))))
;; Load buffers first because the cursor, window-start, or
;; hscroll might be altered by
gitextract_xt80m82j/
├── .dir-locals.el
├── .github/
│ ├── .nosearch
│ ├── FUNDING.yml
│ └── workflows/
│ ├── ci.yml
│ └── melpazoid.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG.md
├── Cask
├── LICENSE
├── Makefile
├── README.md
├── easysession.el
├── extensions/
│ ├── easysession-magit.el
│ └── easysession-scratch.el
└── tests/
├── .nosearch
└── test-easysession.el
Condensed preview — 17 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (243K chars).
[
{
"path": ".dir-locals.el",
"chars": 191,
"preview": ";; pre-commit-elisp .dir-locals.el | https://github.com/jamescherti/pre-commit-elisp\n((nil . ((pre-commit-elisp-load-pat"
},
{
"path": ".github/.nosearch",
"chars": 0,
"preview": ""
},
{
"path": ".github/FUNDING.yml",
"chars": 67,
"preview": "# These are supported funding model platforms\n\ngithub: jamescherti\n"
},
{
"path": ".github/workflows/ci.yml",
"chars": 1984,
"preview": "---\n#\n# This file is free software; you can redistribute it and/or modify\n# it under the terms of the GNU General Public"
},
{
"path": ".github/workflows/melpazoid.yml",
"chars": 1276,
"preview": "---\n\n# melpazoid <https://github.com/riscy/melpazoid> build checks.\n\n# If your package is on GitHub, enable melpazoid's "
},
{
"path": ".gitignore",
"chars": 17,
"preview": "flycheck_*\n*.elc\n"
},
{
"path": ".pre-commit-config.yaml",
"chars": 463,
"preview": "---\n\nrepos:\n - repo: https://github.com/pre-commit/pre-commit-hooks\n rev: v2.3.0\n hooks:\n - id: check-yaml\n "
},
{
"path": "CHANGELOG.md",
"chars": 17899,
"preview": "# easysession - Changelog\n\n**URL:** https://github.com/jamescherti/easysession.el\n\n**Author:** [James Cherti](https://ww"
},
{
"path": "Cask",
"chars": 61,
"preview": "(source gnu)\n(source melpa)\n\n(package-file \"easysession.el\")\n"
},
{
"path": "LICENSE",
"chars": 35149,
"preview": " GNU GENERAL PUBLIC LICENSE\n Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
},
{
"path": "Makefile",
"chars": 1858,
"preview": "#\n# This file is free software; you can redistribute it and/or modify\n# it under the terms of the GNU General Public Lic"
},
{
"path": "README.md",
"chars": 40673,
"preview": "# easysession.el: Easily persist and restore Emacs sessions (windows, tab-bar, file buffers, scratch, Dired, narrowing, "
},
{
"path": "easysession.el",
"chars": 115208,
"preview": ";;; easysession.el --- Persist and restore your sessions (desktop.el alternative) -*- lexical-binding: t; -*-\n\n;; Copyri"
},
{
"path": "extensions/easysession-magit.el",
"chars": 4717,
"preview": ";;; easysession-magit.el --- Persist and restore Magit buffers -*- lexical-binding: t; -*-\n\n;; Copyright (C) 2026 Emre Y"
},
{
"path": "extensions/easysession-scratch.el",
"chars": 3853,
"preview": ";;; easysession-scratch.el --- Persist and restore the scratch buffer -*- lexical-binding: t; -*-\n\n;; Copyright (C) 2024"
},
{
"path": "tests/.nosearch",
"chars": 0,
"preview": ""
},
{
"path": "tests/test-easysession.el",
"chars": 13175,
"preview": ";;; test-easysession.el --- Easysession tests -*- lexical-binding: t -*-\n\n;; Copyright (C) 2024-2026 James Cherti | http"
}
]
About this extraction
This page contains the full source code of the jamescherti/easysession.el GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 17 files (231.0 KB), approximately 54.2k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.