Runic.Platform.Linux.Portal 0.8.0-preview.3

This is a prerelease version of Runic.Platform.Linux.Portal.
There is a newer version of this package available.
See the version list below for details.
dotnet add package Runic.Platform.Linux.Portal --version 0.8.0-preview.3
                    
NuGet\Install-Package Runic.Platform.Linux.Portal -Version 0.8.0-preview.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Runic.Platform.Linux.Portal" Version="0.8.0-preview.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Runic.Platform.Linux.Portal" Version="0.8.0-preview.3" />
                    
Directory.Packages.props
<PackageReference Include="Runic.Platform.Linux.Portal" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Runic.Platform.Linux.Portal --version 0.8.0-preview.3
                    
#r "nuget: Runic.Platform.Linux.Portal, 0.8.0-preview.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Runic.Platform.Linux.Portal@0.8.0-preview.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Runic.Platform.Linux.Portal&version=0.8.0-preview.3&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Runic.Platform.Linux.Portal&version=0.8.0-preview.3&prerelease
                    
Install as a Cake Tool

Runic.Platform.Linux.Portal

Direct XDG desktop portal file dialogs and URI opening, independent of GTK. The session portal chooses the desktop backend: a GTK Runic window can therefore use KDE's file chooser on Plasma. This package uses the MIT-licensed Tmds.DBus.Protocol 0.95.1 with NativeAOT-compatible bindings; Registry and Notification bindings are generated from pinned upstream XML.

// Configure once at application composition. Install the matching .desktop file.
var portals = new PortalApplication("org.example.MyApp",
    diagnostic => Console.Error.WriteLine($"{diagnostic.Code}: {diagnostic.Remediation}"));
var parent = new Gtk3PortalWindowOwner(desktopNativeOwner); // Runic.Platform.Linux
// Or Gtk4PortalWindowOwner from Runic.Platform.Linux.Gtk4.
var files = portals.CreateFileDialogs(parent);
var launcher = portals.CreateFileLauncher(parent);
// Register these as application-scoped services and dispose them at shutdown.
var settings = portals.CreateSettings();
var notifications = portals.CreateNotifications();

PortalApplication is immutable and has no connection or disposal requirement. Its settings, notifications, file dialogs, file handoffs and URI opening all use one application ID, registering each owned host connection before portal calls. Existing static factories remain available for desktop-inferred identity. Runic.Application.Views.Desktop's services.AddRunicPlatformServices() uses those static factories for GTK 4 windows; compose the services yourself to pass a PortalApplication. It also registers notifications with PortalPlatformProvider.CreateNotifications, with the application ID passed to AddRunicPlatformServices(applicationId), or desktop-inferred identity without one.

Calls are bound to a specific portal service owner. An interrupted operation returns unavailable; it is never replayed automatically. Notifications detect portal replacement and recreate/register their connection before the next call. File operations open a fresh identified connection per request. Settings keep one connection and watch SettingChanged signals, reconnecting after portal replacement. Only the notification service owns the application's activation bus name.

Presentation-bound requests require a valid exported X11/Wayland parent. Failure to export never silently opens an unparented dialog. An application with no native presentation can deliberately call CreateUnparentedFileDialogs(). Cancellation and owner replacement close the portal request, discard late responses and release the parent lease. The provider subscribes before invoking the portal, including when an older portal returns a different request handle. Transport waits are bounded; the user may keep a healthy chooser open until cancellation or owner closure.

Only one local file:// selection is accepted. The portal owns persistent document grants; disposing a file lease does not revoke the user's permission store.

Saves are atomic, and platform.files.save is available. The provider classifies each save selection:

  • Host path. An unsandboxed application, or a sandbox whose filesystem access already covers the path, receives the real path. The save stages .runic-<guid>.tmp beside the final target of a symbolic link, copies the permission bits, synchronizes it and renames it over the target.
  • Document view. Otherwise the portal returns a document below the mount that org.freedesktop.portal.Documents.GetMountPoint reports (or /run/flatpak/doc), confirmed as the FUSE filesystem. The save does not resolve links there. It stages .runic-<guid>.tmp in the document directory, which the document portal stores as a private host tempfile .xdp-.runic-<guid>.tmp-XXXXXX beside the real file, and renames it onto the exact document name. The portal performs that rename with the host's renameat2. This is how GLib's g_file_replace saves from a sandbox.

On Linux the commit is a direct renameat2: a plain rename over an existing file, and RENAME_NOREPLACE for a new one, so a name that appeared since selection is a Conflict. A refused rename (EXDEV, EBUSY, for example a bind-mounted file) is NotCommitted and is never replaced by a copy. A read-only or revoked document grant makes BeginWriteAsync return Unavailable(AtomicReplaceUnavailable) before it changes anything. In every refusal the file is unchanged; offer Save As.

Through the document view, extended attributes and ACLs are not carried over and the directory sync is a no-op: after a crash just after the rename, the previous content may reappear, but never a partial file. If the application or portal stops mid-save, a hidden .xdp-* tempfile can remain beside the document. Document portals older than 1.14 reject RENAME_NOREPLACE; a new file is then created after re-checking that the name is still absent.

LinuxPlatformProvider.CreateGtkNativeFileDialogs(owner) remains an explicit GTK3 opt-in for unsandboxed applications and saves the same way. There is no automatic fallback to toolkit dialogs, including when a portal fails.

Directory selection uses OpenDirectoryAsync through the same providers for GTK3 and GTK4. It sends FileChooser OpenFile with directory=true, requiring FileChooser version 3 or newer. The provider refuses older portals before showing a chooser and accepts one exact local directory URI. Its directory lease retains presentation ownership; persistent portal document permissions remain owned by the user's permission store.

OpenUriAsync(owner, uri) uses OpenURI for HTTP, HTTPS and mailto links. Local-file opening uses the separate CreateFileLauncher(owner) service and passes a retained file descriptor. Ordinary text clipboard access remains with the selected GTK provider; the Clipboard portal is tied to Remote Desktop/Input Capture sessions. Background, Inhibit, Print and ScreenCast permissions are not requested by a generic window or file service.

Install a session D-Bus service, xdg-desktop-portal and the backend appropriate to your desktop (for example xdg-desktop-portal-kde on Plasma). The development flake provides both GTK and KDE backend binaries without starting or reconfiguring the user's portal services. Sandboxed packaging must expose the portal bus interface.

Protocol checks are explicit TUnit cases (category DBus) that need a session bus. Run them on an isolated bus:

direnv exec . dbus-run-session -- dotnet run --project tests/dotnet/Runic.Platform.Linux.Portal.Tests -- --treenode-filter "/*/*/*/*[Category=DBus]"

The suite's default run includes a real document-portal save through the session bus's portal. Give it a private runtime directory, so an isolated bus never touches the desktop session's document mount: XDG_RUNTIME_DIR=$(mktemp -d) XDG_DATA_HOME=$XDG_RUNTIME_DIR/data dbus-run-session -- .... Never start a second document portal against your own XDG_RUNTIME_DIR.

The GTK4 production-provider directory journey uses a real native window and exported parent with a controlled FileChooser service on an isolated session bus:

direnv exec . dbus-run-session -- env GDK_BACKEND=x11 xvfb-run -a dotnet run --project tests/native/Runic.Desktop.Gtk4.Smoke -- --directory

It covers exact directory access, dismissal, caller cancellation and native owner closure. It does not automate a desktop backend's chooser UI.

Desktop services

Application-scoped appearance and notification providers, and owned file opening, application choice and reveal, are described in the desktop services guide. It includes composition, native API choices, installation/activation requirements, retained file access and the per-platform verification status.

Status icon

CreateStatusIcon() on PortalApplication or PortalPlatformProvider shows an application's icon in the desktop's status area, with a tooltip, an attention state and a menu. Status icons have no portal: the icon is a StatusNotifierItem with a com.canonical.dbusmenu menu, registered with org.kde.StatusNotifierWatcher. Plasma, Waybar's tray and GNOME with the AppIndicator extension provide that watcher.

await using var icon = portals.CreateStatusIcon();
icon.Activated += (_, activation) => { /* worker thread: dispatch to the application */ };
await icon.ShowAsync(new DesktopStatusIconState(new DesktopIcon { ThemeName = "applications-system" }, "My app")
{
    Menu = [new DesktopMenuCommand("open", "Open window"), new DesktopMenuSeparator(), new DesktopMenuCommand("quit", "Quit")],
});

Without a watcher or a status area showing icons, ShowAsync returns unavailable and reports status-icon-no-watcher or status-icon-no-host. The icon stays ready and registers once a watcher starts, and again after it restarts. HideAsync releases the icon's bus name, which removes it. A sandbox must allow owning org.kde.StatusNotifierItem-* names and talking to the watcher.

Plasma fills a submenu when it first opens, so the first hover over a submenu can show nothing. The VM checks in nixos/status-icon drive the icon through Plasma 6 and through Waybar's tray.

Login startup

CreateLoginStartup(applicationId) on PortalApplication or PortalPlatformProvider starts the application when the user logs in. Outside a sandbox it writes ~/.config/autostart/<applicationId>.desktop, which every XDG desktop reads at login, and marks it with X-Runic-Application. It never changes or removes an entry it did not write, and an entry the user turned off (Hidden=true or X-GNOME-Autostart-enabled=false) stays off: enabling reports PermissionDenied. Disabling removes the entry, and with it the user's choice.

Inside Flatpak or Snap the entry is out of reach, so EnableAsync and DisableAsync ask the Background portal, which may ask the user and passes the request's Reason. The portal cannot report whether autostart is on, so IsEnabledAsync returns unavailable there. Without an ID the provider works only in a sandbox, where the portal knows the application.

var startup = portals.CreateLoginStartup();
await startup.EnableAsync(new LoginStartupRequest(["--background"]) { Reason = "Keep watching for updates." });
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Runic.Platform.Linux.Portal:

Package Downloads
Runic.Platform.Linux

The GTK 3 window services (portal parent, GTK dialogs and clipboard) that Runic.Application.Views.Desktop selects on Linux.

Runic.Application.Views.Desktop

Runic Window and View host adapter for Runic Desktop.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.12.0 0 10/27/2026
0.11.0 2,514 10/25/2026
0.10.0 5,065 10/22/2026
0.9.1 838 10/21/2026
0.9.0 175 10/21/2026
0.8.0-preview.6 1,362 10/19/2026
0.8.0-preview.5 396 10/17/2026
0.8.0-preview.4 983 10/17/2026
0.8.0-preview.3 882 10/15/2026
0.8.0-preview.2 141 10/15/2026
0.8.0-preview.1 194 10/15/2026
0.7.0-preview.9 587 10/14/2026
0.7.0-preview.8 114 10/12/2026
0.7.0-preview.7 72 10/12/2026
0.7.0-preview.6 1,121 10/9/2026
0.7.0-preview.5 151 10/9/2026
0.7.0-preview.4 82 10/8/2026
0.7.0-preview.3 220 10/8/2026
0.7.0-preview.2 86 10/7/2026
0.7.0-preview.1 582 10/7/2026
Loading failed