* Persist on-screen notification popups across shell restarts
Mirror every popup to its own file under
~/.local/state/omarchy/notifications/ for exactly as long as it is on
screen: written when the toast appears, deleted when it expires, is
dismissed, is acted upon, or is replaced via freedesktop replaces_id.
On startup the directory is read back and still-valid popups re-shown,
so toasts survive the restart omarchy-update performs — critical
alerts, which never expire, always make it across.
Restored popups keep ids from the previous server generation, so the
replaces_id cleanup tracks them separately instead of mistaking a
fresh notification's reused id for a replacement, and the startup
restore only discards a persisted file when a live row with a
different timestamp has superseded it. Files are read back with awk
so a torn write can't glue itself onto the next file and take a valid
popup down with it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Close the remaining cross-generation id collisions in popup persistence
Notification ids restart from 1 with every server process, so an id
alone never identifies a notification across a shell restart. The
first round of fixes guarded row removal, but review (and a live
repro) showed the same collision biting everywhere else an id was
used on its own:
- Dismissing or clicking a restored toast resolved liveRefs by id and
could dismiss, or fire the action of, an unrelated fresh
notification, and archive its pending row. Restored rows now never
resolve to a live object, and pending rows are matched by id plus
timestamp.
- parsePopupFiles deduped files by id, so a fresh notification reusing
a restored critical alert's id got that alert's file deleted as a
"stale duplicate" on the next restore. Files are never deduped now:
each one is a popup that was on screen, and the rare genuine
leftover from a crash re-shows once and cleans itself up.
- The restore only skips an entry when a live row matches both id and
timestamp (it is that entry); an id-only match shows both toasts
rather than guessing which one to drop.
- A same-millisecond replaces_id update shares its predecessor's
filename; the replacement's file is no longer deleted alongside the
replaced row.
- A restored popup's reset lifetime is persisted as an absolute
deadline, so a second restart judges it by the clock that actually
governs its display instead of dropping it while still on screen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Replaying an empty history did nothing at all on screen, which reads as
a dead keybinding rather than an empty log.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Use Region { item: popupColumn } instead of manually mirroring the
column's x/y/width/height. It tracks the item's mapped geometry directly,
so it stays correct without assuming the column shares the window's
coordinate space.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The popups used one Wayland layer surface per output sized to their content
(implicitWidth/Height bound to the toast column), so every time a toast was
added or removed the surface resized. During that resize the compositor
briefly scaled a stale buffer, stretching or squishing the cards mid-
transition -- worst with only one or two toasts, where each resize is a
larger relative change.
Make the surface full-screen and fixed, like the OSD overlay, so adding or
removing a toast only changes the content inside it and the surface never
resizes. Mask the input region to the toast column so the otherwise-
invisible full-screen overlay stays click-through.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Model rows stored the live Notification object in a ref role. When the
server destroyed the notification (sender close, DND untrack, dismiss)
the role became a dangling C++ pointer, and the next read segfaulted in
QQmlListModel::data — typically when replaying history over IPC. Track
live notifications in a JS map keyed by originalId instead, cleaned up
on close and untrack, where a stale reference degrades to a catchable
error instead of a crash.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Hyprland gaps_out value (10px out of the box) reads well as a
window-to-window gap but felt cavernous when used as the screen-edge
margin for panels and notifications. Halve it at the source — panels
that anchor via PopupCard.margin, the launcher / clipboard / emojis /
polkit / menu / settings card-width clamps, and the notification popup
margins all read Style.gapsOut, so one tweak moves them in lockstep.
Revert the per-call /2 in the notifications service, since
Style.gapsOut now already encodes the smaller value.
Sit notifications closer to the top and right edges than the default
gapsOut. The bar-height clearance still applies to the top — only the
free gap portion is halved.