The desktop shell is more than a window — it integrates with the host OS for tray presence, notifications, file associations, and launch behavior. All of these are independently toggleable through Settings → Desktop, and persist to the SQLite settings table.

DesktopIntegrationDTO

{
  tray_enabled:       boolean;  // default true — native tray on all three platforms
  close_to_tray:      boolean;  // default false; honored on Linux/Windows. macOS uses Wails's HideWindowOnClose instead.
  start_minimized:    boolean;  // default false — start hidden in tray, no window
  notify_on_complete: boolean;  // default true
  notify_on_error:    boolean;  // default true
  notify_on_update:   boolean;  // default true
}

The shell reads these on startup and reconfigures on change via the OnDesktopIntegrationChange hook — toggling tray off, for example, removes the tray icon without restarting the app.

System tray

The tray runs natively on all three platforms with parity:

  • macOS (v0.2.2+) — native NSStatusItem via the tray_darwin.{go,m,h} Cgo bridge. Earlier versions had a no-op stub; that has been replaced.
  • Linuxgetlantern/systray over libappindicator / StatusNotifierItem.
  • Windowsgetlantern/systray over the standard Shell_NotifyIcon API.

The tray icon shows engine state at a glance via three icon variants, embedded directly in the macOS bridge:

State Icon
Idle tray/icons/idle.png
Active tray/icons/active.png
Error tray/icons/error.png

The tray menu provides: show/hide window, pause-all, resume-all, alt-speed toggle, and quit. “Pause all” and “Resume all” call the matching api.Service methods so they’re best-effort across every torrent.

Close-to-tray

The behavior of clicking the window’s red-X close button differs by platform:

  • Linux / Windows — handled by Mosaic’s custom OnBeforeClose hook, gated on desktop.tray_enabled && desktop.close_to_tray. When both are on, close hides the window instead of quitting; the process keeps running and stays reachable through the tray.
  • macOS — handled by Wails’s built-in HideWindowOnClose: true option (added in v0.2.3). The red-X invokes [NSApp hide:nil], which hides the entire app while keeping it running. Clicking the dock icon (or the tray icon) reopens the window through macOS’s built-in unhide behavior. The close_to_tray toggle is not consulted on macOS because the platform integration is one-shot at app construction.

Start-minimized

With start_minimized: true, Mosaic launches without showing the window. The tray icon is still drawn, and clicking it surfaces the window. Useful for “start with login” setups where you don’t want the window stealing focus on every reboot.

Notifications

Three event classes fire OS-native notifications when their toggle is on:

  • Complete (notify_on_complete) — fires when a torrent transitions to completed: true.
  • Error (notify_on_error) — fires for surfaced engine/persistence errors that the user should know about.
  • Update (notify_on_update) — fires when the auto-updater finishes installing a new version (post-install confirmation).

Disabling all three keeps the desktop quiet without affecting tray presence.

File associations

Mosaic registers itself as a handler for two things:

  1. .torrent files (the application/x-bittorrent MIME type).
  2. The magnet: URL scheme.

macOS

Associations live in Info.plist (CFBundleDocumentTypes for .torrent, CFBundleURLTypes for magnet) — the Info.plist is the registrar. At runtime, Mosaic ships two complementary receivers as defense in depth:

  1. Wails’s Mac.OnFileOpen / Mac.OnUrlOpen — Wails’s AppDelegate intercepts NSAppleEventManager events and routes them to these callbacks. This is the primary path and is what fires for browser-launched magnets and double-clicked .torrent files.
  2. A custom Apple Events bridge at backend/platform/macos_appleevents.{h,m,go} — a defense-in-depth layer that registers its own NSAppleEventManager handler. In current Wails builds the AppDelegate handles the events first so this layer rarely fires, but it’s wired in case an upstream Wails change ever drops the AppDelegate hook.

Linux

The .deb and .rpm packages register the .desktop file and MIME associations during install. For the AppImage, copy a .desktop file into ~/.local/share/applications/ and run update-desktop-database.

Windows

Mosaic uses a per-user install model — registry keys are written under HKCU, not HKCR. On first launch the binary writes:

  • ProgID: HKCU\Software\Classes\MosaicTorrent (with the .torrent extension association under HKCU\Software\Classes\.torrent).
  • URL handler: HKCU\Software\Classes\magnet for the magnet: scheme.
  • Default-Apps capability: HKCU\Software\Mosaic\Capabilities — wires Mosaic into the Windows “Default apps” UI.

Self-healing: even if a previous install left stale keys, the running binary rewrites them on startup, so a fresh install on top of an old one always wins.

The Windows shell additionally forwards .torrent paths to a running instance before binding to its port. If you double-click a torrent and Mosaic is already running, the second invocation hands the path to the first and exits — no duplicate window, no port conflict.

Single-instance launch

Across all platforms, Mosaic enforces single-instance semantics:

  • macOS — Wails’s SingleInstanceLock option with the OnSecondInstanceLaunch callback. When a second invocation runs, Wails routes its argv (and any associated launch data) to the running instance’s callback rather than starting a new process. This is independent of Mac.OnFileOpenOnFileOpen handles in-process Apple Events; OnSecondInstanceLaunch handles a duplicate process launch.
  • Linux — IPC through a per-user XDG_RUNTIME_DIR socket.
  • Windows — Named-event lock with the message-only window pattern (FindWindowExW(HWND_MESSAGE, ...)).

Each platform’s bridge produces a LaunchNotice event the SPA listens for, with the second-instance args attached:

type LaunchNotice = {
  event: 'received' | 'magnet_added' | 'magnet_error' | 'torrent_added' | 'torrent_error';
  count?: number;
  args?: string[];
  id?: string;
  path?: string;
  error?: string;
};

onLaunchNotice in frontend/src/lib/bindings.ts is how the UI shows the “added 3 torrents from Finder” toast.

Dock click (macOS)

On macOS, clicking the dock icon when the window is closed or hidden re-opens (or surfaces) the window. This is wired up in the Mac shell’s bridge; on other platforms the equivalent is the tray-icon click.

Sources

If you’re poking at this for your own integration:

  • macOS file-open bridge: Mac.OnFileOpen — see Wails docs and the project’s notes
  • Windows associations + IPC: project commits tagged fix(windows): ...
  • Tray + notifications: backend/api/service.go DesktopIntegrationDTO
  • Single-instance pattern: see Wails options + each platform’s lifecycle entry point