Window Suspend & Session Restore
August 4, 2026 · View on GitHub
One mechanism backs the suspend-window action and the four [session]
options: suspend_on_close leaves a placeholder behind on every
client-initiated close, restore_windows brings windows that were still open
at logout back after a restart, and restore_camera and restore_bookmarks
restore where the canvas was framed and the bookmarks you set.
Suspended windows
suspend-window closes the target window but leaves a suspended window —
a compositor-drawn stand-in — at its exact canvas position and size, with the
app's name centered in it.
A suspended window only exists for an app driftwm can relaunch.
suspend-window resolves the target's app_id against your installed
.desktop entries (exact filename match, then StartupWMClass, then a
case-insensitive filename match); Terminal=true entries don't count —
relaunching one would open a bare terminal, not the app. No match means no
suspended window: the action logs why and closes the window normally instead of
leaving a placeholder that could never come back.
Every stand-in wears the same chrome: a textless title bar (the centered name
already labels it) carrying a close button. A server-decorated window's
stand-in reuses the bar it already had; a client-decorated window's
stand-in keeps its exact original footprint by shrinking the body under the
bar, and is handed the full size back when it relaunches. The close button
dismisses the stand-in — as do close-window, a second suspend-window
press, and msg close.
It's draggable, resizable, raisable, and focusable like any window. Pressing
Enter while it's focused, or clicking/tapping the centered name, relaunches
the app; while the relaunch is pending the name reads <app> launching…. The
new window takes over the stand-in's exact geometry and z-order slot.
It's a regular action — bind it in [keybindings], [mouse], [gestures],
or [touch] like any other. There's no default binding:
[keybindings]
"mod+shift+s" = "suspend-window"
A suspended window differs from a live window in two ways:
- Excluded from Alt-Tab and focus history, the same as a pinned widget — it's still focusable by hovering or clicking, but cycling and MRU never land on it, and neither does a taskbar's window list.
- Unpinnable, unfullscreenable, unfittable —
toggle-pin-to-screen,toggle-fullscreen,fit-window,fit-window-snappedandfill-windowno-op on it: each needs a client to configure or a screen slot to pin to. Two things that sound like they belong here don't.zoom-to-fit-snappedonly moves the camera, so it frames a suspended window's cluster like any other. And a suspended window can be resized —grow-window,shrink-windowanddriftwm msg resizewrite its size directly, with no client to configure, down to a 120px floor — a floor on the visible stand-in, which is what amsg resizereply describes, so 120x120 leaves one exactly that big on screen.
If the window was fullscreen or screen-pinned when suspended, it's returned to the canvas first, at its most recent windowed size.
suspend_on_close
[session]
suspend_on_close = true
With this on, a client-initiated close converts into a suspended window
instead of the window vanishing. The compositor can't tell one client-initiated
close from another — a CSD × click and Ctrl+Q both just destroy the
toplevel — so the flag covers all of them for an eligible window: SSD ×, CSD
×, an in-app quit, a shell exiting in a terminal. Widgets, dialogs (a
toplevel with a parent), and modal toplevels are never eligible — same as
suspend-window itself.
Escape hatches, for closes you want to stay real closes:
-
The
close-windowaction,msg close, and a taskbar's close button all close for real, even with the flag on. -
Closing a suspended window (its own close button,
close-windowwhile it's focused, ormsg close) dismisses it — it doesn't re-suspend. -
A window rule can override the flag per app:
[[window_rules]] app_id = "kitty" suspend_on_close = false # this terminal should always really close
Tip
A crash leaves a suspended window too — the compositor can't tell an app
crashing from it quitting cleanly. With suspend_on_close on, a crashed
app's window and position survive, and Enter brings it right back.
restore_windows
[session]
restore_windows = true
On a graceful shutdown — quit/Super+Ctrl+Shift+Q or a logout that sends
SIGTERM/SIGHUP — every eligible live window is saved. On the next launch they
come back as dormant suspended windows at the positions they were at; nothing
auto-launches, you relaunch each one same as any other suspended window (or
leave it be). A kill -9, a crash, or unplugging the machine skips the save
entirely.
Suspended windows themselves are always saved and restored, regardless of
this flag; restore_windows only decides whether still-open windows are
saved too.
The window you had focused comes back focused, as focus on its stand-in — so it
wears a focus ring, Enter relaunches it, and placement = "auto" puts your
first new window beside it. The restored focus is applied only when the stand-in
is visible at launch; otherwise the canvas starts unfocused. A focused
suspended window carries its focus across a restart whether or not this flag
is on.
A window rule can override the flag per app, in either direction: false keeps
one app off the canvas after a restart while the rest of your session comes back,
and true brings one app back while the flag stays off for everything else.
[[window_rules]]
app_id = "footclient"
restore_windows = false
Two things to keep in mind:
- The flag is independent of
suspend_on_close: that one governs closes, this one the logout save. Set both tofalsefor an app that should never leave a stand-in behind. - Key the rule on
app_id. Saved records carry no title, so atitlecriterion narrows only what gets saved; on the way back the rule is read offapp_idalone, and decides for every saved window of that app. The config reference has the rest.
restore_camera
[session]
restore_camera = true
With this on, each output's camera position and zoom are restored across restarts too, so your canvas comes back framed exactly where you left it. It's off by default — the default config starts every output at its centered camera. The flag is read at launch, so flipping it mid-session takes effect on the next launch, not immediately. (Cameras are always saved regardless, so turning it on later restores what your session had.)
restore_bookmarks
[session]
restore_bookmarks = true
Bookmarks (named canvas points — see [navigation.bookmarks] and the
go-to-bookmark / set-bookmark / move-to-bookmark actions) are a runtime
registry seeded from config. With this off (the default), the registry resets to
the config seeds on every launch, so a set-bookmark or driftwm msg bookmark
edit lasts only for the session. Turn it on to overlay the saved registry on top
of the config seeds at launch, so a restored bookmark wins per name and config
seeds fill the names the save lacks. Like the camera flag, it's read at launch.
The session file
The session lives at ~/.local/state/driftwm/session.json (respects
XDG_STATE_HOME). It's written through immediately on anything you'd notice
(suspending, dismissing, relaunching) and debounced (~1s) for continuous
changes like dragging a suspended window. A file written by an older driftwm is
read and converted in place; one from a newer version, or that fails to parse
or can't be read at all, is quarantined next to it as
session.json.corrupt.<timestamp> or session.json.unreadable.<timestamp>,
and startup continues with an empty session.
Each saved window's position and size describe its stand-in's visual frame,
the same convention as window rules and driftwm msg state.
Relaunching & matching
Relaunch (Enter, clicking the name, or msg relaunch) spawns the app with an
activation token and waits for its window to come back, matching it to the
stand-in by, in order:
- The activation token, if the app presents it back (most native Wayland toolkits do). This works whether the app maps a fresh window or — as a single-instance app does — forwards the token to its already-running window; either way that window moves into the stand-in's slot, sized to fit. Note that the app picks the answering window: one with several windows open that forwards the token to an existing sibling will have that sibling moved, abandoning its old spot.
- App identity, as a 5-second fallback for apps that ignore the token: the oldest pending relaunch of the same app_id adopts the next window of that app_id to map.
Limitations
- Apps that never present the token back and map no new window (some single-instance apps just focus an existing window without forwarding the activation token) leave nothing to adopt. The stand-in reverts to dormant (showing the app's name again) after about 30 seconds.
- The 5-second fallback window is a capture hazard. While a relaunch is
pending, any window of that
app_idcan be captured into the stand-in's rect — one you launched by hand, or the answer to a second pending relaunch of the same app (multiple relaunches match first-come, first-served by spawn order). The window that was meant for the slot then places itself normally. - An app that reports a different
app_idon relaunch than it was suspended under only adopts via the activation token — the identity fallback won't recognize it as the same app.
Nested sessions
A nested (winit) driftwm doesn't persist a session by default, so it can't
clobber the session file of the compositor it's running inside. Opt in with
--session-file <path>:
driftwm --backend winit --session-file /tmp/driftwm-nested-session.json
Suspended windows work within the run either way — they're on the canvas the
moment they're created, in any backend. The path is what makes them durable:
the udev backend uses the default one, and a winit run persists only with
--session-file.
IPC
Suspended windows are visible and controllable over the IPC socket too — see IPC › Suspended windows.