--- 
canonical: 'https://mwop.net/blog/2024-10-21-wezterm-resurrect.html'
title: 'Using resurrect.wezterm to manage Wezterm session state'
author: "[Matthew Weier O'Phinney](https://mwop.net)"
created: '2024-10-21T17:28:21-05:00'
updated: '2024-10-21T17:28:21-05:00'
tags:
  - lua
  - tmux
  - wezterm

---
One of my goals when adopting [Wezterm](https://wezfurlong.org/wezterm/index.html) was to replace tmux.
To do that, I needed not just the ability to open additional tabs/windows and to split into panes, but also a feature I'd come to rely on heavily in the tmux ecosystem: session saving and restoration, which I accomplished with the [tmux-resurrect](https://github.com/tmux-plugins/tmux-resurrect) plugin.

I tried a number of options, but was eventually pointed to [resurrect.wezterm](https://github.com/MLFlexer/resurrect.wezterm).

In this post, I'll detail how I've configured it, as well as a workflow I've developed for interacting with it that gives me (a) reasonable satisfaction that I won't lose work, and (b) additional flexibility for branching off work.





> ## A note on sharing sessions
> 
> What this solution **does not do** is allow me to share a Wezterm session, or open it simultaneously in another wezterm session.
> For that, you need a Unix muxer session, and resurrect.wezterm does not play well with it.
> As such, you will need to do any such sessions without wezterm.resurrect.

## Configuration

I've been finding it's most maintainable to throw my configuration for specific plugins or features into their own modules whenever possible.
As such, I started by creating a `resurrect/config.lua` file that would do the following:

- Setup encryption for the saved state files
- Setup periodic saving, so I don't have to remember to save or worry about what happens if there's a crash before I save.
- Setup the maximum number of lines per pane to save.
- Return the default keybinding I want to use with the module.

That file ends up looking like the following:

```lua
-- File: resurrect/config.lua
-- resurrect.wezterm configuration and settings
--
-- This module:
-- * Configures the resurrect.wezterm plugin
-- * Configures event listener configuration (via an additional required file)
-- * Returns wezterm keybinding configuration for resurrect-related actions.
--
-- The main wezterm configuration is then responsible for merging the
-- keybindings with other keybindings, or setting up its own.

local config    = {}
local wezterm   = require 'wezterm'
local resurrect = wezterm.plugin.require("https://github.com/MLFlexer/resurrect.wezterm")

-- resurrect.wezterm encryption
-- Uncomment the following to use encryption.
-- If you do, ensure you have the age tool installed, you have created an
-- encryption key at ~/.config/age/wezterm-resurrect.txt, and that you supply
-- the associated public_key below
resurrect.set_encryption({
    enable      = true,
    method      = "age",
    private_key = wezterm.home_dir .. "/.config/age/wezterm-resurrect.txt",
    public_key  = "THE-PUBLIC-KEY-VALUE-GOES-HERE",
})

-- resurrect.wezterm periodic save every 5 minutes
resurrect.periodic_save({
    interval_seconds = 300,
    save_tabs = true,
    save_windows = true,
    save_workspaces = true,
})

-- Save only 5000 lines per pane
resurrect.set_max_nlines(5000)

-- Default keybindings
-- These will need to be merged with the main wezterm keys.
config.keys = {
    {
        -- Save current and window state
        -- See https://github.com/MLFlexer/resurrect.wezterm for options around
        -- saving workspace and window state separately
        key = 'S',
        mods = 'LEADER|SHIFT',
        action = wezterm.action_callback(function(win, pane) -- luacheck: ignore 212
            local state = resurrect.workspace_state.get_workspace_state()
            resurrect.save_state(state)
            resurrect.window_state.save_window_action()
        end),
    },
    {
        -- Load workspace or window state, using a fuzzy finder
        key = 'L',
        mods = 'LEADER|SHIFT',
        action = wezterm.action_callback(function(win, pane)
            resurrect.fuzzy_load(win, pane, function(id, label) -- luacheck: ignore 212
                local type = string.match(id, "^([^/]+)") -- match before '/'
                id         = string.match(id, "([^/]+)$") -- match after '/'
                id         = string.match(id, "(.+)%..+$") -- remove file extension

                local opts = {
                    window          = win:mux_window(),
                    relative        = true,
                    restore_text    = true,
                    on_pane_restore = resurrect.tab_state.default_on_pane_restore,
                }

                if type == "workspace" then
                    local state = resurrect.load_state(id, "workspace")
                    resurrect.workspace_state.restore_workspace(state, opts)
                elseif type == "window" then
                    local state = resurrect.load_state(id, "window")
                    -- opts.tab = win:active_tab()
                    resurrect.window_state.restore_window(pane:window(), state, opts)
                elseif type == "tab" then
                    local state = resurrect.load_state(id, "tab")
                    resurrect.tab_state.restore_tab(pane:tab(), state, opts)
                end
            end)
        end),
    },
    {
        -- Delete a saved session using a fuzzy finder
        key = 'd',
        mods = 'LEADER|SHIFT',
        action = wezterm.action_callback(function(win, pane)
            resurrect.fuzzy_load(
                win,
                pane,
                function(id)
                    resurrect.delete_state(id)
                end,
                {
                    title             = 'Delete State',
                    description       = 'Select session to delete and press Enter = accept, Esc = cancel, / = filter',
                    fuzzy_description = 'Search session to delete: ',
                    is_fuzzy          = true,
                }
            )
        end),
    }
}

require 'resurrect/events'

return config
```

I've setup keybindings that mimic what I had in tmux:

- Leader-S will save the current workspace session
- Leader-L will give me a way to select a workspace session to load
- Leader-D will give me a way to select a workspace session to delete

(I map `Leader` to `Ctrl-a`, which is a common convention in both `screen` and `tmux`, so I don't lose any muscle memory here.)

In my main `wezterm.lua` file, I then make use of my [merge.all function](/blog/2024-10-21-wezterm-keybindings) to merge the returned keybindings configuration:

```lua
local resurrect = require 'resurrect/config'
config.keys = merge.all(config.keys, resurrect.keys)
```

But what is that `require 'resurrect/events'` line for?

## Listening to resurrect events

When I save a session manually or load a session, I'd like some notification that the operation was successful.
resurrect.wezterm emits a number of events for different workflow states, and you can tie into those.
I did exactly that, and had my handlers use my [notify.send](/blog/2024-10-21-wezterm-notify-send) module to emit the notifications:

```lua
-- File: resurrect/events.lua
-- resurrect.wezterm event listener configuration
--
-- This module configures event listeners for the resurrect.wezterm plugin.

local wezterm               = require 'wezterm'
local notify                = require '../notify'
local suppress_notification = false

wezterm.on('resurrect.error', function (error)
    notify.send("Wezterm - ERROR", error, 'critical')
end)

wezterm.on('resurrect.periodic_save', function ()
    suppress_notification = true
end)

wezterm.on('resurrect.save_state.finished', function (session_path)
    local is_workspace_save = session_path:find("state/workspace")

    if is_workspace_save == nil then
        return
    end

    if suppress_notification then
        suppress_notification = false
        return
    end

    local path = session_path:match(".+/([^+]+)$")
    local name = path:match("^(.+)%.json$")
    notify.send("Wezterm - Save workspace", 'Saved workspace ' .. name .. "\n\n" .. session_path)
end)

wezterm.on('resurrect.load_state.finished', function(name, type)
    local msg  = 'Completed loading ' .. type .. ' state: ' .. name
    notify.send("Wezterm - Restore session", msg)
end)
```

If you follow the write-up in the [resurrect.wezterm README](https://github.com/MLFlexer/resurrect.wezterm/blob/main/README.md#events) file, you'll note that my approach is a bit different, particularly when it comes to tracking a periodic save.
Why?
Well, when a periodic save happens, it saves the window, the workspace, and all tabs, but each of those triggers separately, and each triggers a `resurrect.save_state.finished` event on completion.
As a result, the periodic_save state often gets reset by one of these events, which causes another event to flow through.
The upshot is that if I followed the documented example, I was still seeing notifications each time `periodic_save` would trigger.
Since I'm really only concerned about _workspace_ save events, I've modified the logic to only trigger if it's a workspace save, which I can identify by searching for the string "state/workspace" in the session filename passed to the callback.
(It's not perfect, but it's good enough for my needs.)

From there, I can do the normal logic around identifying if I'm within a `periodic_save` event.

I've also done some logic so I can see the name of the session at a glance, as well as see the full path to the file (for debugging purposes).

One final thing to note is my handling of the `resurrect.error` event. I use the "critical" urgency flag to my `notify.send` functionality here so that the notification requires manual dismissal.
Doing this ensures I do not miss these errors!

## Day to day usage

The `periodic_save` settings mean that as I'm working, Wezterm is periodically saving my open sessions, meaning I never have to lose my place.
But how do I restore?

The interesting thing about resurrect.wezterm is that when you load a session, _it does not change the current workspace name_.
This means that if you start in the "default" workspace, and then load your "project" workspace, _you're still in the "default" workspace, and that's where `periodic_save` will save the workspace contents_.

This may sound bad, but I've found in practice that it's actually a huge benefit.
**It allows me to "fork" my workspace state.**
I can name the workspace something different, and its state will diverge... which allows me to go back to the original state if I want to later.

So, my workflow has become:

- Change the name of the current workspace workspace to what I want it to be saved as eventually; this may be the name of a previously saved session!
- If I want to start from a previous saved session, load the saved workspace.
- Manually save only when I am planning to close a terminal window or reboot, but otherwise have `periodic_save` handle saving state.

I find this to be an improvement over tmux-resurrect!

## Final thoughts

I've tried a few different approaches to saving sessions since adopting Wezterm, but this is the one that has finally given me all the features — and then some! — that I had in tmux-resurrect.
While being able to load and mirror an existing session would sometimes be useful, it's a niche feature for me; I never do XP-style programming anymore, and I rarely find myself in a position where I want or need to load a current session into another window.
However, I've often wished I could go back in time to a previous state, and this set of tools gives me that.
