Snapshot a Hyprland workspace and recall it later — including the layout.
Not just "which apps were open and on which workspace", but the actual dwindle arrangement: which window is split against which, in what direction, at what ratio.
$ hyprecall save comms
saved comms (3 windows, ws 3)
$ hyprecall show comms
| split
- slack: Eugenia (DM) - Slack
- split
- chrome-x.com__-Profile_3: Home / X
- foot: dbsps@omarchy:~
$ hyprecall restore comms 4 # onto an empty workspace
+ slack: /usr/lib/slack/slack --gtk-version=3 -s
+ chrome-x.com__-Profile_3: omarchy-launch-webapp https://x.com/
+ foot: foot --working-directory=/home/dbsps
done
Restores are pixel-exact and take about a second:
slack (12, 38, 1681, 1300) -> (12, 38, 1681, 1300) OK
chrome-x.com... (1707, 38, 681, 1300) -> (1707, 38, 681, 1300) OK
foot (1607, 695, 781, 643) -> (1607, 695, 781, 643) OK
Session save and restore for Hyprland is a crowded and very young field — most
of it started within the last few months, with several people solving the same
problem in parallel without knowing about each other. hyprecall was written
before its author went looking at what else existed, so nothing here is
borrowed. These are simply the projects nearest to it, and in a couple of cases
better answers than it:
hypr-persistandomarchy-desktop-presetalso reconstruct the dwindle tree from window geometry and replay it withpreselect— the same approach as here, arrived at independently. Three people converging on one trick is reasonable evidence the trick is right.omarchy-sessionis well ahead ofhyprecallon everything to do with relaunching: terminal working directories, browser profile and mode restoration, grouped tabs, undo, and post-restore verification. If you care more about what comes back than about where it lands, use that instead.
What hyprecall adds is that the layout claim is checkable rather than
asserted — see Verifying a restore — plus window-class
to .desktop resolution, so Chromium web apps come back as themselves rather
than as blank browser windows.
The premise everyone stopped at is wrong. The tree does not need to be exposed.
Capture. Dwindle is a guillotine partition, so the tree is recoverable from
the rectangles in hyprctl clients: find the single x- or y- line that cleanly
separates the windows into two non-empty groups with nothing straddling it —
that line is a split, and its position gives the ratio. Recurse. Ratios are
stored as parent-relative fractions, so a snapshot survives a resolution change.
Replay. layoutmsg preselect <l|r|u|d> forces the split direction of the
next window to open. Walk the tree in order, preselect, launch exactly one
window, wait for it to map, repeat. Then set each split's ratio as it is
created, while the new window still spans that node's whole rectangle.
- Hyprland 0.56+
- Python 3.11+
footandgum, for the name prompt and the picker- Built against Omarchy, whose Lua config makes
hyprctl dispatchevaluate its argument as Lua. On a stock.confHyprland the dispatch layer inbin/hyprecallneeds the classic string form instead.
git clone https://github.com/USER/hyprecall.git
cd hyprecall && ./install.shThis symlinks bin/hyprecall into ~/.local/bin, and adds a managed block to
~/.config/hypr/bindings.lua and ~/.config/hypr/hyprland.lua (both backed up
first) binding:
| Key | Action |
|---|---|
SUPER+SHIFT+F5 |
Snapshot this workspace, prompting for a name |
SUPER+SHIFT+F6 |
Pick a snapshot and rebuild it on the active workspace |
./install.sh --uninstall removes all of it.
hyprecall save NAME [ws] snapshot a workspace
hyprecall restore NAME [ws] [--force] rebuild it (refuses a non-empty workspace)
hyprecall list list snapshots
hyprecall show NAME print the saved tree
hyprecall rm NAME delete a snapshot
Snapshots are plain JSON in ~/.local/share/hyprecall/, meant to be edited by
hand when a command needs adjusting.
In order:
- Your override, from
~/.config/hyprecall/overrides.json—{"window-class-regex": "command"}. /proc/<pid>/cmdline, when it can actually identify the window.- The
.desktopentry that owns the window class — matched onStartupWMClass, or for a Chromium web app by normalising the class (chrome-192.168.0.120__protect_dashboard_all-Profile_3) against the URL in each entry'sExecline.
Step 3 exists because step 2 fails in exactly the cases you care about: one Chromium process serves every web-app window, so all of them report the plain browser command line.
Layout claims are easy to make and hard to keep, so this ships with the check.
--verify re-reads the workspace after a restore and diffs every window's
rectangle against the snapshot:
$ hyprecall restore prime 4 --verify
chromium (12, 38, 1581, 1300) -> (12, 38, 1581, 1300) OK
foot (1607, 695, 781, 643) -> (1607, 695, 781, 643) OK
org.omarchy.agent (1607, 38, 781, 643) -> (1607, 38, 781, 643) OK
EXACT MATCH
Anything other than EXACT MATCH is a bug — please open an issue with the
output and the snapshot JSON.
- Dwindle only. Master and scrolling layouts are not guillotine partitions in the same way and are not handled.
- Grouped (tabbed) windows are not handled yet.
- Fullscreen windows report the whole monitor as their geometry; a snapshot taken with one on the workspace will warn and infer the wrong tree.
- Browser tabs are not state. A restored browser window is a fresh one.
Apache-2.0 - see LICENSE.