Runic.Platform.Linux.Portal
0.12.0
dotnet add package Runic.Platform.Linux.Portal --version 0.12.0
NuGet\Install-Package Runic.Platform.Linux.Portal -Version 0.12.0
<PackageReference Include="Runic.Platform.Linux.Portal" Version="0.12.0" />
<PackageVersion Include="Runic.Platform.Linux.Portal" Version="0.12.0" />
<PackageReference Include="Runic.Platform.Linux.Portal" />
paket add Runic.Platform.Linux.Portal --version 0.12.0
#r "nuget: Runic.Platform.Linux.Portal, 0.12.0"
#:package Runic.Platform.Linux.Portal@0.12.0
#addin nuget:?package=Runic.Platform.Linux.Portal&version=0.12.0
#tool nuget:?package=Runic.Platform.Linux.Portal&version=0.12.0
Runic.Platform.Linux.Portal
The Linux provider of the application-scoped Runic.Platform services, built on XDG
desktop portals, D-Bus services and freedesktop.org specifications, independent of
GTK. It also serves the portal file dialogs, file handoff and URI opening that
Runic's hosts create for each window. The session portal chooses the desktop
backend: a GTK Runic window can therefore use KDE's file chooser on Plasma. This
package makes its D-Bus calls through Runic.Platform.Linux.DBus, with NativeAOT-compatible
proxies generated at build time from pinned upstream portal XML.
Every type is annotated [SupportedOSPlatform("linux")], and PortalApplication
throws PlatformNotSupportedException on another operating system.
// 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}"));
// Register these as application-scoped services and dispose them at shutdown.
var settings = portals.CreateSettings();
var notifications = portals.CreateNotifications();
var volumes = portals.CreateVolumes();
PortalApplication is the package's entry point. It is immutable and has no
connection or disposal requirement. Its services all use one application ID,
registering it with the portal before the first portal call, and report setup
problems to one diagnostic sink. Windows and macOS have static
providers instead (WindowsPlatformProvider, MacOSPlatformProvider), because
there the process carries its identity (the AppUserModelID or the bundle); the
portals identify an application by the desktop ID it registers on its connection.
File dialogs, file handoff and URI opening belong to a window. Runic.Application.Views.Desktop's
DesktopPlatformServices.ForWindow and services.AddRunicPlatformServices() create them for
each GTK 3 or GTK 4 window, parented to it, with desktop-inferred identity.
AddRunicPlatformServices(applicationId) creates the notifications with the
application ID, or desktop-inferred identity without one. Its inhibition comes from
PortalApplication.CreateInhibition(): an application-scoped Inhibit request with
the empty parent window identifier, so it works without a window and is not tied to
one, as the Windows and macOS inhibition is. An application without a window uses
CreateUnparentedFileDialogs() for dialogs that the portal shows without a parent.
The services of a process share one session-bus connection per desktop identity:
the application's ID, or desktop-inferred identity for the window services that
Runic's hosts create (inside Flatpak and Snap the sandbox supplies the identity, so
there is one). Volumes use one system-bus connection. A connection that closes is
replaced on the next request; the shared connections stay open for the process.
Calls are bound to a specific portal service owner. An interrupted operation
returns unavailable; it is never replayed automatically, and a pending request ends
as unavailable when the portal service is replaced or exits. After a portal restart
the next call registers the application ID with the new portal first. Settings
watch SettingChanged signals and follow a replaced portal.
The appearance comes from the portal's org.freedesktop.appearance keys, so it is
only as complete as the portal and its backend. Ubuntu 24.04 (xdg-desktop-portal 1.18
with the GNOME backend 46) publishes the colour scheme and contrast, so the accent
colour and reduced motion stay null there. A newer portal whose backend implements
them publishes all four; CI checks all four against Fedora 44's (xdg-desktop-portal
1.22 with xdg-desktop-portal-gnome 50), changed through GSettings as GNOME Settings
does.
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>.tmpbeside 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.GetMountPointreports (or/run/flatpak/doc), confirmed as the FUSE filesystem. The save does not resolve links there. It stages.runic-<guid>.tmpin the document directory, which the document portal stores as a private host tempfile.xdp-.runic-<guid>.tmp-XXXXXXbeside the real file, and renames it onto the exact document name. The portal performs that rename with the host'srenameat2. This is how GLib'sg_file_replacesaves 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.
Document apps
OpenDocumentAsync and SaveDocumentAsync (Runic.Platform README, "Document apps")
classify their selection the same way, so a document opened from a document view
saves back through the view, as many times as the application saves. Writing needs
the portal's write grant: an OpenFile result the backend granted read-only (such
as through a chooser's read-only choice) or a revoked grant returns
Unavailable(AtomicReplaceUnavailable) from BeginWriteAsync, while
OpenReadAsync keeps reading. A document's bookmark (CreateBookmarkAsync) is its
path, also in a document view: the permission store keeps a persistent document's
path valid across launches, so IDesktopFileBookmarks.OpenDocumentAsync reopens it
and saves through the view again. A view cannot follow a file moved on the host
(NotFound) and cannot hold sibling files: a new name in the document directory is
a private temporary file, so lock files beside the document do not work.
There is no automatic fallback to GTK's own dialogs, including when a portal fails.
File dialog filters become FileChooser filters (case-insensitive glob patterns,
such as *.[pP][nN][gG], and (1, type) for each MIME type) with the first as
current_filter; the response's current_filter is matched back to the
request's filters as the result's SelectedFilter; the initial folder
becomes current_folder, the folder's bytes with a trailing NUL; and
OpenFilesAsync sets multiple and accepts every returned local file URI. A
dialog without these options sends the same request as before. Portals before
FileChooser version 4 ignore current_folder when opening files.
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; OpenDirectoriesAsync adds
multiple and accepts every returned one. Its directory lease retains
presentation ownership; persistent portal document permissions remain owned by
the user's permission store.
The file launcher's LaunchUriAsync(uri) uses OpenURI for HTTP, HTTPS and mailto links. A valid Windows Settings URI
(ms-settings:<page>) returns Unavailable(BackendUnavailable) without a D-Bus call. Relative URIs, paths and other
schemes throw ArgumentException before any D-Bus call, and cancelling the portal's
application chooser is Failed(UserDismissed). Local-file opening uses the file
launcher's LaunchAsync 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.
Diagnostics
PortalApplication passes each setup message to its diagnostic sink as a
PortalDiagnostic with a Code, a Message and an optional Remediation, the
same three fields as RunicWindowDiagnostic and DesktopDiagnostic, and a
Severity: Warning for a problem, Information for what a service did. They are
for logs only: the codes and text may change in any release, so branch on the
services' results, not on these. Use Severity to choose a log level; Runic
Desktop logs Warning entries as warnings and Information entries at debug
level. A sink receives informational entries too, and is called on whichever
thread the service is on; an exception it throws is ignored.
| Code | Severity | Meaning |
|---|---|---|
portal-identity-registered |
Information | The portal connection registered the application ID. |
portal-identity-registration-failed |
Warning | The portal rejected the application ID; install a matching desktop entry. |
portal-request-unavailable |
Warning | A portal request could not be made. |
portal-parent-unavailable |
Warning | The window could not be exported as the portal dialog's parent. |
portal-notification-request-accepted |
Information | The portal accepted a notification; delivery is not confirmed. |
portal-notification-action-received |
Information | The desktop invoked a notification action. |
portal-notification-activation-unmatched |
Information | An activation names a notification this process did not show, for example after a restart; it is untrusted. |
portal-notification-owner-changed |
Information | The notification portal was replaced; the service registers again. |
portal-notification-identity-unspecified |
Warning | Notifications without an application ID may lose actions or history. |
portal-notification-permission-denied |
Warning | The desktop refused notifications. |
portal-notification-backend-unavailable |
Warning | The notification portal is missing or failed. |
status-icon-no-watcher |
Warning | No StatusNotifierWatcher runs, so no status area shows the icon. |
status-icon-no-host |
Warning | The watcher has no status area showing icons. |
status-icon-register-failed |
Warning | Registering the icon failed; it registers again later. |
status-icon-bus-unavailable |
Warning | The session bus could not be reached. |
status-icon-bus-lost |
Information | The session bus connection ended; the icon registers again. |
status-icon-activations-dropped |
Warning | Activations arrived faster than the Activated handler returned; those beyond the queue were dropped. |
global-shortcuts-unavailable |
Warning | The desktop has no GlobalShortcuts portal backend, or the portal refused an unidentified application. |
global-shortcuts-events-dropped |
Warning | Activations and binding changes arrived faster than the handlers returned; those beyond the queue were dropped. |
login-startup-identity-unspecified |
Warning | Login startup outside a sandbox needs an application ID. |
login-startup-foreign-entry |
Warning | Another autostart entry uses the application's name. |
login-startup-portal-unavailable |
Warning | The Background portal is missing or failed. |
open-registration-foreign-entry |
Warning | A desktop entry or MIME definition of the application's ID that Runic did not write exists; nothing was registered. |
dbus-activation-name-taken |
Warning | Another process owns the application's ID on the session bus, so D-Bus activation is not served. |
secret-service-unavailable |
Warning | No Secret Service answers on the session bus. |
secret-service-plain-session |
Information | The Secret Service offered only an unencrypted session. |
secret-portal-unavailable |
Warning | Inside a sandbox neither the Secret Service nor the Secret portal answered. |
secret-file-invalid |
Warning | The sandbox's libsecret keyring file is damaged or in another format. |
secret-service-encryption-required |
Warning | The Secret Service offered only an unencrypted session, which the store's RequireEncryption refuses. |
trash-portal-unavailable |
Warning | The Trash portal could not move the item. |
desktop-applications-sandboxed |
Information | Inside Flatpak or Snap the host's applications are out of reach. |
desktop-application-unknown |
Warning | No usable desktop entry has the requested ID. |
desktop-application-not-associated |
Warning | The application does not open the content type. |
desktop-application-terminal |
Warning | The application runs in a terminal, which the provider does not start. |
desktop-application-exec-invalid |
Warning | The desktop entry's Exec key cannot be parsed. |
desktop-application-local-files-only |
Warning | The application opens only local files. |
desktop-application-activation-failed |
Warning | D-Bus activation of the application failed. |
launcher-entry-application-id-required |
Warning | A window's badge or progress names the application's desktop entry, which needs the application ID. |
mimeapps-write-failed |
Warning | The user's mimeapps.list could not be written. |
thumbnail-cache-unavailable |
Information | The host's thumbnail cache is not visible inside the sandbox. |
thumbnailer-access-denied |
Warning | The sandbox does not allow talking to the thumbnailer. |
network-monitor-unavailable |
Warning | Neither the NetworkMonitor portal nor NetworkManager answers, so network status is unavailable. |
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
PortalApplication.CreateStatusIcon() 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 { IconName = "applications-system" }, "My app")
{
Menu = [new DesktopMenuItem("Open window", "open"), new DesktopMenuSeparator(), new DesktopMenuItem("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.
A chosen menu item's activation carries its CommandId and CommandParameter;
items are unique by the two together.
The status area talks to the icon over the session bus, which does not say
which program is calling. Any program in the user's session can therefore
activate the icon or click a menu item, as the status area does. Do not bind a
destructive or irreversible action to a menu item without a confirmation in the
application's own window. The icon checks every call's signature and bounds
what calls can cost. The item and the menu each answer one call at a time, in the
order the status area sent them, and refuse calls beyond a short queue with
org.freedesktop.DBus.Error.LimitsExceeded. Activated runs one activation at a
time, and up to 32 more wait while a handler runs; later ones are dropped and
reported once as status-icon-activations-dropped, so return from the handler
quickly. A menu call takes at most 1024 entry IDs or events, and repeated IDs are
answered once. A second icon shown at the same time uses a session-bus connection
of its own, since status areas look for each icon at fixed object paths.
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.
Global shortcuts
PortalApplication.CreateGlobalShortcuts() binds keys that start an application's
actions while another application has the focus, through the
org.freedesktop.portal.GlobalShortcuts portal. The desktop owns the keys: each
action's PreferredShortcut is sent as its preferred_trigger in the form of the
XDG shortcuts specification
(CTRL+SHIFT+s), and the desktop decides, asks the user or keeps the key the user
chose before. It stores the keys per application ID and action ID, so keep the IDs
stable.
await using var shortcuts = portals.CreateGlobalShortcuts();
shortcuts.Activated += (_, activation) => { /* worker thread: activation.Id, activation.PlatformContext */ };
var result = await shortcuts.BindAsync([new DesktopGlobalShortcut("show", "Show the main window")
{
PreferredShortcut = DesktopShortcut.Parse("Primary+Alt+Space"),
}]);
The portal binds a set once per session, so each BindAsync closes the previous
session and binds the whole set in a new one. A binding is Bound when the desktop
reports a key, with its own text (trigger_description, such as GNOME's
Press <Control><Alt>space or KDE's Ctrl+Alt+Space) and the key as a
DesktopShortcut where that text has one; otherwise it is Unbound. ListAsync
asks the portal again. BindingsChanged reports the user's changes from the
desktop's settings, which ConfigureAsync opens where the portal has version 2 of
the interface. Activated reports key presses with the desktop's activation token,
not releases. When the portal restarts, the set is bound again in a new session and
BindingsChanged reports the result.
The portal requires an application ID: outside Flatpak and Snap, create the
PortalApplication with the installed desktop ID; without it requests fail with
PermissionDenied and global-shortcuts-unavailable. KDE Plasma 5.27 and later,
GNOME 48 and later and Hyprland have a backend; elsewhere, including X11 sessions
without one, requests report BackendUnavailable and global-shortcuts-unavailable.
Runic does not grab keys on X11 itself.
Login startup
PortalApplication.CreateLoginStartup() starts the application with the
PortalApplication's ID 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." });
Registration and D-Bus activation
PortalApplication.CreateOpenRegistration() registers the application's URL
schemes and file types for the current user, for an application that installs
itself (an AppImage, a tarball, dotnet run). Outside a sandbox it writes
$XDG_DATA_HOME/applications/<applicationId>.desktop with
Exec="<executable>" %U, MimeType= (x-scheme-handler/<scheme> and the
file types) and X-Runic-Registered=true, and, for a file type whose extension
the shared MIME-info database does not give its MimeType, a definition with
the extension's glob in $XDG_DATA_HOME/mime/packages/<applicationId>.xml.
update-mime-database and update-desktop-database run when they are
installed; GIO also watches the directories. A file type without a MimeType
uses the database's type for its extension. A registration replaces the
application's earlier one and never sets a default (mimeapps.list).
An entry of the same ID that the application's package installed in
XDG_DATA_DIRS, or one in XDG_DATA_HOME without the marker, is refused with
Conflict and open-registration-foreign-entry, and nothing is written.
Inside Flatpak or Snap the manifest exports the entry and the MIME definitions,
so every request reports Unavailable(BackendUnavailable).
PortalApplication.ExportApplicationAsync(instance) serves D-Bus activation
for a desktop entry with DBusActivatable=true: it owns the application ID on
the session bus and answers org.freedesktop.Application at
/org/example/Notes (the ID with . as / and - as _). Activate
becomes a launch of the running instance without arguments, Open a launch
with the URIs, which the instance's open requests deliver, and
ActivateAction a launch with the Exec arguments of the entry's
[Desktop Action <name>] (listed in Actions); an activation-token or
desktop-startup-id becomes the launch's PlatformContext. Another process
owning the name is ResourceBusy with dbus-activation-name-taken.
Notifications of the same ID share the name and the object, which answers
their actions. The desktop starts the application through a D-Bus service
file (share/dbus-1/services/<applicationId>.service, Exec= the program);
GNOME and KDE then deliver later opens to the running process without
starting another.
var claim = await ApplicationInstance.ClaimAsync(id, args);
if (claim is PlatformResult<ApplicationInstanceClaim>.Success { Value: ApplicationInstanceClaim.Primary { Instance: var instance } })
{
await using var opens = DesktopPlatformServices.CreateOpenRequests(new(["org.example.notes"]) { IncludeFiles = true }, instance, launch);
var exported = await new PortalApplication(id).ExportApplicationAsync(instance);
}
Secret storage
PortalApplication.CreateSecretStore() keeps
secrets in the desktop's Secret Service (org.freedesktop.secrets): GNOME
Keyring, KeePassXC or KWallet. Secrets travel over an encrypted
dh-ietf1024-sha256-aes128-cbc-pkcs7 session; a service without it gets a
plain session and the secret-service-plain-session diagnostic.
CreateSecretStore(new DesktopSecretStoreOptions { RequireEncryption = true })
refuses that session instead: requests fail with Failed(Other) and the
secret-service-encryption-required diagnostic. Set it when the application
knows its users' providers, as GNOME Keyring and KeePassXC support the
algorithm. Items use
libsecret's generic schema (service and account attributes), so
secret-tool lookup service <service> account <account> reads them, and are
labelled <service> (<account>). Binary secrets (SetBytesAsync) are
stored with the application/octet-stream content type, text ones as
text/plain. A locked keyring is unlocked through the service's own prompt;
without a prompter the result is Failed(Locked). With
AllowUserInteraction = false the store dismisses an unlock or create prompt
without showing it and reports Failed(Locked).
Without a Secret Service, requests report Unavailable(BackendUnavailable) and
the secret-service-unavailable diagnostic.
Inside Flatpak the store uses the Secret Service when the sandbox may talk to
it: --talk-name=org.freedesktop.secrets (or --socket=session-bus) in the
finish arguments, read from /.flatpak-info. Inside Snap it uses the Secret
Service while the password-manager-service interface answers. Otherwise it
keeps secrets as libsecret does in a sandbox, so libsecret-based code and
secret-tool in the same sandbox share them: the Secret portal
(org.freedesktop.portal.Secret, served by GNOME Keyring's portal backend)
hands the application a secret that encrypts
$XDG_DATA_HOME/keyrings/default.keyring, in libsecret's file format
(PBKDF2-SHA256 key, AES-128-CBC items with HMAC-SHA256, hashed attributes).
The file lives in the application's own data directory. Without the portal,
requests report Unavailable(BackendUnavailable) and the
secret-portal-unavailable diagnostic. A file that is not in that format
reports Failed(InvalidData) and the secret-file-invalid diagnostic and is
never overwritten; an item that fails its MAC reads the same way, and storing
the secret again replaces it. The portal has no non-interactive flag: its backend
may ask once to unlock the login keyring.
The Secret Service has no per-application isolation: any process of the same user on the session bus can read every item, so a service and account name is no access boundary. The Secret portal file is per Flatpak application.
See the secret storage design.
Trash
PortalApplication.CreateTrash() follows the
freedesktop.org Trash specification, as GIO and KIO do, so Files, Dolphin and
gio trash --list show and restore the items. Outside a sandbox it moves an
item into $XDG_DATA_HOME/Trash, or, on another file system, into the trash at
the top of its mount ($topdir/.Trash/$uid or $topdir/.Trash-$uid), with an
info file recording where it was. It renames and never copies, so a location
without a usable trash reports Unavailable(BackendUnavailable) and the item
stays. A trashed directory's size goes to the trash's directorysizes.
ListAsync, RestoreAsync and EmptyAsync read the home trash and the
topdir trashes this user owns on every mounted file system; a restore never
replaces what is at the original path and recreates missing parents. Inside
Flatpak or Snap it asks the Trash portal, which trashes what the application can
write; it is sent a symbolic link as the link itself, which xdg-desktop-portal
1.22.1 refuses, so a link reports Unavailable(BackendUnavailable) there, as do
listing, restoring and emptying.
See the
trash design.
Volumes
PortalApplication.CreateVolumes() lists, mounts, unmounts and ejects
volumes through UDisks2 (org.freedesktop.UDisks2) on the system bus, as GNOME
Files and Dolphin do; it needs no portal, no window and no GTK. UDisks2 asks
Polkit, whose authentication agent may prompt; without an agent, a request
that needs one is Failed(PermissionDenied). UDisks2 chooses the mount point,
such as /run/media/<user>/<label>. Creating the volumes connects to nothing;
the first request opens the system bus, and disposal closes it. Without
UDisks2 every request is Unavailable(BackendUnavailable). Inside Flatpak, add
--system-talk-name=org.freedesktop.UDisks2 to the finish arguments; inside
Snap, connect the udisks2 interface. UnlockAsync(id, passphrase, token)
unlocks a LUKS container (Encrypted.Unlock) and returns its cleartext
filesystem, a volume with an Id of its own; MountAsync(id, options, token)
passes the options, with ro for read-only, as UDisks2's options. See the
volumes design.
Network status
PortalApplication.CreateNetworkStatus() asks the NetworkMonitor portal
(org.freedesktop.portal.NetworkMonitor), which xdg-desktop-portal implements
itself from GLib's network monitor, so it answers sandboxed and unsandboxed
applications on every desktop that runs the portal: GetStatus from version 3,
GetAvailable, GetMetered and GetConnectivity in version 2, and the changed
signal. Its connectivity levels 1 to 4 are Local, Limited, CaptivePortal and
Internet. Where no portal answers, such as under a minimal window manager, it
reads NetworkManager's State, Connectivity and Metered on the system bus and
follows their PropertiesChanged: Connectivity 2 is a captive portal, Metered
1 and 3 (yes, guessed yes) are metered and 2 and 4 are not, and a connected
State without a connectivity check is Local or Unknown. When neither
answers, reads are Unavailable(BackendUnavailable) and the
network-monitor-unavailable diagnostic is reported. A watch reports
Unavailable when its source stops, looks for one again every few seconds and
reports a fresh state once one answers. It needs no window and no GTK. Inside
Flatpak the portal needs no permission; NetworkManager needs
--system-talk-name=org.freedesktop.NetworkManager.
Badges and progress
A window's SetBadgeAsync and SetProgressAsync on GTK 3 and GTK 4 emit the Unity
launcher API's com.canonical.Unity.LauncherEntry.Update signal on the session bus
for application://<id>.desktop, with count and count-visible for a count,
urgent for a dot, and progress and progress-visible; paused and failed
progress look normal, and indeterminate progress shows no bar. An object below the
application's object path answers Query for a dock that starts later. KDE
Plasma's task manager, Ubuntu Dock and Dash to Dock show them; stock GNOME Shell
does not, and the call still succeeds. The badge and progress belong to the
application, which all windows share: the most recent call wins, and a window's
go when it closes. The entry needs the application's ID, from
AddRunicPlatformServices(applicationId) or, inside Flatpak, the Flatpak ID;
without one the calls report Unavailable(BackendUnavailable) and the
launcher-entry-application-id-required diagnostic.
Desktop applications
PortalApplication.CreateApplications() reads
the desktop's application database without GIO: desktop entries from the XDG
data directories, the shared MIME-info database (globs, magic, aliases,
subclasses, icons and descriptions), the MIME Apps mimeapps.list files and
mimeinfo.cache, and icon themes. GetContentTypeAsync with NameOnly never
touches the file. LaunchAsync starts a desktop entry's Exec without a
shell, expanding %f %F %u %U %i %c %k into whole arguments, or calls
org.freedesktop.Application for a DBusActivatable entry through
Runic.Platform.Linux.DBus; a Terminal=true entry reports
Unavailable(BackendUnavailable). SetDefaultApplicationAsync changes one key
of $XDG_CONFIG_HOME/mimeapps.list and keeps every other byte. The icon theme
comes from the Settings portal, then KDE's kdeglobals, then hicolor. Inside
Flatpak or Snap only content types and icons work. See the
desktop applications design.
Thumbnails
PortalApplication.CreateThumbnails() reads
the freedesktop.org thumbnail cache, $XDG_CACHE_HOME/thumbnails, which GNOME
Files, Dolphin and Thunar fill. An entry counts only when its Thumb::URI and
Thumb::MTime (and Thumb::Size, if present) match the file; a matching entry
in fail/ means no thumbnail. On a miss it asks the session's
org.freedesktop.thumbnails.Thumbnailer1 service, such as Tumbler, to make one
and reads the cache again. GNOME and KDE run no such service, so there only
thumbnails their file managers already made are returned; running a desktop's
.thumbnailer programs itself is not supported. Inside Flatpak, add
--filesystem=xdg-cache/thumbnails:ro and
--talk-name=org.freedesktop.thumbnails.Thumbnailer1 to the finish arguments;
without them requests return no thumbnail and the diagnostic sink reports
thumbnail-cache-unavailable or thumbnailer-access-denied. See the
thumbnails design.
| Product | Versions 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. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- Runic.Platform.Linux.DBus (= 0.12.0)
- Runic.Platform.Runtime (= 0.12.0)
- Tmds.DBus.Protocol (>= 0.95.1)
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 |