Avalonia: macOS and Linux support (multi-target, hotkeys, themes, plugins, packaging)
Draft
TA
2h ago+20280+20280+01234567890123456789012345678901234567890123456789-8838-8838-0123456789012345678901234567890123456789
Summary
Makes the Avalonia Flow Launcher usable on macOS (Apple Silicon) and Linux (x64/arm64, Wayland and X11), without changing Windows behavior.
- macOS: ships as a signed menu-bar
.appwith a global hotkey (default ⌥Space), Flow themes, app indexing and 10 bundled plugins. - Linux: a native Wayland client with an X11 fallback. It uses global hotkeys through the GlobalShortcuts portal or X key grabs, indexes desktop entries, sends notifications to the desktop's notification server, and starts every program in its own systemd scope. It ships as a self-contained tarball, and there is a user install script for NixOS.
Structure
- Targets.
Flow.Launcher.Plugin,Infrastructure,Core,Avaloniaand 10 bundled plugins targetnet10.0;net10.0-windows10.0.19041.0.net10.0covers macOS and Linux. The Explorer and WindowsSettings plugins stay Windows-only. - No
#if. Platform code is split by file, with the rules defined inDirectory.Build.targets:*.Windows.cscompiles only for the Windows target.*.CrossPlatform.cscompiles only fornet10.0.*.xaml.cs(WPF code-behind) is excluded fromnet10.0.- Types are
partial, and shared code calls platform behavior through partial methods. Inside*.CrossPlatform.cs, macOS and Linux branch onOperatingSystem.IsMacOS()/IsLinux(). Windows code was moved verbatim.
- Shared Win32/registry code. It stays in shared files and is marked
[SupportedOSPlatform("windows")]..editorconfigkeeps CA1416 silent, because CsWin32's per-API version annotations would flood the Windows build with warnings. - WPF enums on
net10.0.Key,ModifierKeys,MessageBox*andWindowStateare generated from the WPF reference assemblies with identical names and values. Settings and hotkeys therefore stay compatible.
Shared changes (all platforms)
- Themes.
ThemeLoaderparses Flow's WPF theme.xamlwith System.Xml, then resolvesBasedOnstyles, Base.xaml, dynamic and static resources, and Light/Dark palettes into Avalonia resources. Themes switch live, and the settings preview uses the same resources. Before this change, the Avalonia main window ignored Flow themes on every OS. - SVG icons.
ImageLoaderrenders.svgthrough Svg.Controls.Avalonia, which the app already pulled in via Markdown.Avalonia. - Plugin loader. It now reuses identical assemblies the host ships, even if they are not loaded yet. Before, plugins loaded their own
MemoryPack.Core, so the host could not serialize plugin caches; for example, the Program index was rebuilt on every start. - Stale home page results. Typing quickly cancels each query before its first batch arrives, and that batch was the one that replaced the home page results. The home page then stayed below the new results, and the window stayed at full height. A pending clear is now kept until a batch actually applies it.
SharedCommands.AppProcess. New SDK helper for starting programs on the user's behalf. On Linux it uses a per-app systemd scope (see below); elsewhere it isProcess.Start. Program, Shell, Url,SearchWeb,FilesFoldersand the host's open/run paths use it.Flow.Launcher.Localization0.0.7. 0.0.6 only matched\Languages\paths, soLocalizewas not generated off Windows.
macOS features
- Hotkeys. Global hotkeys use Carbon
RegisterEventHotKey, which needs no Accessibility permission. Registration is exclusive and detects conflicts with system shortcuts. The hotkey recorder follows the active keyboard layout. - Window behavior.
NSApp activateIgnoringOtherAppsbrings Flow to the front when it is shown. Hiding Flow returns focus to the previous app. The window opens on the current Space. - Icons.
.icnsand.appicons are converted once withsipsand cached as PNG. Plugin icon paths written with\are normalized. - Plugins:
- Program: indexes
.appbundles, falling back toplutilfor binary or damaged plists. - Sys: power commands via
pmset/osascript, plus Trash. - Shell: runs
$SHELL -l -c, or opens Terminal.app. - ProcessKiller: works.
- BrowserBookmark: reads Chromium and Firefox macOS profiles.
- Url: opens a custom browser with
open -a. - PluginsManager: works.
- Program: indexes
- Host services.
ShowMsgBoxuses a realosascriptdialog. Previously it returnedNone, which let PluginsManager install plugins without confirmation.- Folders open in Finder via
open/open -R. - Python and Node are found from the login shell or Homebrew.
- Windows-only settings are hidden.
- Packaging.
Scripts/macos/build-app.shbuildsOutput/macOS/Flow Launcher.app: self-contained, ad-hoc signed,LSUIElement(no Dock icon). Start at login is handled by a LaunchAgent. Build and install steps are indocs/macos.md.
Linux features
- Windowing.
- Native Wayland via
Avalonia.Wayland(UseWaylandWithFallback).FLOW_LAUNCHER_BACKEND=x11forces XWayland/X11. - The search window and notifications have their own app ids (
com.flowlauncher.FlowLauncher.Search/.Notification), so compositor rules can match them. Under X11 the search window is_NET_WM_WINDOW_TYPE_DIALOG, so tiling window managers float it without rules. - Hyprland keeps floating Wayland windows at their mapped size and ignores client resizes. Under Hyprland the search window therefore reserves the height of the longest list, keeps its border top-aligned and leaves the rest transparent.
docs/linux.mdhas the window rules for Hyprland, Sway and niri.
- Native Wayland via
- Hotkeys.
- Wayland:
org.freedesktop.portal.GlobalShortcuts, after registering the app id with the portal's host Registry. - X11: XCB key grabs that ignore Num Lock and Caps Lock and report conflicts.
- Without either:
Flow.Launcher.Avalonia --toggle, sent to the running instance through the single-instance socket. It can be bound in any compositor.
- Wayland:
- Desktop integration.
- Flow installs its desktop entry on start, unless a package already provides one. Start at login uses an XDG autostart entry.
- Notifications go to
org.freedesktop.Notifications(GNOME, KDE, mako, dunst, …), and buttons become notification actions. Flow's own notification window is only a fallback when no server is running. - Files are revealed through
org.freedesktop.FileManager1. - Blur themes are drawn opaque, because there is no portable blur protocol.
- The runtime picker for plugin environments uses zenity or kdialog.
- Per-app scopes. Programs started from Flow run in
app-flowlauncher-<app>-<random>.scope, created bysystemd-run --user --scope. Under uwsm this is inapp-graphical.slice, likeuwsm app. They therefore survive Flow exiting, restarting or crashing. Before, stopping Flow's autostart unit killed everything launched from it.AppProcesswaits until the process is in its scope before returning, so an immediate Flow exit (e.g.RestartApp) cannot take it down. - Plugins:
- Program: indexes desktop entries from the XDG data dirs, including Flatpak and Nix profiles. It honors
NoDisplay,Hidden,OnlyShowIn/NotShowInandTryExec, parsesExec, and resolves icons from the GTK/KDE icon theme (SVG preferred).Terminal=trueentries run in$TERMINALor a known terminal. - Sys: power commands via
systemctl, session and lock vialoginctl, and the freedesktop Trash. - Shell: runs
$SHELL, and opens a terminal for "leave open". - BrowserBookmark: reads Chromium-family and Firefox profiles, including Flatpak and Snap.
- Url: works.
- ProcessKiller: hides session-critical processes.
- Program: indexes desktop entries from the XDG data dirs, including Flatpak and Nix profiles. It honors
- Packaging.
Scripts/linux/build.sh [linux-x64|linux-arm64]builds a self-contained tarball. It runs in CI as thebuild-linuxjob for both RIDs.flake.nixprovides a dev shell (.NET 10 SDK, jj, native libs) and aruntime-libspackage.Scripts/linux/install-nixos.shinstalls into~/.local. It patches the apphost's loader and every native library's RUNPATH to the GC-rootedruntime-libs, so noLD_LIBRARY_PATHleaks into launched programs.- Docs are in
docs/linux.md.
Not supported yet
- Plugins. Explorer and WindowsSettings (both platforms).
- Settings panels. WPF-only plugin settings panels, including Url and WebSearch.
- Windows-only features. Dialog Jump, portable mode, Squirrel auto-update, theme blur/backdrop and the Korean IME option.
- Clipboard. Copying files to the clipboard, and the
{clipboard}/{active_explorer_path}shortcuts. - Linux/Wayland.
- Clients cannot position windows, so Search Window Location has no effect and needs compositor rules.
- The cursor's display is only known under X11.
- "Ignore hotkeys in fullscreen" works only under X11.
- On Hyprland the transparent part of the search window still takes clicks.
Verification
macOS 26, arm64
- Build.
dotnet build Flow.Launcher.sln --no-incrementalbuilds both targets with 0 errors. The WPF app's Windows-onlytaskkillpre-build step was replaced with a no-op on PATH for this build. - Bundle launch.
Scripts/macos/build-app.sh, thenopen -n "Output/macOS/Flow Launcher.app":lsappinforeportsUIElement.- The log shows
RegisterEventHotKey returned noErr,Registered toggle hotkey: Alt + Space,Theme <Win11Light> applied: 24 light / 24 dark resources resolved,Loaded 10 pluginsandPlugins ready. - The Program cache is saved.
- Screenshots. Window-only screenshots, taken by the theme work, confirmed Win11Light and several dark themes. A live theme switch was also checked.
- Components. The
osascriptdialog result parsing,sipsicon conversion for 5 apps, and LaunchAgent create/lint/remove were each run in isolation.
Linux (NixOS, Hyprland 0.57.1, x64)
- Build.
Flow.Launcher.Avaloniabuilds fornet10.0andnet10.0-windows10.0.19041.0(the Windows build was compiled only). The bundled plugins build fornet10.0.Scripts/linux/build.sh linux-x64produces the tarball. - Hyprland, native Wayland.
hyprctl globalshortcutslistscom.flowlauncher.FlowLauncher:alt-space, and dispatching it toggles the window.- With the documented rule the window floats and takes focus. Typing (via
wtype) shows results: "nautilus" finds Files with its SVG theme icon. - Esc hides the window, and Settings opens.
- X11 and XWayland.
- In X11 mode,
xdotool key alt+spacetoggles the window through the XCB grab. - Under XWayland in headless sway, the window resizes to its content.
- In X11 mode,
- Headless sway, isolated session.
- The window sizes to its results under native Wayland.
- With fast typing, the stale home page results are gone after the fix (2 results → 199 px, 1 result → 133 px).
- The Hyprland reservation was checked by faking the instance signature: the window stays 580×439 with the border at content height.
- Notifications. Checked with mako on a private D-Bus session.
makoctl listshows Flow's notification (App name: Flow Launcher,Desktop entry: com.flowlauncher.FlowLauncher), and no Flow notification window is created. - Scopes. Checked with a throwaway program run as a systemd user service that exits right after launching.
- The child started through
AppProcesslands inapp-graphical.slice/app-flowlauncher-sleep-….scopeand survives the parent unit stopping, 3/3 times. - A plain
Process.Startchild is killed. - Arguments splitting and id escaping (
My\x2dApp) were checked, and missing programs raiseWin32Exception.
- The child started through
- NixOS install. After
install-nixos.shthe app runs withoutLD_LIBRARY_PATH(Wayland, fonts, icons). The XDG autostart unit started by uwsm runs it and registers the hotkey.
Not run
- macOS. Pressing the hotkey and typing queries; this needs Accessibility permission for automation, which the test terminal didn't have.
- Linux.
- GNOME and KDE.
- linux-arm64.
- The
build-linuxCI job. - Clicking a notification action.
- Launching an app from Flow and then restarting Flow on a live session; only the scope helper was tested this way.
- Unit tests. The test project targets Windows/WPF.
- Windows. Anything at run time.
Stacked on #4706 (.NET 10 + Avalonia 12); this PR targets avalonia-net10.