Profile persistence and startup profile selection
Two related quality-of-life improvements landed today around how the app decides which profile to activate at startup, and how it remembers that decision across sessions.
The problem: alphabetical roulette
Before this change, the startup profile was determined by a single line:
layer_registry.profiles().min_by_key(|p| &p.name) Whichever profile name sorted first alphabetically became the active profile on every launch. No "last used" memory, no user intent. Just alphabetical order. If you had spent the session using a specific profile, closing and reopening the app silently reset to whatever name happened to sort first.
The fix: preferences.json
A new preferences.json file lives alongside devices.json in the app
config directory. It is written every time the user explicitly activates or deactivates a profile.
{
"version": 1,
"profile_active": true,
"last_active_profile_id": "starter-right"
} On startup, the backend reads this file before initialising the engine. The selection logic is:
- If
profile_activeisfalse: start with the built-in empty profile (no mappings). The user explicitly asked for no profile. - If
profile_activeistrueandlast_active_profile_idnames a profile that still exists: activate it. - If the named profile no longer exists (deleted between sessions): fall back to alphabetically-first.
- If there are no profiles at all: use the built-in empty profile.
Persistence lives on the Rust side so the engine is initialised with the correct profile before
the frontend even loads. Doing this in localStorage and calling activate_profile from the frontend would have caused a brief flicker. The wrong
profile would be active during the window between page load and the first invoke.
Bug: deactivate didn't stick across restarts
The initial implementation had a subtle flaw. When deactivate_profile was called,
it saved last_active_profile_id: null to disk. On the next launch, this was
indistinguishable from a first-launch state where preferences.json doesn't exist
yet. In both cases the field is absent or null, and both fell through to the alphabetical
fallback. So the app always activated a profile on restart regardless of whether the user had
explicitly deactivated.
The fix is the profile_active boolean. It defaults to true (so
first-launch still picks the seeded starter profile), and is only set to false by
an explicit deactivate_profile call. The startup logic now checks this flag first
and short-circuits to the empty built-in if it is false, skipping all fallbacks.
The field uses #[serde(default = "default_true")] so any preferences.json written before this field existed (or on a fresh install before
the first deactivate) reads as true and behaves identically to before.
Device-aware profile suggestions
A related UX gap: the app already warned when a dual profile was active with only one device connected, but said nothing about the reverse. Two devices connected with only a single-hand profile active. A dismissible suggestion banner now appears on the Devices page in that case:
"You have two devices connected. Consider switching to a dual profile."
The banner includes a direct link to the Profiles page and a Dismiss button. The dismiss is
session-only and resets on the next launch so the user is reminded if the condition still holds.
Auto-switching was deliberately not implemented: changing the active profile clears the layer
stack and fires on_exit actions, which would be disruptive mid-session without
explicit user intent.