Desktop Integration
Tray, notifications, close-to-tray, file associations, magnet handler, single-instance launch, dock-click.
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
NSStatusItemvia thetray_darwin.{go,m,h}Cgo bridge. Earlier versions had a no-op stub; that has been replaced. - Linux —
getlantern/systrayoverlibappindicator/StatusNotifierItem. - Windows —
getlantern/systrayover 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
OnBeforeClosehook, gated ondesktop.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: trueoption (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. Theclose_to_traytoggle 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 tocompleted: 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:
.torrentfiles (theapplication/x-bittorrentMIME type).- 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:
- 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.torrentfiles. - 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.torrentextension association underHKCU\Software\Classes\.torrent). - URL handler:
HKCU\Software\Classes\magnetfor themagnet: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
SingleInstanceLockoption with theOnSecondInstanceLaunchcallback. 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 ofMac.OnFileOpen—OnFileOpenhandles in-process Apple Events;OnSecondInstanceLaunchhandles a duplicate process launch. - Linux — IPC through a per-user
XDG_RUNTIME_DIRsocket. - 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.goDesktopIntegrationDTO - Single-instance pattern: see Wails options + each platform’s lifecycle entry point