tauri-apps / tauri-apps/plugins-workspace
[bug][window-state] autosave window states
- Dominant language
- Rust
- Stars
- 1.8k
- Forks
- 602
- Avg merge
- 4d 14h
- Merged PRs (30d)
- 9
Description
### Plugin
`window-state` (tested against `tauri-plugin-window-state` v2.4.1, Tauri 2.11, Windows 11).
### Summary
The window-state plugin only **persists to disk on `RunEvent::Exit`**. The per-window
`WindowEvent::CloseRequested` handler updates the *in-memory* cache but never writes it out. As a
result, any app whose process does **not** cleanly reach `RunEvent::Exit` on close silently loses
its window geometry — it looks like "restore is broken" when in fact **save never ran**.
This reliably bites apps that keep a **live background thread** (audio/CPAL, MIDI, a port/process
poller, an embedded HTTP server, file watchers): on close the process can be torn down — or linger
and then be force-killed by the user/OS — before the event loop delivers `RunEvent::Exit`. The
`%APPDATA%//.window-state.json` file is then never created/updated. It also overlaps
with the symptom in #1111 (state not saved when closing via the macOS menu).
A scripted `WM_CLOSE` in a test harness usually *does* reach `Exit`, which masks the bug — so it
only shows up after a real user close of a real app with background work.
### Root cause (in `src/lib.rs`)
- `Builder::build` → `.on_event(|app, event| { if let RunEvent::Exit = event { app.save_window_state(...) } })`
is the **only** place that writes the file to disk.
- The per-window handler registered in `on_window_ready`:
```rust
WindowEvent::CloseRequested { .. } => {
let mut c = cache.lock().unwrap();
if let Some(state) = c.get_mut(&label) {
let _ = window_clone.update_state(state, state_flags); // <-- updates the in-memory cache ONLY
}
}
```
updates the cache but does **not** flush to disk. So the flush is entirely dependent on
`RunEvent::Exit` firing, which is not guaranteed on close.
### Impact
- Silent data loss of the exact thing the plugin exists to provide (size/position restore).
- Hard to diagnose: the state directory may not even be created, so it reads as "restore broken."
### Proposed fix
Make the plugin persist **independently of `RunEvent::Exit`**. Two options (not mutually
exclusive):
1. In the `CloseRequested` arm of the per-window handler, after `update_state`, also **write to
disk** (`app_handle().save_window_state(state_flags)`), not just mutate the cache.
`CloseRequested` fires synchronously in the event loop before any teardown/hang.
2. Optionally add a `Builder` opt-in (e.g. `.save_on_close(true)` / `.eager_save(true)`) for those
who want to keep the current Exit-only behavior as default.
Optionally also saving on `WindowEvent::Focused(false)` covers force-quits/crashes that never send
a clean close at all.
This would also resolve the observable symptom in #1111 without needing an upstream tao/wry fix,
since the plugin would no longer rely on the exit path.
### Workaround (for anyone hitting this now)
Register a `Builder::on_window_event` and flush yourself:
```rust
.on_window_event(|window, event| {
if matches!(
event,
tauri::WindowEvent::CloseRequested { .. } | tauri::WindowEvent::Focused(false)
) {
use tauri_plugin_window_state::AppHandleExt;
let _ = window.app_handle().save_window_state(
tauri_plugin_window_state::StateFlags::SIZE
| tauri_plugin_window_state::StateFlags::POSITION
| tauri_plugin_window_state::StateFlags::MAXIMIZED,
);
}
})
```
Verified across five Tauri apps with live background threads (audio, MIDI, a port poller, an HTTP
server, session/file watchers): with this handler the state file is written on a real close and on
focus-loss, and survives a hard force-kill; without it, apps that don't reach `RunEvent::Exit`
never saved.
Happy to open a PR for option (1)/(2) if the maintainers agree on the preferred shape.
### Related
- #1111 — [bug][v2][window-state] window state not saved when closing through menu on macOS
(same underlying "flush only on exit" limitation, different trigger).
Contributor guide
Research direction
Start in src/lib.rs by reading Builder::build, the RunEvent::Exit handler, and the on_window_ready CloseRequested handler. Trace how update_state changes the cache and how save_window_state writes it, then ensure a real window close persists the updated state without relying only on RunEvent::Exit; verify the state file is written and restores geometry after closing.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- desktop
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 62/100