Hacking i3: Automatic Layout

(Day 20 of 30 Days of Blogging)

Another post about making a good window manager even better!

Window layouts in i3 basically come in two types: horizontally or vertically stacked. By combining those two you can arrange windows however you want.

The default layout in i3 is horizontal, which means when you open up too many windows it ends up looking like this:

Of course what you’re supposed to prevent everything getting squished like that is to split windows to the opposite layout so they grow in the other direction. But that requires a key press, and we don’t use tiling WMs to manually position windows, dammit!

Luckily i3 has a capable API and others have used it to automatically perform that splitting, such as i3-alternating-layout and i3_workspaces. After playing with those I didn’t really find the idea of binary tree layouts very practical. Maybe it’s my 13” laptop screen, but windows just get too small.

What I really wanted was a two-column layout that splits the screen in half, with a vertical stack of windows on each side. Well, after a lot of API struggling:

No windows were manually split in this video 😮 Just pure, sweet terminal spam. Moving windows between the columns works as you’d expect. You can still overload the screen with too many windows, but hey, a short and wide terminal is at least still readable.

Here’s the source for that:

#!/usr/bin/env python3
#
# Automatically splits windows so workspaces are laid out in 2 columns.

from i3ipc import Connection, Event

COLUMNS = 2

def move_container (con1, con2):
    con2.command("mark __column-layout");
    con1.command("move window to mark __column-layout")
    con2.command("unmark __column-layout");

def layout (i3, event):
    if event.change == "close":
        for reply in i3.get_workspaces():
            if reply.focused:
                workspace = i3.get_tree().find_by_id(reply.ipc_data["id"]).workspace()

                if len(workspace.nodes) == 1 and len(workspace.nodes[0].nodes) == 1:
                    child = workspace.nodes[0].nodes[0]
                    move_container(child, workspace)
    else:
        window = i3.get_tree().find_by_id(event.container.id)
        if window is not None:
            workspace = window.workspace()
            if workspace is not None and len(workspace.nodes) >= COLUMNS:
                for node in workspace.nodes:
                    if node.layout != "splitv":
                        node.command("splitv")

i3 = Connection()
i3.on(Event.WINDOW_NEW, layout)
i3.on(Event.WINDOW_CLOSE, layout)
i3.on(Event.WINDOW_MOVE, layout)
i3.main()

As always, make sure you have python3 and the latest version of i3ipc installed.

For larger and wider screens you might experiment increasing COLUMNS to 3. I tend to stick with 2 even when I had a desktop computer, my workflow usually ends up being [thing I’m working on] on the left column and a bunch of terminals on the right.

If you’re still reading this far, you’ll probably like these other posts on i3: