Skip to content

Instantly share code, notes, and snippets.

@hylophile
Last active July 10, 2023 04:28
Show Gist options
  • Select an option

  • Save hylophile/2797f8a331f18507658c693391c43c12 to your computer and use it in GitHub Desktop.

Select an option

Save hylophile/2797f8a331f18507658c693391c43c12 to your computer and use it in GitHub Desktop.
Saving and loading layouts for Wezterm (without writing layout definitions manually)

This module allows you to save the layout of the current OS window to a file and load it on demand. All tabs and panes are saved along with their current working directory and the running program / shell command.

Note that recreating panes isn't implemented in the smartest possible way - the first pane will be split to the right repeatedly. As such, recreating more than 2-3 panes won't yield the best results. I mostly use this module for recreating multiple running programs (file watchers, viewers, logs...) in various tabs to easily jump back into a project, so I haven't had a need for accurate pane recreation yet.

Setup

  • Put layouts.lua into your wezterm config dir, e.g. ~/.config/wezterm/layouts.lua

  • Create a layouts directory in your config dir, e.g. mkdir ~/.config/wezterm/layouts (this could be automated)

  • In your ~/.config/wezterm/wezterm.lua, import the module and add keybindings for saving and loading layouts:

    local layouts = require("layouts")
    
    keys = { 
        { key = "F2", mods = "CTRL", action = wezterm.action_callback(layouts.serialize_window) },
        { key = "F3", mods = "CTRL", action = wezterm.action_callback(layouts.deserialize_window) },
        ...
    } -- or however your config.keys are defined

    With these, Ctrl+F2 will ask you for a <name> and save the current window layout in ~/.config/wezterm/layouts/<name>.json. It also shows you the currently saved layouts, so that you can easily overwrite them (and don't have to wonder how that one layout was called again).

    Ctrl+F3 will show you a list of your saved layouts. Select one, and the window will be created.

  • For recreating running programs, you'll need to set up Shell Integration. See notes below.

Notes on shell integration

We need shell integration / user variables because we want to recreate precisely the last shell command at the time of loading a layout, which is not necessarily the currently running process at the time of saving a layout. (E.g., with ls | entr -s 'echo hi', the current process is just entr with argv set to ["-s","echo hi"]). To fix that, the shell integration provides the WEZTERM_PROG.

One other integration is necessary for zathura (and maybe others): It sets the cwd to "" when it's running, so we can't accurately reproduce open PDFs with it. That's why we need an additional WEZTERM_CWD user variable. Wezterm's upstream shell integration doesn't provide WEZTERM_CWD. If you don't use zathura, you don't need to do this as the module will fallback on the cwd property that wezterm sets.

If you use fish, this is all you need (in your ~/.config/fish/config.fish):

function set_wezterm_user_vars --on-event fish_preexec
    printf "\033]1337;SetUserVar=%s=%s\007" WEZTERM_PROG (echo -n $argv | base64 --wrap 0)
    printf "\033]1337;SetUserVar=%s=%s\007" WEZTERM_CWD (echo -n (pwd) | base64 --wrap 0)
end

function unset_wezterm_prog --on-event fish_postexec
    printf "\033]1337;SetUserVar=%s=%s\007" WEZTERM_PROG ""
end

Note that we use base64 --wrap 0 here, because base64 wraps after 76 characters, so long shell commands will be cut off when setting the user vars and won't be reproducible. (If you don't use fish, you'll need to adapt this in Wezterm's upstream shell integration, as it doesn't use this flag (PR?).)

To test whether shell integration is setup correctly (again, having WEZTERM_CWD set properly here is not strictly necessary):

  • open only one terminal window
  • run a command (because the user variables might not be set yet)
  • open the Debug REPL (Ctrl+Shift+L by default) and compare:
> wezterm.mux:all_windows()[1]:tabs()[1]:panes()[1]:get_user_vars()
{
    "WEZTERM_CWD": "/home/hylo",
    "WEZTERM_PROG": "",
}
  • run a long-running command and compare:
> wezterm.mux:all_windows()[1]:tabs()[1]:panes()[1]:get_user_vars()
{
    "WEZTERM_CWD": "/home/hylo",
    "WEZTERM_PROG": "ls | entr -s 'echo hi'",
}
  • quit the long-running command and compare:
> wezterm.mux:all_windows()[1]:tabs()[1]:panes()[1]:get_user_vars()
{
    "WEZTERM_CWD": "/home/hylo",
    "WEZTERM_PROG": "",
}
local wezterm = require("wezterm")
local act = wezterm.action
local module = {}
local function write_layout(name, window_config)
local file = io.open(wezterm.config_dir .. "/layouts/" .. name .. ".json", "w")
if file then
file:write(wezterm.json_encode(window_config))
file:close()
end
end
local function get_layouts()
local layout_files = io.popen("ls " .. wezterm.config_dir .. "/layouts")
if layout_files then
local lines = {}
for line in layout_files:lines() do
local layout = string.gsub(line, ".json$", "")
table.insert(lines, layout)
end
layout_files:close()
return lines
end
end
function module.serialize_window(current_window, current_pane)
local window_config = {}
for i_tab, tab in pairs(current_window:mux_window():tabs()) do
window_config[i_tab] = {}
for j_pane, pane in pairs(tab:panes()) do
local user_vars = pane:get_user_vars()
local prog = user_vars.WEZTERM_PROG
if prog == "" then
prog = nil
end
local cwd = user_vars.WEZTERM_CWD
if cwd == "" or cwd == nil then
cwd = pane:get_foreground_process_info().cwd
end
window_config[i_tab][j_pane] = {
prog = prog,
cwd = cwd,
}
end
end
local saved_layouts = ""
for _, layout in pairs(get_layouts()) do
saved_layouts = saved_layouts .. " " .. layout .. "\n"
end
current_window:perform_action(
act.PromptInputLine({
description = "Saved layouts:\n\n" .. saved_layouts .. "\nEnter name for this layout:",
action = wezterm.action_callback(function(_, _, line)
if line then
write_layout(line, window_config)
end
end),
}),
current_pane
)
end
local function send_prog(pane, pane_config)
if pane_config.prog then
pane:send_text(pane_config.prog .. "\n")
end
end
local function load_other_panes(original_pane, tab_config)
for j, new_pane_config in pairs(tab_config) do
if j ~= 1 then
local _, new_pane, _ = original_pane:split({ cwd = new_pane_config.cwd })
send_prog(new_pane, new_pane_config)
end
end
end
local function load_window(filename)
local file = io.open(wezterm.config_dir .. "/layouts/" .. filename .. ".json", "r")
if file then
local window_config = wezterm.json_parse(file:read())
file:close()
local first_tab_config = window_config[1]
local first_pane_config = first_tab_config[1]
local _, first_pane, window = wezterm.mux.spawn_window({
cwd = first_pane_config.cwd,
})
send_prog(first_pane, first_pane_config)
load_other_panes(first_pane, first_tab_config)
for i, tab_config in pairs(window_config) do
if i ~= 1 then
local _, pane, _ = window:spawn_tab({
cwd = tab_config[1].cwd,
})
send_prog(pane, tab_config[1])
load_other_panes(pane, tab_config)
end
end
end
end
function module.deserialize_window(current_window, current_pane)
local choices = {}
for _, layout_file in pairs(get_layouts()) do
table.insert(choices, { label = layout_file })
end
current_window:perform_action(
act.InputSelector({
action = wezterm.action_callback(function(_, _, _, layout)
if layout then
load_window(layout)
end
end),
title = "Load layout:",
choices = choices,
}),
current_pane
)
end
return module
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment