hypr-recall

hypr-recall & You

A (Mostly True) Story About Windows

01

The Tragedy

You had fourteen windows open. They were perfect. Firefox on workspace 2 — not because it belongs there, but because that's where you put it three months ago and you have never questioned it since. Three Ghostty terminals in a lovely little row on workspace 1, each one doing something vaguely important. Zed occupying exactly 49.3% of the screen, because you dragged that column divider to precisely the right place and it felt, in that moment, like art.

Then you restarted Hyprland.

The windows are gone. Not destroyed — windows don't really die, they simply stop being open. But they are gone all the same. Gone like old friends who moved to another city without leaving a forwarding address. Gone like socks. Gone like the particular energy you had at 2pm last Tuesday.

"A workspace without its windows is just a number."
— someone, probably

This is where hypr-recall comes in. hypr-recall is a small Rust program that remembers your windows so you don't have to. It is not magic. It is approximately 1,700 lines of fairly earnest code and a logo that was generated by an AI and then had its background removed by a different AI.1 But it works.

02

The Spell of Saving

First, you must save. This is the important part. You cannot restore what was never saved. This is true of sessions, of relationships, and of documents you were editing when the power went out.

 hypr-recall save

That's it. One command. hypr-recall will ask Hyprland — very politely, via IPC — what windows you have open, where they are, and how wide each column is relative to your monitor. It will write all of this to a small JSON file at ~/.local/share/hypr-recall/session.json and then it will stop running, because it is a CLI tool and not a daemon and it has boundaries.

What gets saved: every tiled, non-floating window on a real workspace. The app class, the executable path, and the column width as a ratio of your monitor width (so 0.493 means "about half"). Windows are stored in left-to-right order within each workspace.

What doesn't get saved: floating windows, special workspaces, the ambient anxiety you carry while working. Some things are beyond the reach of any CLI tool.

The resulting file looks something like this:

{
  "version": 1,
  "active_workspace": 2,
  "workspaces": [
    {
      "workspace": 1,
      "windows": [
        { "class": "com.mitchellh.ghostty", "exe": "/usr/bin/ghostty",        "col_width": 0.493 },
        { "class": "dev.zed.Zed",            "exe": "/usr/bin/zed",           "col_width": 0.493 },
        { "class": "com.mitchellh.ghostty", "exe": "/usr/bin/ghostty",        "col_width": 0.493 }
      ]
    },
    {
      "workspace": 2,
      "windows": [
        { "class": "firefox", "exe": "/usr/lib/firefox/firefox", "col_width": 0.989 }
      ]
    }
  ]
}

You can look at this file. You can edit it. It is just JSON. It will not judge you for opening it in Vim even though you have Zed installed. Use hypr-recall edit to open it in $EDITOR if you'd like the program to judge you on your behalf.

03

The Art of Recall

Now the fun part. You've restarted. Your screen is empty and a little bleak. You type:

 hypr-recall restore

hypr-recall reads the session file. Then, with the quiet determination of someone who has a list and intends to follow it, it begins.

For each workspace: it focuses that workspace, then launches each app in the saved left-to-right order. It doesn't just fire them all off and hope for the best — it waits. Specifically, it subscribes to Hyprland's IPC event socket and listens for openwindow events.2 When your window appears, hypr-recall knows. It is, in this sense, watching.

Don't worry about that last sentence. It's fine. It's your own computer.

Once all the windows for a workspace are open, hypr-recall sorts them into the correct column order using a series of swapcol l dispatches — essentially sliding columns leftward, one by one, until everything lines up.3 Then it resizes each column to its saved width ratio.

Finally, it focuses whichever workspace you were on when you saved. Then it stops. It has done its job.

Sometimes the binary you saved isn't there anymore — an update moved it, a versioned install path shifted underneath it, whatever. hypr-recall doesn't take this personally. It looks for a .desktop entry matching the window's class and launches that instead. If nothing resolves either, it prints a warning and skips just that one app, rather than throwing up its hands and abandoning the entire restore over a single missing Discord.

While all of this is happening, a spinning overlay bearing the hypr-recall logo floats above your workspaces. It shows which workspace is currently being restored ("restoring workspace 2 / 4") so you know it is not stuck. It disappears the moment the restore completes. You can disable it with overlay = false in the config file if you find it unsettling.

The dry run, for the anxious

If you'd like to know what would happen without committing to the bit, there is --dry-run:

 hypr-recall restore --dry-run

  workspace 1 (3 windows)
    com.mitchellh.ghostty            → launch 2
    dev.zed.Zed                      → launch 1
    discord                          → SKIP (binary missing, no .desktop match)

  workspace 2 (1 window)  ← active
    firefox                          → skip (1 already open)

Note that Firefox was already open, so hypr-recall would skip it. hypr-recall is not in the business of opening duplicate windows. There are enough of those already. Discord, meanwhile, gets the capital-letter SKIP — its recorded binary is gone and no .desktop entry could stand in for it, so the dry run tells you now instead of letting you find out mid-restore.

Just the one workspace, please

Sometimes you do not want the whole world back. You closed one workspace by accident — the rest of your desktop is perfectly happy where it is, thank you. For this there is --workspace (or -w, for the hurried):

 hypr-recall restore work --workspace 2

Only workspace 2 from the work session is rebuilt. Everything else is left untouched — even the part where hypr-recall chases down stray windows only ever herds them toward the one workspace you asked about. It minds its own business, mostly.

Saving works the same way, and for the same reason: maybe you only reshuffled workspace 2 and would like the rest of the saved session to remain exactly as it was, thank you very much.

 hypr-recall save work --workspace 2

Only workspace 2 is re-captured and merged into the work session file — the other saved workspaces are left precisely as they were. Close every window on workspace 2 first and it is quietly removed from the file instead, on the theory that an empty workspace is not a workspace worth remembering.

Watching the gears turn

By default a restore narrates only the broad strokes: which workspace it is on, and when it has finished. But should something go sideways and you wish to see every launch, every column nudged left, every stray window herded home, add --verbose (or -v):

 hypr-recall restore -v

The diagnostics are murmured to standard error rather than standard out, so hypr-recall restore -v 2>trace.log keeps your terminal serene while quietly writing the whole confession to a file.

Or go the other way. A restore launched from hyprland.lua on login has no terminal to narrate to anyway — its broad-strokes progress lines just vanish into Hyprland's own log, unread and unloved. Tell it to stop bothering with --quiet (or -q):

 hypr-recall restore -q

Warnings and errors still get through — quiet mode only silences the happy path. A restore that goes well says nothing at all, which is, upon reflection, exactly what you asked for.

04

Special Cases & Strange Apps

Some applications — Firefox, Zed, Chromium — do not open a blank window when launched. They look at their saved state, sigh contentedly, and restore their own previous session. This is called being a session-restore app, and it is both a blessing and a source of subtle confusion.

The confusion: if you launch Firefox twice, you don't get two Firefoxes. You get one Firefox, mildly bewildered, possibly with a "Firefox is already running" dialog. Firefox is a singleton. It has opinions about this.

hypr-recall knows about this. For session-restore apps, it launches once and then waits for however many windows the app decides to open on its own — up to 20 seconds.4 If your app closes immediately after launching (which is what happens when a second instance hands off to the first), hypr-recall notices and extends the wait. This is called single-instance handoff detection and it works most of the time, which is as much as you can ask of anything.

If you have an app that behaves this way but isn't on hypr-recall's built-in list, you can tell it:

 hypr-recall restore --session-restore-app my.special.App
05

Making It Automatic

The most useful version of hypr-recall is one you never have to think about. Put this in your hyprland.lua:

-- restore on login
hl.exec("hypr-recall restore")

-- save on shutdown, so you never lose more than the current session
hl.on("hyprland.shutdown", function()
    os.execute("hypr-recall save")
end)

-- optional: save every 10 minutes, for the paranoid (affectionately)
hl.timer(function()
    hl.exec("hypr-recall save")
end, { timeout = 600000, type = "repeat" })

With this configuration, hypr-recall runs quietly in the background of your life, watching over your workspaces with the calm vigilance of a library cat. You will forget it exists. And then one day Hyprland will crash — perhaps because you were compiling something ambitious, perhaps for reasons unknown to science — and when you log back in, everything will be where you left it.

You will feel, briefly, that the universe is on your side.

06

Advanced Spellwork

Per-app launch arguments

Some apps need a little extra coaxing. A flag here, a profile path there. You can add a launch_args array to any window entry in the session file:

{
  "class": "firefox",
  "exe": "/usr/lib/firefox/firefox",
  "launch_args": ["--profile", "~/.mozilla/work"],
  "col_width": 0.989
}

Edit the file once with hypr-recall edit. Every subsequent restore will pass those arguments. hypr-recall does not question why you need four Firefox profiles. It simply obeys.

Alternatively — and this is the cleaner approach if you're sure the args will always apply — you can set them in the config file instead, and they'll survive even if you delete and re-save the session:

The config file

~/.config/hypr-recall/config.toml is optional. If it doesn't exist, everything uses sensible defaults. If it does, it looks something like this:

# ~/.config/hypr-recall/config.toml

overlay = true             # overlay with workspace progress during restore
settle_delay_secs = 4     # wait for late-opening windows before stray sweep

# apps to treat as session-restore apps beyond the built-in list
session_restore_apps = ["my.electron.App"]

# per-app settings
[apps.firefox]
launch_args = ["--profile", "~/.mozilla/work"]

[apps.my.electron.App]
session_restore = true

Config-file launch args take precedence over anything in the session JSON. Unknown keys are rejected outright — a typo in the config file is an error, not a mystery.

Checking on things

At any moment you can ask hypr-recall what it knows:

 hypr-recall status

  session saved 3 hours ago
  3 workspaces · 8 windows

  workspace 1 (3 windows)
    ghostty    49%  49%  49%
  workspace 2 (4 windows)  ← active
    firefox    99%
    zed        49%  60%
  workspace 3 (1 window)
    ghostty    99%

The percentages are column widths. 99% means "basically fullscreen, but Hyprland insists on keeping a small gap because it has principles."

07

Named Sessions

By default, hypr-recall saves to a file called session.json. This is fine. One session, one name, the same file every time. It has the advantage of simplicity and the disadvantage of not being able to save your "work" layout separately from your "I am watching a video while things compile" layout.

If you'd like more than one, you can give your sessions names:

 hypr-recall save work
hypr-recall: saved 'work' — 9 windows across 3 workspaces

 hypr-recall save gaming
hypr-recall: saved 'gaming' — 2 windows across 1 workspace

 hypr-recall restore work
hypr-recall: restoring 'work'

Each named session is stored as its own JSON file: ~/.local/share/hypr-recall/work.json, ~/.local/share/hypr-recall/gaming.json, and so on. The default (no name given) remains session.json, exactly as before.

To see everything you've saved:

 hypr-recall list

hypr-recall: saved sessions

  work                 2 min ago        3 workspaces, 9 windows
  session              3 hr ago         2 workspaces, 5 windows
  gaming               2 days ago       1 workspace, 2 windows

Sorted newest first. If you've never saved anything, it will tell you that too, gently.


And that, more or less, is everything.
Go save your session. Right now. Before you forget.

1. The background was removed by remove.bg, which is a website that exists for exactly this purpose and does it remarkably well. We tried ImageMagick first. The edges were not good.

2. Window detection is event-driven: hypr-recall subscribes to Hyprland's IPC event stream instead of busy-waiting for windows to appear. (It does still read back hyprctl clients between launches to count windows, and it is not above a short settling delay while the compositor catches up.)

3. This is insertion sort, technically. But "insertion sort" doesn't sound nearly as interesting as "sliding columns leftward, one by one."

4. Twenty seconds is a long time. If your app hasn't appeared in twenty seconds, something has gone wrong that is probably not hypr-recall's fault, but hypr-recall will tell you about it anyway.