winui shell
Decision (maintainer, 2026-09-02, #304): the Windows installer becomes a native WinUI 3 application. Wails and the embedded web frontend are retired on Windows. The Linux side stays GTK4/libadwaita (already native).
This document is the architecture and the cut-over plan. It is written
against the code on main as of c666581, and it deliberately keeps every
contract the harness and the deployer already depend on.
What stays, what goesβ
| Piece | Today | After |
|---|---|---|
| Install engine (Phase 1 pipeline, BCD, ESP, vault, sessions, uninstall, recovery) | Go, app/, in-process behind Wails bindings | Go, app/, unchanged logic, exposed by wootc.exe serve |
| Windows UI | Wails v2 + 2,300 lines of vanilla JS/CSS in app/frontend/ | WinUI 3 (C#, .NET 8, Windows App SDK) in shell/ |
Headless CLI (install, status, uninstall, recover) | wootc.exe <cmd> | unchanged |
E2E drive mode (C:\wootc\e2e-drive.json β e2e-drive-state.json) | directives executed as DOM events in lib/e2e.js | same files, same directives, executed against the shell's view-models |
Branding (app/branding/<brand>/brand.json, theme.css) | CSS variables set from GetBranding() | XAML resources set from the same GetBranding() payload; theme.css retired |
| Linux migration dashboard | wootc-dashboard Wails/webkit build in ci.yml (built, never shipped) | removed with Wails; payload/migration/*-gui (GTK4/Adw) remain the Linux UI |
| GUI tests | Playwright over a dev-server build of the frontend | view-model unit tests (xUnit) + a UI Automation smoke on windows-latest |
| WebView2 runtime dependency | required (download on Windows 10) | gone |
Architectureβ
Bluefin-Installer.exe (WinUI 3 shell, runs as the logged-in user, NOT elevated)
β spawns, with the UAC prompt, one child:
βΌ
wootc.exe serve (Go engine, requireAdministrator manifest, no window)
β JSON-RPC 2.0, newline-delimited, over the child's stdin/stdout
β requests: the 14 methods that were Wails bindings
β notifications: install:progress, vm:progress
βΌ
C:\wootc\β¦ state.json, install\, disks\ (unchanged)
Three properties fall out of this split and are the reason for it:
- The engine never changes shape.
StartInstall,GetSystemInfo,GetBranding,GetUninstallInfo,UninstallWith,BootIntoLinux,GetLastRun,E2EDriveDirective/E2EDriveReportand the rest keep their names and JSON DTOs. This is exactly what #297 asks for ("thin Wails adapter; preserve exported method names and DTO shapes") βserveis that adapter, minus Wails. - Identity is explicit. The shell runs as the human; the engine runs
elevated. The shell passes the interactive user's name and profile into
StartInstallinstead of the engine guessing it from an elevated token (the over-the-shoulder UAC problem of #225/#317 disappears structurally). - One shell binary, branded at runtime. The shell skins itself from
GetBranding()(accent, background, card, text, font, product name, tagline, catalog). Per-brand builds differ only in exe name, icon and VERSIONINFO, which the release matrix already varies.
The serve protocolβ
wootc.exe serve reads JSON-RPC 2.0 requests from stdin and writes responses
and notifications to stdout, one JSON object per line. Nothing else ever
writes to stdout in this mode; logs go to stderr and C:\wootc\logs\.
| Method | Params | Result |
|---|---|---|
GetSupportPolicy | β | SupportPolicy |
GetSystemInfo | β | SystemInfo |
GetBranding | β | Branding |
GetImages | β | []Image |
GetSessionCandidates | β | as today |
StartInstall | InstallConfig | null or error |
CancelInstall | β | null |
GetStatus | β | InstallStatus |
DefragDrive | β | null |
Reboot | β | null |
ExistingInstallFound | β | bool |
GetUninstallInfo | β | UninstallInfo |
UninstallWith | UninstallOptions | null |
BootIntoLinux | β | null |
GetLastRun | β | LifecycleState |
E2EDriveDirective | β | string |
E2EDriveReport | string | null |
Shutdown | β | null (engine exits 0) |
Notifications: {"method":"install:progress","params":ProgressEvent} and
{"method":"vm:progress","params":β¦} β the same payloads runtime.EventsEmit
sends today. The engine exits when stdin closes, so a crashed shell cannot
leave an elevated engine alive; an install already past "Making Linux
bootable" finishes its step and disarms exactly as CancelInstall does.
The protocol is documented by a Go test that round-trips every method
through a pipe and pins the DTO JSON against golden files, so a field rename
in app.go fails the test before it breaks the shell.
Shell layoutβ
shell/
Wootc.Shell/ WinUI 3 app (net8.0-windows10.0.19041, WindowsAppSDK)
App.xaml(.cs) startup: spawn engine, GetBranding, apply theme, route
Engine/EngineClient.cs process + JSON-RPC client, typed DTOs (generated)
Engine/Dto.cs generated from app/*.go json tags (go:generate)
ViewModels/ Launchpad, Progress, Done, Control, Recovery
Views/ one XAML page per view-model
Branding/BrandTheme.cs Branding β ResourceDictionary (accent, backdrop, font)
Drive/E2EDrive.cs e2e-drive.json directives β view-model actions β report
Wootc.Shell.Tests/ xUnit: view-model state machine, drive directives, DTO goldens
Wootc.Shell.UiTest/ FlaUI smoke: launch, land on Launchpad, Install disabled/enabled reasons
Screens map one-to-one from app/frontend/src/screens/: launchpad,
progress, done, control, plus recovery from #331. vmpreview is not
carried over: #318 cuts pre-install Try-in-VM from 1.0. migrate is Linux-only
and leaves with Wails.
Design language: Fluent, Mica backdrop, system light/dark, the brand accent
as the app accent, the brand font when installed with a Segoe UI Variable
fallback. The frameless custom title bar of the Wails app (#175) becomes the
standard WinUI title bar with ExtendsContentIntoTitleBar.
Elevationβ
The shell manifest does not request administrator. On first engine call
the shell starts wootc.exe serve with the runas verb; Windows shows one
UAC prompt naming the engine's publisher. Every later call rides the same
child. If the user declines, the shell shows the launchpad read-only with the
reason ("wootc needs permission to change startup settings") and an
Allow button that retries β the same honesty the battery and BitLocker
gates already practice.
E2E and GUI testsβ
run-e2e.sh --gui-installkeeps working unchanged: the harness writese2e-drive.json; the shell'sE2EDrivepollsE2EDriveDirectiveevery 2 s (aslib/e2e.jsdoes), appliesinstallto the Launchpad view-model (image card, username, hostname, passwords, encryption), enforces the same image-integrity gate (imageMismatch), clicks Install, and on the Done screen honoursreboot. It reports throughE2EDriveReportwith the same state JSON the harness parses today.tests/gui/gui.spec.js(Playwright against the web frontend) is replaced byWootc.Shell.Tests(view-model logic, runs on Linux underdotnet testwith the WinUI project excluded) andWootc.Shell.UiTest(FlaUI onwindows-latest: launch, screenshot each screen, assert the Install button's disabled reason text). The GUI screenshot gallery job renders from the UiTest screenshots.- The CDP-based recipe in
tests/e2e/phase1/README.mdis deleted; it never worked with stock Wails and has no WinUI equivalent.
Packaging and deliveryβ
WinUI 3 cannot be published as a single-file exe. Two delivery forms, in order:
- Now (unsigned, alpha):
dotnet publishself-contained, unpackaged (WindowsPackageType=None,WindowsAppSDKSelfContained=true), wrapped by Inno Setup into oneBluefin-Installer-Setup.exeper brand. No runtime to download, installs to%LOCALAPPDATA%\Programs\<Brand>, adds the Add/Remove entry the engine registers today. SmartScreen warns, exactly as it does for the unsigned Wails exe. - When signed (#229/#230): the same publish output as MSIX per brand,
winget
InstallerType: msixreplacingportableinpackaging/winget/*.yaml.in. MSIX cannot be installed unsigned, so signing is a hard prerequisite for this form, not for the shell itself.
The engine exe ships inside the shell's folder; the release keeps publishing
wootc.exe on its own for the headless and harness paths.
Build and CIβ
windows-latestjob:dotnet restore/build/testforshell/,dotnet publishper brand frompackaging/brands.sh(#319) with-p:AssemblyName=<exeName> -p:ApplicationIcon=β¦ -p:Version=β¦, Inno Setup via theinnosetupChocolatey package, artifacts uploaded beside the Go artifacts. Go engine build unchanged.ci.ymldropswootc-dashboard(Linux Wails) and the Wails Windows build once the shell is the release artifact; until then both build.- Windows App SDK floor: Windows 10 1809 (10.0.17763).
docs/user-guide.mdrequirements line gains it; every supported machine already meets it.
Cut-over planβ
| Phase | Deliverable | Depends on |
|---|---|---|
| A | wootc.exe serve + protocol golden tests + DTO generator; Wails untouched | #297 seams (done as part of it) |
| B | shell/ scaffold, CI build on windows-latest, brand matrix, Inno packaging; ships as a preview asset next to the Wails exe | A |
| C | Screens complete; drive mode; UiTest gallery; one green --gui-install E2E with the shell | B, #331 for the Recovery screen |
| D | Release artifacts switch to the shell; Wails exe kept one release as wootc-legacy.exe | C |
| E | Delete Wails, app/frontend/, wailsjs/, migration_linux.go, dashboard build, Playwright GUI tests; docs/ truth pass | D + one clean release |
Each phase is one PR series; nothing in AβC changes what a user downloads.
What this does not fix, said plainlyβ
- Signing. SmartScreen and the UAC publisher line are the same problem before and after; #229 is the fix. WinUI does not help or hurt it.
- The Linux side on KDE. Bazzite and Aurora users still see GTK windows for the migration tools. A Breeze-aware theme is the cheap step; Kirigami twins are a separate decision.
- Windows 10 without WebView2 was the one concrete usability cost of Wails; it is gone. Windows 10 below 1809 was never supported.