RCN Initiative #46 · Companion Brief · ← main dashboard

The winit shell facade, specified

Requirements and traps for a framework-neutral OS-shell layer, "Tauri plugins, but for winit". Distilled from one shell-integration app built in ten Rust GUI frameworks, plus a headless-Linux probe round. Every trap below was hit in one of those builds.

distilled 2026-08-08 from the SPEC-4 round · 10 frameworks × 11 capabilities · macOS tested · Linux container probed · Windows tested 2026-08-08 on one machine (report/21) · full spec: report/19-shell-facade-spec.md

Why this layer needs an owner

the numbers

Nine of ten frameworks assemble the same five crates (tray-icon, muda, global-hotkey, rfd, arboard, plus notify-rust), and every integration re-discovers the same traps. On Linux the layer is GTK-coupled and Wayland-unserved; winit itself has no menu/tray API (winit#403, open since 2018). The protocol pieces exist in Rust. ksni implements StatusNotifierItem, ashpd covers the GlobalShortcuts portal, and DBusMenu crates exist. Nobody maintains a framework-neutral facade that puts them together.

the muda Edit-role trap was hit independently by iced, egui and xilem. ⌘V dies app-wide, silently. Four more frameworks dodged it only by prior knowledge.
100%
of tested macOS-written tray apps died at launch on Linux after compiling with zero source changes. muda panics instead of returning Err. Build-only CI passes.
≤1
bus factor of muda, tray-icon and global-hotkey. The historical author is near zero commits in 12 months; the volunteer on top of all three has no visible funding. De-GTK work has no owner.
9/10
frameworks pay a 50–100 ms polling tax. No tray/menu/hotkey channel has waker integration. Only floem wired delivery into a real wakeup.

Acceptance checklist

a facade is done when a plain winit app passes all nine

These ten implementations are the reference answers. All ten achieved every capability on macOS (zero not-achievable cells). The Linux column is what the facade must fix. The Windows column, measured 2026-08-08 on one machine and written up in report/21, starts further back. Three of the ten frameworks' tray apps never compiled, and only the tray, hotkey, toast and dark-mode cells were exercised.

CapabilityAcceptance testmacOS in corpusLinux in corpusWindows in corpus
System tray + menuicon visible; clicks delivered; live label update10/10panics or silently invisible5/7 built apps rantauri/freya/vizia never built; egui/xilem died on the hotkey panic; icon evidence: iced tray-shot only
Global hotkeychord fires while app unfocused10/10works on X11Wayland unserved0/7 registeredWin+Shift+9 already held by the OS taskbar (desktop-state-dependent); egui/xilem panic, the other five log + continue
Native menubarreal OS menubar; working Edit accelerators10/10 via trap T1impossible on plain winit (E0277)untested
Native dialogsopen/save panels without blocking the loop10/10untestedrfd XDG-portal is the candidateuntested
Clipboard text + imagepaste a screenshot into the window10/10untesteduntestedonly probe: dioxus paste_image correctly errored on an empty clipboard
File dropwindow receives real paths from the file manager10/10 (event-level)untesteduntested
Notificationbanner attributed to the appweakest celltraps T10–T12untestedposted (dioxus)notify-rust toast from a bare unsigned exe, which refutes the AUMID/shortcut risk note
Live dark modepalette follows the OS without restart10/10untesteddark theme detected (iced, floem)theme-changed events logged; no live OS toggle exercised
Close-to-tray + reopenclose hides; tray/Dock restores; process survives 0 windows10/10Dock-reopen: floem/tauri onlyuntesteduntested

Three extra acceptance rules.

The trap catalog

15 primary traps · each observed in at least one framework · full detail in report/19
Menus & the muda layer T1–T5 · the three-shape channel trap is the headline

T1 · Predefined Edit roles: dead selectors + stolen key equivalents. PredefinedMenuItem::{cut,copy,paste} dispatch Cocoa responder-chain selectors no winit view implements. The items no-op, and NSMenu consumes ⌘X/⌘C/⌘V before the focused widget sees them. Paste breaks app-wide, silently. Every fix re-injects synthetic clipboard events into one hard-coded widget.

T2 · One global MenuEvent handler slot, three failure shapes. (a) a separately-resolved muda owns a different static channel, so clicks vanish; (b) last registration wins, so dioxus's menubar events arrive as tray events; (c) floem works only because its muda 0.17 and tray-icon's muda 0.19 are separate compiled instances, which makes version divergence load-bearing for correctness right now. A facade needs per-menu closures, not a global static.

T3 · Use-after-free on first menu click (muda 0.17): raw *const MenuChild stored unretained; the idiomatic append-then-init shape reads freed memory on the first click. Menus must own their items.

T4 · Creation ordering. Main thread, after the run loop starts, with a !Send handle. Every framework paid differently (boot task, creator closure, external-loop embedding, timer tick). The API must enforce the contract, not document it.

T5 · Linux menubar on plain winit is a compile error. Menu::init_for_gtk_window requires W: IsA<gtk::Window> (E0277 retained). This is the hard boundary a DBusMenu backend removes.

Linux tray & hotkey T6–T9 · panics where it isn't silent

T6 · muda panics ("GTK has not been initialized") at the first gtk::Menu::new. macOS-written apps compile unchanged and die at launch; graceful-degradation code never runs.

T7 · Bare TrayIconBuilder::build without GTK returns Ok for an icon that can never function; with GTK but no StatusNotifier host on the bus (DBus NameHasOwner → false, captured) everything still reports success. Zero feedback either way.

T8 · tray-icon's winit path requires a parallel GTK event loop on a second thread (its own example does this). ksni removes the GTK loop entirely.

T9 · global-hotkey is X11-only (though there it worked end-to-end: registration and delivery of an xdotool-fired chord, probe retained). No Wayland portal; delivery is poll-only; Pressed and Released both arrive; observed cross-process contention behavior was inconsistent between apps, so the facade must define that contract.

Notifications T10–T12 · the least reliable helper of the round

T10 · From an unbundled binary, mac-notification-sys resolves bundle id "use_default" via LaunchServices. macOS 26 then pops a blocking "Where is use_default?" chooser while .show() returns Ok (hit by freya, vizia, xilem independently).

T11 · The helper pumps the main runloop: from inside a winit callback → panic-abort ("tried to handle event while another event is currently being handled", xilem + vizia); from a slint::Timer callback → abort ("Recursion in timer code"). The safe shape (detached thread) then silently drops the banner for unbundled binaries. egui also saw eframe stop scheduling frames after the first notification (2 of 3 runs; cause unproven) and rejected the crate. Three frameworks shipped osascript subprocesses instead.

T12 · Ok(()) proves nothing about a banner. Acceptance tests must observe display, from a bundled, identified .app. The facade must be identity-aware and fail loud from bare binaries.

Lifecycle, clipboard & delivery T13–T15 · hide/reopen leaks, the PNG round-trip, the polling tax

T13 · Hide/reopen is where frameworks leak: eframe never runs App::ui for hidden viewports (first attempt: a window that could never come back); gpui's app-level hide is silently ignored during NSMenu dismissal (300 ms settle-delay fix); 8 of 10 frameworks cannot drop the Dock icon (ActivationPolicy::Accessory unreachable); only floem and tauri expose Dock-icon reopen.

T14 · Clipboard images: arboard hands back raw RGBA and four frameworks PNG-encode it again just to display it. gpui's multi-entry clipboard with native image decode (ClipboardEntry::Image) is the reference shape.

T15 · The polling tax: no tray/menu/hotkey channel has waker integration, so the standard integration is a 50–100 ms timer pump (iced needs a non-default executor feature just to have a timer). Delivery must be callback/waker-based with a main-thread bridge. floem's ExtSendTrigger/create_ext_action is the tested reference.

What good looks like

tested reference shapes to steal

▲ gpui's focus-aware menubar

  • The cohort's only focus-aware Edit menu. MenuItem::os_action routes Cut/Copy/Paste to the focused editor's actions; accelerators derive from the keymap.
  • Zero tauri-apps crates for menubar/dialogs/clipboard. Native panels come over a oneshot channel; multi-entry clipboard decodes pasteboard images natively.
  • Caveats: the API exists off-macOS but the implementation does not; no tray/toast in core; thin re-entrancy guards.

▲ floem's closures and wakeups

  • Menu items carry action closures, so there is no MenuId bookkeeping and no global event channel. That dissolves trap T2 by construction.
  • ExtSendTrigger is the only first-class foreign-thread wakeup in the cohort, exactly the delivery mechanism T15 asks for. Plus AppEvent::Reopen.
  • Cost observed: none of it is documented outside the source.

▲ freya owns the globals

  • The tray feature registers muda/tray-icon's global handlers itself and forwards into one typed callback. The app never links its own muda copy, so the channel-splitting trap cannot happen, and creation ordering is handled by construction.
  • This is the shape a facade's framework adapters should take.

▲ slint's declarative ceiling

  • SystemTrayIcon + MenuBar as DSL elements make the shell ~80% declarative, with reactive menu labels. That's the shape to copy.
  • Gaps: no standard Edit roles, single-shot tray handle creation (silent stderr-only failure), external file drop needs an unstable feature.

Baselines to beat (same spec, LoC incl. markup): dioxus 394 · slint 447 · egui 514 · tauri 559 · floem 610 · iced 682 · vizia 720 · xilem 774 · freya 865 · gpui 1,575. Shell integration grew dependency trees by 8–112 crates per framework. A facade should get the glue closer to the dioxus/slint LoC without forcing a webview.

Linux target architecture

replace, don't wrap, the GTK paths

The probe results (retained in linux/probes/)

  • muda_on_winit gives E0277 at compile time. No winit-facing menubar attach API exists on Linux.
  • tray_without_gtk returns Ok for a non-functional icon (GTK criticals on stderr only).
  • tray_with_gtk_no_host is a silent success with no StatusNotifier host on the bus.
  • global_hotkey_x11 worked end-to-end (registration + xdotool-fired delivery).

The building blocks

  • ksni (StatusNotifierItem) removes both the parallel GTK loop and the GTK-uninitialized panic.
  • ashpd GlobalShortcuts portal adds the Wayland half global-hotkey lacks; keep the X11 grab as the proven fallback.
  • DBusMenu. The corpus never names a specific crate, so selecting and auditing one is an early facade task.
  • Make absence loud. Ship the DBus NameHasOwner check the probe used, and return Err(NoHost) instead of a silent success.

Open questions

for the effort, not the corpus
  1. Where does it live? rust-windowing (winit has wanted to shed this scope since #403), tauri-apps (owns the incumbents, concentration risk), or a neutral org. The study's funding recommendation #2 notes no dedicated public funding source exists for this work today.
  2. Facade vs upstream fixes? Several traps are one-line-ish upstream fixes (muda: return Err without GTK; retain menu items; document the Edit-role constraint). The trap catalog is the issue list, and filing it has value independent of any facade.
  3. Specify the fuzzy contracts. Hotkey cross-process contention, TCC permission inheritance, and notification identity were observed behaving inconsistently. The facade must define and test these, not inherit them.
  4. Windows. The 2026-08-08 campaign (report/21) gives the checklist its first measured Windows column, but from one machine, one GPU/driver and one desktop session, and only the tray/hotkey/toast/dark-mode cells were exercised. Dialogs, file drop, clipboard image, menubar and close-to-tray still need a dedicated Windows pass before claiming three platforms.