Repository navigation
Conversation
MalpenZibo
left a comment
There was a problem hiding this comment.
Thanks for this!! I left some points to be addressed.
| let _ = output.send(ServiceEvent::Update(event)).await; | ||
|
|
||
| if reload_events { | ||
| break; |
There was a problem hiding this comment.
This removes the break after TrayEvent::Registered, which is what made new items work
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::{get_icon_from_name, is_safe_icon_name}; |
There was a problem hiding this comment.
This replaces the existing test module instead of extending it: the is_safe_icon_name tests (accepts_ordinary_icon_names, rejects_path_traversal_components, …) from 32ee24b "prevent path traversal via icon names" are gone. The function is still there and still used, but these tests guard a security fix, so please keep them and add the new ones next to them
| // Continuous drain loop: probe well-known names every 3s (not 30s). | ||
| // 30s is too slow for late-start — a client started after ashell | ||
| // would take up to 30s to appear. | ||
| let mut interval = tokio::time::interval(Duration::from_secs(3)); |
There was a problem hiding this comment.
Together with is_status_notifier_item, every tick now probes every name on the bus, well-known and unique: a proxy plus a Get IconName on /StatusNotifierItem for each one.
On my session that's 140 names, so around 45 D-Bus calls per second, forever, mostly to processes that answer with an error. Before, it was every 30s and only for names containing StatusNotifierItem.
For late-starting clients, it's enough to probe a name once, when it appears: NameOwnerChanged is already handled in this loop, and a single full scan at startup covers names that existed before. Then the periodic tick can stay at 30s (or go away).
| let status_str = item_proxy | ||
| .status() | ||
| .await | ||
| .unwrap_or_else(|_| "Passive".to_owned()); |
There was a problem hiding this comment.
If reading Status fails, the item becomes Passive, and is_visible now hides Passive items
There was a problem hiding this comment.
fixed
Fix tray items disappearing when the Status property can't be read
Previously, StatusNotifierItem::new() fell back to "Passive" when reading the Status property failed:
copy
let status_str = item_proxy
.status()
.await
.unwrap_or_else(|_| "Passive".to_owned());
Combined with the new is_visible() filter in the tray module (which hides Passive items per the SNI spec), this meant any client that doesn't implement the Status property — or whose read fails transiently — was silently hidden from the tray forever. A failing read is not the same as an explicit Passive reply: clients that skip Status clearly intend to be shown (that's why they register in the first place).
The fix falls back to "Active" instead, so only an explicit Passive response hides an item:
copy
let status_str = item_proxy
.status()
.await
.unwrap_or_else(|_| "Active".to_owned());
Semantics after the change:
Passive received explicitly (the client replied so) → item is hidden, per the SNI spec — unchanged.
Read fails (client doesn't implement Status) → item stays visible, no longer silently dropped from the tray.
The NewStatus signal handler already ignores read failures (it emits nothing rather than resetting the status), so no further changes were needed there.Fix tray items disappearing when the Status property can't be read
Previously, StatusNotifierItem::new() fell back to "Passive" when reading the Status property failed:
copy
let status_str = item_proxy
.status()
.await
.unwrap_or_else(|_| "Passive".to_owned());
Combined with the new is_visible() filter in the tray module (which hides Passive items per the SNI spec), this meant any client that doesn't implement the Status property — or whose read fails transiently — was silently hidden from the tray forever. A failing read is not the same as an explicit Passive reply: clients that skip Status clearly intend to be shown (that's why they register in the first place).
The fix falls back to "Active" instead, so only an explicit Passive response hides an item:
copy
let status_str = item_proxy
.status()
.await
.unwrap_or_else(|_| "Active".to_owned());
Semantics after the change:
Passive received explicitly (the client replied so) → item is hidden, per the SNI spec — unchanged.
Read fails (client doesn't implement Status) → item stays visible, no longer silently dropped from the tray.
The NewStatus signal handler already ignores read failures (it emits nothing rather than resetting the status), so no further changes were needed there.
|
|
||
| /// SNI tooltip (`org.kde.StatusNotifierItem` `ToolTip`). | ||
| /// Protocol surface; not yet surfaced in the UI. | ||
| #[allow(dead_code)] |
There was a problem hiding this comment.
Let's remove the dead_code if it's not used
| // Dedup by stable SNI `Id` first (some clients register under | ||
| // multiple names — e.g. unique-name and well-known-name — but | ||
| // share the same `Id`), then fall back to `name`-based lookup. | ||
| let existing_idx = self.data.0.iter().position(|item| { |
There was a problem hiding this comment.
Dedup by Id relies on item_id, which is one of the dead fields above. If two names really register the same item, it would be better to understand why and avoid the double registration in the watcher, rather than overwrite one item with the other here.
| ("en-US", include_str!("../i18n/en-US/ashell.ftl")), | ||
| ("fr-FR", include_str!("../i18n/fr-FR/ashell.ftl")), | ||
| ("de-DE", include_str!("../i18n/de-DE/ashell.ftl")), | ||
| ("ru-RU", include_str!("../i18n/ru-RU/ashell.ftl")), |
There was a problem hiding this comment.
Thanks for the translation but could you open a dedicated PR for this?
|
fix all issues, and rebase |
MalpenZibo
left a comment
There was a problem hiding this comment.
Thanks for addressing the previous points; most of them look good now! I left a few inline comments. In general:
- Please trim the comments: the PR adds ~180 comment lines, most of them restating the code. We keep comments for the non-obvious "why" only.
- Please give the last commit a proper message and update the PR description: it still mentions a 3s poll and context menu / secondary activate / scroll, which aren't in the code.
| Ok(owner) => owner, | ||
| Err(_) => continue, | ||
| }; | ||
| // Probe every name (well-known AND unique) concurrently so a slow |
There was a problem hiding this comment.
This still probes every name on the bus (unique ones included) on each 30s tick, forever. A single full scan at startup plus the NameOwnerChanged probe should be enough, so the periodic full scan can go.
| /// a bare path (for well-known-name registrations). We split on the | ||
| /// first `/` to recover the unique sender and dedup against the | ||
| /// items already tracked in `self.items`. | ||
| async fn sync_registered_items( |
There was a problem hiding this comment.
This is a no-op: registered_status_notifier_items() is built from self.items, and then every entry whose sender is already in self.items is skipped, which is all of them. Please remove it (and the call at line 212).
| .and_then(xdg_icons::get_icon_from_name), | ||
| // 1. `IconPixmap` (preferred) — pick the largest pixmap by pixel count. | ||
| if let Ok(icons) = item_proxy.icon_pixmap().await | ||
| && let Some(icon) = dbus::best_icon_pixmap(&icons) |
There was a problem hiding this comment.
pixmap_to_icon already picks the largest valid pixmap. best_icon_pixmap picks the largest before validation, so if that one is malformed we lose the pixmap icon entirely and fall back to the name. Please drop best_icon_pixmap and pass the full vec to pixmap_to_icon as before.
| let status = ItemStatus::from(status_str.as_str()); | ||
|
|
||
| // Attention/overlay icons: pixmap first (preferred), then icon name. | ||
| let attention_pixmap = item_proxy |
There was a problem hiding this comment.
Attention and overlay icons are read only once here and never refreshed: NewAttentionIcon / NewOverlayIcon aren't handled, so when an app switches to NeedsAttention later the icon can be stale or missing. Either handle the signals or leave attention/overlay out of this PR.
| let active_icon = if item.status == ItemStatus::NeedsAttention { | ||
| item.attention_icon | ||
| .as_ref() | ||
| .or(item.overlay_icon.as_ref()) |
There was a problem hiding this comment.
Per the spec the overlay icon is meant to be drawn on top of the main icon, not used as a replacement for it.
| @@ -222,7 +441,9 @@ impl StatusNotifierWatcher { | |||
|
|
|||
| #[zbus(property)] | |||
| fn protocol_version(&self) -> i32 { | |||
There was a problem hiding this comment.
Do you have a reference for clients checking > 0? As far as I know the KDE watcher returns 0 here.
|
|
||
| /// `GetGroupProperties` — batch read properties for a set of item ids. | ||
| /// Returns a D-Bus value (array of `{sv}` dicts), deserialized on demand. | ||
| fn get_group_properties( |
There was a problem hiding this comment.
These aren't used anywhere, please remove them.
|
|
||
| Clicking on an icon will open the corresponding application or menu. The module only appears when applications have tray icons. | ||
|
|
||
| The tray fully implements the KDE **Status Notifier Item (SNI)** protocol |
There was a problem hiding this comment.
Context menu / secondary activate / scroll aren't implemented, so I'd avoid "fully implements" here.
What
Category,Status,Id/ItemId,Title,AttentionIcon*,OverlayIcon*Passiveitems are now hidden (per spec);NeedsAttentionitems display attention/overlay iconsItemsRemovedsignal handling for dbusmenuRegisteredItemson each tick to catch clients that registered before the watcher existedbest_icon_pixmap()picks the largest pixmap;current_icon_from_proxy()falls back through pixmap → icon name → XDG themeItemStatus,best_icon_pixmap,pixmap_to_icon,split_service_name, menu helpers (is_separator,is_visible,renderable_children),is_blocklisted, XDG icon normalizationcom.canonical.dbusmenu.gitignore: added.vscode/Breaking change
Passive SNI items are now hidden from the tray (previously shown alongside Active). Matches the SNI spec