UniGetUI is an Avalonia desktop app (C#/.NET 10) providing a GUI for CLI package managers (WinGet, Scoop, Chocolatey, Pip, Npm, .NET Tool, PowerShell Gallery, Cargo, Vcpkg).
Solution entry points:
src/UniGetUI.Windows.slnx- official Windows solution; builds the Avalonia app and Windows-specific package-manager integrationssrc/UniGetUI.Avalonia.slnx- cross-platform Avalonia solution
The codebase follows a layered, modular structure with ~40 projects:
UniGetUI.Avalonia/- Avalonia entry point, AXAML pages, controls, and app shell (Program.cs,Views/MainWindow.axaml)SharedAssets/- shared app icons, symbols, splash images, and utility scripts used by packagingUniGetUI.Core.*- Shared infrastructure:Logger,Settings,Tools(includesCoreTools.Translate()),IconEngine,LanguageEngineUniGetUI.PackageEngine.Interfaces- Contracts:IPackageManager,IPackage,IManagerSource,IPackageDetailsUniGetUI.PackageEngine.PackageManagerClasses- Base implementations:PackageManager(abstract),Package, helpers (BasePkgDetailsHelper,BasePkgOperationHelper,BaseSourceHelper)UniGetUI.PackageEngine.Managers.*- Concrete manager implementations (one project per manager:WinGet,Scoop,Chocolatey,Pip,npm, etc.)UniGetUI.PackageEngine.Operations- Install/update/uninstall operation orchestrationUniGetUI.Interface.*- Enums, telemetry, background API
Each manager extends PackageManager and must override three abstract methods:
protected override IReadOnlyList<Package> FindPackages_UnSafe(string query);
protected override IReadOnlyList<Package> GetAvailableUpdates_UnSafe();
protected override IReadOnlyList<Package> GetInstalledPackages_UnSafe();Each manager also provides three helper classes (in a Helpers/ subfolder):
*PkgDetailsHelperextendsBasePkgDetailsHelper- overridesGetDetails_UnSafe,GetInstallableVersions_UnSafe,GetIcon_UnSafe, etc.*PkgOperationHelperextendsBasePkgOperationHelper- overrides_getOperationParameters,_getOperationResult*SourceHelperextendsBaseSourceHelper- overridesGetSources_UnSafe,GetAddSourceParameters, etc.
The constructor sets Capabilities, Properties, and wires the helpers. See src/UniGetUI.PackageEngine.Managers.Scoop/Scoop.cs as a clean reference implementation.
These four managers share BaseNuGet / BaseNuGetDetailsHelper, which talk to a NuGet feed
over HTTP. Each source picks its protocol independently, in NuGetV3ServiceIndex.GetServiceIndexUrl:
- A source URL with the
filescheme - whatnew Uri()produces for a local folder or a UNC share such asC:\packagesor\\server\share- is a local folder feed, served byNuGetLocalFeed. There is no HTTP endpoint to call, so search, details, icons, versions and updates read the.nupkgfiles and their embedded.nuspecstraight from disk. This check runs before the V3 one, everywhere. - A source URL whose path ends in
index.json, or whose last path segment isv3, is a NuGet V3 feed. Its service index is fetched once per session and cached. - Every other source URL is treated as a V2/OData feed and keeps the legacy code path.
Detection is purely by URL shape, so it costs no probe request and no V2 feed changes behaviour.
A local folder feed is scanned at most three directories deep, which covers both the flat layout
and the <id>/<version>/<id>.<version>.nupkg layout, and each parsed manifest is cached against
its file's size and write time. Installers on such a feed are copied from disk instead of being
downloaded (DownloadOperation), which refuses a destination that is the package file itself.
Every .nupkg in the folder is opened during a search, so its contents are treated as untrusted:
a manifest is rejected above 4 MiB (checked against the declared size and while decompressing,
since the declared one can lie) and parsed with DTD processing prohibited, and an embedded
<icon> is extracted under the same bounds into the package's icon cache directory, named from
the package version, a digest of the archive's path, write time and size, and an allow-listed
extension - never from the entry path, so a crafted entry name cannot escape that directory, and
two feeds carrying the same id and version keep their own icons. Note that GetIconLocal re-reads
a replaced archive whichever way its timestamp moved, but Package.GetIconUrlIfAny caches the
resolved address for the process against the package's versioned hash, so a package replaced in
place under the same version keeps showing the icon it had until a restart.
Resource @type |
Used for |
|---|---|
SearchQueryService (3.5.0 → 3.0.0-beta → unversioned) |
package search |
PackageBaseAddress/3.0.0 |
version enumeration, update detection, .nupkg and embedded-icon URLs |
RegistrationsBaseUrl (3.6.0 → 3.4.0 → 3.0.0-rc → 3.0.0-beta → unversioned) |
package metadata, listed/unlisted checks |
A feed needs PackageBaseAddress or RegistrationsBaseUrl to be usable. If it advertises no
SearchQueryService — or its search returns nothing — search degrades to an exact
package-id lookup against the flat container. Flat-container version lists include unlisted
versions, so an update candidate is confirmed against its registration leaf before being
surfaced.
There is no V2 fallback for a URL that looks like a V3 service index: such a URL is not a valid V2 root, so a failed or malformed service index is reported as an error rather than retried against a fabricated V2 endpoint. V2-only feeds never enter the V3 path at all.
- .NET Tool —
https://api.nuget.org/v3/index.json. Safe to repoint because the manager does not support custom sources and never passes the source URL to thedotnetCLI. Also filters search onpackageType=DotnetTool. - PowerShell / PowerShell 7 — sources are enumerated from
Get-PSRepository(orGet-PSResourceRepositoryon PowerShell 7), so the URL is CLI-owned identity (PowerShellSourceHelpercompares it literally to chooseRegister-PSRepository -Default) and must not be rewritten. Both helpers ask for an untruncated two-column table and read it by column position (ManagerTable), because a repository name or path containing a space breaks whitespace splitting and a long location is otherwise cut byFormat-Table; a location that is not an http URL - a folder or a UNC share - is kept rather than discarded. The PowerShell Gallery serves no V3 service index, so it stays on V2; a V3-capable custom repository (Azure Artifacts, GitHub Packages, JFrog, MyGet) is picked up automatically. - Chocolatey — sources come from
choco source listandcommunity.chocolatey.orgserves noindex.json, so it stays on V2. Note that only its updates and version listing are CLI-driven; its search, details and icons run through the sharedBaseNuGetHTTP path.
V3 has no IsLatestVersion, so the client picks "latest" itself using the comparator described
in the next section.
CoreTools.VersionStringToStruct treats -, _, / and # as plain numeric separators and
drops the letters around them. That is correct for ecosystems where a trailing suffix is a build
or port revision, and wrong for ecosystems where it is a pre-release - the same string orders
the opposite way depending on the feed it came from:
| String | Debian / Scoop / Homebrew / vcpkg | npm / crates.io / NuGet | PyPI |
|---|---|---|---|
1.0.0-1 |
newer (revision) | older (pre-release) | newer (implicit post-release) |
1.0.0rc1 |
n/a | n/a | older (pre-release) |
So the comparison is per-manager, not global. IPackageManager.CompareVersions(a, b) returns the
usual negative/zero/positive, or null when two versions cannot be meaningfully compared:
- Default (
PackageManager.CompareVersions) - the numeric comparison above. Used by Scoop, WinGet, Apt, Dnf, Pacman, Homebrew, vcpkg, Snap and Flatpak. SemanticVersioninUniGetUI.Core.Tools- SemVer 2.0 with NuGet's fourth numeric segment allowed. Used byBaseNuGet(so Chocolatey, .NET Tool, Windows PowerShell and PowerShell 7), npm, Bun and Cargo.PythonVersioninUniGetUI.Core.Tools- PEP 440, matching Python's packaging library. Used by Pip.
Each override falls back to the base implementation when its own parser rejects the input, so an unparseable version behaves exactly as it did before.
Three call sites consume it: Package.NewerVersionIsInstalled(), which decides whether an update
is offered at all; BaseNuGet.KeepUpdatesNewerThanInstalled; and the
UninstallPreviousVersionsOnUpdate match, which removes superseded copies. Display sorting
deliberately does not - those lists mix managers, and a comparison between two packages from
different ecosystems has no single correct answer.
When adding a manager, pick the comparator its registry actually uses. Getting it wrong hides
real updates or offers downgrades, and the tests in PackageTests.cs assert both directions:
that SemVer and PEP 440 managers order pre-releases below their releases, and that revision
ecosystems keep reading a trailing suffix as newer.
# Restore & test (from src/)
dotnet restore UniGetUI.Windows.slnx
dotnet test UniGetUI.Windows.slnx --verbosity q --nologo /p:Platform=x64
# Publish release build
dotnet publish src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj /p:Configuration=Release /p:Platform=x64 -p:RuntimeIdentifier=win-x64- Target framework:
net10.0-windows10.0.26100.0(min10.0.19041) - Build generates secrets via
src/UniGetUI.Avalonia/Infrastructure/generate-secrets.ps1and integrity tree viascripts/generate-integrity-tree.ps1 - Self-contained, publish-trimmed (partial), Windows App SDK self-contained
- Tests use xUnit (
[Fact],Assert.*)
The PR check (.github/workflows/dotnet-test.yml) fails on any whitespace or code-style diff, even when the build and tests pass. After changing any C# file, apply both formatters from the repo root and commit the result:
dotnet format whitespace src --folder
dotnet format style src/UniGetUI.Windows.slnx --no-restoreTo reproduce the CI gate exactly, add --verify-no-changes --verbosity minimal to both commands; they must exit with code 0. Run both — whitespace and style catch different issues, and passing one does not imply passing the other.
Release packages are self-contained, fully trimmed NativeAOT binaries on every supported RID. Treat NativeAOT safety as a non-negotiable production requirement: changes must work without runtime-generated code or metadata that the trimmer cannot prove is required.
-
Consider every
IL2xxxtrim warning andIL3xxxAOT warning a defect. Do not blanket-suppress warnings or use a broadRequiresUnreferencedCode,RequiresDynamicCode,DynamicDependency,DynamicallyAccessedMembers, linker descriptor, or root assembly as a workaround. An exception must be narrowly scoped, explain the concrete runtime requirement, and have a publish-path test that proves it is safe. -
Use
JsonSerializerContext/JsonTypeInfo<T>source-generated metadata for every production JSON serialization or deserialization path, including generic helpers, HTTP payloads, settings, IPC, telemetry, and persisted state. Do not call reflection-based serializer overloads or retain a reflection fallback for unknown application types. Add the closed type to a context and test the persisted or transported path. -
Do not introduce production reflection-driven activation or discovery (
Assembly.Load,Type.GetType,Activator, member lookup/invocation), runtime generic construction, expression compilation, runtime code generation, or dynamically loaded plugins. Prefer explicit registrations, direct constructors, generated metadata, and compiled XAML bindings. If a framework-owned dynamic boundary cannot be removed, isolate it and add published-NativeAOT runtime coverage. -
New shipping COM interop must use source-generated interfaces and wrappers (
GeneratedComInterface,StrategyBasedComWrappers) or raw ABI calls; do not addComImport,CoClass, RCW activation, or legacy marshal-to-object APIs to a NativeAOT path. Define the ABI precisely, balance COM reference ownership, and test both x64 and arm64 where applicable. -
Prefer
LibraryImportfor new P/Invokes. ExistingDllImportdeclarations are acceptable only when their marshalling is supported by NativeAOT; keep platform guards, calling conventions, ownership, callback lifetimes, and x64/arm64 struct layouts explicit and covered. -
Audit all new or changed startup, XAML/view-loading, dependency-injection, serialization, COM/WinRT, P/Invoke, and package-manager code against the publish output, not only a Debug build. Run the relevant NativeAOT publish profile, for example:
dotnet publish src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj --configuration Release --runtime win-x64 --self-contained true /p:Platform=x64 /p:PublishProfile=Win-x64-NativeAot -
When modifying a shared or portable path, retain the same safety guarantees for all checked-in NativeAOT publish profiles:
win-x64,win-arm64,linux-x64,linux-arm64,osx-x64, andosx-arm64. Record unsupported runtime-matrix coverage as a gap, never as a pass.
Use these rules when changing Avalonia diagnostics/devtools behavior:
- Build-time switch is
EnableAvaloniaDiagnosticsinsrc/Directory.Build.props. - Default policy: enabled in
Debug, disabled inRelease. src/UniGetUI.Avalonia/UniGetUI.Avalonia.csprojmust conditionAvaloniaUI.DiagnosticsSupporton$(EnableAvaloniaDiagnostics).- Compile-time diagnostics code in
src/UniGetUI.Avalonia/Program.csmust be gated by#if AVALONIA_DIAGNOSTICS_ENABLED(not#if DEBUG). - Runtime controls are developer-only and intentionally not listed in
docs/CLI.md. - Runtime precedence in
Program.cs: CLI flags >UNIGETUI_AVALONIA_DEVTOOLSenvironment variable >Autodefault. - Accepted runtime env/CLI values for mode parsing:
auto,enabled,disabled,on,off,true,false,1,0. Automode must remain WSL-safe (DevTools disabled by default on WSL).- If diagnostics were excluded at build time, runtime toggle requests should log a no-op warning.
File-based settings via Settings.Get(Settings.K.*) / Settings.Set(Settings.K.*, value) and Settings.GetValue(Settings.K.*) / Settings.SetValue(Settings.K.*, value). Setting keys are defined in the Settings.K enum in SettingsEngine_Names.cs. Boolean settings are stored as file existence; string settings as file content.
Use Logger.Info(), Logger.Warn(), Logger.Error(), Logger.Debug(), Logger.ImportantInfo() from UniGetUI.Core.Logging. Accepts both string and Exception parameters.
Use CoreTools.Translate("text") for all user-facing strings. Parameterized: CoreTools.Translate("{0} packages found", count). In XAML, use the TranslatedTextBlock control. Translation assets live under src/Languages/; do not assume Tolgee-based automation exists in this repository.
- Types, methods, properties: PascalCase
- Private fields:
__doubleUnderscoreor_singleUnderscoreprefix - Internal unsafe methods: suffix
_UnSafe(e.g.,FindPackages_UnSafe) - Nullable enabled globally;
LangVersionislatest - Code style enforced in build (
EnforceCodeStyleInBuild=true)
FALSE_PACKAGE_NAMES,FALSE_PACKAGE_IDS,FALSE_PACKAGE_VERSIONSstatic arrays filter CLI parsing noise- Manager initialization flows through
Initialize()->_loadManagerExecutableFile()->_loadManagerVersion()->_performExtraLoadingSteps() - Operations that may fail return
OperationVeredict(note: intentional misspelling used throughout codebase)
src/UniGetUI.Tuiis the Consolonia terminal front-end library, hosted by the main executable'stuicommand (unigetui tuioruniget tui). Pages must reach the engine throughTuiEngine(neverPEInterfacedirectly), so that--fake-datacan swap in the fake managers fromFakeData/.- Run it with fake data:
dotnet run --project src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj -- tui --fake-data. Nothing real is installed; all state lives in a sandbox. - Dialogs are in-window overlays (
Views/Dialogs/TuiModal), not Avalonia popups. Popups capture input unreliably in Windows console hosts. - Colours come from the active theme: use
TuiPalettebrushes (Theme/TuiPalette.cs), neverColor.Parsein a control, so themes and live theme switching reach it. - End-to-end tests in
src/UniGetUI.Tui.Tests/E2Edrive the real UI by keystrokes against the fake data set. Add a test for any new page action.
| Purpose | Path |
|---|---|
| Windows solution | src/UniGetUI.Windows.slnx |
| Cross-platform solution | src/UniGetUI.Avalonia.slnx |
| Shared build props | src/Directory.Build.props |
| Version info | src/SharedAssemblyInfo.cs |
| Manager interface | src/UniGetUI.PackageEngine.Interfaces/IPackageManager.cs |
| Base manager class | src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs |
| Package class | src/UniGetUI.PackageEngine.PackageManagerClasses/Packages/Package.cs |
| Settings engine | src/UniGetUI.Core.Settings/SettingsEngine.cs |
| Setting keys | src/UniGetUI.Core.Settings/SettingsEngine_Names.cs |
| Logger | src/UniGetUI.Core.Logger/Logger.cs |
| CI test workflow | .github/workflows/dotnet-test.yml |
| Terminal UI (Consolonia) | src/UniGetUI.Tui/ — see docs/TUI.md |
| Terminal UI tests (E2E, fake data) | src/UniGetUI.Tui.Tests/ |