Zonecraft
Two monitors, a dozen windows, and the same five minutes of dragging every morning. Zonecraft lets you draw that arrangement once, keep it as a profile, and drop every window where it belongs — without anything ever moving on its own.
ZONECRAFT · GNOME SHELL EXTENSION
Why this exists
Every tiling extension for GNOME wants to take the wheel: you open a window and it gets thrown somewhere by a rule you did not write. The window manager, meanwhile, has no memory — unplug the second display and the arrangement you had is gone for good. I wanted the opposite contract: the layout is mine, it is reusable, and nothing moves until I say so.
1 # What was broken
2
3 - automatic tilers move windows for you
4 - no reusable layouts per monitor setup
5 - unplug a display, lose the arrangement
6 - ~5 min of dragging, every single day
7
8 > goal: draw it once, apply it on demand
The decisions I made
Three decisions carry the whole thing. The core is pure TypeScript with no gi:// imports, so grids, geometry and monitor matching run under Vitest without ever booting a Shell. Monitors are bound by role relative to the primary display — primary, left-1, right-2 — instead of by index, so replugging a screen does not scramble a profile. And track sizes are integer weights out of 10 000 rather than floats, so rounding never drifts across a resize.
1 src/core/ pure TS · no gi:// · vitest
2 ├── grid.ts tracks, merge, unmerge
3 ├── geometry.ts weights → rectangles
4 └── monitors.ts displays → roles
5 │
6 src/runtime + src/ui Meta · St · Clutter
7 src/prefs.ts GTK4 · libadwaita
8 GSettings versioned JSON
9
10 // trade-off: you confirm every time
The part I'm proud of
A profile cannot store monitor indexes: those are reassigned the moment you replug a cable. So each display is classified by where its centre sits relative to the primary one — the dominant axis picks the direction, the distance picks the rank. The whole thing is one comparison and one sort, it has no GNOME dependency, and it is the reason a saved layout survives a docking station.
1 // bind by role, never by index
2 const dx = mCenter.x - primaryCenter.x;
3 const dy = mCenter.y - primaryCenter.y;
4 const direction =
5 Math.abs(dx) >= Math.abs(dy)
6 ? (dx < 0 ? 'left' : 'right')
7 : (dy < 0 ? 'above' : 'below');
8 bucket.sort((a, b) => a.distance - b.distance);
9
10 // → primary · left-1 · right-2 · above-1
What changed afterwards
I will not put download numbers here, because there are none yet: 0.1.0 targets Fedora 44 / GNOME 50 and has not gone through the extensions.gnome.org review. What shipped is a version I use every day, with the safety rails built in from the start — partial apply, clean cancel, one-level undo, and not a single network call. What I would do differently: the assignment overlay grew into the largest file in the project before the profile model had settled, and I should have frozen the data shape first.
1 ## Shipped in 0.1.0
2
3 profiles reusable, one per monitor role
4 grids free rows/cols · merged zones
5 safety partial apply · cancel · undo
6 privacy 0 network calls · 0 telemetry
7
8 ### Next
9 EGO review · import/export · app rules