Skip to content

YedPool/Wezurrect

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

447 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

resurrect.wezterm

Resurrect your terminal environment!⚰️ A plugin to save the state of your windows, tabs and panes. Inspired by tmux-resurrect and tmux-continuum.

Screencastfrom2024-07-2918-50-57-ezgif com-resize

Features

  • Restore your windows, tabs and panes with the layout and text from a saved state.
  • Restore shell output from a saved session.
  • Save the state of your current window, with every window, tab and pane state stored in a json file.
  • Restore the save from a json file.
  • Re-attach to remote domains (e.g. SSH, SSHMUX, WSL, Docker, ect.).
  • Optionally enable encryption and decryption of the saved state.

Quick Start

One-Line Install

Paste into your terminal. This creates a new config or patches your existing one (with backup):

PowerShell (Windows):

$f="$HOME\.wezterm.lua"; if (!(Test-Path $f)) { @"
local wezterm = require("wezterm")
local config = wezterm.config_builder()
local resurrect = wezterm.plugin.require("https://github.com/YedPool/resurrect.wezterm")

resurrect.setup(config)

return config
"@ | Set-Content $f -Encoding UTF8; Write-Host "Created $f with resurrect enabled" } elseif (Select-String -Path $f -Pattern "resurrect" -Quiet) { Write-Host "resurrect is already in your config" } else { Copy-Item $f "$f.bak"; $c = Get-Content $f -Raw; $req = 'local resurrect = wezterm.plugin.require("https://github.com/YedPool/resurrect.wezterm")'; $c = $c -replace '(local\s+config\s*=\s*wezterm\.config_builder\(\))', "`$1`n$req"; $c = $c -replace '(return\s+config)', "resurrect.setup(config)`n`$1"; Set-Content $f $c -Encoding UTF8; Write-Host "Updated $f (backup saved to $f.bak)" }

Bash (macOS / Linux):

f="$HOME/.wezterm.lua"; if [ ! -f "$f" ]; then cat > "$f" << 'EOF'
local wezterm = require("wezterm")
local config = wezterm.config_builder()
local resurrect = wezterm.plugin.require("https://github.com/YedPool/resurrect.wezterm")

resurrect.setup(config)

return config
EOF
echo "Created $f with resurrect enabled"; elif grep -q "resurrect" "$f"; then echo "resurrect is already in your config"; else cp "$f" "$f.bak"; sed -i.tmp '/config_builder()/a local resurrect = wezterm.plugin.require("https://github.com/YedPool/resurrect.wezterm")' "$f"; sed -i.tmp 's/return config/resurrect.setup(config)\nreturn config/' "$f"; rm -f "$f.tmp"; echo "Updated $f (backup saved to $f.bak)"; fi

Then restart WezTerm. On first launch it automatically downloads the plugin from GitHub.

Manual Install

If you prefer to set things up by hand, or the one-liner didn't work with your config:

1. Locate your config file:

OS Path
Windows C:\Users\<username>\.wezterm.lua
macOS ~/.wezterm.lua
Linux ~/.wezterm.lua or $XDG_CONFIG_HOME/wezterm/wezterm.lua

If you don't have one yet, create it. See the WezTerm config docs for details.

2. Add these two lines to your config:

Add the require line near the top (after local config = wezterm.config_builder()):

local resurrect = wezterm.plugin.require("https://github.com/YedPool/resurrect.wezterm")

Add the setup call before return config:

resurrect.setup(config)

A complete minimal config looks like this:

local wezterm = require("wezterm")
local config = wezterm.config_builder()
local resurrect = wezterm.plugin.require("https://github.com/YedPool/resurrect.wezterm")

-- your existing config here (colors, fonts, shell, etc.)

resurrect.setup(config)

return config

3. Restart WezTerm. The plugin downloads automatically on first launch.

setup(config) automatically configures:

  • Per-instance state saving -- each WezTerm window saves independently (no more clobbering between windows)
  • Event-driven + periodic (5 min) state saving of workspaces, windows, and tabs
  • Instance selector on startup -- if saved instances exist, shows a selector to restore/delete/rename them
  • Claude Code session restoration -- detects Claude Code processes and resumes them via --resume <session-id>
  • Claude Code SessionStart hook in ~/.claude/settings.json (and ~/.claude-alt/settings.json for multi-account claude2 setups)
  • Status bar showing last save time and tab titles
  • Keybindings: Alt+S (full save), Alt+R (instance selector / restore), Alt+W (save workspace), Alt+Shift+W (save window), Alt+Shift+T (save tab)

No manual plugin installation needed -- wezterm.plugin.require() auto-fetches from GitHub on first launch.

Setup Options

All options are optional. Defaults are shown below:

resurrect.setup(config, {
  periodic_interval    = 300,   -- seconds between periodic saves (default: 5 min)
  restore_delay        = 3,     -- seconds to wait before sending restore commands
  save_workspaces      = true,  -- save workspace state
  save_windows         = true,  -- save window state
  save_tabs            = true,  -- save tab state
  keybindings          = true,  -- add Alt+S/R/W/Shift+W/Shift+T bindings
  status_bar           = true,  -- show save time + tab titles in right status
  claude_hooks         = true,  -- auto-configure Claude Code SessionStart hook
  auto_restore_prompt  = true,  -- show instance selector on startup if saved instances exist
  retention_days       = 7,     -- auto-delete instance states older than this many days
})

Set any option to false to disable that feature. For example, to skip keybindings and add your own:

resurrect.setup(config, { keybindings = false })

-- Add your own custom bindings here
config.keys = { ... }

Advanced Setup (Manual Configuration)

If you need fine-grained control over each component, you can configure them individually instead of using setup().

  1. Require the plugin:
local wezterm = require("wezterm")
local resurrect = wezterm.plugin.require("https://github.com/MLFlexer/resurrect.wezterm")
  1. Saving workspace, window and/or tab state based on name and title:
local resurrect = wezterm.plugin.require("https://github.com/MLFlexer/resurrect.wezterm")

config.keys = {
  -- ...
  {
    key = "w",
    mods = "ALT",
    action = wezterm.action_callback(function(win, pane)
        resurrect.state_manager.save_state(resurrect.workspace_state.get_workspace_state())
      end),
  },
  {
    key = "W",
    mods = "ALT",
    action = resurrect.window_state.save_window_action(),
  },
  {
    key = "T",
    mods = "ALT",
    action = resurrect.tab_state.save_tab_action(),
  },
  {
    key = "s",
    mods = "ALT",
    action = wezterm.action_callback(function(win, pane)
        resurrect.state_manager.save_state(resurrect.workspace_state.get_workspace_state())
        resurrect.window_state.save_window_action()
      end),
  },
}
  1. Loading workspace or window state via. fuzzy finder:
local resurrect = wezterm.plugin.require("https://github.com/MLFlexer/resurrect.wezterm")

config.keys = {
  -- ...
  {
    key = "r",
    mods = "ALT",
    action = wezterm.action_callback(function(win, pane)
      resurrect.fuzzy_loader.fuzzy_load(win, pane, function(id, label)
        local type = string.match(id, "^([^/]+)") -- match before '/'
        id = string.match(id, "([^/]+)$") -- match after '/'
        id = string.match(id, "(.+)%..+$") -- remove file extention
        local opts = {
          relative = true,
          restore_text = true,
          on_pane_restore = resurrect.tab_state.default_on_pane_restore,
        }
        if type == "workspace" then
          local state = resurrect.state_manager.load_state(id, "workspace")
          resurrect.workspace_state.restore_workspace(state, opts)
        elseif type == "window" then
          local state = resurrect.state_manager.load_state(id, "window")
          resurrect.window_state.restore_window(pane:window(), state, opts)
        elseif type == "tab" then
          local state = resurrect.state_manager.load_state(id, "tab")
          resurrect.tab_state.restore_tab(pane:tab(), state, opts)
        end
      end)
    end),
  },
}
  1. Optional, enable encryption (recommended): You can optionally configure the plugin to encrypt and decrypt the saved state. age is the default encryption provider. Rage and GnuPG encryption are also supported.

4.1. Install age and generate a key with:

$ age-keygen -o key.txt
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

Note

If you prefer to use GnuPG, generate a key pair: gpg --full-generate-key. Get the public key with gpg --armor --export your_email@example.com. The private key is your email or key ID associated with the gpg key.

4.2. Enable encryption in your Wezterm config:

local resurrect = wezterm.plugin.require("https://github.com/MLFlexer/resurrect.wezterm")
resurrect.state_manager.set_encryption({
  enable = true,
  method = "age" -- "age" is the default encryption method, but you can also specify "rage" or "gpg"
  private_key = "/path/to/private/key.txt", -- if using "gpg", you can omit this
  public_key = "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p",
})

Warning

FOR WINDOWS USERS

Due to Windows limitations with stdin, errors cannot be returned from the encrypt function.

Tip

If the encryption provider is not found in your PATH (common issue for GUI apps on Mac OS), you can specify the absolute path to the executable. e.g. method = "/opt/homebrew/bin/age"

Alternate implementations are possible by providing your own encrypt and decrypt functions:

resurrect.state_manager.set_encryption({
  enable = true,
  private_key = "/path/to/private/key.txt",
  public_key = "public_key",
  encrypt = function(file_path, lines)
    -- substitute for your encryption command
    local cmd = string.format(
      "%s -r %s -o %s",
      pub.encryption.method,
      pub.encryption.public_key,
      file_path:gsub(" ", "\\ ")
    )

    local success, output = execute_cmd_with_stdin(cmd, lines)
    if not success then
      error("Encryption failed:" .. output)
    end
  end,
  decrypt = function(file_path)
    -- substitute for your decryption command
    local cmd = { pub.encryption.method, "-d", "-i", pub.encryption.private_key, file_path }

    local success, stdout, stderr = wezterm.run_child_process(cmd)
    if not success then
      error("Decryption failed: " .. stderr)
    end

    return stdout
  end,
})

If you wish to share a non-documented way of encrypting your files or think something is missing, then please make a PR or file an issue.

How do I use it?

I use the builtin resurrect.state_manager.periodic_save() to save my workspaces every 15 minutes. This ensures that if I close Wezterm, then I can restore my session state to a state which is at most 15 minutes old.

I also use it to restore the state of my workspaces. As I use the plugin smart_workspace_switcher.wezterm, to change workspaces whenever I change "project" (git repository). I have added the following to my configuration to be able to do this whenever I change workspaces:

-- loads the state whenever I create a new workspace
wezterm.on("smart_workspace_switcher.workspace_switcher.created", function(window, path, label)
  local workspace_state = resurrect.workspace_state

  workspace_state.restore_workspace(resurrect.state_manager.load_state(label, "workspace"), {
    window = window,
    relative = true,
    restore_text = true,
    on_pane_restore = resurrect.tab_state.default_on_pane_restore,
  })
end)

-- Saves the state whenever I select a workspace
wezterm.on("smart_workspace_switcher.workspace_switcher.selected", function(window, path, label)
  local workspace_state = resurrect.workspace_state
  resurrect.state_manager.save_state(workspace_state.get_workspace_state())
end)

You can checkout my configuration here.

Configuration

Periodic saving of state

resurrect.state_manager.periodic_save(opts?) will save the workspace state every 15 minutes by default. You can add the opts table to change the behaviour. It exposes the following options:

---@param opts? { interval_seconds: integer?, save_workspaces: boolean?, save_windows: boolean?, save_tabs: boolean? }

interval_seconds will save the state every time the supplied number of seconds has surpassed. save_workspaces will save workspaces if true otherwise not. save_windows will save windows if true otherwise not. save_tabs will save tabs if true otherwise not.

Event-driven saving of state

resurrect.state_manager.event_driven_save(opts?) saves state immediately whenever the pane or tab structure changes (new split, new tab, closed pane), rather than waiting for a periodic timer. This is the recommended approach when you want state to always be current.

resurrect.state_manager.event_driven_save({
  save_workspaces = true,  -- default: true
  save_windows    = false, -- default: false
  save_tabs       = false, -- default: false
  user_var        = nil,   -- optional: name of a user variable to also trigger saves
})

save_workspaces, save_windows, and save_tabs mirror the same options in periodic_save.

user_var enables an additional save trigger via shell integration. When set, a save fires whenever the shell sends an OSC 1337 SetUserVar sequence with that variable name. This is useful for saving on directory change. Example shell integration (zsh/bash):

# In your .zshrc / .bashrc — fires only when $PWD changes
_wezterm_precmd() {
  if [[ "$PWD" != "$_WEZTERM_LAST_PWD" ]]; then
    _WEZTERM_LAST_PWD="$PWD"
    printf "\033]1337;SetUserVar=WEZTERM_SAVE=%s\007" "$(printf 1 | base64)"
  fi
}
precmd_functions+=(_wezterm_precmd)

Then pass the matching variable name to event_driven_save:

resurrect.state_manager.event_driven_save({ user_var = "WEZTERM_SAVE" })

event_driven_save also keeps current_state up to date on every save, which is required for resurrect_on_gui_startup to restore the correct workspace.

Resurrecting on startup

If you use setup(), startup restoration is automatic. On startup:

  1. Old instances (older than retention_days) are cleaned up
  2. If saved instances exist and auto_restore_prompt is true, an instance selector appears
  3. If no instances exist, it falls back to the legacy current_state mechanism

You can dismiss the selector (Esc) to start fresh.

For manual configuration without setup(), you can still use the legacy approach:

wezterm.on("gui-startup", resurrect.state_manager.resurrect_on_gui_startup)

This will read a file which has been written by the resurrect.state_manager.write_current_state("workspace name", "workspace") function.

Note

For this to work, you must include a way to write the current workspace, be it via. the resurrect.state_manager.periodic_save event or when changing workspaces.

Limiting the amount of output lines saved for a pane

resurrect.state_manager.set_max_nlines(number) will limit each pane to save at most number lines to the state. This can improve performance when saving and loading state.

save_state options

resurrect.state_manager.save_state(state, opt_name?) takes an optional string argument, which will rename the file to the name of the string.

restore_opts

Options for restoring state:

{spawn_in_workspace: boolean?, -- Restores in the workspace
relative: boolean?, -- Use relative size when restoring panes
absolute: boolean?, -- Use absolute size when restoring panes
close_open_tabs: boolean?, -- Closes all tabs which are open in the window, only restored tabs are left
close_open_panes: boolean?, -- Closes all panes which are open in the tab, only keeping the panes to be restored
pane: Pane?, -- Restore in this window
tab: MuxTab?, -- Restore in this window
window: MuxWindow, -- Restore in this window
resize_window: boolean?, -- Resizes the window, default: true
on_pane_restore: fun(pane_tree: pane_tree)} -- Function to restore panes, use resurrect.tab_state.default_on_pane_restore

Windows not resizing correctly

Some users has had problems with window_decorations and window_padding configuration options, which caused issues when resizing, see comment. To avoid this, set the resize_window to false.

Restoring into the current window

To restore a window state into the current window use the restore_window function with restore_opts containing the window and close_open_tabs like so:

local opts = {
  close_open_tabs = true,
  window = pane:window(),
  on_pane_restore = resurrect.tab_state.default_on_pane_restore,
  relative = true,
  restore_text = true,
}
resurrect.window_state.restore_window(pane:window(), state, opts)

This will restore the state into the passed window and additionally close all the tabs in the window, such that only the restored tabs are visible after restoring.

fuzzy_load opts

the resurrect.fuzzy_loader.fuzzy_load(window, pane, callback, opts?) function takes an optional opts argument, which has the following types:

---@alias fmt_fun fun(label: string): string
---@alias fuzzy_load_opts {
  title: string, -- dialog title, default: "Load state"
  description: string, -- description, default: "Select State to Load and press Enter = accept, Esc = cancel, / = filter"
  fuzzy_description: string, -- description in fyzzy search mode, default: "Search State to Load: "
  is_fuzzy: boolean, -- enter directly in fuzzy mode, default: true
  ignore_workspaces: boolean, -- does not show workspaces, default: false
  ignore_tabs: boolean, -- does not show tabs, default: false
  ignore_windows: boolean, -- does not show windows, default: false
  fmt_window: fmt_fun, -- format function for window state name (wezterm.format)
  fmt_workspace: fmt_fun, -- format function for workspace state name
  fmt_tab: fmt_fun, -- format function for tab state name
  fmt_date: fmt_fun, -- format function for date
  show_state_with_date: boolean, -- show last update of the state file, default: false
  date_format: string, -- date formatting, default: "%d-%m-%Y %H:%M:%S"
  ignore_screen_width: boolean, -- whether or not to shrink the list if the window is too narrow, default: true
  name_truncature: string, -- when state name is truncated, this string replaces the truncation
  min_filename_size: number -- minimum size of state name in case of truncation
}

This is used to format labels, ignore saved state, change the title and change the behaviour of the fuzzy finder.

Change the directory to store the saved state

resurrect.state_manager.change_state_save_dir("/some/other/directory")

Warning

FOR WINDOWS USERS

You must ensure that there is write access to the directory where the state is stored, as such it is suggested that you set your own state directory like so:

-- Set some directory where Wezterm has write access
resurrect.state_manager.change_state_save_dir("C:\\Users\\<user>\\Desktop\\state\\")

Events

This plugin emits the following events that you can use for your own callback functions:

  • resurrect.error(err)
  • resurrect.file_io.decrypt.finished(file_path)
  • resurrect.file_io.decrypt.start(file_path)
  • resurrect.file_io.encrypt.finished(file_path)
  • resurrect.file_io.encrypt.start(file_path)
  • resurrect.file_io.sanitize_json.finished(data)
  • resurrect.file_io.sanitize_json.start(data)
  • resurrect.fuzzy_loader.fuzzy_load.finished(window, pane)
  • resurrect.fuzzy_loader.fuzzy_load.start(window, pane)
  • resurrect.state_manager.delete_state.finished(file_path)
  • resurrect.state_manager.delete_state.start(file_path)
  • resurrect.state_manager.load_state.finished(name, type)
  • resurrect.state_manager.load_state.start(name, type)
  • resurrect.state_manager.event_driven_save.start(opts)
  • resurrect.state_manager.event_driven_save.finished(opts)
  • resurrect.state_manager.periodic_save.start(opts)
  • resurrect.state_manager.periodic_save.finished(opts)
  • resurrect.file_io.write_state.finished(file_path, event_type)
  • resurrect.file_io.write_state.start(file_path, event_type)
  • resurrect.instance_manager.save_instance.finished(instance_id)
  • resurrect.instance_manager.delete_instance.finished(instance_id)
  • resurrect.tab_state.restore_tab.finished
  • resurrect.tab_state.restore_tab.start
  • resurrect.window_state.restore_window.finished
  • resurrect.window_state.restore_window.start
  • resurrect.workspace_state.restore_workspace.finished
  • resurrect.workspace_state.restore_workspace.start

Example: sending a toast notification when specified events occur, but suppress on periodic_save():

local resurrect_event_listeners = {
  "resurrect.error",
  "resurrect.state_manager.save_state.finished",
}
local is_periodic_save = false
wezterm.on("resurrect.periodic_save", function()
  is_periodic_save = true
end)
for _, event in ipairs(resurrect_event_listeners) do
  wezterm.on(event, function(...)
    if event == "resurrect.state_manager.save_state.finished" and is_periodic_save then
      is_periodic_save = false
      return
    end
    local args = { ... }
    local msg = event
    for _, v in ipairs(args) do
      msg = msg .. " " .. tostring(v)
    end
    wezterm.gui.gui_windows()[1]:toast_notification("Wezterm - resurrect", msg, nil, 4000)
  end)
end

State files

State files are json files, which will be decoded into lua tables. This can be used to create your own layout files which can then be loaded. Here is an example of a json file:

{
   "window_states":[
      {
         "size":{
            "cols":191,
            "dpi":96,
            "pixel_height":1000,
            "pixel_width":1910,
            "rows":50
         },
         "tabs":[
            {
               "is_active":true,
               "pane_tree":{
                  "cwd":"/home/user/",
                  "domain": "SSHMUX:domain",
                  "height":50,
                  "index":0,
                  "is_active":true,
                  "is_zoomed":false,
                  "left":0,
                  "pixel_height":1000,
                  "pixel_width":1910,
                  "process":"/bin/bash", -- value is empty if attached to a remote domain
                  "text":"Some text", -- not saved if attached to a remote domain, see https://github.com/MLFlexer/resurrect.wezterm/issues/41
                  "top":0,
                  "width":191
               },
               "title":"tab_title"
            }
         ],
         "title":"window_title"
      }
   ],
   "workspace":"workspace_name"
}

Delete a saved state file via. fuzzy finder

You can use the fuzzy finder to delete a saved state file by adding a keybind to your config:

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

config.keys = {
  -- ...
  {
    key = "d",
    mods = "ALT",
    action = wezterm.action_callback(function(win, pane)
      resurrect.fuzzy_loader.fuzzy_load(win, pane, function(id)
          resurrect.state_manager.delete_state(id)
        end,
        {
          title = "Delete State",
          description = "Select State to Delete and press Enter = accept, Esc = cancel, / = filter",
          fuzzy_description = "Search State to Delete: ",
          is_fuzzy = true,
        })
    end),
  },
}

Augmenting the command palette

If you would like to add entries in your Wezterm command palette for renaming and switching workspaces:

local workspace_switcher = wezterm.plugin.require("https://github.com/MLFlexer/smart_workspace_switcher.wezterm")

wezterm.on("augment-command-palette", function(window, pane)
  local workspace_state = resurrect.workspace_state
  return {
    {
      brief = "Window | Workspace: Switch Workspace",
      icon = "md_briefcase_arrow_up_down",
      action = workspace_switcher.switch_workspace(),
    },
    {
      brief = "Window | Workspace: Rename Workspace",
      icon = "md_briefcase_edit",
      action = wezterm.action.PromptInputLine({
        description = "Enter new name for workspace",
        action = wezterm.action_callback(function(window, pane, line)
          if line then
            wezterm.mux.rename_workspace(wezterm.mux.get_active_workspace(), line)
            resurrect.state_manager.save_state(workspace_state.get_workspace_state())
          end
        end),
      }),
    },
  }
end)

FAQ

Pane CWD is not correct on Windows

If your pane CWD is incorrect then it might be a problem with the shell integration and OSC 7. See Wezterm documentation.

How do I keep my plugins up to date?

Manually

Wezterm git clones your plugins into a plugin directory. Enter wezterm.plugin.list() in the Wezterm Debug Overlay (Ctrl + Shift + L) to see where they are stored. You can then update them individually using git pull.

Automatically

Add wezterm.plugin.update_all() to your Wezterm config.

Testing

Tests are run with Busted via LuaRocks.

All OSes:

luarocks install busted

Run tests:

eval "$(luarocks path)"
busted

Windows notes:

  • PowerShell is the most reliable shell for running LuaRocks and Busted (Git Bash/MSYS can mangle arguments and paths).
  • If luarocks install busted fails while building native dependencies (for example luasystem), install a GCC toolchain (MinGW-w64 or MSYS2 MinGW64) and make sure its bin directory is on PATH for the PowerShell session.

PowerShell usage:

Invoke-Expression (luarocks path)
busted

Contributions

Suggestions, Issues and PRs are welcome! The features currently implemented are the ones I use the most, but your workflow might differ. As such, if you have any proposals on how to improve the plugin, then please feel free to make an issue or even better a PR!

Technical details

Restoring of the panes are done via. the pane_tree file, which has functions to work on a binary-like-tree of the panes. Each node in the pane_tree represents a possible split pane. If the pane has a bottom and/or right child, then the pane is split. If you have any questions to the implementation, then I suggest you read the code or open an issue and I will try to clarify. Improvements to this section is also very much welcome.

Disclaimer

If you don't setup encryption then the state of your terminal is saved as plaintext json files. Please be aware that the plugin will by default write the output of the shell among other things, which could contain secrets or other vulnerable data. If you do not want to store this as plaintext, then please use the provided documentation for encrypting state.

About

A plugin to restore windows, tabs and panes for Wezterm inspired by tmux-resurrect

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

  • Lua 96.8%
  • Shell 3.2%