Skip to content

Linux tray contract: TrayIcon over StatusNotifierItem + dbusmenu - #130

Merged
turinglambdaai merged 1 commit into
mainfrom
feat/linux-tray-icon
Oct 3, 2026
Merged

turinglambdaai merged 1 commit into
mainfrom
feat/linux-tray-icon

Conversation

@turinglambdaai

Copy link
Copy Markdown
Owner

Closes #118.

What

rivet::system::TrayIcon — the Linux counterpart of the Windows Shell_NotifyIconW adapter (same class name for symmetry): an explicit opt-in that hosts an org.kde.StatusNotifierItem plus a com.canonical.dbusmenu menu over the session bus through GDBus (already a dependency of every Linux host build).

  • Opt-in stays an application decision, per the rationale in the old header comment: TrayIcon::available() (a GetNameOwner probe on org.kde.StatusNotifierWatcher) and the new "tray" entry in Capabilities() report a reachable watcher. Without one, the constructor fails clearly — same contract as Notifications on a bus-less session (verified: isolated dbus-run-session → "tray is unavailable: no StatusNotifierItem watcher is running").
  • Menu: flat list of TrayMenuItem — labelled items with enabled + click callback, separators; set_menu replaces and bumps the dbusmenu layout revision (LayoutUpdated). Watcher-facing surface implements the methods clients actually call (GetLayout, GetGroupProperties, GetProperty, Event, EventGroup, AboutToShow, AboutToShowGroup, dbusmenu v3), pinned against the Ubuntu AppIndicator extension's client code.
  • In-place updates: set_icon / set_tooltip emit NewIcon / NewToolTip; ToolTip serializes KDbusToolTipStruct.
  • Watcher restarts: a NameOwnerChanged watch re-registers the item whenever the watcher reappears, so GNOME Shell restarts and AppIndicator extension reloads do not strand a running process without its icon.
  • Threading: dispatch (property reads, method calls, click callbacks) lands on the thread-default main context the icon was constructed on — for GTK hosts, the main-loop thread. Documented on the class.
  • Left-click Activate reaches an optional activation handler (KDE; the GNOME extension opens the menu instead, matching upstream SNI behavior).
  • Integration self-check exercises the tray when the capability is present and skips otherwise (headless-CI safe); docs/system-services.md and the CHANGELOG updated.

Verification (real desktop, not just compile)

  • -Wall -Wextra clean compile of system_services.cpp and syntax-check of the integration self-check.
  • A live driver on a real Ubuntu 26.04 / GNOME 50 session (ubuntu-appindicators as the watcher):
    • registration shows up in RegisteredStatusNotifierItems (icon visible in the top bar);
    • GetLayout returns the full tree (root + items with correct labels/enabled/separator properties);
    • a delivered Event "clicked" fires the menu callback on the main loop; a click on a disabled item does not;
    • set_icon mid-run flips the icon and subsequent property reads see the new name.
  • The watcherless path in an isolated D-Bus session fails clearly instead of crashing.

Deliberately out of scope (follow-ups)

Bug-for-bug note: the two @a{sv}-style format annotations in the GVariant serialization were caught by the live run (bare array positions consumed a pre-built GVariant* as a builder and aborted under the watcher's first layout fetch) — exactly why the verification is a real session, not a mock.

The Linux adapter deliberately omitted tray hosting because desktop
support is compositor-dependent; that left downstream hosts without any
system-tray surface (Windows has Shell_NotifyIconW), and close-to-tray
apps like SyncPilot lose their only persistent entry point.

Add an explicit opt-in rivet::system::TrayIcon: org.kde.StatusNotifierItem
plus a com.canonical.dbusmenu menu over the session bus (GDBus, already a
dependency). TrayIcon::available() and the new 'tray' capability report a
reachable watcher, so tray presence remains an application decision; on a
watcherless session the constructor fails clearly. Icon, tooltip and the
flat menu (labelled items, separators, enabled state) update in place
through NewIcon/NewToolTip/LayoutUpdated; left-click Activate reaches an
optional activation handler; the item re-registers whenever the watcher
reappears, so GNOME Shell restarts and extension reloads do not strand a
running process without its icon. Dispatch (property reads, method calls,
clicks) lands on the thread-default main context the icon was constructed
on — for GTK hosts, the main-loop thread.

Verified end to end against the live GNOME StatusNotifierWatcher:
registration appears in RegisteredStatusNotifierItems, GetLayout returns
the full tree, a delivered click Event fires the menu callback, set_icon
flips the icon mid-run; an isolated dbus-run-session without a watcher
fails clearly. The integration self-check exercises the tray when the
capability is present and skips otherwise.
@turinglambdaai
turinglambdaai merged commit f37908f into main Oct 3, 2026
14 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Linux: tray / StatusNotifierItem contract (tracking the documented honest gap)

1 participant