Give your app a memory and a magnifying glass
A friendly desktop app remembers useful choices and offers a clear way to inspect what the retained runtime is doing.
You will leave with a compiled storage example, a verified DevTools feature setup, and a precise list of current limits.
01
Four homes for four kinds of data
AppStorage resolves conventional XDG paths or one explicit root. Resolution validates paths but does not create directories or files.
Config
Preferences that describe how the user wants the app to behave.
State
Restorable session data such as saved top-level window snapshots.
Data
Durable application-owned content.
Cache
Disposable data that the application can rebuild.
Resolution is read-only
AppStorage::for_app uses app-specific overrides, XDG homes, conventional HOME paths, then temporary fallbacks. AppStorage::single_dir places all four categories below one root.
Save one preference and restore the window
The example writes an explicit preference document, then gives AppStorage to the application so the host can manage window snapshots.
[package]
name = "app-storage"
version = "0.1.0"
edition = "2021"
rust-version = "1.88"
publish = false
[dependencies]
ailloli_ui = "=0.1.0-beta.2"
serde_json = "1"use ailloli_ui::prelude::*;
use serde_json::json;
fn main() -> ailloli_ui::Result<()> {
let storage = AppStorage::for_app("friendly-notes").resolve()?;
let mut preferences = storage.read_preferences()?.unwrap_or_default();
preferences
.values
.entry("accent".to_owned())
.or_insert_with(|| json!("orange"));
storage.write_preferences(&preferences)?;
App::new()
.storage(storage)
.window(
Window::new("main")
.title("Friendly notes")
.size(800.0, 600.0)
.content(|| Text::new("Your window can remember its size.")),
)
.run()
}02
Automatic and explicit persistence
The convenience layer deliberately automates only the window lifecycle. Your application still decides when preferences deserve a write.
Window snapshots
App::storage reads windows.json at startup, restores a saved inner size, and writes fresh snapshots after a clean event-loop exit.
Preferences
read_preferences and write_preferences are explicit. Unknown JSON values are preserved inside a versioned document.
Same-directory replacement
Writes use a deterministic temporary file beside the destination, sync its bytes, then rename it. Concurrent writers to the same target are not coordinated, and syncing the parent directory is best effort.
Optional compatibility view
The home symlink view is disabled by default, never replaces collisions, and can leave earlier entries in place if a later creation fails.
One subtle detail
The persistent snapshot model stores size, position, maximized state, and fullscreen state. In beta.2, the convenient App path currently restores only the saved inner size.
03
Turn on the inspector deliberately
The devtools feature connects core snapshots, the inspector UI, runtime instrumentation, and the optional winit integration.
Cargo feature
Enable ailloli_ui/devtools for the retained-tree inspector. It is not part of the default feature set.
Ctrl + Shift + I
Show or hide the local inspector overlay.
Ctrl + Shift + C
Toggle element picking, then click a retained element to select it.
Escape
Leave picker mode first, then hide the overlay.
Presentation modes
The public model supports Hidden, Overlay, DockRight, and DockBottom. The host interprets the selected mode.
What the snapshot sees
Nodes, keys, bounds, layout details, selection, hover, frame identity, and derived warnings such as duplicate keys or missing layout.
Integrated filter limit
The lower-level panel model supports a text filter, but the beta.2 winit integration does not retain SetFilter yet. Do not rely on filtering in the integrated overlay.
Feature setup
The same fixture checks the ordinary inspector and the terminal-enriched variant.
[package]
name = "devtools-features"
version = "0.1.0"
edition = "2021"
rust-version = "1.88"
publish = false
[features]
default = ["devtools"]
devtools = ["ailloli_ui/devtools"]
devtools-terminal = ["ailloli_ui/devtools_terminal"]
[dependencies]
ailloli_ui = "=0.1.0-beta.2"use ailloli_ui::App;
fn main() {
let address = "127.0.0.1:9229"
.parse()
.expect("the loopback address is valid");
let _app = App::new().devtools_remote_addr(address);
#[cfg(feature = "devtools-terminal")]
let _terminal_snapshot_size = std::mem::size_of::<ailloli_ui::TerminalDebugSnapshot>();
}The storage example and both DevTools feature variants compile on Rust 1.88.
04
A loopback bridge, not a public server
App::devtools_remote_addr and AILLOLI_UI_DEVTOOLS_REMOTE can start a detached JSONL bridge for snapshots and a small command set.
Loopback only
Non-loopback addresses are refused. Keep the bridge on the same machine.
Bounded clients and lines
The server accepts at most eight concurrent clients and disconnects a client whose JSONL line exceeds 64 KiB.
Small command surface
Remote clients can select, hover, change the presentation mode, or ping. They do not receive arbitrary application execution.
Debug data is still data
The bridge provides no authentication or encryption. Loopback is its safety boundary, so enable it only for a local debugging session and treat snapshots as sensitive debug data.
05
Terminal inspection is a second opt-in
devtools_terminal adds bounded terminal snapshots and stable cell identities. It extends devtools and remains separate from PTY process ownership.
Try it yourself
Exercise: inspect one real widget
Continue with the small app from Chapter 10 so the inspector has a real retained tree to explore.
- 1In that app, enable the devtools feature, run it, and open the overlay with Ctrl+Shift+I.
- 2Enable the picker with Ctrl+Shift+C, then select a button in your app.
Use the local overlay first. The remote loopback bridge is not required for this exercise.
The inspector shows the selected button and its bounds, with no remote client running.