tauri-apps / tauri-apps/plugins-workspace

[bug][window-state] autosave window states

Open
#3,474 1 comment 1 reaction 0 assignees View on GitHub
plugin: window-state type: feature request
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.