Harden persistence and improve runtime UI reliability - #323
Merged
Conversation
…ions A session file written by a newer wayscriber loaded as an empty session, and with the default backup_retention of 1 the first save rotated it to the backup slot and the second save replaced that backup - two autosaves after a downgrade and the newer-version drawings were gone for good. The runtime load now preserves a verbatim copy of the too-new file under a versioned side name (session.json.vN-preserved) before returning the empty session, so no later rotation can reach it. The first preserved copy wins, and the read-only named-candidate open keeps its no-sidecars contract by never preserving. Also close two crash windows in the save path itself: - every rename that commits a session artifact (primary, recovery, markers, Save As) now syncs the parent directory, matching what durable_io already does everywhere else, so a reported-successful save can no longer roll back after a power loss - the rotation no longer removes the old backup before renaming over it; rename replaces atomically, and the removal only created a window with no backup at all
A serde mapping error anywhere in config.toml made the overlay fall back to full defaults with nothing but a log line - theme = "drak" cost the session every customization in the file, the total-loss variant of the failure mode the keybinding work (#293) was built to eliminate. Loading now salvages per top-level entry: each section of Config is serde(default), so every entry is re-checked on its own, entries that map cleanly stay in effect, and only the failing section runs on defaults for the session. The file is never touched. A syntax error still fails the load, since there is no parsed document to salvage. Both outcomes are now reported instead of only logged: the overlay raises a warning toast (with a Settings chip) plus a desktop notification naming the unreadable sections, following the existing keybinding-problem reporting. When the salvaged section is [keybindings], source presence no longer marks the shipped defaults as authored.
…succeeds
In ClipboardAndFile mode a save failure was stashed and only surfaced if
the clipboard copy also failed. When the copy succeeded, the error was
dropped after a log line and the user got a "Screenshot Captured"
notification while the file they asked for was never written.
CaptureResult now carries the save failure alongside the partial
success. The event loop reports it as a warning ("Copied to clipboard,
but the file was not saved"), and keeps the overlay open with the same
save-to-file fallback action the clipboard-failure path already offers,
so the bytes in hand can still land on disk.
…s cap Malformed queue, control, and action entries were renamed into quarantine/ and never deleted. At 1024 entries the capacity checks turned fail-stop: ActionJournal::open and quarantine_entry bailed, Daemon::run failed, and under the shipped Restart=on-failure unit the daemon sat in a permanent 5-second crash-restart loop until the runtime directory was wiped by hand - and on systems without XDG_RUNTIME_DIR the ledger lives on persistent storage, so not even a reboot cleared it. Opening a ledger now trims each quarantine directory to a 64-entry inspection tail (newest by ctime, which the rename into quarantine stamps), and the mid-run capacity checks collect instead of killing a running daemon. Removal failures are logged and skipped so a collection hiccup cannot become another way to wedge; the fail-stop capacity checks stay in place behind the collection as a backstop.
…nt loop Three paths violated the backend's own no-blocking invariant by running heavy work on the thread that dispatches input and paints: - canvas PNG export re-rendered the full canvas at physical resolution and PNG-encoded it inline, freezing the UI for the whole export - board PDF export rendered every page of every board plus PDF encoding inline; multi-board sessions stalled for seconds - the paste fingerprint probe spawned wl-paste twice with 300 ms timeouts each, inline, inside event dispatch Exports now travel as a deferred render job that the capture worker executes on a blocking task before feeding the existing delivery pipeline, so completion, error formatting, and notifications all flow through the paths that already handle async screenshots. The snapshots were already ownership-clean; they simply move to the worker. The fingerprint probe reuses the clipboard paste controller: the worker carries the probe result back as a completion, and the paste decision continues on the main thread where the local fallback shapes live, with a stale-request guard for the window the inline call never had.
AGENTS.md declares tools/lint-and-test.sh the full local CI, but ci.yml had drifted from it: check-config-writers.py - the guard behind the "only explicit edits write config.toml" invariant - and check-process-sites.py never ran on pull requests, so the invariants they enforce were only checked on machines that remembered to run the script.
side_row_button_rects passed grid_layout its columns and total_items arguments swapped. The two call sites cap text columns at the button count, so the values are currently always equal and the swap was invisible - but the moment a row grows past the cap (a sixth boards button; the popover variant already has six), the grid would produce fewer rects than buttons, and since callers zip rects with buttons the trailing button - Delete, as it happens - would silently vanish from paint and hit testing in text mode.
The example claims every value in it is a configured default, but the guard tests only checked key presence, never values - and three drifts had already accumulated: the example and docs shipped clear_preset_1..5 as empty while the code binds Ctrl+1..5, head_at_end as false while the code defaults true, and the docs' arrow "Defaults" list still said 30.0 degrees for an angle the code sharpened to 24.0. The new test parses the example, serializes it next to Config::default(), and diffs the trees path by path. Legitimate differences are allowlisted with their reasons: env-dependent page navigation, and sections that spell out effective built-in defaults for fields whose compiled default is "unset" (boards, quick colors, the sample preset slot, toolbar order).
Review-verified deletions, each confirmed unreferenced by grep and by a
clean warning-free build of both feature sets:
- assets/icons/*.svg: leftovers from the embedded-SVG era, referenced
nowhere since the switch to procedural painters
- five toolbar_icons wrappers whose "referenced by the toolbar-gtk
frontend only" comments were false - the GTK frontend uses
draw_icon_pin/draw_icon_unpin directly
- the capture status channel: an Arc<Mutex<CaptureStatus>> plus
get_status() with zero production consumers - the UI reads
CapturePoll exclusively, and every test assertion on it duplicated
an outcome assertion the same test already made
- is_clipboard_available ("Phase 2" scaffolding with no callers),
resolve_damage_regions (only its own tests called it),
invalidate_help_overlay_cache (dead, and redundant - the cache key
already includes the style), invalidate_text_cache (exported, never
called)
Stale annotations corrected so the compiler can resume catching real
rot: pointer-constraints fields were annotated "kept for potential
future pointer lock support" while pointer lock uses them today;
manager_available, log_dir, save_snapshot, load_snapshot, and
AUTOSAVE_HISTORY_FALLBACK_DEPTH carried allows despite live callers;
pointer press named a parameter _conn and used it four times.
drag_log and color_log took an already-built string, so the ~10 format! calls per pointer-motion event in the toolbar drag path allocated on every move with logging disabled. Both now take a closure and only build the message when their debug env toggle is on. The command palette rebuilt KeybindingsConfig::default() - nine sub-configs allocating a Vec<String> per action - on every rebindability check, including the row-action tooltip lookup the renderer runs up to twice per frame while the pointer hovers the strip. The compiled-in default keymap is immutable data; it is now built once behind a OnceLock (config::keybindings::default_keybindings).
…proof deadline The 16 MiB publication test carried a 5-second timeout, which the full parallel suite can exceed when it saturates the machine - the broker then SIGKILLs the helper and the test fails with status 137. The test proves byte-count integrity, not latency, so the deadline is now deliberately generous.
Eight popup surfaces each hand-wrote the list of other surfaces their opener closes, and the lists disagreed: the radial menu closed all of them, help closed only the radial menu, and the color picker, palette, board picker, and context menu each excluded a different subset. The keyboard router's precedence chain masked most of it, but pointer-driven openers bypass that chain - help opened from a toolbar or status-chip click landed on top of an open color picker or board picker and starved it of keys. A ModalSurface registry now owns the rule: opening a surface closes every other open surface through its canonical closer, and the two deliberate exceptions are data on the enum with their reasons - the tour survives every opener except the palette (it guides the user into opening surfaces), and a context menu over the board picker stays, because the picker's page rows have their own context menus. The registry also becomes the single source for the other enumerations of the same set: modal_owns_text_input and modal_blocks_canvas_key_repeat now derive from it instead of listing surfaces by hand, and engaged_modal() exposes the precedence question directly. The board picker's two ~35-line verbatim-duplicate openers fold into one parameterized body along the way. New tests pin the previously-buggy help-over-popup paths, both coexistence exceptions, and the pairwise invariant that no opener leaves an excluded surface behind.
handle_save_requested guarded only is_saving. A Save pressed during an in-flight Reload wrote the pre-reload draft and was then judged against a document it never saw, leaving stale fields marked clean and the next save rejected by the revision guard. The guarded ConfigDocument means no data was lost, but the UI reported a state the file did not have.
action_label falls back to the literal word "Action" when an action has
no ActionMeta, and every user-facing message naming a shortcut goes
through it - conflict toasts, invalid-binding notifications,
skipped-default notices, the migration banner. Twelve configurable
selection actions had no entry, so a user who bound one of them into a
collision was told their shortcut conflicted with "Action".
Adds entries for the twelve (z-order front/back, the six nudges, and
the four edge moves) with all surface flags false, so they gain a name
without joining the command palette, help, or toolbar contracts. A new
test pins the invariant: no configurable action may be shown as
"Action".
Investigating the reported "Clear Canvas"/"Clear canvas" drift showed
it is not drift - the overlay writes shortcut names in Title Case and
the configurator in sentence case, deliberately and consistently, and
99 of 148 differ by more than case because the settings list needs
names that stand alone ("Select pen tool") where the palette does not
("Pen Tool"). No cross-crate label test is added, because the two
vocabularies are meant to differ.
render_rect normalizes negative width/height before painting, and rect_fill_hit normalizes the same way, but rect_outline_hit treated any non-positive extent as a bare point at the origin. A rectangle carrying negative extents therefore drew its outline normally and could not be clicked anywhere along it. Outline hit testing now normalizes to the painted rectangle. Only a rectangle with no extent on either axis is still treated as a point; one collapsed axis paints as a line, which the edge tests already handle.
Every failure inside ToolbarSurface::render - shm pool creation, buffer creation, wrapping the buffer in a cairo surface, creating the context, attaching the buffer - returned Ok(()) after discarding the error. The surface stayed dirty, so it retried on every frame and failed the same way, and the user saw a blank or stale toolbar with nothing in the log to explain it. Each path now returns the real error with context. Because the retry is per frame, an unconditional warning would repeat at frame rate, so the caller routes failures through report_render_failure: the first failure of a streak warns, every 300th warns again so a persistent problem stays visible, and the rest go to debug. A successful render clears the streak.
An unreadable session was backed up and logged, then reported as LoadSnapshotOutcome::Empty - indistinguishable from having no session at all. The overlay restored an empty canvas and said nothing, so the user's drawings appeared to have vanished and only the log knew their bytes had been preserved. backup_corrupt_session now reports where it put them, and the load returns EmptyAfterCorruption carrying that path. A dedicated variant rather than a field on Empty so the compiler names every site that has to decide: the fallback and candidate paths treat it as empty, and the two places a user learns anything say what happened - the overlay raises a 20-second error toast naming the backup, and opening a named session fails with the reason and the path instead of "contains no usable session data".
Page navigation and the xdg focus-loss behaviour pick their defaults from XDG_CURRENT_DESKTOP and friends at call time, so two tests that compare defaults against checked-in values were really testing where they ran. The keybinding snapshot pins the non-GNOME variant, so it failed on any GNOME or Ubuntu machine; the example-drift guard papered over the same problem by allowlisting the two page keys. Both now build their values inside a shared with_scrubbed_desktop_env helper. The drift guard scrubs both sides, because parsing the example runs serde's default functions for every key it omits - which is how the focus-loss default leaked in even after the Config::default() side was scrubbed - and the page-key allowlist entries are gone. A new test pins the GNOME branch that the snapshot cannot record. Verified by running the suite with XDG_CURRENT_DESKTOP=ubuntu:GNOME.
KeyBinding::parse rejects only an empty string and a modifier-only one, so a typo like "Ctlr+Z" parsed cleanly - as a key literally named "Ctlr+Z". Nothing ever produced that name, so the binding conflicted with nothing, reported nothing, and simply never fired. The user's undo stopped working with no way to find out why. Parsing stays permissive, because a future build may deliver names this one does not, and rejecting would silently change the meaning of a file the load never writes. Instead the key name is checked against the vocabulary the input layer actually produces - any single character, or one of NAMED_KEYS - and an unrecognized one is reported through the existing InvalidKeybinding channel with its own wording and the binding left in place. InvalidKeybinding now carries a KeybindingProblem saying whether the string was dropped or merely doubted. Near misses get a suggestion: a misspelled modifier (which the parser folds into the key, since it does not recognize it) and a one-edit typo on a named key both propose the intended spelling. A test pins NAMED_KEYS against the input layer's own key-to-name mapping, so a key the backend learns to deliver fails until the vocabulary records it.
…amily The non-empty render test enumerated the 34 render_* entry points in svg.rs, which is the newer icon family. The ~30 older proportional-style painters - zoom, session, security, history, and the chrome glyphs - had no coverage at all, so one of them silently painting nothing at 18px would have shipped. Adds a test over all 64 public draw_icon_* painters at the five sizes the chrome asks for, which is the surface callers actually use.
A build that does not recognize a key in runtime-ui.toml preserves it verbatim so a newer build still finds it. If a later build learns to manage that key, the preserved copy and the managed value collide while encoding, and the encoder treated that as fatal: the whole file became unencodable. Because the conflict lives in the in-memory wire state rather than on disk, the resulting persistence incident recurred on every retry, so no toolbar state could be saved again for the life of the install. The managed value is authoritative by construction, so it now displaces the preserved copy and logs that it did. Encoding a file is no longer a fallible operation on this axis.
The six session write sites hand-rolled the temp-write/fsync/rename dance and created their temporaries with no mode, so under a default umask the user's drawings, their backup, the recovery artifact, and every marker landed at 0644 - world-readable, while the runtime-UI store and durable_io have always used 0600 for far less sensitive data. The main save also had no cleanup: it creates its temporary before rotating the previous session into the backup slot, so every early return between those points left the file behind, temp_path stepped to .tmp1, .tmp2, and nothing ever collected the strays. The three markers and the recovery artifact are pure write-then-rename, so they now go through durable_io::write_atomic, which owns the whole sequence including 0600, the parent sync, and removing its temporary on every failure path. The main save and Save As keep their own ordering - writing the payload before rotating is what leaves the original intact when the write fails - and gain the mode plus a Drop guard that removes the temporary unless a rename defused it. Tests pin both properties, and both were confirmed to fail against the previous behaviour.
The status bar settings popover - clickable segments plus the eleven segment toggles - applied to the current run only. config.toml is an authored input that the overlay never writes, so flipping a toggle updated the effective config in memory and the next launch read the configured value back: every change the user made came back on. The toolbars solve this already. Pins, minimized state, the side pane, collapsed sections, item visibility and order, board pins, positions, and the top display mode all persist through runtime-ui.toml as runtime overrides layered over their configured seed, which keeps config.toml authored-only while still surviving a restart. Status-bar content is the same kind of state - chrome the user arranges from the overlay - so it now persists the same way. Adds two seed targets, StatusBarInteractive and StatusBarItem(item), through the whole path: the wire format (a status_bar_interactive scalar and a status_bar_items map, both readable as unknown keys by older builds, so no version bump), the seed registry, the event policy, the mutation values, and the startup restore. StatusBarItem gains a stable config_id, which is a compatibility surface: renaming one drops the user's saved choice for that segment. Because their configured values are now seeds rather than plain preferences, the four affected fields move out of the seed-neutral guard list, and the events move out of the authored-only list into a test that pins their new persistence. An end-to-end test toggles both kinds, restarts, and checks they held - and that config.toml is byte-identical throughout.
…rame filtered_commands walks the whole command registry and scores each entry, allocating a joined shortcut string and lowercasing per fuzzy comparison. It ran on every keystroke and every 55ms repeat tick, and - because the renderer asks for the row list twice per frame and each ask re-filters - also two to three times per rendered frame while the palette is open, for thousands of transient allocations a frame. The ranking now caches against everything score_command reads: the query, the recents that bias it, and a new keymap revision standing in for the shortcut labels it folds in. The revision is bumped by the two places that replace the keymap, so a rebind re-ranks rather than serving a stale list. A test exercises all three invalidation paths.
intent_for_hit and drag_intent_for_hit matched the same fifteen HitKinds and repeated every slider target and spec between them - about ninety duplicated lines whose only real differences were the two kinds that are genuinely phase-sensitive: a click has no drag meaning, and a toolbar item drag opens a gesture on press and reports movement within it after. Every new hit kind or retuned slider had to be written twice, in step. One event_for_hit now takes the phase and both entry points call it. start_drag likewise follows the enum - every kind but Click opens a drag - instead of a third hand-maintained list that had to be extended alongside the other two. Net 48 lines smaller. The existing equivalence test (slider_press_and_drag_use_same_pointer_mapping) covers the shared path; a new test pins the two phase-sensitive kinds so the shared mapper cannot quietly start treating one like the other.
…copies packaging/wayscriber.service and render_user_service_unit described the same unit and had already drifted: the packaged copy set PATH=/usr/local/bin:/usr/bin:/bin while the renderer prepends the directory the binary was installed to, so a packaged daemon and a user-installed one resolved helper tools differently. The packaged file is now the renderer's output for a /usr/bin install, pinned by a test that prints the expected contents when they diverge, so the copies cannot drift again. Since the renderer is now the single source, it also stops emitting the install directory twice when it is already one of the system directories - which is what a /usr/bin install produced before, and why the two files could not simply be compared.
The width planner and the tree builder each carried their own copy of the same compact/regular triple for chrome button size, gap, and right margin, and each recomputed the block width from them. A retuned compact chrome had to be edited in both or the width the planner reserved and the width the builder painted would disagree - and the planner is what decides whether the strip fits at all. Both now read one ChromeMetrics::for_plan, which also owns the block-width sum.
[ui] theme = "light" installs the light Theme, but only surfaces that read theme::current() ever responded - the status pill hairline, the zoom chip accents, the radial menu. Every modal popup painted the const dark tokens directly, so with light mode configured a single frame could hold a light radial menu floating over a dark command palette. The five panel backgrounds and five borders those popups share now resolve through the installed theme. Dark is byte-identical to the tokens it shipped with, because the popups deliberately differ in tint and this change is about light mode rather than a dark restyle; under a light theme each takes the theme's popover surface or hairline border while keeping its own alpha, so their differing translucency survives. Resolution takes the theme as an argument, with the accessors passing current(), because that is a process-wide OnceLock and a test cannot install one variant and then the other. Both variants are pinned: dark against the exact constants, light against the theme plus per-token alpha.
PDF page labels selected a font face and drew through Cairo's "toy" text API, which does no shaping and no font fallback. A board named in CJK, Arabic, Hebrew, or any script the configured family lacks was measured as nothing and painted as boxes or reordered glyphs - in the exported file, where the user cannot correct it. Labels now go through the same shaped-text layer the rest of the overlay uses, so they get fallback fonts and correct reordering. The local ellipsizer - the fourth in the codebase - measures through that layer too rather than the toy API, so a label is trimmed by the width that will actually be drawn instead of one the exported glyphs never had.
Five hand-maintained exclusion lists decided when a popover closes, one per popover, each spelling out the events that must not dismiss it - the Settings list alone named forty-one of the event enum's variants. Adding a control meant remembering which list it belonged in, and forgetting closed the popover out from under the pointer the first time the control was used. Each event now declares the popovers it operates inside, once, and every dismissal rule derives from that. A slice rather than one owner because some controls genuinely belong to two: the shapes picker is anchored beside the overflow flyout so their toggles spare each other, the three overflow-anchored menus spare each other's toggles and the shared scrollbar, and Open Configurator is reachable from both Session and Settings. Getting that wrong is what the first attempt at this did, and the existing dismissal tests caught it. The neighbouring `unreachable!` is downgraded too: an item drag start whose policy names no order group is metadata drift between two tables, not an impossible state, so it now refuses the drag and logs instead of taking the overlay down. A test pins the pairing it was asserting.
A single #[allow(dead_code)] on the model module covered all 13.7k lines of it, so nothing inside could ever be reported - the module is adopted incrementally by both frontends, and the allow that covered the staged shapes covered the finished ones too. Scoped to the four files that actually hold staged shapes (2.3k lines), leaving the other ~11.5k - the specs, snapshot mapping, and settings panes that are fully live - policed again. Lifting it found three functions with no callers anywhere, now deleted, and one method read only by the GTK settings pane, which had been dead in a no-default-features build without anyone being able to see it; it now carries a feature-scoped allow saying who reads it.
… checks Stopping the overlay signalled the child and then alternated try_wait with a 50ms sleep for up to two seconds. The daemon runs a single thread, so every one of those sleeps was time SIGTERM, SIGINT, and tray events sat queued and unserviced - and shutdown_after_run takes the same path, so it was latency on quit too. A pidfd becomes readable exactly when its process exits, so the wait is now a poll on the child's pidfd for the whole remaining budget: an overlay that exits promptly is observed immediately rather than up to 50ms late, and a slow one no longer costs forty wakeups. The sleep remains as the fallback for when no pidfd could be opened, which happens when the child races us to exit. This is the termination half of the finding. Readiness still spins at 10ms for up to five seconds in wait_until_ready; moving that to inotify means reworking a two-file identity handshake and is left for its own change rather than half-applied here.
Twelve overlay toggles applied to the current run only: the toolbar's icon mode, more-colors, context-aware UI, preset toasts, tool preview, and delay sliders; the status bar master switch, the board and page badges, and the always-on floating badge; the history pane's custom-step section; and the input HUD. Each reset to the configured value on the next launch, so arranging the overlay had to be redone every time. They now persist the way the toolbars and status-bar segments already do: a runtime override in runtime-ui.toml layered over the configured seed, leaving config.toml an authored input the overlay never writes. Each is a scalar in the existing V1 format, which older builds carry through as an unknown key, so no version bump. Drawing state is deliberately left alone - a thickness change is still this run's - and the classification test now says so rather than listing these among the ephemeral ones.
Hiding a section from the toolbar was a this-run-only preference: it moved the effective config the running overlay reads, never disk, so the next start brought the section back. Section visibility now persists as its own runtime-UI target rather than as an `ItemVisibility` item override, because a section row carries a whole section's visibility and has a layout-mode baseline behind it that an individual item does not. Persisting it means it must stop writing the effective config. Seeds are derived from that config, so a toggle that moved both would hand reconciliation a seed already equal to its own override and the override would be pruned as redundant at the next unrelated seed refresh -- a session load, an output change. The authored config stays the baseline the override is measured against. A restore that already agrees with the configured layout records nothing, so a section the user never touched keeps following the layout mode instead of being pinned by its own restore.
Section visibility persisted as the boolean it currently resolves to, which folds in the layout mode. That is fine only while the layout mode itself cannot move: a restored `true` means "shown under whichever mode this run ends up in", so a run that restores a different mode than the one the value was recorded under would pin every section to the old mode's baseline. Store the explicit visibility setting instead -- the same `Default` / `Shown` / `Hidden` an individual item override already persists. It says what the user chose rather than what the layout happened to show, so `Default` restores a section to following the mode instead of freezing it, and no comparison against the live baseline is needed to avoid pinning one.
Cycling the layout preset from the strip lasted only for the run: it moved the effective config, never disk, and the next start came back on the authored preset. The preset now persists as a runtime-UI override and restores ahead of the section settings, so a section the user pinned keeps its own visibility while every untouched section follows the restored preset. The restore deliberately does not re-run the preset's section defaults; the section settings apply immediately afterwards and would be overwritten by the baseline. Dropping the authored write takes the legacy mirror re-baseline with it. It existed so the config-to-items fold would not read the old preset's flags back as pinned sections on the next seed rebuild, which only mattered because the switch wrote the config those seeds derive from. The config no longer moves, so the fold has nothing to re-baseline, and with the layout preference gone no preference disturbs the seeds at all.
The click highlight and the ring the highlight tool keeps on screen were this-run-only preferences, so both came back off after a restart. They persist together as one target because they move together: picking the highlight tool switches the click highlight on as a side effect, and that is the same choice the explicit toggles make. The keyboard and the command palette reach `InputState` before the backend sees the change, so their rollback cannot be read back off the live state. They now hand in the pre-change snapshot they already took to detect the change at all, through the same `begin_toolbar_mutation_with_rollback` seam the pin toggles use, and land in the same runtime-UI target as the toolbar path. Presenter mode forces the click highlight on while it runs, so what persists is the value presenter will restore -- the user's own, not the mode's. The ring is never forced, so it persists as it stands.
The status bar, the floating board/page badge, the zoom chip and the input HUD have no toolbar control -- the keyboard and the command palette are the only way to reach them -- and none of them survived a restart. They now persist through the same runtime-UI targets the toolbar controls use. Keyboard changes land inside `InputState` before the backend sees them, so each hands in the pre-change value it already captured as the mutation's rollback, the way the pin toggles do. The snapshot that decides whether a change was the user's own now also watches focus mode. Focus hides the status bar on the way in and puts it back on the way out, and neither is the user choosing to live without a status bar; without the guard, entering focus mode would persist its own housekeeping as a preference. Drops the seed-neutrality guard along with them. It asserted that these three fields move no runtime seed, which was true only while they had no seed to move.
"Applies to this run — edit defaults in the configurator (F11)." was true when every overlay preference toggle was a current-run change. It is not true of any of them any more, so it was telling users their choice would be lost when it now survives the restart. With the last of these toggles persisted, the whole authored-preference layer has nothing left to do: no event names an effective-config field, so `preferences.rs` goes with the notice. The layer was not merely redundant. Seeds derive from the effective config, so a toggle that wrote it moved the very baseline its own durable override is measured against; the next unrelated seed refresh would find the override equal to its seed and prune it as redundant, silently undoing the persistence. The test that pinned this down now asserts the invariant directly: no overlay control writes `config.toml`, and none writes the effective config either. The presenter-mode guards move to the values they protect -- `user_click_highlight_enabled` and `user_tool_preview` -- so what persists is still the value presenter will restore rather than the one it is holding. Documents what `runtime-ui.toml` now carries and corrects the seven "Nothing — this run only" rows in the persistence table.
The `ToolbarPersistence` doc still said an overlay preference toggle is `Ephemeral` and comes back from `config.toml` on the next start, and the section-row branch still carried the `Ephemeral` fallback its own routing above had made unreachable. Says what the two variants mean now, and why the authored value must stay put for the override layered over it to survive.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This branch hardens session and configuration persistence, fixes several user-visible correctness issues, removes blocking work from the Wayland event loop, and consolidates duplicated toolbar/modal behavior.
It also adds regression coverage and CI guards for the failure modes addressed here.
Highlights
Session and configuration safety
0600, including artifacts created by older releases.Capture, daemon, and backend correctness
Runtime UI and toolbar behavior
Export, packaging, and maintenance
config.example.tomlvalues to compiled defaults.dead_codesuppressions.