Skip to content

stddlg: tray icon primitive - a pumped tray_* API with Linux (StatusNotifierItem over a dlopened libdbus), macOS and Windows backends - #3952

Merged
borisbat merged 13 commits into
masterfrom
bbatkin/tray-icon
Sep 6, 2026
Merged

stddlg: tray icon primitive - a pumped tray_* API with Linux (StatusNotifierItem over a dlopened libdbus), macOS and Windows backends#3952
borisbat merged 13 commits into
masterfrom
bbatkin/tray-icon

Conversation

@borisbat

@borisbat borisbat commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Why. The watchdog needs a place people click to see the supervised program's state and open its web page, and stddlg had no tray primitive.

What changes.

  • stddlg gains a pumped tray API: create, RGBA8 icon, tooltip, menu, tray_poll handing each TrayEvent to a block, notify, destroy.
  • Linux: a StatusNotifierItem plus dbusmenu over a dlopened libdbus-1, so the static watchdog stays dependency-free and a headless box reports no tray.
  • macOS: an NSStatusItem with an NSMenu on right click.
  • Windows: Shell_NotifyIcon on a hidden window, a popup menu, a balloon.
  • GTK dialogs become the DAS_STDDLG_GTK option (off); Linux CI apt lists gain libdbus-1-dev.
  • tests/stddlg drives the Linux item over a private dbus-daemon; the macOS and Windows arms skip loudly elsewhere.
  • The module gets its stdlib doc page.

Observable behavior.

  • A daslang program on a desktop -> a tray icon with a menu, click and menu events through tray_poll.
  • Linux lanes without libdbus-1-dev -> the stub backend, and the test skips with the reason.
  • A non-positive or duplicate menu id, or a mismatched icon size -> a panic naming the value.

Where to look. dasStdDlgTrayLinux.cpp (D-Bus marshalling, the dbusmenu layout), dasStdDlgTray.cpp (the single instance, replaying state set before create), tests/stddlg/test_tray.das.

Validation, claims, ledger

Validation

  • Real clicks on all three platforms: Linux in an XFCE panel (Debian 12 VM), macOS 26 on an M5, Windows on zen2 - icon from pixels, notification, left click event, right-click menu, menu item event.
  • The Linux test arms ran 12/12 in the Debian VM with DAS_STDDLG_GTK=ON (this Mac has no dbus-daemon, so they skip here); the macOS arm ran here with accessibility granted; the Windows arms ran on zen2 over ssh (session 0 -> reports unavailable and skips, by design; a person at the box ran the probe interactively).
  • The Windows module built with MSVC 19.51 (Ninja) with no warnings from the new files.
  • The linux_arm per-PR cells install no packages, so their stddlg carries the stub backend; the Linux test arm skips there with the reason.
  • Two external review rounds plus an in-house round; every accepted finding is in the fix commit. Rejected on evidence: "link ApplicationServices on macOS" (the module resolves CGSessionCopyCurrentDictionary through the AppKit link and runs), "tray_available must probe the watcher on Linux" (by design it says the item can be exported; a panel may start later, the doc says so), "performClick: re-enters the action on macOS" (a compiled probe showed it pops the attached menu).

Claims - stated, not tested

  • Watcher re-registration after a panel restart (NameOwnerChanged on org.kde.StatusNotifierWatcher) is implemented but not exercised by restarting a panel; a break shows as the icon not returning after the panel restarts.
  • Windows: the legacy (non-version-4) callback decoding, the TaskbarCreated re-add and the elevated-process message filter are implemented from the documented shell behavior, not exercised.
  • macOS: the double-click event and the stale-event guard are implemented from a compiled AppKit probe of the handler, not from a manual double click.

Not done

  • Two suggestions from the workflows checklist audit, recorded rather than applied: widen that checklist's "per-PR check" definition to cover what a lane installs, and add a package-list parity rule across the Linux lanes. The concrete case behind them: the linux_arm cells install no packages at all, so the tray test skips there.
  • Wiring the tray into the watchdog (status menu, open the web page, notifications through the tray, the module's static twin linked into bin/watchdog) is the next arc.
  • dbusmenu icons and submenus; dark-mode icon variants on Windows; a dbus runtime package on the CI runner so the Linux arms run there (today they skip when the daemon is absent).

borisbat and others added 11 commits September 6, 2026 08:40
…, tooltip, menu, poll, notify, destroy) with a Linux StatusNotifierItem + dbusmenu backend over a dlopened libdbus-1, so the static watchdog keeps its zero-dependency deploy and a headless box reports no tray; the GTK dialog backend now autodetects gtk+-3.0 instead of the hardcoded LINUX_GTK off; tests/stddlg drives the item over a private dbus-daemon with dbus-send and busctl (EventGroup is what XFCE sends); the Linux CI apt lists gain libdbus-1-dev; plans/tray_icon.md pins the four cross-platform decisions

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…sory-policy NSApplication pumped from tray_poll (left click reports click with top-left screen coordinates, right click reports right_click and pops the NSMenu, osascript posts the notification), and Shell_NotifyIcon on a hidden window with NOTIFYICON_VERSION_4 callbacks, TrackPopupMenu on WM_CONTEXTMENU, a balloon for notify and a TaskbarCreated re-add; tests/stddlg gains a macOS arm that clicks the status item through System Events and skips where accessibility is denied

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…r I/O beside the clipboard (standard dialogs and the tray icon as two groups), every function, the TrayEvent annotation and the TrayEventKind enumeration carry handmade descriptions, and module-stddlg.rst shows the pumped tray loop

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…or ssh session reports unavailable and skips, an interactive desktop reaches ready

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…end - the per-PR linux_arm cells install no libdbus-1-dev, so their stddlg carries the stub while the runner still has dbus-daemon and dbus-send

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…dbus-1 headers being absent (present headers with an unavailable child is a failure), the macOS arm asks System Events whether accessibility is on before treating a click error as a skip, the Windows arm treats an unavailable child in a session with SESSIONNAME as a failure; the fixture exercises tray_set_tooltip, a second tray_create and tray_menu_clear over a committed menu, and a pure-daslang test covers the tray_set_icon size rejections; skills/daslang: try/recover continues after a panic in both tiers (probe-verified), the skill claimed otherwise

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tooltip or menu set before tray_create, a second create replaces the tooltip, menu ids are positive and unique (a panic otherwise), icon sides are capped at 1024, tray_poll declares its external effect and TrayEventKind gets its AOT cast; Linux: NameOwnerChanged is read typed and only from the bus, handlers act on method calls only, replies flush, the watcher registration is a no-reply call (no blocking round-trip in the filter), a lost bus stops the pump, the filter is removed on teardown, the bus name carries a per-process instance counter, properties are one table for Get and GetAll, an interface mismatch is answered, layout ids carry the entry ids and recursionDepth 0 answers the root alone, a middle click reports nothing; Windows: a failed NIM_SETVERSION falls back to the legacy callback layout, TaskbarCreated passes the message filter for elevated processes, a menu cannot be re-entered while open, the window's user data is cleared before DestroyWindow, the old icon is destroyed after the shell has the new one, every modify keeps the tooltip flags, the icon mask is zeroed; macOS: clickCount and modifiers are read only from mouse events, every entry point runs under an autorelease pool, the activation policy changes only for a prohibited (unbundled) process, the destructor detaches the target from the button and the menu items, a nil button fails create, notify waits for osascript, the icon rep is tagged sRGB at 18 points; GTK dialogs are an explicit DAS_STDDLG_GTK option (default off) linked privately, dbus detection is Linux-only, the AOT suite row is guarded on DAS_STDDLG_DISABLED and the fixture compiles under AOT; the tests drive the menu by entry id and pin the separator id and the shallow layout; the docs state the contracts (double click sequence, coordinate units, notify meaning, one owning thread)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… tray_menu_add's handmade doc follows the signature's new hash

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ile for a type with values or fields carries one description line then one line per value or field in declaration order, since the generator pairs lines by position and a reordered line ships wrong text on a green build; skills/daslang: the try/recover bullet says what recover exposes (this_context().last_exception), that a function with try/recover runs interpreted under the JIT, and that an uncaught error unwinds to the CLI or the embedding host

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 6, 2026 16:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are confirmed correctness issues in the Windows backend resource cleanup and in updated try/recover JIT behavior documentation that should be fixed before merging.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a cross-platform tray icon primitive to stddlg with a pumped event API and platform backends (Linux StatusNotifierItem/dbusmenu over dlopened libdbus-1, macOS NSStatusItem, Windows Shell_NotifyIcon), plus tests, CI package wiring, and stdlib documentation generation.

Changes:

  • Implement tray_* API surface in stddlg, including icon/tooltip/menu, polling events, notifications, and teardown.
  • Add platform backends for Linux/macOS/Windows with a stub fallback when unavailable.
  • Add tests/stddlg tray tests/fixture and wire stddlg into docs generation + CI apt deps.
File summaries
File Description
tests/stddlg/test_tray.das End-to-end tray tests (Linux private dbus, macOS System Events click, Windows availability smoke) + icon validation via try/recover.
tests/stddlg/_fixture_tray_child.das Child fixture process that creates a tray icon/menu, prints readiness/events, and exits on Quit.
tests/aot/CMakeLists.txt Registers stddlg as an irregular AOT module entry.
tests/.das_test Enables visiting/running tests/stddlg only when stddlg module is present.
skills/daslang/SKILL.md Updates language error-handling documentation for panic/try/recover semantics.
plans/tray_icon.md Design plan and API/backends overview for the new tray primitive.
modules/dasStdDlg/src/dasStdDlgTrayWin.cpp Windows tray backend (hidden window + Shell_NotifyIcon + popup menu + balloon).
modules/dasStdDlg/src/dasStdDlgTrayOsx.mm macOS tray backend (NSStatusItem + NSMenu + osascript notify + event pumping).
modules/dasStdDlg/src/dasStdDlgTrayNotImplemented.cpp Stub backend reporting tray unavailable.
modules/dasStdDlg/src/dasStdDlgTrayLinux.cpp Linux tray backend using StatusNotifierItem + dbusmenu over dlopened libdbus-1.
modules/dasStdDlg/src/dasStdDlgTray.h Shared tray types (TrayEvent/TrayEventKind/menu entries) and exported API declarations.
modules/dasStdDlg/src/dasStdDlgTray.cpp Shared singleton tray state, validation, menu commit semantics, polling dispatch into blocks.
modules/dasStdDlg/src/dasStdDlg.cpp Binds tray API + TrayEventKind/TrayEvent into the stddlg module and AOT requirements.
modules/dasStdDlg/CMakeLists.txt Adds DAS_STDDLG_GTK option and Linux libdbus detection; wires per-platform tray sources + dl linkage.
doc/source/stdlib/sec_io.rst Includes generated stddlg stdlib page in the IO section.
doc/source/stdlib/handmade/structure_annotation-stddlg-TrayEvent.rst Handmade field descriptions for TrayEvent.
doc/source/stdlib/handmade/REVIEW.md Adds checklist contract for handmade stdlib doc files in this folder.
doc/source/stdlib/handmade/module-stddlg.rst Handmade module overview and example usage for dialogs + tray API.
doc/source/stdlib/handmade/function-stddlg-tray_set_tooltip-0xf092482c835c1d2c.rst Handmade description for tray_set_tooltip.
doc/source/stdlib/handmade/function-stddlg-tray_set_icon-0xe126b52a4b7c1211.rst Handmade description for tray_set_icon.
doc/source/stdlib/handmade/function-stddlg-tray_poll-0x1c7677f5cea511f8.rst Handmade description for tray_poll.
doc/source/stdlib/handmade/function-stddlg-tray_notify-0x5c787075552c88c2.rst Handmade description for tray_notify.
doc/source/stdlib/handmade/function-stddlg-tray_menu_commit-0x43055d8cfdf8bcd4.rst Handmade description for tray_menu_commit.
doc/source/stdlib/handmade/function-stddlg-tray_menu_clear-0x95f1589b262e9f0d.rst Handmade description for tray_menu_clear.
doc/source/stdlib/handmade/function-stddlg-tray_menu_add-0x217904f74519bac8.rst Handmade description for tray_menu_add.
doc/source/stdlib/handmade/function-stddlg-tray_menu_add_separator-0xbcba6f378554c9fd.rst Handmade description for tray_menu_add_separator.
doc/source/stdlib/handmade/function-stddlg-tray_destroy-0x93694153a9ba56ff.rst Handmade description for tray_destroy.
doc/source/stdlib/handmade/function-stddlg-tray_create-0xaab57b2f09568baf.rst Handmade description for tray_create.
doc/source/stdlib/handmade/function-stddlg-tray_available-0xb5aaad0e79b612d7.rst Handmade description for tray_available.
doc/source/stdlib/handmade/function-stddlg-get_dlg_save_file-0x98b88f96c0333013.rst Handmade description for get_dlg_save_file.
doc/source/stdlib/handmade/function-stddlg-get_dlg_open_file-0x8a861e0382d97a4.rst Handmade description for get_dlg_open_file.
doc/source/stdlib/handmade/function-stddlg-get_dlg_ok_from_user-0xbab6f6592464a334.rst Handmade description for get_dlg_ok_from_user.
doc/source/stdlib/handmade/function-stddlg-get_dlg_ok_cancel_from_user-0xcb485d243dbe2e54.rst Handmade description for get_dlg_ok_cancel_from_user.
doc/source/stdlib/handmade/function-stddlg-dlg_init-0x49221f9edcb2acf2.rst Handmade description for dlg_init.
doc/source/stdlib/handmade/enumeration-stddlg-TrayEventKind.rst Handmade value descriptions for TrayEventKind.
doc/reflections/das2rst.das Adds stddlg module documentation generation and grouping for tray vs dialogs.
.github/workflows/release.yml Installs libdbus-1-dev in the workflow apt deps.
.github/workflows/nightly_daspkg_index.yml Installs libdbus-1-dev in the workflow apt deps.
.github/workflows/fatman.yml Installs libdbus-1-dev in the workflow apt deps.
.github/workflows/extended_checks.yml Installs libdbus-1-dev in the workflow apt deps.
.github/workflows/build.yml Installs libdbus-1-dev in the workflow apt deps (multiple job sections).
Review details
  • Files reviewed: 41/41 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread modules/dasStdDlg/src/dasStdDlgTrayWin.cpp
Comment thread skills/daslang/SKILL.md Outdated
…and a return inside the try block is a compile error there - the bullet claimed the function stays interpreted

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 6, 2026 17:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

tests/stddlg/_fixture_tray_child.das contains unescaped nested quotes in interpolated strings (e.g., the tray_create / tray_notify lines), which will break compilation of the new test fixture.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

tests/stddlg/_fixture_tray_child.das:35

  • This string interpolation contains unescaped double quotes inside the outer double-quoted string, which will break parsing/compilation.
  • Files reviewed: 41/41 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread tests/stddlg/_fixture_tray_child.das
… shell refuses balloons while still hosting the icon), and the fio close-kills arm retries its temp-dir removal for a bounded time - on Windows a just-terminated child can hold a handle in that directory for a moment, which reddened the 32-bit lane on a run master had passed

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 6, 2026 17:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The new tray tests assume spawn_process always succeeds (null handles can crash the suite) and Linux tray_available can report true without verifying the session bus is actually reachable.

Review details

Suppressed comments (5)

Previously missed (2) — in code that hasn't changed since the last review.

modules/dasStdDlg/src/dasStdDlgTrayLinux.cpp:783

  • TrayPlatformAvailable on Linux only checks that DBUS_SESSION_BUS_ADDRESS is set and libdbus can be dlopened. This can return true even if the address is stale/unreachable (so tray_available says “available” but tray_create immediately fails to connect). Consider probing the session bus connection here (without triggering libdbus autolaunch) so availability matches actual reachability.
    tests/stddlg/test_tray.das:61
  • start_bus assumes spawn_process always succeeds; if it returns null, the subsequent process_drain/process_poll will error and the test will crash instead of reporting a clear failure/skip. Add a null-handle check right after spawn_process and fail early with a diagnostic.

This issue also appears in the following locations of the same file:

  • line 78
  • line 129
  • line 170

tests/stddlg/test_tray.das:81

  • start_child assumes spawn_process always succeeds; if it returns null, wait_for_line/drain_child will attempt to drain a null process handle. Add a null-handle check immediately after spawning the child so the test reports a clear failure instead of crashing.
    tests/stddlg/test_tray.das:134
  • dbus_send doesn’t handle spawn_process failure: if it returns null, process_poll/process_drain/close_process will operate on a null handle. Guard the handle and emit a clear marker into the captured output so the caller’s assertions fail with actionable context.
    tests/stddlg/test_tray.das:173
  • apple_script assumes osascript spawn always succeeds; if spawn_process returns null, process_wait/process_drain/close_process will act on a null handle. Add a null check and return a diagnostic string so the test can skip/fail with the captured reason.
  • Files reviewed: 42/42 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@borisbat
borisbat merged commit de719af into master Sep 6, 2026
32 checks passed
@borisbat
borisbat deleted the bbatkin/tray-icon branch September 6, 2026 18:11
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.

2 participants