Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ jobs:
uses: actions/cache@v5
with:
path: ${{ env.NUGET_PACKAGES }}
key: ${{ runner.os }}-nuget-${{ hashFiles('global.json', 'src/**/*.csproj', 'src/**/*.props', 'src/**/*.targets', 'src/**/*.sln') }}
key: ${{ runner.os }}-nuget-${{ hashFiles('global.json', 'src/**/*.csproj', 'src/**/*.props', 'src/**/*.targets', 'src/**/*.sln', 'src/**/*.slnx') }}
restore-keys: |
${{ runner.os }}-nuget-

Expand Down Expand Up @@ -160,17 +160,17 @@ jobs:

- name: Restore dependencies
working-directory: src
run: dotnet restore UniGetUI.sln
run: dotnet restore UniGetUI.Windows.slnx

- name: Run tests
working-directory: src
shell: pwsh
run: |
# Retry once to handle flaky tests (e.g. TaskRecyclerTests uses Random)
dotnet test UniGetUI.sln --no-restore --verbosity q --nologo
dotnet test UniGetUI.Windows.slnx --no-restore --verbosity q --nologo /p:Platform=x64
if ($LASTEXITCODE -ne 0) {
Write-Host "::warning::First test run failed, retrying..."
dotnet test UniGetUI.sln --no-restore --verbosity q --nologo
dotnet test UniGetUI.Windows.slnx --no-restore --verbosity q --nologo /p:Platform=x64
if ($LASTEXITCODE -ne 0) { exit 1 }
}

Expand All @@ -188,13 +188,19 @@ jobs:

$TargetFramework = "$PortableTargetFramework-windows$WindowsTargetPlatformVersion"
dotnet publish src/UniGetUI/UniGetUI.csproj /noLogo /p:Configuration=Release /p:Platform=$Platform -p:RuntimeIdentifier=win-$Platform -v m
if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed" }
if ($LASTEXITCODE -ne 0) { throw "dotnet publish WinUI failed" }

dotnet publish src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj /noLogo /p:Configuration=Release /p:Platform=$Platform -p:RuntimeIdentifier=win-$Platform --self-contained true -v m
if ($LASTEXITCODE -ne 0) { throw "dotnet publish Avalonia failed" }

# Stage binaries
$PublishDir = "src/UniGetUI/bin/$Platform/Release/$TargetFramework/win-$Platform/publish"
$AvaloniaPublishDir = "src/UniGetUI.Avalonia/bin/$Platform/Release/$TargetFramework/win-$Platform/publish"
if (Test-Path "unigetui_bin") { Remove-Item "unigetui_bin" -Recurse -Force }
New-Item "unigetui_bin" -ItemType Directory | Out-Null
Get-ChildItem $PublishDir | Move-Item -Destination "unigetui_bin" -Force
New-Item "unigetui_bin/Avalonia" -ItemType Directory | Out-Null
Get-ChildItem $AvaloniaPublishDir | Move-Item -Destination "unigetui_bin/Avalonia" -Force

# Backward-compat alias
Copy-Item "unigetui_bin/UniGetUI.exe" "unigetui_bin/WingetUI.exe" -Force
Expand Down Expand Up @@ -330,7 +336,7 @@ jobs:
uses: actions/cache@v5
with:
path: ${{ env.NUGET_PACKAGES }}
key: ${{ runner.os }}-nuget-${{ hashFiles('global.json', 'src/**/*.csproj', 'src/**/*.props', 'src/**/*.targets', 'src/**/*.sln') }}
key: ${{ runner.os }}-nuget-${{ hashFiles('global.json', 'src/**/*.csproj', 'src/**/*.props', 'src/**/*.targets', 'src/**/*.sln', 'src/**/*.slnx') }}
restore-keys: |
${{ runner.os }}-nuget-

Expand Down
18 changes: 6 additions & 12 deletions .github/workflows/dotnet-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,28 +57,22 @@ jobs:

- name: Install dependencies
working-directory: src
run: |
dotnet restore UniGetUI.sln
dotnet restore UniGetUI.Avalonia.slnx
run: dotnet restore UniGetUI.Windows.slnx

- name: Check whitespace formatting
run: dotnet format whitespace src --folder --verify-no-changes --verbosity minimal

- name: Check code style formatting (WinUI solution)
- name: Check code style formatting (Windows solution)
working-directory: src
run: dotnet format style UniGetUI.sln --no-restore --verify-no-changes --verbosity minimal
run: dotnet format style UniGetUI.Windows.slnx --no-restore --verify-no-changes --verbosity minimal

- name: Check code style formatting (Avalonia solution)
- name: Build Windows solution
working-directory: src
run: dotnet format style UniGetUI.Avalonia.slnx --no-restore --verify-no-changes --verbosity minimal

- name: Build Avalonia solution
working-directory: src
run: dotnet build UniGetUI.Avalonia.slnx --no-restore --verbosity minimal
run: dotnet build UniGetUI.Windows.slnx --no-restore --verbosity minimal /p:Platform=x64

- name: Run Tests
working-directory: src
env:
GITHUB_TOKEN: ${{ github.token }}
run: dotnet test UniGetUI.sln --no-restore --verbosity q --nologo
run: dotnet test UniGetUI.Windows.slnx --no-restore --verbosity q --nologo /p:Platform=x64

4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
UniGetUI is a WinUI 3 desktop app (C#/.NET 10, Windows App SDK) providing a GUI for CLI package managers (WinGet, Scoop, Chocolatey, Pip, Npm, .NET Tool, PowerShell Gallery, Cargo, Vcpkg).

Solution entry points:
- `src/UniGetUI.sln` - official Windows application based on WinUI 3
- `src/UniGetUI.Windows.slnx` - official Windows solution; builds the WinUI 3 launcher/classic app and the Avalonia app
- `src/UniGetUI.Avalonia.slnx` - experimental cross-platform Avalonia port

## Architecture
Expand Down Expand Up @@ -94,7 +94,7 @@ Use `CoreTools.Translate("text")` for all user-facing strings. Parameterized: `C

| Purpose | Path |
|---|---|
| Solution | `src/UniGetUI.sln` |
| Windows solution | `src/UniGetUI.Windows.slnx` |
| Experimental cross-platform solution | `src/UniGetUI.Avalonia.slnx` |
| Shared build props | `src/Directory.Build.props` |
| Version info | `src/SharedAssemblyInfo.cs` |
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ Before reading: All of the rules below are guidelines, which means that they sho
## Formatting:
- Run `pwsh ./scripts/install-git-hooks.ps1` once after cloning to enable the repository pre-commit hook.
- The pre-commit hook runs `dotnet format whitespace src --folder` on staged files under `src` when the `dotnet` CLI is available, and stops the commit if it had to rewrite files so you can review the changes and commit again.
- CI enforces whitespace formatting with `dotnet format whitespace src --folder --verify-no-changes` and code-style verification with `dotnet format style src/UniGetUI.sln --no-restore --verify-no-changes` in `.github/workflows/dotnet-test.yml`.
- CI enforces whitespace formatting with `dotnet format whitespace src --folder --verify-no-changes` and code-style verification with `dotnet format style src/UniGetUI.Windows.slnx --no-restore --verify-no-changes` in `.github/workflows/dotnet-test.yml`.
- The pre-commit hook intentionally does not run `dotnet format style` because solution loading makes it take roughly the same time for one staged C# file as for the full solution.
- If you want to check the same style rules locally before pushing, run `dotnet format style src/UniGetUI.sln --no-restore --verify-no-changes` from the repository root.
- If you want to check the same style rules locally before pushing, run `dotnet format style src/UniGetUI.Windows.slnx --no-restore --verify-no-changes` from the repository root.
- If you want to prepare a dedicated formatting-only commit, run `dotnet format whitespace src --folder` from the repository root.

## Coding:
Expand Down
17 changes: 14 additions & 3 deletions scripts/build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ $ErrorActionPreference = 'Stop'

$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$SrcDir = Join-Path $RepoRoot "src"
$WindowsSolution = Join-Path $SrcDir "UniGetUI.Windows.slnx"
$PublishProject = Join-Path $SrcDir "UniGetUI" "UniGetUI.csproj"
$AvaloniaPublishProject = Join-Path $SrcDir "UniGetUI.Avalonia" "UniGetUI.Avalonia.csproj"
$BinDir = Join-Path $RepoRoot "unigetui_bin"
$BuildPropsPath = Join-Path $SrcDir "Directory.Build.props"
[xml] $BuildProps = Get-Content $BuildPropsPath
Expand All @@ -50,6 +52,7 @@ if ([string]::IsNullOrWhiteSpace($PortableTargetFramework) -or [string]::IsNullO

$TargetFramework = "$PortableTargetFramework-windows$WindowsTargetPlatformVersion"
$PublishDir = Join-Path $SrcDir "UniGetUI" "bin" $Platform $Configuration $TargetFramework "win-$Platform" "publish"
$AvaloniaPublishDir = Join-Path $SrcDir "UniGetUI.Avalonia" "bin" $Platform $Configuration $TargetFramework "win-$Platform" "publish"

# --- Version stamping ---
if ($Version) {
Expand All @@ -66,26 +69,34 @@ Write-Host "Building UniGetUI version: $PackageVersion"
# --- Test ---
if (-not $SkipTests) {
Write-Host "`n=== Running tests ===" -ForegroundColor Cyan
dotnet test (Join-Path $SrcDir "UniGetUI.sln") --verbosity q --nologo --ignore-failed-sources
dotnet test $WindowsSolution --verbosity q --nologo --ignore-failed-sources /p:Platform=$Platform
if ($LASTEXITCODE -ne 0) {
throw "Tests failed with exit code $LASTEXITCODE"
}
}

# --- Build / Publish ---
Write-Host "`n=== Publishing $Configuration|$Platform ===" -ForegroundColor Cyan
dotnet clean (Join-Path $SrcDir "UniGetUI.sln") -v m --nologo
dotnet clean $WindowsSolution -v m --nologo /p:Platform=$Platform

dotnet publish $PublishProject /noLogo /p:Configuration=$Configuration /p:Platform=$Platform --ignore-failed-sources -v m
if ($LASTEXITCODE -ne 0) {
throw "dotnet publish failed with exit code $LASTEXITCODE"
throw "dotnet publish WinUI failed with exit code $LASTEXITCODE"
}

dotnet publish $AvaloniaPublishProject /noLogo /p:Configuration=$Configuration /p:Platform=$Platform -p:RuntimeIdentifier=win-$Platform --self-contained true --ignore-failed-sources -v m
if ($LASTEXITCODE -ne 0) {
throw "dotnet publish Avalonia failed with exit code $LASTEXITCODE"
}

# --- Stage binaries ---
if (Test-Path $BinDir) { Remove-Item $BinDir -Recurse -Force }
New-Item $BinDir -ItemType Directory | Out-Null
# Move published output into unigetui_bin
Get-ChildItem $PublishDir | Move-Item -Destination $BinDir -Force
$AvaloniaBinDir = Join-Path $BinDir "Avalonia"
New-Item $AvaloniaBinDir -ItemType Directory | Out-Null
Get-ChildItem $AvaloniaPublishDir | Move-Item -Destination $AvaloniaBinDir -Force

# WingetUI.exe alias for backward compat
Copy-Item (Join-Path $BinDir "UniGetUI.exe") (Join-Path $BinDir "WingetUI.exe") -Force
Expand Down
2 changes: 2 additions & 0 deletions src/Languages/lang_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,8 @@
"Follow system color scheme": "Follow system color scheme",
"Application theme:": "Application theme:",
"UniGetUI startup page:": "UniGetUI startup page:",
"Use classic mode": "Use classic mode",
"Restart UniGetUI to apply this change": "Restart UniGetUI to apply this change",
"Proxy settings": "Proxy settings",
"Other settings": "Other settings",
"Connect the internet using a custom proxy": "Connect the internet using a custom proxy",
Expand Down
16 changes: 16 additions & 0 deletions src/UniGetUI.Avalonia/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ public override void OnFrameworkInitializationCompleted()
ApplyTheme(CoreSettings.GetValue(CoreSettings.K.PreferredTheme));
var mainWindow = new MainWindow();
desktop.MainWindow = mainWindow;
Program.SecondaryInstanceArgsReceived += args =>
HandleSecondaryInstanceArgs(mainWindow, args);

if (CoreData.WasDaemon)
{
Expand Down Expand Up @@ -139,6 +141,20 @@ private static async Task StartupAsync(MainWindow mainWindow)
await AvaloniaBootstrapper.InitializeAsync();
}

private static void HandleSecondaryInstanceArgs(MainWindow mainWindow, string[] args)
{
bool isDaemonLaunch = args.Contains(AvaloniaCliHandler.DAEMON);
CoreData.IsDaemon = isDaemonLaunch;

if (isDaemonLaunch)
return;

if (!mainWindow.IsVisible)
mainWindow.Show();

mainWindow.Activate();
}

public static void ApplyTheme(string value)
{
Current!.RequestedThemeVariant = value switch
Expand Down
75 changes: 75 additions & 0 deletions src/UniGetUI.Avalonia/Infrastructure/AppRestartHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System.Diagnostics;
using Avalonia.Controls.ApplicationLifetimes;
using UniGetUI.Avalonia.Views;

namespace UniGetUI.Avalonia.Infrastructure;

internal static class AppRestartHelper
{
private const string LauncherExecutableName = "UniGetUI.exe";

public static void Restart()
{
string executablePath = ResolveRestartExecutablePath(AppContext.BaseDirectory);
Process.Start(new ProcessStartInfo(executablePath)
{
UseShellExecute = false,
});

if (MainWindow.Instance is { } mainWindow)
{
mainWindow.QuitApplication();
return;
}

(global::Avalonia.Application.Current?.ApplicationLifetime
as IClassicDesktopStyleApplicationLifetime)?.Shutdown();
}

internal static string ResolveRestartExecutablePath(string baseDirectory)
{
if (!OperatingSystem.IsWindows())
return Environment.ProcessPath
?? throw new InvalidOperationException("Could not resolve the current executable path.");

foreach (string candidate in GetWindowsLauncherCandidates(baseDirectory))
{
if (File.Exists(candidate))
return candidate;
}

return Environment.ProcessPath
?? throw new FileNotFoundException(
$"Could not find the UniGetUI launcher '{LauncherExecutableName}'."
);
}

private static IEnumerable<string> GetWindowsLauncherCandidates(string baseDirectory)
{
yield return Path.Combine(baseDirectory, "..", LauncherExecutableName);
yield return Path.Combine(baseDirectory, LauncherExecutableName);

var directory = new DirectoryInfo(Path.GetFullPath(baseDirectory));
while (directory is not null)
{
string launcherBinDirectory = Path.Combine(directory.FullName, "UniGetUI", "bin");
if (Directory.Exists(launcherBinDirectory))
{
foreach (
string candidate in Directory
.EnumerateFiles(
launcherBinDirectory,
LauncherExecutableName,
SearchOption.AllDirectories
)
.OrderByDescending(File.GetLastWriteTimeUtc)
)
{
yield return candidate;
}
}

directory = directory.Parent;
}
}
}
39 changes: 39 additions & 0 deletions src/UniGetUI.Avalonia/Program.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
using System;
using Avalonia;
using UniGetUI.Avalonia.Infrastructure;
using UniGetUI.Core.Data;
using UniGetUI.Core.Logging;

namespace UniGetUI.Avalonia;

sealed class Program
{
private static Mutex? _singleInstanceMutex;

internal static event Action<string[]>? SecondaryInstanceArgsReceived;

// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
Expand All @@ -25,9 +31,42 @@ public static void Main(string[] args)

CoreData.WasDaemon = CoreData.IsDaemon = args.Contains(AvaloniaCliHandler.DAEMON);

if (!TryRegisterSingleInstance(args))
return;

BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}

private static bool TryRegisterSingleInstance(string[] args)
{
if (!OperatingSystem.IsWindows())
return true;

_singleInstanceMutex = new Mutex(
initiallyOwned: true,
name: CoreData.MainWindowIdentifier,
createdNew: out bool createdNew
);

if (createdNew)
{
SingleInstanceRedirector.StartListener(args =>
SecondaryInstanceArgsReceived?.Invoke(args)
);
return true;
}

if (SingleInstanceRedirector.TryForwardToFirstInstance(args))
{
_singleInstanceMutex.Dispose();
_singleInstanceMutex = null;
return false;
}

Logger.Warn("Could not redirect to the existing Avalonia instance; starting a new one");
return true;
}

// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
Expand Down
1 change: 1 addition & 0 deletions src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
<ItemGroup>
<Compile Remove="Infrastructure\**" />
<Compile Include="Infrastructure\AvaloniaOperationRegistry.cs" />
<Compile Include="Infrastructure\AppRestartHelper.cs" />
<Compile Include="Infrastructure\WindowsAppNotificationBridge.cs" />
<Compile Include="Infrastructure\WindowsTrayService.cs" Condition="$([MSBuild]::IsOSPlatform('Windows'))" />
<Compile Include="Infrastructure\MacOsNotificationBridge.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Avalonia.Controls.ApplicationLifetimes;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using UniGetUI.Avalonia.Infrastructure;
using UniGetUI.Avalonia.ViewModels;
using UniGetUI.Core.Tools;

Expand All @@ -22,12 +22,6 @@ public partial class SettingsBasePageViewModel : ViewModelBase
[RelayCommand]
private static void RestartApp()
{
var exe = Environment.ProcessPath;
if (exe is not null)
System.Diagnostics.Process.Start(
new System.Diagnostics.ProcessStartInfo(exe) { UseShellExecute = true });
(global::Avalonia.Application.Current?.ApplicationLifetime
as IClassicDesktopStyleApplicationLifetime)
?.Shutdown();
AppRestartHelper.Restart();
}
}
Loading
Loading