stddlg: tray icon primitive - a pumped tray_* API with Linux (StatusNotifierItem over a dlopened libdbus), macOS and Windows backends - #3952
Conversation
…, 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>
There was a problem hiding this comment.
🟡 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 instddlg, including icon/tooltip/menu, polling events, notifications, and teardown. - Add platform backends for Linux/macOS/Windows with a stub fallback when unavailable.
- Add
tests/stddlgtray tests/fixture and wirestddlginto 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.
…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>
There was a problem hiding this comment.
🟡 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
… 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>
There was a problem hiding this comment.
🔵 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
Why. The watchdog needs a place people click to see the supervised program's state and open its web page, and
stddlghad no tray primitive.What changes.
stddlggains a pumped tray API: create, RGBA8 icon, tooltip, menu,tray_pollhanding eachTrayEventto a block, notify, destroy.DAS_STDDLG_GTKoption (off); Linux CI apt lists gain libdbus-1-dev.tests/stddlgdrives the Linux item over a private dbus-daemon; the macOS and Windows arms skip loudly elsewhere.Observable behavior.
tray_poll.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
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).linux_armper-PR cells install no packages, so their stddlg carries the stub backend; the Linux test arm skips there with the reason.Claims - stated, not tested
Not done
linux_armcells install no packages at all, so the tray test skips there.dbusruntime package on the CI runner so the Linux arms run there (today they skip when the daemon is absent).