Session management: xdg-session-management-v1 client, mandatory App::app_id / save_state / restore_state, runtime-managed state persistence and clean exit on signals (0.3.0)
Applications built on ltk had no way to come back where the user left them: the toolkit hardcoded `app_id = "ltk"` on every toplevel, never wrote anything to disk, and died on SIGTERM without a chance to save. This release gives the runtime the whole plumbing and asks each application only for the bytes worth keeping, in the spirit of Android's saved-instance state. The `App` trait gains three mandatory methods, deliberately without default bodies so every application states its position: `app_id()` (reverse-DNS, used for `xdg_toplevel.set_app_id`, the AccessKit application name and the state directory — the `app_id` element of the deprecated `window_config` tuple is now ignored and a one-time warning reports a mismatch), `save_state() -> Option<Vec<u8>>` and `restore_state(Vec<u8>)`. The bytes are opaque; the trait carries no serde bound. Their rustdoc is the contract: when the runtime saves, where the files live, when the bytes come back and when they do not, what must never go in them, and a worked serde_json example. The runtime persists under `$XDG_STATE_HOME/<app_id>/` (falling back to `~/.local/state`): `session.json` holds the compositor session id, a clean-exit marker and the writer's pid; `state.bin` holds the application bytes. Writes are atomic (temp file + rename, mode 0600, directory 0700) and best-effort. State is saved every 30 s when the bytes changed, once after the event loop exits (which covers `on_close_requested`, `requested_exit` and lost connections), and on SIGTERM/SIGINT — a calloop signal source, installed before any thread exists, now turns those into a clean exit of the loop instead of process death. `restore_state` runs synchronously in `try_run` before the window is created and before the first `view()`, and only when the process is relaunched as part of a session restore (`LTK_SESSION_RESTORE=1`, removed from the environment before the app can spawn children) or when the previous run left `clean_exit: false`; a plain launch starts fresh. A second concurrent instance detects the live pid and runs with persistence disabled rather than clobbering the first. The compositor side of geometry restore goes through `xdg-session-management-v1`. Neither wayland-protocols nor sctk ship generated code for it yet, so the XML is vendored under `protocols/` and `wayland-scanner` generates the client module in-tree (`src/protocol/`), resolving the crate names through sctk's reexports so the bindings stay on the crate instances sctk links. Before the first commit of a `ShellMode::Window` toplevel the runtime binds `xdg_session_manager_v1`, calls `get_session(reason, stored_id)` and `restore_toplevel(toplevel, "main")`; the three window-creation paths in `run.rs` are folded into one `make_window` helper so the attach always sits immediately before `commit()`. `created` persists the id, `replaced` destroys the objects and stops persisting. Compositors without the global lose only the geometry half. Layer-shell and session-lock surfaces skip the whole machinery. Every `App` implementor in the tree is updated: the twelve examples (`showcase`, `scroll` and `mini_shell` persist real state; the rest return `None`), both integration tests (`event_loop_flow` gains `save_restore_round_trip`), the in-source and markdown doctests, README, onboarding, cookbook (new recipe "Surviving relaunch: session state") and architecture docs, and the changelog. `src/session_state.rs` carries unit tests over a temporary state directory. `Makefile install` now copies `protocols/` into the cargo registry — without it downstream builds would fail inside the proc-macro — and `debian/copyright` covers the vendored XML. The trait change is breaking, hence 0.3.0. Also fixes the pre-existing `viewport_tests` module in `render/mod.rs`, which used `Length` without importing it and broke `cargo test`.
This commit is contained in:
@@ -33,12 +33,14 @@ Where things live under `src/`, one line each:
|
||||
- `core.rs` — `UiSurface`, runtime-free embedding.
|
||||
- `draw/` — the per-frame drawing pipeline shared by both backends.
|
||||
- `egl_context.rs` — EGL bootstrap for the GPU path.
|
||||
- `event_loop/` — the Wayland run loop: frame scheduling, invalidation, clipboard / data device, text editing and IME, tooltips, focus, subsurfaces, perf guardrails.
|
||||
- `event_loop/` — the Wayland run loop: frame scheduling, invalidation, clipboard / data device, text editing and IME, tooltips, focus, subsurfaces, session management, perf guardrails.
|
||||
- `gles_render/` — GPU backend (EGL + GLES2 / GLES3).
|
||||
- `input/` — pointer, keyboard and touch handling, the gesture machine, dispatch.
|
||||
- `layout/` — composable arrangers for `Element` trees.
|
||||
- `protocol/` — in-tree `wayland-scanner` bindings for protocols `wayland-protocols` does not generate yet (`xdg-session-management-v1`).
|
||||
- `render/` — software rendering surface used by every widget.
|
||||
- `secure_mem.rs` — volatile wipe of secret buffers behind `TextEdit`'s secure mode.
|
||||
- `session_state.rs` — `$XDG_STATE_HOME/<app_id>/` session id + app-state files (atomic writes, clean-exit marker).
|
||||
- `system_fonts.rs` — primary-font resolution and the per-glyph fallback chain.
|
||||
- `text_shaping.rs` — BiDi reordering and rustybuzz (HarfBuzz) shaping.
|
||||
- `theme/` — theme documents, slot stores, the embedded fallback.
|
||||
@@ -68,8 +70,10 @@ In practice, that model is easiest to adopt in three steps:
|
||||
**Always implement**
|
||||
|
||||
- `type Message` — your message enum.
|
||||
- `app_id(&self) -> &str` — reverse-DNS id shared by the toplevel `app_id`, the a11y tree and the `$XDG_STATE_HOME/<app_id>/` session directory.
|
||||
- `view(&self) -> Element<Msg>` — main surface contents.
|
||||
- `update(&mut self, msg: Msg)` — state transitions.
|
||||
- `save_state(&self) -> Option<Vec<u8>>` / `restore_state(&mut self, Vec<u8>)` — opaque bytes the runtime persists (every 30 s when changed, on close, on `SIGTERM`/`SIGINT`) and hands back before the first frame on a session restore or after an unclean exit — never on a plain launch. `None` / no-op for shell components and stateless tools.
|
||||
|
||||
**Implement when your app is multi-surface**
|
||||
|
||||
@@ -102,6 +106,7 @@ In practice, that model is easiest to adopt in three steps:
|
||||
|
||||
**Implement for window / toplevel lifecycle**
|
||||
|
||||
- `app_id()` — also the key of the compositor-side `xdg-session-management-v1` session; ltk issues `restore_toplevel` before the first commit so size/position come back on every launch.
|
||||
- `window_config()` — deprecated forced-window escape hatch; prefer `shell_mode()`.
|
||||
- `on_close_requested()` — return `false` to veto a close (compositor request, titlebar button, layer-shell closed event).
|
||||
- `on_toplevel_event(event)` — open / close notifications from `ext-foreign-toplevel-list-v1`, keyed by a stable handle id.
|
||||
@@ -109,7 +114,7 @@ In practice, that model is easiest to adopt in three steps:
|
||||
|
||||
Relatedly, `ltk::try_run( app )` is the fallible variant of `ltk::run` — it returns a `RunError` (no Wayland connection, missing protocol) instead of aborting, for apps that want a CLI fallback or a clean diagnostic.
|
||||
|
||||
The defaults for everything else are sensible enough that a minimal app overrides only the four methods in the first group.
|
||||
The defaults for everything else are sensible enough that a minimal app overrides only the items in the first group.
|
||||
|
||||
Another way to read the trait is by API layer:
|
||||
|
||||
@@ -321,6 +326,8 @@ Downstream consumers shipping into regulated environments (EN 301 549, WCAG 2.1
|
||||
|
||||
**xdg-activation-v1 — wired in.** Both directions work: a token found in `$XDG_ACTIVATION_TOKEN` at startup is used to activate the app's own window once it maps (so an external launcher can raise an ltk window with focus), and an app that spawns children requests fresh tokens through `App::take_activation_requests` and receives them via `App::on_activation_token` to place in the child's environment.
|
||||
|
||||
**xdg-session-management-v1 — wired in (client side).** Before the first commit of a `ShellMode::Window` toplevel the runtime binds `xdg_session_manager_v1`, calls `get_session( reason, stored_id )` and `restore_toplevel( toplevel, "main" )`, so the compositor restores geometry on every launch. The session id from `created` is persisted to `$XDG_STATE_HOME/<app_id>/session.json` next to `state.bin`, the app's own `App::save_state` bytes (saved every 30 s when changed, on close and on `SIGTERM`/`SIGINT`, which the runtime now turns into a clean exit). App state is handed back through `App::restore_state` only for reason `session_restore` (`LTK_SESSION_RESTORE=1` in the environment, set by the shell) or `recover` (previous run left `clean_exit: false`) — a plain launch starts fresh, Android-style. Layer-shell and session-lock surfaces have no toplevel and skip all of it. Compositors without the global lose only the geometry half; the file-based state and reason detection still work. Multi-toplevel sessions and the compositor implementation in forge are tracked separately.
|
||||
|
||||
**Fractional scale — deferred.** `wp_fractional_scale_v1` (so 125 % / 150 % outputs render natively instead of via compositor downscale) remains tracked as upcoming protocol work.
|
||||
|
||||
**Software/GLES parity gaps — see [`docs/backends.md`](./backends.md).** The software backend renders gradients as a flat fill from the first stop, skips outer and inset shadows and backdrop blur, and hard-cuts the bottom-edge fade; `oklab` gradient interpolation falls back to linear-light on both backends. The capability matrix is the canonical per-feature table and must be updated in the same patch that closes any of these gaps.
|
||||
|
||||
Reference in New Issue
Block a user