Repository navigation
Conversation
|
Thanks for the PR. You can get images as well and show them there, but I can't remember why I stopped working on this a few months ago. There is some genuine restriction when running from an unpacked app or from Python outside of the Store, but I can't remember what it was. Anyway, I'll check when I have time, maybe I can find some reference on the Discord server if I wrote something about it there. |
|
Just wanted to start by saying thanks for this awesome project! It keeps getting better with every release. On the restriction, I went digging. Notification access seems to be the one permission Windows doesn't track per app for non-Store apps. Mic, camera and location all keep a list of individual exes under the consent store, which is what Settings shows per app. This one has no such list at all: So YASB never shows up under Settings > Privacy & security > Notifications, it just inherits whatever the single global toggle says. I also built the exe and ran the same checks on it and on Python, expecting a difference. There wasn't one: What did bite me was app names coming back empty in the compiled build, until I added One gap on my side: the code ignores the result of the access request, so if someone has that toggle off they'd just get an empty panel saying "no new notifications", with no idea why. I can make it say access is off instead, and the counter would keep working since it comes from WNF. Small change, happy to push it if you want it here. On images, I'd rather leave those for a separate PR. The point of this one is just getting notifications out of the Windows panel and into YASB, and images feel like a new feature on top of that. That said, if you'd rather have them in before this gets merged, I'm happy to build them here too, just say the word. |
497cb6b to
68dfecc
Compare
Added the access fix. While testing it I ran into something else: clear all was announcing an empty Action Center before checking whether the removals had actually gone through, so anything Windows refused to remove stayed on screen while the widget claimed there was nothing left. It now reads the list back and reports what is really there. On images I owe you a correction. The toast's own images aren't reachable from the listener. I sent a toast carrying |
Thanks, that last comment was exactly what I needed, the 20-per-app limit explains the +1 completely. Before the four UI points: five commits landed after you tested, so a couple of these were already fixed on my side and you were looking at the older head. Sorry about the timing. Here is where each of your points stands. Some apps do not show icons (Firefox)Fixed. Firefox is not in the Apps folder, so
The content cannot be transparentFixed, and you were right that it was impossible rather than just unstyled. A Now the viewport and the content widget both have Worth knowing for the other popups: any widget that puts a The layout is slow with 20-30 itemsFixed, but the layout was not the problem. Building 60 fully styled items offscreen costs about 32 ms, so that was never where the time went. It was the icons: They are now extracted by a Measured on 30 notifications from 6 distinct senders, cold cache: Two smaller ones went in alongside it: the popup's application-wide event filter now leaves early for anything that is not a mouse press, since it was doing the full inside-the-popup geometry check for every event of every widget while a popup is open, and the menu no longer rebuilds itself when the listener replies with a list identical to the one it is already showing. This is worth stealing for the GitHub widget if it turns out to be the same cause there, the per-item layout cost looks fine in both, so I would check what that popup does synchronously while building before touching the layout. The countI took your suggestion, as an opt-in option rather than a behaviour change: I did not try to make the number itself agree with the menu. From what you described the count is the correct side and the listener is the capped one, and there is no API that hands out the notification the listener is holding back, so The docs note is updated with the version-dependent explanation from earlier, plus the per-app cap. Clear AllAlso fixed after you tested. ImagesStill keeping those out of this PR, but they are working on a branch of mine and I will open it separately once this lands. The short version, in case it saves you time later: the listener genuinely cannot reach them, but the payload Windows stored can. The catch is that the file the payload points at belongs to the sender and is usually deleted within seconds of the banner, a browser writes the icon a website asked for into a temp file and clears it almost immediately, while the notification itself stays in the Notification Center until it is dismissed. So the picture has to be copied aside the first time it is seen or it is gone by the next refresh, which is probably the restriction you ran into. |
|
@esershnr sorry for the delay, things have been really busy on my end. I plan to dive into this next week so we can review the approach and figure out the best way forward for this feature... |
Until now the widget could only report a count: clicking it sent Win+N and handed the user off to the Windows Action Center, so the notifications themselves were never visible inside YASB. This renders them in YASB's own popup instead, the way the other popup widgets work, while keeping the Action Center one click away. - Replace the hand-off to the Windows Action Center with a popup menu listing the toasts it holds, with app icons, per-app grouping, relative timestamps and per-item dismiss - Extend the existing notification listener to read and broadcast toast contents instead of only the unread count, converting WinRT objects on its own thread and reusing its asyncio loop for refresh and removal - Add a Do Not Disturb toggle backed by the shared DndService, and a footer link that still opens the Windows Notification Center - Clicking a notification brings the sending app to the front, only the dismiss button removes it - Group senders that expose no app info under a generic header instead of leaving them untitled, since unregistered AUMIDs make the listener return E_NOTIMPL - Default callbacks.on_left is now toggle_menu, toggle_notification keeps the previous Action Center behaviour - Apply the tooltip option that the widget already accepted but never used
The listener discarded the result of the access request, so a user who had turned off "Let apps access my notifications" saw an empty menu claiming there were no new notifications. Windows grants this permission through a single global switch for apps installed outside the Store, with no per-app entry, so nothing hinted at the cause. - Check the access status before every read rather than only at startup, since the user can revoke it at any time, and broadcast it to the widgets when it changes - Skip reading the list while access is denied instead of asking for it on every refresh - Say access is turned off in the menu, with a link to the Windows settings page that holds the switch - The count on the bar is unaffected, it still comes from WNF
Clearing announced an empty Action Center before checking whether the removals had taken effect, so a notification Windows refused to remove, or one that arrived while the loop was running, stayed on screen while the widget claimed there was nothing left. Zeroing the cached total also made the WNF callback skip the next update, because it compares against that value. - Read the notifications back after the removal loop and broadcast the real remainder - Leave the count on the bar to the WNF callback rather than forcing it to zero - Document that the count Windows reports and the number of entries in the menu can differ, and that an individual removal can fail ClearNotifications() would avoid the loop but is not usable here, it fails with ERROR_NOT_FOUND for apps installed outside the Store.
Notifications from apps installed outside the Store had no icon in the menu. The shell draws icons from the Apps folder, which only holds registered apps, so anything else came back empty and the item was left blank. Windows itself falls back to the icon the app registers under its AUMID, which is how the same toast still shows a picture in the Notification Center. - Read IconUri from Software\Classes\AppUserModelId, user hive first, when the Apps folder has nothing - Trim the transparent margin before fitting it: what is registered here is often a tile asset with its own padding, which otherwise lands visibly smaller than the icons the shell hands out - Keep the shape and centre it rather than stretching it to the caller's box - Skip ms-resource:// icons, which need the package that owns them to resolve Firefox writes exactly this registration, and it is the case reported here. On this machine it takes the senders with an icon from 200 of 279 to 227.
Clearing left one notification per app behind. The listener hands out at most twenty notifications per app while the Action Center keeps one more, so removing the ones it gave us cleared the menu but not the Action Center: the leftover kept its place there and stayed in the count on the bar, which then disagreed with a menu that said there was nothing left. - Clear the notification history of each sender after removing what the listener gave us, which takes the notification it could not hand out - Clear per app rather than in one call, since ClearNotifications() fails with ERROR_NOT_FOUND for apps installed outside the Store Sending 26 toasts from one app leaves the Action Center holding 21 and the listener returning 20. Clear all now ends with both at zero instead of one still there.
A popup installs an event filter on the whole application while it is open, so every event of every widget crosses into Python and is checked against the popup geometry, even though only a mouse press can close it. Filling a popup with widgets generates thousands of those events, and each one paid for an isinstance check and two attribute lookups before being let go. Test the event type first and return before doing anything else. Rebuilding a menu of 30 notifications makes about 4,000 filter calls and they cost 39% less; the build as a whole goes from 63ms to 45ms.
…changed Opening the menu drew the list the widget already held and asked the listener for a fresh one at the same time, then rebuilt every item when the reply arrived. That reply is the same list almost every time, so opening the menu built it twice to end up with the same thing on screen, which is what made a long list feel slow to open. Compare the incoming list with the one on screen and only rebuild when it differs.
…ffer The note said the count "is not always the number of entries in the Action Center" without saying why, which reads as if the number were unreliable. It is not: it is the number Windows publishes for the shell, and it means different things on Windows 10 and 11. Say which state each version reports, and name the two cases where the count really does sit above the menu: a notification an app has past its limit, which the listener will not hand out, and a badge, which is counted but is not something the list can show.
bbbc73a to
ddc15e6
Compare
The menu shows a notification's text and the icon of the app that sent it, but not the images the toast itself brought, so a message with a photo in it looks like one without. The listener cannot hand them over: NotificationBinding exposes text elements and nothing else. Windows does keep the raw toast payload in a database of its own, and the images it draws from it are local files, so they can be read from there instead. - Read the payload from wpndatabase.db and resolve its <image> elements to file paths, one read per refresh rather than one per notification - Draw them the way the Notification Center does: an app logo override in the app icon's place, cropped to a circle when the toast asks for it, the hero image above the item and the inline images below the text - Resolve absolute paths, file://, ms-appdata:// and ms-appx:// sources, the last two against the sending package, and check that a package relative path stays inside it - Never fetch a remote image: Windows only downloads those for apps installed from the Store, and asking for one would put YASB on the network on behalf of the sender - Match a database row to a notification by id, and drop it when the sender disagrees, since the id is the only thing linking the two - Add menu.show_images and menu.image_max_height, on by default, and document both - Cache the decoded pixmaps with a bound: image paths churn where the app icons keyed by AUMID do not
Messaging apps do not ship the sender's picture with the toast, they point at it: WhatsApp sends its contact photos as an https address on its own CDN, and so the menu fell back to the WhatsApp icon for every message while the Notification Center showed the person who wrote. Windows fetches those pictures itself to draw the toast, and notes in the registry where it put each one, so the file is already on disk under a name derived from the address. - Look a remote source up in that record and use the copy Windows already downloaded - Keep YASB off the network: a picture Windows did not keep is simply not shown, since requesting one would tell the sender when the menu was opened - Re-read the record only after Windows has touched it, because it grows to thousands of entries on a machine that has been running for a while and has to be enumerated - A record can outlive the file it points at, so the path is still checked before use
The icon beside a notification was fixed at 20px, which reads small next to the three lines of text an item can carry, and a stylesheet cannot correct it: Qt draws a label's pixmap at whatever size it was built at, so width and height only grow the box around it. - Add menu.app_icon_size, between 8 and 64, following the option the other widgets with icons already expose - Default to 36, which sits better against the text and closer to what the Notification Center itself shows - Apply it to the picture a toast supplies in the icon's place as well, so an override does not change the size of the row
…hannel Every image was converted to RGBA and only then scaled down, so the expensive part of the work ran at four bytes per pixel over the full size of the original. A photo from a messaging app took 49ms to prepare that way and 11ms the other way round, which is what a menu full of pictures was spending before it could be shown. - Scale first and convert afterwards, in both the app logo and the hero and inline images - Convert up front only for palette and bilevel images, which cannot be resampled - Keep the whole of it inside the open file: an image that already fits its box is not resized at all, so its pixels are still waiting to be read A menu of 30 notifications, each from a different app and carrying its own picture, opens in 812ms instead of 1.9s.
…r own way Two toasts that carry a picture were losing it before it reached the menu. A file: url is written differently by different senders. The screenshot tool writes the separators as backslashes and percent encodes them along with the colon after the drive letter, and the standard conversion does not recognise that as a drive at all: it produced a path with a leading separator that no file matches, so the screenshot never appeared. The sender check was also too strict. A browser files a website's notifications under an identifier of its own for that site, while the listener reports them as coming from the browser, so the two disagree and every website notification was dropped as if the row belonged to someone else. They still agree on the package, which is what is now compared. Verified by sending the same image both ways: previously only the plain form arrived, now both do.
…rops its copy A website notification lost its icon after a while. The picture in the payload is a temporary file the browser writes to have the toast drawn and deletes straight afterwards, so the icon was there when the notification arrived and gone by the time the menu was opened again, leaving the browser's own icon in its place. Windows keeps its own copy for exactly this reason: a browser registers an icon per website alongside the notification handler, which is what the Notification Center goes on showing. Read that when the payload has nothing left to point at. It resolves through the package relative path we already handle, so this is one more column on the query rather than a new way of finding files.
A scroll area fills its viewport with the palette background, and setWidget turns the same fill back on for whatever is put inside it. Neither is reached by the stylesheet rule on the QScrollArea, so the list sat on an opaque slab between the popup and its own background and no amount of CSS could make it see-through.
The number on the bar and the list in the menu come from two different places and stop agreeing once an app has sent more notifications than Windows keeps for it, so a bar reading 22 can sit above a menu holding 21. max_count writes anything past it as 9+, which keeps the bar from advertising a number the menu cannot account for and from widening as the count grows. Off by default, and the tooltip and the menu header still carry the real number.
The shell hands an app icon over COM in ten to a hundred milliseconds, and this was asking for one sender after another while building the menu, on the thread that had to draw it. A menu holding half a dozen apps stalled for a third of a second on its first open, which is most of what made the popup feel slow. The icons are now extracted by a pool and dropped into the item once they are ready, and the list is asked for them as soon as it changes, so by the time anybody clicks they are usually already there. A slot that is still waiting holds its space with a transparent stand-in, so nothing shifts when the picture lands.
A sender deleting the picture it drew a toast with does not always do it in one step, and a file caught halfway is there but empty. That passed for a usable image, so the fallbacks that exist for a missing one were skipped and the item was left with no picture at all rather than the app's own icon.
The file a payload points at belongs to the sender and rarely outlives the toast banner. A browser writes the icon a website asked for into a temporary file and clears it within seconds, while the notification itself sits in the Notification Center until somebody dismisses it. Reading the payload again then found nothing, so the item quietly dropped to the sender's registered icon while Windows carried on showing the picture the website chose. The first read now takes a copy and every read after it is served from that copy. The copies are dropped as soon as their notification is, so this holds no more than the Notification Center does.
UserNotification.app_info raises for a sender whose AUMID is not registered, so those notifications arrived with no name and no icon and were collected under a generic Other header. Windows knows perfectly well who sent them: it files the toast under a handler of its own, with the name it heads the group with in the Notification Center, and the payload read for the images already carries both. They are used only where the listener gave nothing. What it does report stays untouched, because that is the sender the shell will activate on a click, while the one in the database is how Windows files it, which for a browser is one entry per website and activates nothing.
The number on the bar was the one Windows publishes for the shell, and it drifts. Dismissing a notification takes it off that count, but activating one from the Notification Center does not, so every notification the user clicked there left a unit behind: four on the bar against three in the Notification Center and three in the menu, and nothing to bring it back down short of emptying the list. It is now counted from the list the menu itself is drawn from, so the two cannot disagree. The published number is still read, for the one case where there is no list to count: with notification access turned off the listener returns nothing while Windows goes on counting. The write to that state is still what says the list has changed, but a write is now taken at its change stamp rather than at its value, so one that leaves the number where it was no longer passes for nothing having happened.
…with A browser sends every website's notifications under one AUMID of its own, so the menu collected them all under the browser: a YouTube notification was headed Microsoft Edge and carried the site's icon where the channel's picture belongs. The Notification Center heads that same notification with www.youtube.com and the site's own icon, because it groups by the sender the database files the toast under, which for a browser is one entry per website, complete with the name and icon it registered for it. The menu now groups by that sender too, and heads the section with the name and icon the database holds for it. An app registers neither, so an app is headed by the name the listener reports, exactly as before; only websites gain a header icon. The AUMID the listener reports is untouched, since that is still the one the shell activates. That frees the item beneath the header to show the picture the toast itself carries, the channel's rather than the site's. The site's icon is still what an item falls back to when the menu is not grouped and there is no header to carry it. The header icon is drawn into the header label rather than placed beside it in a layout, because a stylesheet reaches a widget and not its children: split in two, the font and colour written for .section-header would stop reaching the name. Its size is menu.section_icon_size.
Clicking a notification in the menu only raised the app it came from, which for anything that says something specific is not what the notification was for: a screenshot notification opened the Snipping Tool rather than the screenshot, and a website notification opened the browser rather than the page. Windows hands the sender the launch string of that particular toast, and the payload the menu already reads for its images carries it. A toast that asks for protocol activation names a URI, which is opened like any other link. Everything else goes to the sender's toast activator, the COM class an app registers for this call, in the registry for a plain desktop app and in the manifest for a packaged one. A sender that registered neither is still only raised, and keeps its notification. An activated notification is removed, the way Windows removes one, which also keeps the count honest. The handover is done off the GUI thread: it can mean starting an app that is not running, and the bar should not be waiting on that. Raising a window still happens on the thread that owns them. The package lookup toast images already had is now shared, since finding a packaged sender's activator means finding where it is installed.
No problem at all, there's no rush. For what it's worth, I've been running this branch daily on Windows 11 25H2 for a while now, together with feat/notifications-images on top of it, and haven't hit any issues in normal use. As I mentioned before, I haven't opened a PR for that branch yet: the plan is to sort out whatever is left on the menu side first, and once this lands, open the images branch, which is where the widget really comes into its own. If you want to try it in the meantime, it's on my fork: https://github.com/esershnr/yasb/tree/feat/notifications-images One note: I've just rebased this branch onto current main (2.0.8 dev) so you can review it against what's there now. No conflicts, and nothing in the PR itself changed. |
A toast names its images itself, and a source written as a file: URL with a host, or as a plain UNC path, was turned into a path on that host and resolved to see whether it was there. Resolving a network path is already a request to the host behind it, and one that offers it the user's credentials, while the documentation promises that YASB never asks the network for an image. Refuse a path on a network share before it is touched. What Windows downloads or keeps for a toast is always on a local drive, so nothing it shows is lost: on this machine all 48 downloaded images and every sender icon resolve as before.
…indings The toast activation module declared ShellExecuteW, AllowSetForegroundWindow and CoInitializeEx for itself, while the bindings package already carries the first two and aumid.py already initialises COM the same way for the icon extraction. Use those instead. CoCreateInstance stays local, the way dnd_api and the keyboard service declare it.
…l activation The launch string of a toast is written by its sender, and ShellExecute opens a program or a file just as readily as a URI. The shell only ever launches a URI for protocol activation, so do the same: a launch string needs a URI scheme of at least two letters, which keeps a drive letter from passing for one, and a file: URI is turned away as the path it is. A toast whose launch string is not a URI is treated like one that cannot be handed over, so its app is only brought to the front.
… missing Both lookups were cached whatever they returned, so an app installed while YASB was running stayed without an activator or an install folder until the next restart, and clicking its notifications only raised it. Remember what was found and nothing else. A missing activator costs a registry read and a manifest on a click, and a missing package is one a notification still names after it was uninstalled, so neither happens often enough to be felt.
The copy of a toast image is all that is left of the picture once the sender has cleaned up after itself, and it was deleted the first time its notification was missing from a read. A read that came back short once would have taken it for good, leaving the notification without its picture when the next read brought it back. Delete a copy once its notification has been missing from two reads in a row. A notification that has really gone is still missing the next time, which is soon, since opening the menu is a read of its own.
Hero, inline and app logo images were decoded and scaled while the menu was being built, on the thread that then had to draw it. A photo takes around ten milliseconds, so a menu full of them paid for it on the click, the same cost that had already been taken off the app icons. Decode them in the icon pool as soon as the list arrives, with the cache keys the menu looks them up by, so the menu finds them ready. One that is not ready yet is still decoded on the spot as before, so nothing is drawn differently: 256 images compared across four scale factors come out identical to the pixel, and building the menu for twelve of them goes from 15 ms on the GUI thread to 0.1 ms. Activating a notification shares the pool, and is now queued ahead of pictures nobody is waiting on.
…r's icon A website that sends a notification without a picture of its own was shown with a blank document for an icon. The listener names the browser as the sender, and the name Edge gives itself there is not one the Apps folder lists, so the shell hands back its generic file icon. The Notification Center shows no icon on such a notification at all, and heads its group with the site's own icon instead. Never fall back to the browser for a website's notification. A website is told apart by the notification database filing it as a sender of its own while the listener names the browser, which is not the case for any app. Without a picture of its own the item has no icon and the text takes the space, or, with the new show_site_icons option, the site's icon. It is off by default, and the site's icon heads its section either way.
Upstream moved app icons to IconService, Windows declarations into the bindings and the codebase to Pyright strict, so the notifications menu follows: - Take app icons from IconService instead of a loader and cache of its own - Move the registered IconUri fallback for senders the Apps folder does not list from the removed aumid_icons.py into icons/sources.py, where every AppIcon gets it - Declare INotificationActivationCallback in the shell32 bindings and create it with create_com_object, and add its header to the SDK probe - Keep the popup event filter's early return for events other than a click - Type the notification code for Pyright strict
ddc15e6 to
0200f61
Compare
|
Small change of plan. I said I'd open the images branch as a separate PR after this one, but since the review hasn't started yet, I've folded it in here so you only have to go through it once. You also mentioned images in your first comment, and honestly the menu makes a lot more sense with them. The description is updated to cover everything. Main moved quite a bit in the meantime (IconService, the bindings move, strict Pyright), so I brought the branch up to date and moved it onto those:
If you'd still rather review the menu and the images separately, let me know and I'll split them again. |
|
It's fine, the image can be in the same PR, but I want to check why these icons are messed up first. Edit: I just checked the files, and it's an image cache, not an icon cache, so I think it's fine. |
|
Awesome, thanks for the review! I've been running this for a long time on Windows 11 25H2 and haven't hit a single issue. I haven't done any extreme stress testing, but in day-to-day use it's been solid. I'd really love to see this merged so I can use the official upstream .exe with this feature. :D |
|
But I think this layout could be better, and you can get rid of most of these options. For example, |
If that's how you feel, just tell me what you'd improve and I'll update it however you see fit |
|
@esershnr You don't need There's no shared id, so you have to match the items to the listener by order (history is newest first, the listener oldest first) and check that the text is the same, but that's still far better than relying on an undocumented database. Can you switch to that and drop the sqlite part? Keep the In general there are a lot of changes here and it's hard to follow. To be fair, it all looks overcomplicated to me, and the layout has quite a few issues. I'd like to see this simplified before going further. I’ll start on a simpler version based on this one, so we can compare the two. You can test it and tell me if anything is missing. I think we can do this better anyway, and I’ll keep you as co-author since a lot of this is based on your work. |
@amnweb Thanks for taking the time to review this PR so thoroughly. Of course, go ahead with your simpler version, and I'll be glad to test it and let you know if anything is missing. That said, if your time is limited, I'm happy to apply your points to this PR myself (switch to About And thank you for keeping me as co-author, I really appreciate it. This is a repo I genuinely like, and having contributed to it means a lot to me. It will also help me while I'm looking for a job. |
|
It's fine, I just have a little more work to do to stabilize this layout, and everything has been moved to CSS. Tomorrow I'll finish up the API and draft a PR. 2026-10-23.03-33-35.mp4 |
|
@esershnr The other PR has been merged, so we can close this one. |
The widget opens a menu with the notifications Windows holds, grouped by app, instead of only showing a count. Based on amnweb#1070 by Eser Şahiner. - The list comes from UserNotificationListener. Launch, activation type, pictures, attribution and displayTimestamp come from each app's toast history, matched to the list by text since the history has no ids. - Pictures follow the toast schema: appLogoOverride (48 px, round with hint-crop="circle"), hero (364:180) and inline. The image of the Windows 8 ToastImageAndText templates is the logo. Only local files are read (file:///, ms-appx, ms-appdata local), nothing is downloaded. - A click opens protocol links, hands other toasts to the app's toast activator (registry CustomActivator or the Start menu shortcut's ToastActivatorCLSID) out of process, or brings the app forward. - Clear uses ToastNotificationHistory.Clear(appId) per app, then removes what is left with RemoveNotification. Clear(appId) is documented only for apps in the caller's own package but works across apps, and ClearNotifications() fails without package identity. - {count} shows 9+ above nine: Windows counts 21 notifications per app but hands other apps 20, so an exact count can exceed the menu. - App icons fall back to the IconUri an AUMID registers, for senders shell:AppsFolder doesn't list (Firefox, Windows system toasts). - callbacks.on_left now defaults to toggle_menu. Known limitation: while Firefox runs, it ignores an activator call for a notification from its own session and only reacts to the click Windows raises inside its process, so those clicks open nothing. Co-authored-by: Eser Şahiner <7672024+esershnr@users.noreply.github.com>












Until now the notifications widget could only show a count. Clicking it sent
Win+Nand left the rest to the Windows Notification Center, so the notifications themselves were never visible inside YASB. This shows them in a YASB popup instead, like the other popup widgets, with the images they carry, and keeps the Notification Center one click away.DndService, and a footer link to the Notification Centermenublock and styleable through.notification-menuHow it works
The list comes from
UserNotificationListener, on the sameQThreadthe widget already started for the count, so there is no second listener or event loop. Each toast is turned into a smallNotificationItemdataclass before it goes to the GUI thread, since WinRT objects belong to the thread that made them. The widget and the listener talk overEventService:WindowsNotificationsChangedgoes to the widgets,WindowsNotificationRefreshandWindowsNotificationRemovego to the listener. The widget never edits the list itself, it asks the listener and redraws from what comes back.The listener only exposes text, so the images and the launch string are read from the toast payload Windows keeps in
wpndatabase.db. Image sources are resolved to local files only (absolute paths,file://,ms-appdata://,ms-appx://against the sending package). YASB never downloads anything: a remote picture is shown only if Windows already downloaded it for the toast, and a path on a network share is refused before it is touched. The file a toast points at usually belongs to the sender and gets deleted seconds after the banner, so a copy is kept for as long as the notification is in the list.Clicking hands the toast's launch string to the sender: a URI for protocol activation, otherwise the toast activator the app registered (
INotificationActivationCallback). If the app registered neither, it is just brought to the front and keeps its notification.App icons come from
IconService. For apps that aren't in the Apps folder, like Firefox, I added a fallback to the icon they register underSoftware\Classes\AppUserModelId, which is what the Notification Center uses too. It lives inicons/sources.py, so everyAppIcongets it, not only this widget.Behaviour change
callbacks.on_leftnow defaults totoggle_menuinstead oftoggle_label.toggle_notificationstill opens the Windows Notification Center as before, and configs that seton_leftthemselves are not affected.The count on the bar now comes from the same list the menu shows. The WNF number drifts (activating a notification from the Notification Center leaves it one too high), so it is only used when notification access is off and there is no list to count.
Limitations
wpndatabase.dbis undocumented. Everything read from it is optional: if Windows changes it, the menu loses the images and the website headers, and a click opens the app instead of the notification, but the list and the count keep working.Otherheader.Testing
Running it daily on Windows 11 25H2 with WhatsApp (contact photos), Edge and Firefox website notifications, Snipping Tool screenshots, Store apps and PowerShell toasts. Checked dismiss, clear all, clicking through, the DND toggle and live updates while the menu is open.
ruff check,ruff format,pyrightandpytestpass. The SDK comparison tests don't run on my machine (they can't readcl.exeoutput on a Turkish system, same on plain main), so the check for the newINotificationActivationCallbackbinding will first run here in CI. I added its header totests/win32/probe.py.README.mdandschema.jsonare untouched.AI usage disclosure
Per the AI-generated code policy, AI was used as an assistive tool for a significant part of this change. I reviewed every line, verified the behaviour against the real Windows APIs on my machine, and can explain the changes.