Runic.Application.Views.Desktop
0.19.0
dotnet add package Runic.Application.Views.Desktop --version 0.19.0
NuGet\Install-Package Runic.Application.Views.Desktop -Version 0.19.0
<PackageReference Include="Runic.Application.Views.Desktop" Version="0.19.0" />
<PackageVersion Include="Runic.Application.Views.Desktop" Version="0.19.0" />
<PackageReference Include="Runic.Application.Views.Desktop" />
paket add Runic.Application.Views.Desktop --version 0.19.0
#r "nuget: Runic.Application.Views.Desktop, 0.19.0"
#:package Runic.Application.Views.Desktop@0.19.0
#addin nuget:?package=Runic.Application.Views.Desktop&version=0.19.0
#tool nuget:?package=Runic.Application.Views.Desktop&version=0.19.0
Runic.Application.Views.Desktop
Present Runic Windows and Views with Runic Desktop: native windows, embedded WebViews or browser presentations, and the Runic platform services.
dotnet add package Runic.Application.Views.Desktop --prerelease
dotnet add package Microsoft.Extensions.DependencyInjection
This is the default host of the runic-app project template: dotnet new runic-app
opens a native window with this host.
In a project named MyApp:
using Microsoft.Extensions.DependencyInjection;
using MyApp; // the generated AddRunicViews()
using Runic.Application.Views;
using Runic.Application.Views.Desktop;
using Runic.Desktop;
var services = new ServiceCollection();
services.AddScoped<MainViewModel>();
services.AddRunicViews();
await using var provider = services.BuildServiceProvider();
// Windows uses WebView2 and macOS WKWebView; Linux selects a toolkit explicitly.
var options = new DesktopHostOptions { Linux = new() { EmbeddedBackend = LinuxEmbeddedBackend.Gtk3WebKit41 } };
return RunicDesktopHost.Run(provider, options, async host =>
{
// Stops at startup when a registration, WebView runtime or window option is missing.
host.ValidateWindow<MainWindow>().ThrowIfInvalid();
await using var window = await host.OpenWindowAsync<MainWindow>(new RunicWindowOptions { Width = 800, Height = 600 });
await window.WaitForCloseAsync();
return 0;
});
public sealed partial class MainWindow : RunicWindow<MainViewModel>;
The same Window and async host => ... body run on CS-WebUI with
RunicCsWebUiHost.Run(provider, ...) from Runic.Application.Views.CsWebUi.
Entry points
RunicDesktopHost.Run(provider, options, application)starts aDesktopHostand runs the asynchronous application on the event loop the platform needs (DesktopEventLoop): the process main thread on macOS and with GTK 4. Call it fromMainbefore anyawaitand return its result. Ctrl+C and SIGTERM close the open windows, soWaitForCloseAsyncreturns, and cancelhost.Desktop.ShutdownRequested. A body without a window, such as one behind a status icon, setsDesktopHostOptions.HandlesShutdownWithoutWindowand ends its loop on that token; without that option, a signal that arrives while no Window is open or served ends the process with its default action.provideris anyIServiceProvider, such as Generic Host'shost.Services; the configuration guide shows how to start hosted services and readappsettings.jsonwith this host.host.OpenWindowAsync<TWindow>(options, cancellationToken), fromRunic.Application.Views, creates the Window's DI scope, resolves its ViewModel, opens aDesktopSurfaceand its presentation, and attaches the generated Bridge. The surface servesRunicWindowOptions.ContentRoot(wwwby default) and opensStartPage;WidthandHeightreplace the window size. For other surface or window options, pass aRunicDesktopHostOptionstoRunicDesktopHost.Run(provider, desktopOptions, hostOptions, async host => ...), or tonew RunicDesktopHost(provider, desktop, hostOptions)for a host on aDesktopHostyou started yourself.RunicDesktopHost.DefaultWindowOptionsopens the embedded WebView and falls back to an installed browser; change one option of every Window withwith, such as the native spell checker, which an application that brings its own turns off (Spell checking):var hostOptions = new RunicDesktopHostOptions { WindowOptions = RunicDesktopHost.DefaultWindowOptions with { SpellChecking = false }, };A build with
RunicApplicationFrontendDelivery=Embeddedembeds the frontend in the assembly, and the host serves that archive in place ofwww/; see Ship the frontend inside the executable.RunicDesktopHostOptions.Assetsserves anotherIAssetSource, such as an archive read withAssetArchive.ReadAsync.SurfaceOptions, a Window'sContentRootanddotnet runic dev's live frontend take precedence.RunicDesktopHostOptions.ContentSecurityPolicysends aContent-Security-Policywith the HTML of every Window's surface, unlessSurfaceOptionsset one of their own (DesktopSurfaceOptions.ContentSecurityPolicy). It is a header of its own next to the surface'sframe-ancestorspolicy, and the WebView enforces both. The Bridge needs'self'scripts and connections and no'unsafe-eval':DesktopBridgeTransportsends Views pushes as raw data to__runicViewsPush, which the served bridge script hands torunic-desktop-views.jswithout evaluating them, sodefault-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'works and blocks inline, foreign and evaluated scripts. OnlyDesktopSurface.ExecuteJavaScriptAsync, which evaluates a string in the page, needs'unsafe-eval'.RunicDesktopHostOptions.Resourcesserves application files that are not part of the frontend build, such as icons from a cache, from each Window's own address. It is called with each Window as it opens, after its ViewModel is set, and returns that Window'sContentHandlerornull. The surface asks the handler only for a request its content has no file for, before 404, so the page loads<img src="icons/org.example.App.png">alike in a native window, in a browser after a fallback and underRUNIC_APPLICATION_SERVE_ONLY. The request path is decoded and can contain..: map it to known files rather than to the file system.var hostOptions = new RunicDesktopHostOptions { Resources = window => window is DashboardWindow ? icons.ServeAsync : null, };host.OpenWindowAsync<TWindow>(options, activation, cancellationToken)onRunicDesktopHost, and since 0.14 on anyIRunicWindowHost(CS-WebUI opens the Window and ignores the context), opens a Window for a launch or a click: it opens the Window as above and presents it with the desktop's activation token or startup ID, such asApplicationLaunch.PlatformContextof a launch the running instance received. Wayland compositors such as KWin give a new window focus only with that token. A Window that is already open is brought forward withDesktopPlatformServices.PresentAsync(window, context)instead.host.ValidateWindow<TWindow>(options)checks, before any window opens, that the generated Bridge for the ViewModel is registered and thatDesktopHost.Validateaccepts the window options. It returns every problem as aRunicWindowDiagnosticwith a stable code and remediation: an errorbridge-not-registered, the Runic Desktop codes, and a warningviewmodel-not-registeredwhen the container does not report the ViewModel throughIServiceProviderIsService. A container can resolve services it does not report, so only the Bridge check fails; a container withoutIServiceProviderIsServiceis not checked.ThrowIfInvalid()throws aRunicWindowConfigurationException.OpenWindowAsyncruns the same checks before it creates a surface and throws the same exception; aDesktopConfigurationExceptionfrom Runic Desktop becomes itsInnerException. A missing Bridge is logged as event 1050 through the provider'sILoggerFactory.DesktopBridgeWindow<TViewModel>, the Window'sHost, owns the Window's scope, surface, and attachment. It exposesViewModel,Surface,Window,WaitForCloseAsync, andCloseAsync, which stops new operations and waits for accepted ones before releasing the scope.RunicDesktopHost.GetSurface(window)returns theSurfaceof any Window of this host without naming its ViewModel type, andnullfor another host's;DesktopPlatformServices.GetDesktopWindow(window)returns itsDesktopWindowthe same way, alsonullunderRUNIC_APPLICATION_SERVE_ONLY=1, for example to hide it withHideAsyncor subscribe toClosed. It implementsIBridgeWindow, the lifetime contract shared with the CS-WebUI host, and returns the sameRunicWindowCloseResult.A Window with
GuardClose(see Guarding a user close) passes itsCanCloseAsyncto the embedded window asDesktopWindowOptions.ConfirmCloseAsync, ahead of aConfirmCloseAsyncofWindowOptions, so the title-bar close button, Alt+F4 or the window manager asks the navigation regions' departure guards.window.RequestCloseAsync()is the same native close request and shares its decision;CloseAsync, disposal, Ctrl+C and SIGTERM do not ask. An explicitly selected browser, a browser fallback (EmbeddedThenBrowser,Fallbacks), or a custom window host without theCloseConfirmationcapability opens the Window unguarded for the user's close and logs event 1053WindowCloseGuardUnavailable;RequestCloseAsyncstill asksCanCloseAsyncthere.A Window with a close drain (
RunicWindowOptions.CloseDrain, orRunicDesktopHostOptions.CloseDrainfor every Window; experimental,RUNICDESK105) keeps its embedded window open after the user's close,RequestCloseAsyncor a quit was approved, until its operations, awaited commands and drained scopes have ended (see Closing after the Window's work has drained). The host setsDesktopWindowOptions.CloseDrainAsyncfor it, after a drain ofWindowOptions' own. The page stays connected, and the appearance feed and connection bindings stay attached while the window is visible; the drop queue and menu commands stop at once. Ctrl+C, SIGTERM and a quit cancelDesktopHost.ShutdownStarting, drain such windows visibly for up toDesktopHostOptions.ShutdownDrainTimeoutand then cancelShutdownRequested(closing after the work has drained). An installed browser and serve-only mode close at once and log event 1055WindowCloseDrainUnavailable; the Window's scope still waits for durable work.DesktopPlatformServices.AnnounceAsync(window, message, priority)asks the screen reader to speak a status message for a Window, such as "Saved", through its embedded window (DesktopWindow.AnnounceAsync; see Accessibility). WebKitGTK reports no ARIA live region to Orca, so on Linux this is how a page's status reaches a screen reader. A ViewModel takesIWindowPlatformServicesand calls itsAnnounceAsync(message, priority), which works on every host, or takesDesktopWindowPlatformServicesand calls itsAnnounceAsync(message, priority)with aDesktopAnnouncementPriority. All returnSuccess, orUnavailablewithOwnerUnavailablefor a Window without an embedded window (an installed browser, serve-only mode, another host, or a ViewModel's window that has not opened yet),OwnerClosedonce it closed, andBackendUnavailablefor a window host without announcements, such as GTK 4 before 4.14.The window's locale (
IRunicWindowLocale, set before the window opens) gives an embedded window on Windows and Linux its native direction (DesktopWindowOptions.LayoutDirection, right to left forar,he,faand other right-to-left scripts) and the web view's language (DesktopWindowOptions.Language: built-in text such as the context menu and form validation,navigator.language). A later locale change, including one while the window opens, mirrors the open window or returns it to left to right (DesktopWindow.SetLayoutDirectionAsync), and anulllocale returns it to the platform's direction. Changes apply in order, so the window ends with the direction of the last one. The script of a tag counts only right after its language:ar-u-nu-latn(Arabic with Latin digits) stays right to left. The web view's language stays until the window opens again. ALayoutDirectionorLanguagethatWindowOptionssets wins, and aProfilePathkeeps the platform's language, since WebView2 needs one language per profile. macOS takes both from the application bundle. The page sets its own<html lang dir>(syncDocumentLocalein@runic-artifex/views); the pages the window serves itself, such as Resource Not Available, takelanganddirfrom the window locale and their text fromDesktopPlatformTexts.RunicDesktopHost.Placementrestores each Window's size, position and maximized state from the last time it closed, andDesktopPlatformServices.GetWindowState(window)reads them to save. The callback is called with each Window as it opens, after its ViewModel is set, and returns the savedDesktopWindowPlacementornull; it becomesDesktopWindowOptions.Placement, which replaces the Window's size and keeps it on a monitor that exists now (see Window state and restore).GetWindowStatereturns the embedded window'sDesktopWindowState, also after the window closed and until the Window is disposed, so save it afterWaitForCloseAsyncreturns:return RunicDesktopHost.Run(provider, new DesktopHostOptions(), new RunicDesktopHostOptions { Placement = window => window is MainWindow ? settings.MainWindowPlacement : null, }, async host => { await using var window = await host.OpenWindowAsync<MainWindow>(); await window.WaitForCloseAsync(); if (DesktopPlatformServices.GetWindowState(window) is PlatformResult<DesktopWindowState>.Success { Value: var state }) settings.MainWindowPlacement = state.Placement; return 0; });A Window without a native owner, one on CS-WebUI or another host, one served under
RUNIC_APPLICATION_SERVE_ONLYor an installed browser after a fallback, reportsUnavailablewithOwnerUnavailable, asPresentAsyncdoes; a custom window host that reports no state reportsBackendUnavailable. For each change while the window is open, subscribe toDesktopWindow.StateChangedof the host'sDesktopBridgeWindow<TViewModel>.Window. A placement belongs to the machine and operating system that saved it: its units are each platform's. A Window that is already open takes a placement withIRunicWindowPresentation.SetPlacementAsync, from a ViewModel too:ToRunicPlacement()converts a savedDesktopWindowPlacement, andToDesktopPlacement()converts theIRunicWindowPresentation.Placementa ViewModel saved back forRunicDesktopHost.Placement. Both use the units ofDesktopWindowState.Placement(Maximizing and placing the Window).host.UseApplicationMenu(commands)runs the application's menu bar commands by id until the returned object is disposed. They are application-wide, theDesktopHost.MenuCommandsof every host on that Desktop host, so a second call before the first is disposed throws. A Window's root ViewModel that implementsIRunicMenuCommandSourceruns its own while the Window is key (see Menu bar commands). Declare the menus inDesktopHostOptions.MenuBar; macOS and the GTK 3 and GTK 4 windows show them. WithMenuBarPresentation = DesktopMenuBarPresentation.PageinWindowOptionsthe page renders the bar with its own components throughcreateMenuBarControllerin@runic-artifex/views; every Window serves the reserved routerunic.menu-baron its Views connection, with menu bar page protocol 2 (see In-page menu bar), and a page's invoke runs throughDesktopWindow.TryExecuteMenuItemAsync. Each item'sCommandParameteris the command's parameter. A ViewModel that takesIWindowPlatformServicesshows a native context menu withShowContextMenuAsync, whichDesktopWindowPlatformServicesimplements for the embedded window.A Window's root ViewModel that implements
IRunicFileDropTargetreceives the files the user drops onto the embedded window, with their native paths, in its model turn (see Receiving dropped files). A window served underRUNIC_APPLICATION_SERVE_ONLYor shown in an installed browser after a fallback receives none.services.AddRunicPlatformServices()registers native file dialogs, the clipboard, and the file launcher for each window scope:IWindowPlatformServices,IFileDialogs,ITextClipboard,IDesktopClipboard(HTML, RTF, images, files and a format watch), andIDesktopFileLauncher;IWindowPlatformServicesalso sets the window's taskbar badge and progress (Windows), the Dock tile's (macOS) or the application's Unity launcher entry (GTK 3 and GTK 4), whichAddRunicPlatformServices(applicationId)names with the application ID (inside Flatpak the Flatpak ID serves). A ViewModel takes them in its constructor before its window opens. They bind to the window's native owner when it opens, and the provider is selected from the backend that is actually running:- Windows:
Runic.Platform.Windows. - macOS:
Runic.Platform.MacOS. - Linux with GTK 3:
Runic.Platform.Linux. - Linux with GTK 4 (
WithGtk4()): xdg-desktop-portal dialogs and the file launcher throughRunic.Platform.Linux.Portal, parented withRunic.Platform.Linux.Gtk4, plus the GTK 4 clipboard. A GTK 4 process never loads the GTK 3 provider.
Until the window opens, every operation returns
Unavailable(OwnerUnavailable). It returns the same result when the window has no native owner, for example an installed browser after an embedded-window fallback, except for file dialogs and message boxes that ask forOwnerPolicy.PreferOwner, which show without an owner, and the file launcher, which is the application's. After the window closes, every operation returnsUnavailable(OwnerClosed). A Linux process without a GTK backend returnsUnavailable(ProviderNotConfigured).GetSnapshot()reports the same states per capability. The package references every provider package, but loads only the selected provider assembly. An application that targetsnet10.0-windows, such as a WPF application, gets an assembly that references onlyRunic.Platform.Windows, so it does not ship the Linux and macOS providers.dotnet runic doctornames the provider and, on Linux, checks for the session bus and xdg-desktop-portal.DesktopPlatformServices.ForWindow(owner)creates the same services for aDesktopWindowthat the application opened itself.services.AddScoped<NotesViewModel>(); services.AddRunicViews(); services.AddRunicPlatformServices(); public sealed partial class NotesViewModel(IFileDialogs files) : ObservableObject { [ObservableProperty] private string _text = ""; public async Task OpenAsync() { if (await files.OpenFileAsync(new() { Filters = [new("Notes", ["txt", "md"])] }) is not PickerResult<IReadFileLease>.Selected(var lease)) return; await using (lease) await using (var stream = await lease.OpenReadAsync()) using (var reader = new StreamReader(stream)) Text = await reader.ReadToEndAsync(); } // Replaces the file atomically, or leaves it unchanged. public async Task<bool> SaveAsync() { if (await files.SaveFileAsync(new("notes.txt") { Filters = [new("Notes", ["txt"])] }) is not PickerResult<ISaveFileLease>.Selected(var lease)) return false; await using (lease) { if (await lease.BeginWriteAsync(FileWritePolicy.RequireAtomicReplace) is not PlatformResult<IFileWriteTransaction>.Success(var transaction)) return false; await using (transaction) { await using (var writer = new StreamWriter(transaction.Content, leaveOpen: true)) await writer.WriteAsync(Text); return await transaction.CommitAsync() is FileCommitResult.Committed; } } } }Filters, the initial folder and
OpenFilesAsyncfor several files work with every provider; the Runic.Platform guide lists what each platform does with them.On Windows, Linux and for unsandboxed macOS selections, the save writes a staged sibling file and replaces the selection in one step. On Linux this also works inside Flatpak: the xdg-desktop-portal document view stages the sibling as a private host file and renames it onto the chosen file. A sandboxed macOS selection stages in the volume's item replacement directory instead and
NSFileManagerreplaces the chosen file.BeginWriteAsyncreturnsUnavailable(AtomicReplaceUnavailable)on Linux for a read-only or revoked grant before it changes the file, andCommitAsyncreturnsNotCommitted(Conflict)when the file changed or appeared since selection. Afalseresult therefore means the file was not changed: tell the user and offer Save As to another destination. Runic never falls back to overwriting the file in place. The portal guide describes the document-view mechanism and the GTK 3 opt-in. CS-WebUI'sAddRunicPlatformServices()registers the same services, which returnOwnerUnavailable.An editor that saves back to the file it opened keeps a document lease from
OpenDocumentAsync(orSaveDocumentAsyncfor Save As) and writes it on every save. AConflictmeans another program changed the file; reading it again makes its content the one the next save compares with:public sealed partial class EditorViewModel(IFileDialogs files) : ObservableObject { private IDocumentFileLease? _document; [ObservableProperty] private string _text = ""; public async Task OpenAsync() { if (await files.OpenDocumentAsync(new() { Filters = [new("Notes", ["txt", "md"])] }) is not PickerResult<IDocumentFileLease>.Selected(var document)) return; if (_document is not null) await _document.DisposeAsync(); _document = document; await ReloadAsync(); } // Also after a conflict, when the user chooses the other program's version. public async Task ReloadAsync() { if (_document is null || await _document.OpenReadAsync() is not PlatformResult<Stream>.Success(var stream)) return; await using (stream) using (var reader = new StreamReader(stream)) Text = await reader.ReadToEndAsync(); } // Ctrl+S and autosave, as often as needed. public async Task<FileCommitResult?> SaveAsync() { if (_document is null || await _document.BeginWriteAsync(FileWritePolicy.RequireAtomicReplace) is not PlatformResult<IFileWriteTransaction>.Success(var transaction)) return null; await using (transaction) { await using (var writer = new StreamWriter(transaction.Content, leaveOpen: true)) await writer.WriteAsync(Text); return await transaction.CommitAsync(); } } }The lease belongs to the window and is released when it closes. Keep a recent-files entry with
CreateBookmarkAsyncand reopen it withIDesktopFileBookmarks.OpenDocumentAsync, whichAddRunicPlatformServices()registers; the Runic.Platform guide describes conflicts, bookmarks and what each platform supports.- Windows:
AddRunicPlatformServices()also registersIDesktopSettings, the desktop's appearance preferences (color scheme, accent color, contrast and forced colours, reduced motion, text scale). They belong to the application, not to a window, so there is one singleton, whichDesktopPlatformServices.CreateSettings()creates when it is first resolved and the service provider disposes. Windows reads its settings, macOS reads AppKit, and Linux reads xdg-desktop-portal with either GTK version, without loading GTK. On other systems every read returnsUnavailable(ProviderNotConfigured), and so does CS-WebUI's registration. A ViewModel or service takes it in its constructor and watches it off the model context:await foreach (var result in settings.WatchAsync(cancellationToken)) if (result is PlatformResult<DesktopAppearance>.Success { Value: var appearance }) await context.InvokeAsync(() => DesktopScheme = appearance.ColorScheme.ToString(), cancellationToken);The multi-window example stores the app's own theme in a settings file and follows this preference when the theme is "system". Register your own
IDesktopSettingsbefore callingAddRunicPlatformServices(), for example in tests, to replace it.RunicDesktopHostwatches the sameIDesktopSettings(or, without the registration, the platform's own) while any Window is open, and pushes the appearance to each Window's pages (appearance/1, Appearance), so the page'suseAppearancesees the desktop's text scale, accent colour, contrast and forced-colours palette. A serve-only Window, and every Window of a host withDesktopHostOptions.NetworkExposure = AllInterfaces, gets none: their pages may run on another machine. A window the application opens itself gets the same push onceDesktopPlatformServices.AttachWindowServicesattached its content session (a window the application opens itself).AddRunicPlatformServices()also registersIDesktopNotifications, one singleton for the application, whichDesktopPlatformServices.CreateNotifications(applicationId)creates when it is first resolved and the service provider disposes. A ViewModel takes it in its constructor, with noOperatingSystembranch:services.AddRunicPlatformServices("org.example.Notes"); public sealed partial class NotesViewModel(IFileDialogs files, IDesktopNotifications notifications) : ObservableObject { private async Task SavedAsync(string name) => await notifications.ShowAsync(new DesktopNotification("saved", "Notes", $"Saved {name}.")); }The application ID is the one string you pass: on Windows the AppUserModelID your installer registers with the shell (an MSIX package's process uses the package's AppUserModelID instead), on Linux the reverse-DNS ID of the installed desktop entry (
org.example.Notesfororg.example.Notes.desktop); macOS uses the bundle identifier. Runic does not derive it from the assembly or the GTK application ID. The overload checks it for the running system and throwsArgumentExceptionwhen it is invalid there. Without an ID,AddRunicPlatformServices()lets the Linux portal infer the identity from the desktop session, which suits development, and Windows requests returnUnavailable(ProviderNotConfigured); so does CS-WebUI's registration. Creating the service sends nothing and opens no connection; the first request does, and success acknowledges submission, not that the desktop showed it. On Linux, a rejected desktop ID is reported asportal-identity-registration-failed, and portal diagnostics are logged as event 2003 through the provider'sILoggerFactory. Register your ownIDesktopNotificationsfirst to replace it, for example a fake in tests.AddRunicPlatformServices()also registersIDesktopStatusIcon,IDesktopLoginStartupandIDesktopInhibition, one singleton each for the application, created byDesktopPlatformServices.CreateStatusIcon(applicationId),CreateLoginStartup(applicationId)andCreateInhibition(applicationId). Inhibition keeps the system or display awake while an operation holds a lease:if (await inhibition.AcquireAsync(DesktopInhibitionEffects.SystemSleep, "Exporting documents") is PlatformResult<IDesktopInhibitionLease>.Success(var lease)) await using (lease) await ExportAsync();Windows uses a power request and macOS an IOKit power assertion; Linux asks the xdg-desktop-portal Inhibit interface without a parent window, so the request belongs to the application, not to a window. The application services need no native owner and work the same in an installed browser after a fallback; the window services report
OwnerUnavailablethere. The Runic.Platform guide lists every service's result without a native owner. RegisterIDesktopPrivilegedActionsyourself; it needs declared actions.AddRunicPlatformServices()also registersIDesktopVolumes, one singleton created byDesktopPlatformServices.CreateVolumes(), which lists, mounts, unmounts and ejects storage volumes and watches them. Linux uses UDisks2 on the system bus with either toolkit; Windows uses the volume management API and ejects as "Safely Remove Hardware" does; macOS uses Disk Arbitration. See Volumes.AddRunicPlatformServices()also registersIDesktopNetworkStatus, one singleton created byDesktopPlatformServices.CreateNetworkStatus(): online, metered and captive portal, read or watched, from the Network List Manager's connectivity hint on Windows, Network.framework's path monitor on macOS and the NetworkMonitor portal or NetworkManager on Linux; see Network status. Per window it registersIDesktopMessageBoxes, the window's native message boxes: task dialogs on Windows, also for a WPF window, sheets on macOS and GTK message or alert dialogs on Linux; see Message boxes.AddRunicPlatformServices()also registersIDesktopPowerEvents, one singleton created byDesktopPlatformServices.CreatePowerEvents(): sleep, wake, session lock and battery events fromWM_POWERBROADCASTand WTS on Windows,NSWorkspaceand IOKit on macOS, and logind and UPower on Linux. It decides on first use: once aRunicDesktopHostwithServeOnlywas built on the provider, or underRUNIC_APPLICATION_SERVE_ONLY=1, it reportsUnavailableand opens no watches; see Power events.AddRunicPlatformServices()also registersIDesktopRecentDocuments, the application's recent documents (DesktopPlatformServices.CreateRecentDocuments()): Open Recent and the Dock on macOS, Recent items and the Jump List on Windows, andrecently-used.xbelon Linux, the same lists asDesktopHost.NoteRecentDocumentAsync, with each outcome as aPlatformResult. It decides on first use: once aRunicDesktopHostwithServeOnlywas built on the provider, or underRUNIC_APPLICATION_SERVE_ONLY=1, it reportsUnavailable. WithAddRunicPlatformServices(applicationId)Linux names the application by its ID; see Document apps.AddRunicPlatformServices()also registersIApplicationPlatformServices, the one instance ofDesktopPlatformServices.ForApplication(): the application'sFileLauncheropens URIs and files and reveals files without a window, also before the first window opens, behind a status icon and for Windows served under serve-only mode, and itsMessageBoxesshow boxes that ask forOwnerPolicy.PreferOwnerwithout a window, such as a fatal start-up error. Windows hands them to the shell on an STA thread of the application's own, Linux asks the desktop portal without a parent window (either toolkit or none) and macOS asks NSWorkspace on the main thread. A window shown in an installed browser after a fallback uses it as its ownFileLauncher, until the window closes. Code that runs before the container exists callsForApplication()directly.public sealed class HelpLinks(IApplicationPlatformServices platform) { public ValueTask<PlatformResult<PlatformUnit>> OpenLogFolderAsync(string log) => platform.FileLauncher.LaunchAsync(log, DesktopFileOperation.Reveal); }The secret store and the application database are registered without an application ID, also by
AddRunicPlatformServices(applicationId).DesktopPlatformServices.CreateSecretStore(applicationId)andCreateApplications(applicationId)create them for an ID, checked as the overload above checks it; on Linux their portal requests and diagnostics then carry it, and the application database's portal connection registers it as notifications do. Register one before callingAddRunicPlatformServicesto use it. Secrets are stored the same way with or without an ID.DesktopBridgeWindow<TViewModel>.NativeOwner(aDesktopNativeOwner) is the presentation'sINativeWindowOwner, which the services above bind to. The window services (file dialogs, the clipboard, file handoff and message boxes) are created only here, throughDesktopPlatformServices.ForWindow; the providers keep them internal. Pass the owner where an API asks for a window, such asWindowsElevationOptions.Ownerfor the UAC prompt.The owner runs provider callbacks on the window's native thread and becomes unavailable when the window closes or the surface replaces it. A presentation without native dispatch still has an owner, but it reports
IsAvailable == false. Examples are an installed browser after an embedded-window fallback, or a custom host without a native handle. Applications that open aDesktopWindowthemselves create one withnew DesktopNativeOwner(window), or attach a scope's services withDesktopPlatformServices.AttachWindowServices, whoseNativeOwnerit is.DesktopBridgeTransportconnects aDesktopSurfaceto aWindowContentSessiondirectly, for applications that compose a surface themselves. It is anIBridgeConnectionTransport:Servedelivers the Views protocol 4 requests that the page sends on the one capability__runicViews,BroadcastAsyncpushes to every page of the surface andSendAsyncto one connection, both as raw data to__runicViewsPush. A connection is one authenticated WebSocket; its Desktop connection id is both its connection and its client key, so a reconnect is a new connection. A Window'sIRunicConnections(since 0.16) therefore lists the page's one connection, a new one after each reload, and itsCalleris that connection during a page's command or in-page menu click, andnullfor the native menu bar and host shortcuts (Who is connected, and who called). The requests of one connection are admitted in the order the page sent them. Disposing the transport removes the capability and stops pushes; the surface stays open. The per-routeBind,BindAsync,Publish,PublishAsyncandPublishUtf8Asyncof earlier releases are gone; see Custom window hosts for the transport contract.
The surface owns authenticated browser or embedded WebView sessions and
removable capability bindings; the application Window owns its ViewModel scope
and logical View lifetimes. DesktopHostOptions and DesktopWindowOptions
select the browser or embedded backend; the Runic Desktop guide lists the
native prerequisites for each platform.
The package's build targets copy runic-desktop-views.js into www/, also as
runic-views.js. It installs __runicBridge: requests go to __runicViews,
and pushes arrive on the WebSocket as they are sent, so the script does not
poll. Load webui.js (served by the surface) and runic-views.js
before the generated client in index.html; the same page then runs on
CS-WebUI, whose package copies its own client as runic-views.js. A project
that references both hosts sets RunicApplicationHost (desktop or
cswebui); see
the host of a page.
Pages that load runic-desktop-views.js keep working. runic({ desktop: true })
is not needed for a Views page. The adapter uses the same host-neutral client
contract as the CS-WebUI adapter, and its build defaults match
Runic.Application.Views.CsWebUi, so AddRunicViews() is generated in
<RootNamespace>.RunicViewsComposition.
The First Window on Runic Desktop example is a complete application. See the host selection guide to choose between this host and CS-WebUI.
An exception from the application body passed to Run closes the windows,
disposes the host and is rethrown by Run; Runic does not log it. The
crash reporting guide
shows how to log such crashes, capture a dump and keep NativeAOT symbols.
A window the application opens itself
An application that opens its DesktopWindow itself, with
DesktopSurface.OpenWindowAsync, for example to control its start-up order,
gives a DI scope's per-window services that window with
DesktopPlatformServices.AttachWindowServices (since 0.19). Its ViewModels
then take IRunicWindowPresentation, IRunicWindowLocale,
IRunicConnections, IRunicWindowContext and the platform services in their
constructors, as under RunicDesktopHost:
DesktopSurface surface = await desktop.CreateSurfaceAsync(surfaceOptions);
DesktopWindow window = await surface.OpenWindowAsync(new DesktopWindowOptions
{
Browser = BrowserKind.Embedded,
ConfirmCloseAsync = confirmClose, // what IRunicWindowContext.RequestCloseAsync asks
});
await using var scope = services.CreateAsyncScope(); // AddRunicWindowService(), AddRunicPlatformServices()
var viewModel = scope.ServiceProvider.GetRequiredService<ShellViewModel>();
using var transport = new DesktopBridgeTransport(surface);
using var content = new WindowContentSession(transport, new WindowContentSessionOptions
{
RootModel = viewModel,
ModelContext = scope.ServiceProvider.GetService<IRunicModelContext>(),
});
// Attach the generated Bridge to transport and content as before.
await using var windowServices = DesktopPlatformServices.AttachWindowServices(window, scope.ServiceProvider, content);
await window.WaitForCloseAsync();
- Call it once the window is open. ViewModels the scope resolved before keep
their services: the scope's own objects are the ones that are attached, so a
ViewModel that sets the window's colour scheme from its constructor's
IRunicWindowPresentationgets the results of aRunicDesktopHostWindow. - With
content, the window's locale follows and sets the session's, the native layout direction and the language of the surface's own pages follow it,IRunicConnectionslists the page's connections, and the page receives the desktop's appearance (appearance/1) when only this computer can reach it. Presentation changes then arrive in turns of the session's model context. - The direction follows the locale unless the window opened with a direction of
its own that is not its locale's. The web view's language is the one
DesktopWindowOptions.Languagegave it when it opened. IRunicWindowContext.RequestCloseAsynccallsDesktopWindow.RequestCloseAsync, so the window'sConfirmCloseAsyncdecides;IRunicWindowContext.Windowisnull, because there is noIRunicWindow. To keep the window open after an approved close until the application's work has ended, setDesktopWindowOptions.CloseDrainAsync = (_, ct) => new(work.WaitForAcceptedAsync(ct))when you open it; the request then returns at the decision.- It throws
InvalidOperationExceptionfor a closed window or a scope that a Window already uses, andArgumentExceptionfor a session of another surface. A scope withoutAddRunicWindowService()attaches what it has, andDesktopWindowServices.Presentationstill presents the window. - The binding ends when the window closes or the returned
DesktopWindowServicesis disposed: the presentation reportsOwnerClosed, the platform services stop, and the locale and appearance no longer reach the window. It never disposes the scope, the session or the window. A new window takes a new scope.
ViewModels without a scope take DesktopPlatformServices.ForWindowPresentation(window),
one presentation per window, as WpfPlatformServices.ForWindowPresentation does
for a WPF window. The
per-window services table
lists what each way of opening a window provides.
From a command line
An application with a command line, such as my-app ui next to other commands,
starts its Window from a
Runic.CommandLine command
with Runic Command Line 0.7.0-preview.2 or later (see its guide,
Start a UI host on the main thread). RunicDesktopHost.Run
and DesktopEventLoop.Run must be called on the process main thread before any
await, and a command runs asynchronously, so the command does not start the
host itself: it hands the host's start back to Main with
CommandExecutionContext.ContinueOnCallingThread, and Main returns
CommandApp.Run(args) instead of awaiting RunAsync. Run blocks until the
command completed, then runs the continuation on the thread that called it and
returns its result as the exit code:
using Runic.CommandLine;
using Runic.CommandLine.Generated;
// A synchronous Main, or no await before this line: the thread is the process main thread.
return new CommandApp(GeneratedCommandCatalog.Create()) { Name = "my-app" }.Run(args);
internal static class WindowCommands
{
[Command("ui", Description = "Open the main window.")]
internal static async Task Ui(CommandExecutionContext context,
[Option("--settings")] string? path = null, CancellationToken cancellationToken = default)
{
// Asynchronous work and its failures belong to the command, before the host starts.
AppSettings settings = await AppSettings.LoadAsync(path, cancellationToken);
context.ContinueOnCallingThread(() => DesktopEventLoop.Run(new DesktopHostOptions(), async desktop =>
{
await using var provider = new ServiceCollection()
.AddSingleton(settings)
.AddScoped<MainViewModel>()
.AddRunicViews()
.BuildServiceProvider();
var host = new RunicDesktopHost(provider, desktop);
await using var window = await host.OpenWindowAsync<MainWindow>();
await window.WaitForCloseAsync();
return 0;
}));
}
}
DesktopEventLoop.Run and new RunicDesktopHost(provider, desktop) let the
service provider be created and disposed asynchronously on the event loop.
RunicDesktopHost.Run(provider, options, application) works the same way in a
continuation for a provider built before it.
The continuation runs only when the command succeeded and was not cancelled,
after its output was written and its scope disposed, so capture values, such as
settings, rather than the command's scoped services. Help, usage errors and
the other commands never start the host, and a failed command returns its exit
code without opening a Window. Runic.CommandLine stops handling Ctrl+C before
the continuation starts; the Desktop host then handles Ctrl+C and SIGTERM as
described above. Under CommandApp.RunAsync, the hosting adapter's RunAsync
or a directly used CommandExecutor,
CommandExecutionContext.CanContinueOnCallingThread is false and
ContinueOnCallingThread throws.
Test such a command with CommandAppTester.Run from
Runic.CommandLine.Testing, which runs the continuation on the calling
thread, or start the application in serve-only mode with
RunicServeOnlyApplication from Runic.Application.Testing.
Hosted applications
A Runic Desktop application, the host, can show the Windows of another
Runic Desktop application, the guest, in native windows of its own. Typical
hosts are launchers, suite shells and IDE-like applications that run tools as
separate processes. Each guest keeps its own process, ViewModels, translations
and Runic version, so a guest that crashes takes only its own windows with it.
The two talk over the
Runic host protocol, runic-host/1.
The hosted windows example is a
shell that hosts a counter.
Experimental. The host and guest APIs, RunicTestHost and the protocol are
experimental: suppress
RUNICDESK103. They can change in
a minor release, also after 1.0.
Quick start. The guest opts in on a code path of its own. The host starts the guest's executable with the parameter that selects that path:
#pragma warning disable RUNICDESK103 // Hosted application windows are experimental.
// The guest (its Program.cs): only its own `hosted` parameter accepts a host.
if (args is ["hosted"])
return RunicDesktopHost.Run(provider, new DesktopHostOptions(),
new RunicDesktopHostOptions { Guest = RunicGuestOptions.FromStandardInput() },
async host =>
{
await using var window = await host.OpenWindowAsync<MainWindow>();
await window.WaitForCloseAsync();
return 0;
});
// The host: each StartAsync starts a guest process for this host session.
await using var tool = await host.HostedApplications.StartAsync(
new RunicGuestStartInfo("/opt/tool/Tool", ["hosted"]),
new RunicHostedApplicationOptions { Policy = new ToolPolicy() });
Hosted mode describes the guest side and
Hosting another application the host side. Test
a guest without a host application with RunicTestHost
(a hosted application).
An application changes behaviour only when it opts in. An application without a
guest code path cannot be hosted, and one that never calls StartAsync hosts
nothing.
Opt-in and policy. Runic defines no flag or environment variable for hosted
mode, so the processes a guest starts never inherit it. The host decides what a
guest may do with IRunicHostedWindowPolicy: which windows open and how, its
presentation requests, menus, dialogs and window services. Without a policy,
every window, menu and dialog shows unchanged. Window services follow
conservative defaults: the clipboard is read and written only for the window the
user works in, and only http, https and mailto links and files the user
gave the guest are opened. A policy can mark a guest's dialogs, refuse its message boxes or narrow
the launcher. The example's CounterPolicy does both.
Limits. RunicHostedLimits, from the policy's Limits, bounds a guest's
requests. Its defaults are 20 menu requests a second per window, 80 per guest,
one dialog at a time per guest, 10 dialogs per ten seconds, 20 clipboard and
monitor requests a second, 5 opened files or links per ten seconds, and 2
requests a second that bring a window to the front or announce text. The
protocol also bounds messages (1 MiB), waiting requests (64), menus (8 levels,
1,000 entries) and dialog texts
(limits).
Security in short. Both sides opt in. The channel is a loopback WebSocket
that only the host that started the guest can join: a nonce on the guest's
standard input and a key on its standard output prove it, and an upgrade from a
web page is refused. A guest's page gets none of the host's credentials or
cookies, and each guest session has its own web view data store. The guest
reaches its own windows only. Elevated guests, and guests of another user than
their parent, refuse hosted mode. Any process of the same user that knows a
guest's parameter can host it; RunicGuestOptions.AcceptHost is a product
filter, not a security boundary. A guest words its own dialog and menu texts,
so a host that does not trust a guest marks or refuses them in its policy. The
security guide has the
details.
Platform notes.
- Hosts and guests run on Runic Desktop only. A CS-WebUI or hosted application cannot host. A serve-only application hosts only in regions of its pages (browser hosts).
- Hosted windows have no browser fallback: a browser tab would send the user's
own
127.0.0.1cookies to the guest. The host needs an embedded web view. WebView2 and WKWebView are built in. On Linux the host selects one:DesktopHostOptions.Linux.EmbeddedBackend = LinuxEmbeddedBackend.Gtk3WebKit41, orWithGtk4()fromRunic.Desktop.Gtk4. - WebView2 gives each guest session an InPrivate profile in a user data folder of its own, deleted after the session, with a browser process of its own.
- On WKWebView and WebKitGTK, a guest session's data store lives only while one of its windows is open. A guest that opens a window after all its windows closed starts with empty cookies and storage.
- On macOS the one menu bar shows the host's application menu and then the guest's menus while a hosted window is key.
- On Windows a guest runs in a kill-on-close job object, which ends it also when the host crashes. On Linux and macOS it leads a process group, and a guest whose host crashed exits when its standard input ends.
Hosted mode
The application opts in on a code path of its own, reached by a parameter it
defines, such as a hidden hosted command. That path runs
RunicDesktopHost.Run with RunicDesktopHostOptions.Guest:
#pragma warning disable RUNICDESK103 // Hosted application windows are experimental.
if (args is ["hosted"])
{
try
{
return RunicDesktopHost.Run(provider, new DesktopHostOptions(),
new RunicDesktopHostOptions { Guest = RunicGuestOptions.FromStandardInput() },
async host =>
{
// When the host goes away while the Window opens, the open throws; Run then returns 0.
await using var window = await host.OpenWindowAsync<MainWindow>();
await window.WaitForCloseAsync();
return 0;
});
}
catch (RunicHostedApplicationException error)
{
Console.Error.WriteLine($"Not hosted ({error.Code}): {error.Message}");
return 1;
}
}
- Start. Before the body runs,
Runreads the host's offer from standard input, opens the host channel on a loopback listener of its own, writes its address and a random key to standard output and waits for the host to connect and prove that it started this process. A failure, such as no offer or a host thatRunicGuestOptions.AcceptHostdeclines, throwsRunicHostedApplicationExceptionwith itsCodeand logs event 2010. Hosted mode refuses to start withDesktopHostOptions.NetworkExposureother than loopback, or when it runs as another user than its parent or cannot read its parent's (guest-other-user). It refuses to start elevated (guest-elevated), unless the application setsRunicGuestOptions.AllowElevatedHostand its hostRunicGuestStartInfo.AllowElevatedGuest: then its parent must run elevated as the same user (guest-elevated-host-not-elevatedotherwise), which the application checks itself, and it logs event 2025. An unelevated host cannot allow an elevated guest (host-not-elevated). The security guide lists what the user's unelevated processes can still reach. - Windows. No window toolkit loop starts, and the application never shows a
window of its own. Each Window's page is served as in serve-only mode, and the
host opens a window that loads it with a one-time launch address; the host's
window policy may refuse it (
RunicWindowConfigurationExceptionwithhosted-window-refused). The Window'sIRunicWindowPresentation(present, minimize, visibility, full screen, document edited, colour scheme, activity) goes through the host. A user close of the host's window asks the Window's guard (GuardClose) first. A presentation request that the host has not answered within 30 seconds reportsFailedwithUnknown, and an open that the host has not answered by then fails withhosted-window-failed; the application's own event handlers for the presentation run off the channel, so a handler that waits cannot stall it. Recent documents (IDesktopRecentDocuments) reportBackendUnavailable. - Menus and dialogs (
menus/1,dialogs/1, when the host lists them). The application's menu bar (DesktopHostOptions.MenuBar,DesktopHost.SetMenuBar) shows in each hosted window, with each item enabled and checked as the Window's command path decides: the root ViewModel (IRunicMenuCommandSource,IRunicMenuCheckedSource), then the application's commands (UseApplicationMenu). A new bar, such as one with titles in another language, and each change of an item's state reach the host; an item the user chooses in the host's bar, or whose shortcut they press, runs through that same path. Withservices.AddRunicPlatformServices(), the Window'sIWindowPlatformServicesshows its context menus, file dialogs (IFileDialogs, documents included) and message boxes (IDesktopMessageBoxes) as the host's, owned by the host's window: the application gets the same leases and results as from its own dialogs, opens the selected files itself, and one dialog at a time (another reportsResourceBusy). A dialog the host's policy refuses reportsPermissionDenied; one whose window closes reportsOwnerClosed; cancelling the call closes the host's dialog. Every menu or dialog reportsOwnerUnavailable, as in serve-only mode, when the host does not list its feature. - Appearance and locale. The Windows' pages show the host's appearance
(colour scheme, contrast, forced colours and their palette, reduced motion,
text scale, accent colour), never this process's own desktop settings: the
windows show on the host's desktop. With
AddRunicWindowService, the application's locale (IRunicApplicationLocale) follows the host's, already when the body starts, until the application sets one of its own withSetLocaleAsync; settingnullfollows the host's again. Each Window that follows the application follows it too, and the host's window takes its locale's direction.RunicDesktopHost.Guesthas the host'sLocale, its user'sLanguages(to negotiate against the application's catalogs instead ofDesktopLanguages.Preferred) and itsAppearance. A follow that fails is logged (event 2023). - Window services (
window-services/1, when the host lists it). The Window's clipboard (IDesktopClipboard: text, HTML, RTF, PNG images and file lists), monitors (IDesktopMonitors, with a watch that follows the host's changes) and file launcher (IDesktopFileLauncher) are the host's, and the files the user drops onto the host's window reach a root ViewModel that is anIRunicFileDropTarget. The host decides each request: by default it reads and writes the clipboard only for the window the user works in (not one the application presented or opened itself until the user switches to it, uses its menus or drops files onto it), opens onlyhttp,httpsandmailtolinks and the files the user gave the application through its open dialogs or a drop, each for that window, and bounds the rate; a refusal reportsPermissionDenied, a turned-away rateResourceBusy. A page's own copy and paste (Ctrl+C, Ctrl+V) use the host's web view and are not affected. Your first window starts as the application's, not the user's: the application opened it, so a button in it that copies, pastes or opens a link through these services getsPermissionDenieduntil the user switches away and back, clicks its native menu bar (not on GTK 4 yet) or drops files onto it. The same holds after the application presents, shows, maximizes or opens a window, also when the desktop activates it only later, such as macOS when the user next switches to the host. A host that started the application at the user's request can mark its first window as the user's (RunicGuestStartInfo.UserStarted). The clipboard's watch reportsBackendUnavailable: a hosted application is not told when the user copies. Drags out of the window (StartDragAsync), the taskbar badge and progress reportOwnerUnavailable, as in serve-only mode; notifications, the status icon, global shortcuts, settings and the secret store stay the application's own. Announcements (AnnounceAsync) go to the host's window, at a bounded rate; an assertive one from a window the user is not working in is made polite.Presentand showing a window are rate-bounded too. - End. When the host asks the application to shut down, or goes away
(standard input ends or the channel closes), the Windows close,
DesktopHost.ShutdownRequestedandRunicDesktopHost.Guest.HostGoneare cancelled andRunreturns when the body does (event 2011). A Window that is opening or opens after that fails withhosted-host-gone; when the body ends with that failure, or with anOperationCanceledException,Runreturns 0 (event 2014): the host going away is the normal end of a hosted application, so the body need not catch it. A body that has not returned 10 seconds later ends the process (event 2012). RunicDesktopHost.IsHostedandRunicDesktopHost.Guest(the host's name and version) tell the mode. A hosted application does not claimApplicationInstance: each host session starts a process of its own.- Test it without a host application with
RunicTestHostfromRunic.Application.Testing(a hosted application).
Hosting another application
The host side is IRunicHostedApplications, from RunicDesktopHost.HostedApplications or, after
services.AddRunicHostedApplications(), from the services. It starts the guest's executable with the parameter
that selects its hosted mode and shows each of the guest's Windows in a native window of the host (window mode):
#pragma warning disable RUNICDESK103 // Hosted application windows are experimental.
var applications = host.HostedApplications;
applications.WindowOpened += (_, e) => Console.WriteLine($"{e.Window.Application.Guest.Name} opened {e.Window.Type}");
await using var tool = await applications.StartAsync(
new RunicGuestStartInfo("/opt/tool/Tool", ["hosted"]),
new RunicHostedApplicationOptions { Policy = new ToolWindowPolicy() });
var exitCode = await tool.Exited;
- Start.
StartAsyncstarts the executable directly, never through a shell, with standard input and output piped, standard error and the other output lines in the log (events 2021 and 2022,Debug), and withoutRUNIC_APPLICATION_SERVE_ONLYandRUNIC_APPLICATION_CLOSE_AFTER_OPEN. On Windows the guest starts suspended, joins a kill-on-close job object and only then runs, so nothing it starts escapes the job, also when the host crashes; a job that cannot be created or joined fails the start. A batch file (.bat,.cmd) is refused withArgumentException, sincecmd.exewould parse its arguments. On Linux and macOS the guest leads a process group of its own, which ends with it; on Linux it inherits no descriptor but its standard streams (glibc 2.34 and later). A host that exits normally ends its guests' groups; a host that crashes or is killed cannot, and its guests exit when their standard input ends, within 10 seconds, while what a guest started itself may outlive it. The start writes the offer, reads the channel line, connects, without a proxy, and checks the guest's proof withinStartTimeout(30 seconds); a failure ends the guest's process tree and throwsRunicHostedApplicationException(guest-start-failedfor an executable that cannot be started,guest-start-timeout,guest-exited,guest-not-hostable,guest-refused,protocol-unsupported; event 2016). A shutdown or disposal of the host while a start is pending fails it withInvalidOperationExceptionand ends the guest. - Windows. Each guest Window becomes a
RunicHostedWindow, whosePresentationis a nativeDesktopWindowof the host, embedded and without a browser fallback. It loads the guest's one-time launch address top-level from the guest's loopback origin: the host injects none of its credentials, admits no other origin for top-level navigation and gives each guest session a data store of its own that is not persisted. A user's close, andRequestCloseAsync, ask the guest first; the window closes when the guest's Window has. The guest's presentation requests (present, minimize, visibility, full screen, maximize, size, position, document edited, represented file, colour scheme, layout direction, announcements) apply to that window, and its activity, placement and colour scheme go back to the guest; a guest that reads slowly receives the latest placement and activity, not every change.IRunicHostedApplications.WindowOpenedandWindowClosedreport the windows, one after the other and in order: add the handlers beforeStartAsync. A window that finishes opening after its guest went away closes at once and raises neither. - Menus. Each hosted window shows the guest's menu bar, never the host application's own
(
RunicHostedWindow.MenuBaris what it shows). On Windows and Linux it is the window's own bar, in the window or the desktop's global menu. On macOS the process has one menu bar: while a hosted window is key, it shows the host's application menu, whose About, Settings and Quit and Application-role items stay the host's, then the guest's menus: the guest's File menu in the File menu's place, its Help menu last, its others before the Window menu, and an Application-role menu of the guest as an ordinary menu under its own title. The standard Edit, View and Window menus stay in the host's language; the host's other menus give way until a window of the host is key again, and a shortcut both use is the host's. Shortcuts are matched by the host's native menus, and an item the user chooses runs the guest's command (menu-invoke);DesktopWindow.TryExecuteMenuItemAsynconPresentationchooses one from code. A guest's View or Window menu, titled as the host's standard one, joins it on macOS. The guest's context menus show as native context menus of its window, only while the window is active and inside its web view. - Dialogs. The guest's file dialogs (open, open several, save, folder, folders) and message boxes show as the platform's own dialogs owned by the guest's window, one at a time; the guest receives the selected paths and opens them itself, and the host keeps no access to them: a sandboxed host or guest (App Sandbox, Flatpak) can receive paths it may not open, as the picker's grant stays with the process that showed it. Closing the window, the guest giving the dialog up or the guest going away closes a dialog that is shown. The titles, texts and button labels are the guest's; see security for marking them.
- Appearance and locale. The guest starts with the host's application locale (
IRunicApplicationLocale, afterAddRunicWindowService), its user's languages (DesktopLanguages.Preferred) and the appearance its own windows show (fromIDesktopSettings), and the host sends each change. A guest Window's locale gives its window the locale's direction, unless the host's window options fix one (RunicHostedWindowRequest.Locale). - Window services. With
window-services/1a guest uses the host window's clipboard, monitors and launcher, with the host's rights, and receives the files the user drops onto a window whose Window takes them; dropped files without a path, such as macOS file promises, are not forwarded.IRunicHostedWindowPolicy.ServiceAsyncdecides each request (RunicHostedServiceRequest); by default (IsAllowedByDefault) the host reads and writes the clipboard only for the window the user works in (active, and not within two seconds after any guest's request that can activate one of its windows, such as opening, presenting, maximizing or closing one, nor the first activation of a window the guest asked to activate while the desktop deferred it:IsGuestActivated; writes also within a second after the user left it), lists monitors for any window, and opens onlyhttp,httpsandmailtolinks and the files the user gave the guest through the host's open dialogs or drops, while each is still the same file, only for the window the user works in. A policy can widen or narrow this, change a launch (such as reveal instead of open) or forward only some dropped paths; see security before widening it. The guest is never told when the clipboard changes, and cannot drag files out of its windows. A guest's windows start guest-activated, its first one included, so a link the user clicks in a fresh window is refused until the user switches to it. When the user started the guest directly, such as from the host's own menu,RunicGuestStartInfo.UserStartedmakes the first activation of its first window the user's. It is off by default: the guest chooses when it opens that window, so with it set the guest can open it later, while the user works elsewhere, and still have it counted as the user's. - Limits.
IRunicHostedWindowPolicy.Limits(RunicHostedLimits) bounds a window's menu requests (20 a second by default; Runic's guest sends its latest menus again when one is turned away), the dialogs a guest shows at once, across its windows (one by default; another reportsResourceBusyto a Runic guest), its clipboard and monitor requests (20 a second), the files and links it opens (5 per ten seconds), its requests that can activate a window (present, minimize, set-visible, set-full-screen, toggle-maximized, maximize, restore and set-placement: 2 a second after a burst of 3) and its announcements (2 a second after a burst of 5). The host rebuilds a window's native bar at most every 50 milliseconds, however many changes arrive. - Elevated guests. A guest that an elevated host starts runs elevated too, and refuses hosted mode
(
guest-refusedwithguest-elevated). An elevated host whose guest needs administrator rights as well, such as an installer that hosts a check tool, setsRunicGuestStartInfo.AllowElevatedGuest, and the guestRunicGuestOptions.AllowElevatedHost; the guest then checks that its parent runs elevated as the same user. The host must run elevated (host-not-elevatedotherwise) and name the guest by a fully qualified path, in a location only administrators can change; see security. - Permissions. A guest's request for the camera and microphone is granted only as far as the host grants its
own windows (
RunicDesktopHost.WindowOptions.AllowedPermissions); a host that grants nothing grants a guest nothing. A policy can grant more throughRunicHostedWindowDecision.Allow(request.WindowOptions with { ... }). - Policy.
IRunicHostedWindowPolicydecides each window (OpenWindowAsync: refuse, or open with otherDesktopWindowOptionssuch as a saved placement keyed byGuest.ApplicationId) and each presentation request (Present: apply, clamp or refuse; the request a policy returns applies as it is), each menu bar and context menu (Menu: show, show changed entries, such as without an item or with an item of the host's own, whose command is then the host application's, or refuse), each dialog (DialogAsync: show, show with other options of the same kind, such as another initial directory, or refuse) and each window service (ServiceAsync). Without a policy, every window of the guest opens, every request, menu and dialog applies, and window services follow the defaults above. - End. When the guest exits or crashes, or its channel closes, the host closes its windows, ends its process tree
and completes
Exitedwith its exit code (event 2017).StopAsyncasks the guest to shut down and ends it afterStopTimeout(10 seconds);RequestStopAsyncasks each window first;KillAsyncends it at once. A guest whose Windows have a close drain reports eachwindow-closedafter its drain, and the host keeps the window, or the region, open until then; onshutdownthe guest drains within its ownShutdownDrainTimeout(30 seconds), so setStopTimeoutabove it for such a guest. The guests still running stop when the host's Desktop shuts down and whenRunicDesktopHost.Runreturns. A hosted application cannot host; a serve-only one shows guests' windows only in regions (browser hosts).
Each engine keeps a guest session's cookies, storage and caches apart from the host's and from other guests':
| Engine | Data store of a guest session | Isolation |
|---|---|---|
| WebView2 | An InPrivate profile named for the session, in a user data folder of its own in the temporary folder that is deleted after the session | Cookies, storage and caches in memory, in a browser process of its own. The folder holds only the browser's own files. |
| WKWebView | WKWebsiteDataStore.nonPersistentDataStore |
Cookies, storage and caches in memory, shared by the session's windows. |
| WebKitGTK 4.1 (GTK 3) | An ephemeral WebKitWebContext |
Cookies, storage and caches in memory, in a network process of its own. Spell checking and the page language are set when the session's first window opens. |
WebKitGTK 6 (Runic.Desktop.Gtk4) |
An ephemeral WebKitNetworkSession |
Cookies, storage and caches in memory. |
On WKWebView and WebKitGTK the store lives only while a window of the session is open: a guest that opens a window
after all its windows closed starts with an empty store, cookies, localStorage and IndexedDB included. On WebView2 the
store lives for the whole session. HTTP authentication, client certificates and the system's proxy and
trust settings are the engine's and the user's, not the store's. A custom IDesktopWindowHostFactory shows hosted
windows only if its window host implements IDesktopEphemeralDataStoreWindowHost.
Showing a guest's window in a region
With region/1, which Runic's guests of 0.18 and later implement (RunicHostedWindowRequest.CanEmbed), the policy
can show a guest's Window in a region of one of the host's own pages instead of in a window of its own, such as an
installer whose step page shows a check tool between its header and footer. The page declares the region with Runic's
<runic-hosted-region name="check"> element, and the region shows the guest's page in a frame from the guest's own
loopback origin (specification). The
hosted regions guide covers both sides and troubleshooting, and the
hosted region example is such an installer:
#pragma warning disable RUNICDESK103 // Hosted application windows are experimental.
sealed class CheckPolicy : IRunicHostedWindowPolicy
{
public ValueTask<RunicHostedWindowDecision> OpenWindowAsync(RunicHostedWindowRequest request, CancellationToken cancellationToken) =>
ValueTask.FromResult(request.Owner is null && request.CanEmbed
? RunicHostedWindowDecision.Embed("check")
: RunicHostedWindowDecision.Refuse("The installer shows the check in its step page."));
}
Attach.
Embed(name)needs one host window to declare the region, now or later;Embed(hostWindow, name)names the host's Window. Two host windows that declare the name when the window opens fail the guest's open withregion-ambiguous. The window is open at once (RunicHostedWindow.PresentationKindisRegion, andPresentationthrows), andRunicHostedWindow.Regionfollows it:Waitinguntil a page declares the region,Loadingwhile the guest's page loads,Attachedonce it connected,DetachedandEnded. Its events, andIRunicHostedApplications.RegionAttachedandRegionDetached, are raised in order with the window events.Detach and attach again. When the page removes the region, reloads or navigates, the window is detached and stays open; it attaches again with a new frame launch address when a region of the same name appears in the same host window (
RunicHostedRegionOptions.ReattachOnReload, on by default).DetachAsyncandMoveToAsync(window, name)let the host detach the window or move it to another region. When the host window closes, its region windows close.Departures. A departure the host decides, such as a wizard's Back or Next, asks the guest first: register
region.AsDepartureGuard()with the step's navigation entry, or callRequestCloseAsync. The guard lets the departure go ahead once the guest allowed the close and its Window closed.Ended and empty. When the guest closes its Window or goes away, the region is
Endedand the page shows the element's fallback content; showing a "run the check again" button that starts a new guest is the host's choice (RunicHostedApplication.Exitedhas the exit code).In a region.
presentbrings the host window forward and moves the focus into the region only when the host page holds no focus;RegionOptions.FocusOnAttachdoes the same when the window attaches. The guest'sresizeis a preferred size (PreferredWidth,PreferredHeight), which a region declared withsizing="content-height"takes withinMinimumHeightandMaximumHeight; the title of the guest's page (Title,TitleChanged) names the frame, andIsDocumentEditedcan mark the step. Minimize, visibility, full screen, maximize, moves, colour schemes and the represented file do not apply (embedded). The guest's menu bar is not shown; its context menus, dialogs, monitors and announcements are the host window's, and a detached window's dialogs reportOwnerUnavailable.Window services. The guest works in the window when the host window is active and the user moved the focus into the region with Tab, Shift+Tab or a click; focus that
present, the host or the guest's own script moved there is the guest's (IsGuestActivated).Permissions. The frame gets the camera and microphone only as the window would (
RegionOptions.Permissionswidens it).Origins. The guest serves the frame from the loopback family the host page does not use, so no cookie of the host page reaches it. An embedded host allows the guest to fall back to the same family where the other cannot be bound, and logs it (event 2027). The frame's embedder is the origin with which the page connected, such as a development server's. The guest's storage in a region lives in the host window's profile: treat it as session-scoped. A refused or failed attach leaves the window detached (event 2026).
Content-Security-Policy. A host page with a policy allows the frame with
frame-src http:: the frame comes from[::1]on a port that changes with each run, which a CSP host source cannot name. A guest page whose own policy setsframe-ancestors 'none'blocks its own region, and a guest Window that shows a development server's address cannot be attached: a frame shows only content that the guest serves itself.Drops, permissions and keys. The host window's web view routes the attached frame to its guest (
Runic.Desktop/README.md): a drop onto the frame reaches the guest asfiles-droppedunder the policy (RunicHostedServiceKind.FileDrop), never the host page; the frame's camera and microphone requests are decided by the region's permissions, except on WebKitGTK, which denies them; a shortcut the guest's page leaves unhandled runs the host window's menu item. A window host of the application's own, such as Runic.Desktop.Gtk4's, routes none of these: drops onto the frame are discarded there.Crashes. When the engine reports that the frame's content process ended, the region detaches with
crashed; a frame of its own process (WebView2) attaches again with a new code, and a host page whose own process ended (WebKit's frames share it) declares the region again when it reloads.Elevated guests. A guest of the elevated pair admits connections, also its frame's, only from elevated loopback peers (event 2029). On macOS, which has no peer lookup, it refuses region mode, and
Embedleaves the window detached (event 2026,refused).
Browser (serve-only) hosts
A serve-only application (RunicDesktopHostOptions.ServeOnly or RUNIC_APPLICATION_SERVE_ONLY=1) is a browser
host: its pages run in the user's browser, and it shows guests' windows only in their regions
(specification). Start guests and write the policy as above;
RunicHostedWindowRequest.CanOpenWindow is false, and a decision other than Embed fails the guest's open with
hosted-window-refused (event 2020, WindowModeUnavailable).
- Reduced features, as in serve-only mode. The guest's dialogs, menus and window services report
OwnerUnavailable, and itspresentand announcementsBackendUnavailable. Its window opens no owned windows. The region is active while it holds the focus of its page. - Cross-site only. The guest never falls back to the host page's loopback family, since the browser holds other
local applications' cookies: without IPv6, a page on
127.0.0.1shows the region's fallback content (cross-site-unavailable, event 2026). A page opened throughlocalhostgets its frame from[::1], else127.0.0.1. - Elevated guests refuse a browser host's page (
guest-elevated): their windows stay open and detached. - The guest's storage lives in the user's browser profile; see the security guide's Hosted windows in a region.
Components (trusted component mode)
For applications of the same vendor, a region can show the guest's Views as a component in the host page's own
document instead of in a frame (region-component/1), so they take the page's theme and layout and Tab moves through
them as through the page (specification).
Host page and guest code then share one JavaScript realm: the guest's code can read the page and use its Bridge, and the
page holds the guest's token. Both sides opt in:
// The guest: allow component embedding by the hosts of your own suite. A filter, not a security boundary.
Guest = RunicGuestOptions.FromStandardInput() with
{
AllowComponentEmbedding = host => host.Name == "Contoso Installer",
},
// The host's policy: a component where the guest allows it, a frame otherwise.
ValueTask.FromResult(request.CanEmbedComponent
? RunicHostedWindowDecision.EmbedComponent("check")
: RunicHostedWindowDecision.Embed("check"));
- The guest's build. The guest builds its component with the
componenttarget of@runic-artifex/vite-plugin-runic, which writes_runic_component/next to its page. A guest without it refuses a component attach, andEmbedComponentthen leaves the window detached;CanEmbedComponentisfalsefor a guest that does not allow the host. - The host page's Content-Security-Policy must allow the guest's origin for scripts and connections. Components of
an embedded host come from
127.0.0.1on a port that changes with each run: addhttp://127.0.0.1:*toscript-srcandws://127.0.0.1:*toconnect-srcofRunicDesktopHostOptions.ContentSecurityPolicy(see the security guide's "Components in a host page"). - In a component.
RunicHostedRegion.IsComponentistrue. The component has the host page's permissions, focus, drops and keys;RegionOptions.Permissionsandsizing="content-height"do not apply, and the guest sends no title.StatereachesAttachedonce the component connected, as in frame mode. - Elevated guests of the elevated pair offer component mode where they check their peers (Windows, Linux); on macOS
they refuse it as they refuse frames (event 2026,
refused). - The hosted components example is a dashboard with two widget processes as components, under the policy above.
Appearance in a region
With region-appearance/1 (0.19), which Runic's guests of 0.19 and later implement
(RunicHostedWindowRequest.CanOverrideAppearance), the host gives a region window an appearance override, such as a
wizard step in the design the user picked: each member of a RunicHostedAppearance that is not null replaces the host's
desktop value for that window's pages, and null keeps it
(specification):
#pragma warning disable RUNICDESK103 // Hosted application windows are experimental.
var highContrast = new RunicHostedAppearance { ColorScheme = DesktopColorScheme.Dark, Contrast = DesktopContrast.More };
// From the window's open:
RunicHostedWindowDecision.Embed("check", new RunicHostedRegionOptions { Appearance = highContrast });
// Later, attached or detached; null removes the override, and a "System" choice is DesktopColorScheme.NoPreference.
await region.SetAppearanceAsync(new RunicHostedAppearance { ColorScheme = DesktopColorScheme.Light });
- What the guest's pages see. The guest merges the override with the host's desktop appearance for that window only:
its pages, a component's included, receive the result before their first content, and their appearance store takes the
colour scheme, contrast and reduced motion that the override set over the engine's media queries. Windows the region
window owns, the guest's other windows,
RunicGuestSession.Appearanceand the host's own pages keep the desktop's. With forced colours the pages get the override'sSystemColors, else the desktop's palette when the desktop forces colours too;TextScaleis from 0.5 to 4. - The frame. The host page sets the override's colour scheme as the CSS
color-schemeof the region's frame or component. Whether the frame'sprefers-color-schemefollows it depends on the engine and is best effort; a guest page styles from its appearance store (appearance(),onAppearance), not from media queries.prefers-contrast,forced-colorsandprefers-reduced-motioncannot be set on a frame, and a desktop that forces colours keeps forcing them in the frame. SetAppearanceAsyncreplaces the override as a whole and works from any thread, attached or detached: the next page receives it, andMoveToAsyncand re-attaching keep it, since it belongs to the window. The guest receives the latest of overrides set in quick succession. It returnsfalseonly for an ended region and for a guest without the feature (RunicHostedRegion.CanSetAppearance).- Older guests. A 0.18 guest still shows in the region, with the desktop's appearance; the host logs event 2030 once
for the window, and
Appearancerecords the override. - Tests.
RunicTestHostplays the host:RunicTestHostOptions.RegionAppearanceandSetRegionAppearanceAsync(Testing).
The hosted regions guide's design and appearance walks through a design menu, the guest page's side and the CSS limits, and the hosted region example runs it.
Automation
Set RUNIC_APPLICATION_CLOSE_AFTER_OPEN=1 to start a template-shaped
application without a user. With it set, RunicDesktopHost closes every
window it opens, as soon as the presentation has opened and, with
DesktopHostOptions.WaitForConnection, its bridge has connected. Before
closing, it writes:
- a warning to standard error, and event 2002 through a registered
ILoggerFactory, so an inherited setting is visible; RUNIC_APPLICATION_OPENED=<presentation>(for exampleEmbedded, or the browser it fell back to) to standard output.
An application shaped like the runic-app template then sees WaitForCloseAsync
return and exits normally. Applications that keep running after a window
closes, or that open further windows, are not meant to use it. The template
acceptance check uses it to run the GTK 4 template under Xvfb.
Under dotnet runic dev, a full page reload, such as Vite's full-reload
after an edit, closes the View's connection and fails a script in flight
(IOException, then No authenticated WebUI browser is connected until the
page reconnects), so a test waits for the View, for example for an element it
renders, instead of assuming that a call lands; @runic-artifex/vite-plugin-runic
pre-bundles its development client, so a cold Vite dependency cache no longer
reloads the first page.
Without a Vite or Angular development server, dotnet runic dev sets
RUNIC_APPLICATION_DEVELOPMENT_ASSETS to the frontend output directory, such
as Frontend/dist, which the frontend watcher rewrites. In a Development
environment, and when RunicDesktopHostOptions.SurfaceOptions is not set,
each window then serves that directory before its www/ folder (which still
supplies runic-views.js) and reloads when the directory changes: it navigates
to the last HTML page it loaded, or to its start page, without the page's
query, hash or history.pushState route, and its Bridge reconnects as after
any reload. A connected page is navigated through its Bridge; a window whose
page has no Bridge, such as a half-written index.html or a build error page,
is navigated through its WebView instead. Neither runs page script. Outside Development the host ignores the variable. Events 2005 to
2007 report it. The
dotnet-runic README
describes the frontend modes.
Set RUNIC_APPLICATION_SERVE_ONLY=1 to drive the unchanged application from a
browser test, with the same contract as on
CS-WebUI.
RunicDesktopHost then opens no window or browser. Each window creates its
surface, writes RUNIC_APPLICATION_URL=<url> to standard output, and serves
until a line is read from standard input, a termination signal requests
shutdown, or the process ends; then WaitForCloseAsync returns. The URL is
the surface's capability URL, and the surface admits one browser. It takes
precedence over RUNIC_APPLICATION_CLOSE_AFTER_OPEN. As on CS-WebUI there is
no native owner: platform services report OwnerUnavailable, and a Window
with GuardClose opens unguarded (event 1053). NativeOwner throws, so bring
a Window forward with DesktopPlatformServices.PresentAsync(window, context), which
takes the IRunicWindow and reports OwnerUnavailable in this mode and for a
Window on another host. ValidateWindow makes the same
decision: in serve-only mode it skips the presentation checks, such as
webkitgtk-runtime-missing, graphical-session-missing and
presentation-unavailable, which describe what this machine lacks to present a
window, so an application that validates at startup also starts headless. With
a browser fallback, as in the default EmbeddedThenBrowser options, a missing
browser stands in for an installed one, so the embedded WebView's findings,
such as linux-embedded-backend-not-selected, are skipped too and no browser
is needed. It still reports configuration errors, such as window-option-invalid,
browser-unsupported or gtk4-provider-missing, which fail every launch that
presents a window.
An application that serves its Windows on purpose, such as a --browser
command, sets RunicDesktopHostOptions.ServeOnly = true instead (since 0.14):
the same mode, without an environment variable that the application's child
processes would inherit. RunicDesktopHost.ServeOnly tells which mode a host
is in. With either switch, RunicDesktopHost.Run starts no window toolkit loop,
so WithGtk4() on a machine without a display reports no
gtk4-display-unavailable. Open the served page in the user's default browser
with the application's launcher, DesktopPlatformServices.ForApplication(),
which needs no window:
return RunicDesktopHost.Run(provider, new DesktopHostOptions().WithGtk4(),
new RunicDesktopHostOptions { ServeOnly = true }, async host =>
{
await using var window = await host.OpenWindowAsync<MainWindow>();
await DesktopPlatformServices.ForApplication().FileLauncher.LaunchUriAsync(RunicDesktopHost.GetSurface(window)!.Url);
await window.WaitForCloseAsync();
return 0;
});
A served Window can end with its last browser tab, as a --browser command
that opens the URL in the default browser would. The Window's surface counts
its connected pages in ConnectionCount, and a connection event's handler
already sees the count with that event. A reload disconnects before or after
the page connects again, so wait a moment before closing:
var surface = RunicDesktopHost.GetSurface(window)!;
using var connections = surface.SubscribeConnectionEvents(connection =>
{
if (connection is not { Kind: DesktopConnectionEventKind.Disconnected, Surface.ConnectionCount: 0 }) return;
_ = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(1));
if (surface.ConnectionCount == 0) await window.CloseAsync(TimeSpan.Zero);
});
});
OpenInBrowser(surface.Url);
await window.WaitForCloseAsync();
The serve-only protocol, the same on both hosts:
- URL. Each window writes one
RUNIC_APPLICATION_URL=<url>line to standard output once its surface serves. The first line is the first window's. - Window. Directly after its URL line, in the same write, each window
writes
RUNIC_APPLICATION_WINDOW=<url> <type>: its URL again and the full name of its Window class, such asMyApp.SettingsWindow, separated by a space. The type has no spaces: a closed generic Window type is written as its generic definition's full name with its arguments' names without namespaces, such asMyApp.DocumentWindow`1[Report]. Hosts before 0.16 write no such line, and a parser that only readsRUNIC_APPLICATION_URL=lines ignores it. Read the first two space-separated fields; a later release may add more after them. - Stop. Writing a line to standard input ends serving for every window
serving at that moment; end of input, such as
</dev/null, does not. On Runic Desktop a window the application opens after that line serves until the next one; on CS-WebUI the line ends the application. On Runic Desktop on Linux and macOS, SIGTERM or Ctrl+C also requests shutdown. On Windows a test cannot send another process a termination signal, and killing it skips the application's cleanup, so write the line. - Timeouts. Bound both waits, and kill the process tree when the application does not exit after the line.
RunicServeOnlyApplication in
Runic.Application.Testing
implements it for browser tests. A headless Chromium, as in the
First Window on Runic Desktop
browser smoke, does not exercise the embedded WebView engine.
To drive the embedded View itself from an in-process test, run script in it through the window's surface and click the View's own controls, such as a dialog's button:
await using var window = await host.OpenWindowAsync<MainWindow>();
var surface = ((DesktopBridgeWindow<MainViewModel>)window.Host).Surface;
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var title = await surface.ExecuteJavaScriptAsync(
"return document.querySelector('h1')?.textContent ?? ''", timeout.Token);
ExecuteJavaScriptAsync needs a connected
page; with DesktopHostOptions.WaitForConnection the window has connected when
OpenWindowAsync returns. The token bounds the wait. A result larger than
16 MiB of UTF-8 throws a DesktopException with the category LimitExceeded
and the code script-result-too-large; it is never cut.
| 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-windows7.0 is compatible. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.14)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.14)
- Microsoft.Web.WebView2 (>= 1.0.4258.31)
- Runic.Application.Views (= 0.19.0)
- Runic.Assets.Desktop (= 0.19.0)
- Runic.Desktop (= 0.19.0)
- Runic.Platform.Linux (= 0.19.0)
- Runic.Platform.Linux.Gtk4 (= 0.19.0)
- Runic.Platform.Linux.Portal (= 0.19.0)
- Runic.Platform.MacOS (= 0.19.0)
- Runic.Platform.Runtime (= 0.19.0)
- Runic.Platform.Windows (= 0.19.0)
- Tmds.DBus.Protocol (>= 0.95.2)
-
net10.0-windows7.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.14)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.14)
- Microsoft.Web.WebView2 (>= 1.0.4258.31)
- Runic.Application.Views (= 0.19.0)
- Runic.Assets.Desktop (= 0.19.0)
- Runic.Desktop (= 0.19.0)
- Runic.Platform.Runtime (= 0.19.0)
- Runic.Platform.Windows (= 0.19.0)
- Tmds.DBus.Protocol (>= 0.95.2)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Runic.Application.Views.Desktop:
| Package | Downloads |
|---|---|
|
Runic.Application.Views.Wpf
Optional WPF child WebView presentation for existing Runic ViewModels. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.19.0 | 0 | 11/14/2026 |
| 0.18.0 | 1,356 | 11/13/2026 |
| 0.17.1 | 1,878 | 11/11/2026 |
| 0.17.0 | 254 | 11/10/2026 |
| 0.16.0 | 2,161 | 11/4/2026 |
| 0.15.1 | 1,528 | 11/2/2026 |
| 0.15.0 | 261 | 10/31/2026 |
| 0.14.0 | 1,129 | 10/30/2026 |
| 0.13.0 | 2,006 | 10/29/2026 |
| 0.12.1 | 604 | 10/27/2026 |
| 0.12.0 | 225 | 10/27/2026 |
| 0.11.0 | 1,656 | 10/25/2026 |
| 0.10.0 | 3,102 | 10/22/2026 |
| 0.9.1 | 569 | 10/21/2026 |
| 0.9.0 | 177 | 10/21/2026 |
| 0.8.0-preview.6 | 916 | 10/19/2026 |
| 0.8.0-preview.5 | 318 | 10/17/2026 |
| 0.8.0-preview.4 | 746 | 10/17/2026 |
| 0.8.0-preview.3 | 727 | 10/15/2026 |
| 0.8.0-preview.2 | 132 | 10/15/2026 |