Layout saving in i3

Michael Stapelberg
<michael@i3wm.org>
April 2014
Table of Contents

Layout saving/restoring is a feature that was introduced in i3 v4.8.

Layout saving/restoring allows you to load a JSON layout file so that you can have a base layout to start working with after powering on your computer. Dynamic use-cases also come to mind: if you frequently (but not always!) need a grid layout of terminals with ping/traceroute commands to diagnose network issues, you can easily automate opening these windows in just the right layout.

1. Saving the layout

You can save the layout of either a single workspace or an entire output (e.g. LVDS1). Of course, you can repeat this step multiple times if you want to save/restore multiple workspaces/outputs.

i3-save-tree(1) is a tool to save the layout. It will print a JSON representation of i3’s internal layout data structures to stdout. Typically, you may want to take a quick look at the output, then save it to a file and tweak it a little bit:

i3-save-tree --workspace 1 > ~/.i3/workspace-1.json

Please note that the output of i3-save-tree(1) is NOT useful until you manually modify it — you need to tell i3 how to match/distinguish windows (for example based on their WM_CLASS, title, etc.). By default, all the different window properties are included in the output, but commented out. This is partly to avoid relying on heuristics and partly to make you aware how i3 works so that you can easily solve layout restoring problems.

How to modify the file manually is described in [EditingLayoutFiles].

2. Restoring the layout

After restoring the example layout from [EditingLayoutFiles], i3 will open placeholder windows for all the windows that were specified in the layout file. You can recognize the placeholder windows by the watch symbol
[Depending on the font you are using, a placeholder symbol may show up instead of the watch symbol.]
in the center of the window, and by the swallow criteria specification at the top of the window:

When an application opens a window that matches the specified swallow criteria, it will be placed in the corresponding placeholder window. We say it gets swallowed by the placeholder container, hence the term.

Note: Swallowing windows into unsatisfied placeholder windows takes precedence over assignment rules. For example, if you assign all Emacs windows to workspace 1 in your i3 configuration file, but there is a placeholder window on workspace 2 which matches Emacs as well, your newly started Emacs window will end up in the placeholder window on workspace 2.

The placeholder windows are just regular windows, so feel free to move them around or close them, for example.

2.1. append_layout command

The append_layout command is used to load a layout file into i3. It accepts a path (relative to i3’s current working directory or absolute) to a JSON file.

Syntax:

append_layout <path>

Examples:

# From a terminal or script:
i3-msg "workspace 1; append_layout /home/michael/.i3/workspace-1.json"

# In your i3 configuration file, you can autostart i3-msg like this:
# (Note that those lines will quickly become long, so typically you would store
#  them in a script with proper indentation.)
exec --no-startup-id "i3-msg 'workspace 1; append_layout /home/michael/.i3/workspace-1.json'"

3. Editing layout files

3.1. Anatomy of a layout file

Here is an example layout file that we’ll discuss:

{
    // splitv split container with 2 children
    "layout": "splitv",
    "percent": 0.4,
    "type": "con",
    "nodes": [
        {
            "border": "none",
            "name": "irssi",
            "percent": 0.5,
            "type": "con",
            "swallows": [
                {
                    "class": "^URxvt$",
                    "instance": "^irssi$"
                }
            ]
        },
        {
            // stacked split container with 2 children
            "layout": "stacked",
            "percent": 0.5,
            "type": "con",
            "nodes": [
                {
                    "name": "notmuch",
                    "percent": 0.5,
                    "type": "con",
                    "swallows": [
                        {
                            "class": "^Emacs$",
                            "instance": "^notmuch$"
                        }
                    ]
                },
                {
                    "name": "midna: ~",
                    "percent": 0.5,
                    "type": "con"
                }
            ]
        }
    ]
}

{
    // stacked split container with 1 children
    "layout": "stacked",
    "percent": 0.6,
    "type": "con",
    "nodes": [
        {
            "name": "chrome",
            "type": "con",
            "swallows": [
                {
                    "class": "^Google-chrome$"
                }
            ]
        }
    ]
}

In this layout, the screen is divided into two columns. In the left column, which covers 40% of the screen, there is a terminal emulator running irssi on the top, and a stacked split container with an Emacs window and a terminal emulator on the bottom. In the right column, there is a stacked container with a Chrome window:

The structure of this JSON file looks a lot like the TREE reply, see https://build.i3wm.org/docs/ipc.html#_tree_reply for documentation on that. Some properties are excluded because they are not relevant when restoring a layout.

Most importantly, look at the "swallows" section of each window. This is where you need to be more or less specific. As an example, remember the section about the Emacs window:

"swallows": [
    {
        "class": "^Emacs$",
        "instance": "^notmuch$"
    }
]

Here you can see that i3 will require both the class and the instance to match. Therefore, if you just start Emacs via dmenu, it will not get swallowed by that container. Only if you start Emacs with the proper instance name (emacs24 --name notmuch), it will get swallowed.

You can match on "class", "instance", "window_role", "title" and "machine". All values are case-sensitive regular expressions (PCRE). Use xprop(1) and click into a window to see its properties:

$ xprop
WM_WINDOW_ROLE(STRING) = "gimp-toolbox-color-dialog"
WM_CLASS(STRING) = "gimp-2.8", "Gimp-2.8"
_NET_WM_NAME(UTF8_STRING) = "Change Foreground Color"

The first part of WM_CLASS is the "instance" (gimp-2.8 in this case), the second part is the "class" (Gimp-2.8 in this case). "title" matches against _NET_WM_NAME and "window_role" matches against WM_WINDOW_ROLE.

In general, you should try to be as specific as possible in your swallow criteria. Try to use criteria that match one window and only one window, to have a reliable startup procedure.

If you specify multiple swallow criteria, the placeholder will be replaced by the window which matches any of the criteria. As an example:

// Matches either Emacs or Gvim, whichever one is started first.
"swallows": [
    {"class": "^Emacs$"},
    {"class": "^Gvim$"}
]

3.2. JSON standard non-compliance

A layout file as generated by i3-save-tree(1) is not strictly valid JSON:

  1. Layout files contain multiple “JSON texts” at the top level. The JSON standard doesn’t prohibit this, but in practice most JSON parsers only allow precisely one “text” per document/file, and will mark multiple texts as invalid JSON.

  2. Layout files contain comments which are not allowed by the JSON standard, but are understood by many parsers.

Both of these deviations from the norm are to make manual editing by humans easier. In case you are writing a more elaborate tool for manipulating these layouts, you can either use a JSON parser that supports these deviations (for example libyajl), transform the layout file to a JSON-conforming file, or submit a patch to make i3-save-tree(1) optionally output standard-conforming JSON.

4. Troubleshooting

4.1. Restoring a vertically split workspace

When using i3-save-tree with the --workspace switch, only the contents of the workspace will be dumped. This means that properties of the workspace itself will be lost.

This is relevant for, e.g., a vertically split container as the base container of a workspace. Since the split mode is a property of the workspace, it will not be stored. In this case, you will have to manually wrap your layout in such a container:

// vim:ts=4:sw=4:et
{
    // this is a manually added container to restore the vertical split
    "layout": "splitv",
    "percent": 0.5,
    "type": "con",
    "nodes": [

        // the dumped workspace layout goes here

    ]
}