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
21 changes: 20 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

## Project Overview

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 lives in `src/UniGetUI.sln`.
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.Avalonia.slnx` - experimental cross-platform Avalonia port

## Architecture

Expand Down Expand Up @@ -49,6 +53,20 @@ dotnet publish src/UniGetUI/UniGetUI.csproj /p:Configuration=Release /p:Platform
- Self-contained, publish-trimmed (partial), Windows App SDK self-contained
- Tests use **xUnit** (`[Fact]`, `Assert.*`)

## Avalonia DevTools (Developer-Only)

Use these rules when changing Avalonia diagnostics/devtools behavior:

- Build-time switch is `EnableAvaloniaDiagnostics` in `src/Directory.Build.props`.
- Default policy: enabled in `Debug`, disabled in `Release`.
- `src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj` must condition `AvaloniaUI.DiagnosticsSupport` on `$(EnableAvaloniaDiagnostics)`.
- Compile-time diagnostics code in `src/UniGetUI.Avalonia/Program.cs` must be gated by `#if AVALONIA_DIAGNOSTICS_ENABLED` (not `#if DEBUG`).
- Runtime controls are developer-only and intentionally not listed in `cli-arguments.md`.
- Runtime precedence in `Program.cs`: CLI flags > `UNIGETUI_AVALONIA_DEVTOOLS` environment variable > `Auto` default.
- Accepted runtime env/CLI values for mode parsing: `auto`, `enabled`, `disabled`, `on`, `off`, `true`, `false`, `1`, `0`.
- `Auto` mode 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.

## Key Patterns & Conventions

### Settings
Expand Down Expand Up @@ -77,6 +95,7 @@ Use `CoreTools.Translate("text")` for all user-facing strings. Parameterized: `C
| Purpose | Path |
|---|---|
| Solution | `src/UniGetUI.sln` |
| Experimental 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` |
Expand Down
5 changes: 5 additions & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@
<Configurations>Debug;Release</Configurations>
</PropertyGroup>

<PropertyGroup>
<EnableAvaloniaDiagnostics>false</EnableAvaloniaDiagnostics>
<EnableAvaloniaDiagnostics Condition="'$(Configuration)' == 'Debug'">true</EnableAvaloniaDiagnostics>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<Optimize>true</Optimize>
<WholeProgramOptimization>true</WholeProgramOptimization>
Expand Down
183 changes: 176 additions & 7 deletions src/UniGetUI.Avalonia/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Threading;
using Avalonia;
#if AVALONIA_DIAGNOSTICS_ENABLED
using AvaloniaUI.DiagnosticsProtocol;
#endif
using UniGetUI.Avalonia.Infrastructure;
using UniGetUI.Core.Data;
using UniGetUI.Core.Logging;
Expand All @@ -11,10 +13,22 @@ namespace UniGetUI.Avalonia;

internal sealed class Program
{
private const string DevToolsEnvVar = "UNIGETUI_AVALONIA_DEVTOOLS";

private enum DevToolsRuntimeMode
{
Auto,
Enabled,
Disabled,
}

// Kept alive for the lifetime of the process to enforce single-instance
// ReSharper disable once NotAccessedField.Local
private static Mutex? _singleInstanceMutex;

private static DevToolsRuntimeMode _devToolsRuntimeMode = DevToolsRuntimeMode.Auto;
private static string _devToolsRuntimeModeSource = "default";

[STAThread]
public static void Main(string[] args)
{
Expand Down Expand Up @@ -61,6 +75,10 @@ public static void Main(string[] args)
return;
}

(_devToolsRuntimeMode, _devToolsRuntimeModeSource) = ResolveDevToolsRuntimeMode(args);
Logger.Info(
$"Avalonia DevTools runtime mode: {_devToolsRuntimeMode} (source: {_devToolsRuntimeModeSource})");

// ── Single-instance enforcement ────────────────────────────────────
_singleInstanceMutex = new Mutex(
initiallyOwned: true,
Expand Down Expand Up @@ -263,22 +281,173 @@ private static void ReportFatalException(Exception ex)
catch { /* best-effort */ }
}

private static (DevToolsRuntimeMode Mode, string Source) ResolveDevToolsRuntimeMode(
string[] args)
{
if (TryGetDevToolsModeFromCli(args, out DevToolsRuntimeMode cliMode))
{
return (cliMode, "cli");
}

string? envValue = Environment.GetEnvironmentVariable(DevToolsEnvVar);
if (TryParseDevToolsRuntimeMode(envValue, out DevToolsRuntimeMode envMode))
{
return (envMode, "env");
}

if (!string.IsNullOrWhiteSpace(envValue))
{
Logger.Warn(
$"Ignoring invalid {DevToolsEnvVar} value '{envValue}'. Expected one of: auto, on, off, true, false, 1, 0.");
}

return (DevToolsRuntimeMode.Auto, "default");
}

private static bool TryGetDevToolsModeFromCli(
IEnumerable<string> args,
out DevToolsRuntimeMode mode)
{
bool found = false;
mode = DevToolsRuntimeMode.Auto;

foreach (string rawArg in args)
{
string arg = rawArg.Trim();

if (arg.Equals("--enable-devtools", StringComparison.OrdinalIgnoreCase))
{
mode = DevToolsRuntimeMode.Enabled;
found = true;
continue;
}

if (arg.Equals("--disable-devtools", StringComparison.OrdinalIgnoreCase))
{
mode = DevToolsRuntimeMode.Disabled;
found = true;
continue;
}

const string modePrefix = "--devtools-mode=";
if (!arg.StartsWith(modePrefix, StringComparison.OrdinalIgnoreCase))
{
continue;
}

string modeValue = arg[modePrefix.Length..].Trim();
if (TryParseDevToolsRuntimeMode(modeValue, out DevToolsRuntimeMode parsedMode))
{
mode = parsedMode;
found = true;
}
else
{
Logger.Warn(
$"Ignoring invalid --devtools-mode value '{modeValue}'. Expected one of: auto, enabled, disabled.");
}
}

return found;
}

private static bool TryParseDevToolsRuntimeMode(
string? value,
out DevToolsRuntimeMode mode)
{
mode = DevToolsRuntimeMode.Auto;
if (string.IsNullOrWhiteSpace(value))
{
return false;
}

switch (value.Trim().ToLowerInvariant())
{
case "auto":
mode = DevToolsRuntimeMode.Auto;
return true;
case "enabled":
case "enable":
case "on":
case "true":
case "1":
mode = DevToolsRuntimeMode.Enabled;
return true;
case "disabled":
case "disable":
case "off":
case "false":
case "0":
mode = DevToolsRuntimeMode.Disabled;
return true;
default:
return false;
}
}

public static AppBuilder BuildAvaloniaApp()
{
var builder = AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace();

#if DEBUG
builder = builder.WithDeveloperTools(options =>
#if AVALONIA_DIAGNOSTICS_ENABLED
bool isWsl = IsRunningInWsl();
bool shouldEnableDevTools = _devToolsRuntimeMode switch
{
DevToolsRuntimeMode.Enabled => true,
DevToolsRuntimeMode.Disabled => false,
_ => !isWsl,
};

if (_devToolsRuntimeMode == DevToolsRuntimeMode.Auto && isWsl)
{
Logger.Warn("Avalonia DevTools auto mode disabled on WSL to avoid avdt runner crashes.");
}

if (_devToolsRuntimeMode == DevToolsRuntimeMode.Enabled && isWsl)
{
Logger.Warn("Avalonia DevTools explicitly enabled on WSL. This configuration may be unstable.");
}

if (shouldEnableDevTools)
{
options.ApplicationName = "UniGetUI.Avalonia";
options.ConnectOnStartup = true;
options.EnableDiscovery = true;
options.DiagnosticLogger = DiagnosticLogger.CreateConsole();
});
builder = builder.WithDeveloperTools(options =>
{
options.ApplicationName = "UniGetUI.Avalonia";
options.ConnectOnStartup = true;
options.EnableDiscovery = true;
options.DiagnosticLogger = DiagnosticLogger.CreateConsole();
});
Logger.Info(
$"Avalonia DevTools enabled (mode: {_devToolsRuntimeMode}, source: {_devToolsRuntimeModeSource}).");
}
else
{
Logger.Info(
$"Avalonia DevTools disabled (mode: {_devToolsRuntimeMode}, source: {_devToolsRuntimeModeSource}).");
}
#else
if (_devToolsRuntimeMode != DevToolsRuntimeMode.Auto)
{
Logger.Warn(
"Avalonia DevTools runtime toggle was requested, but diagnostics support is not included in this build.");
}
#endif

return builder;
}

#if AVALONIA_DIAGNOSTICS_ENABLED
private static bool IsRunningInWsl()
{
if (!OperatingSystem.IsLinux())
return false;

if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("WSL_DISTRO_NAME")))
return true;

return !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("WSL_INTEROP"));
}
#endif
}
6 changes: 5 additions & 1 deletion src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@
<ApplicationIcon>..\UniGetUI\icon.ico</ApplicationIcon>
</PropertyGroup>

<PropertyGroup Condition="'$(EnableAvaloniaDiagnostics)' == 'true'">
<DefineConstants>$(DefineConstants);AVALONIA_DIAGNOSTICS_ENABLED</DefineConstants>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.7" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.7" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.7" />
<PackageReference Include="Devolutions.AvaloniaTheme.DevExpress" Version="2026.3.13" />
<PackageReference Include="Devolutions.AvaloniaTheme.MacOS" Version="2026.3.13" />
<PackageReference Include="Devolutions.AvaloniaTheme.Linux" Version="2026.3.11" />
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.0-beta3" Condition="'$(Configuration)' == 'Debug'" />
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.0-beta3" Condition="'$(EnableAvaloniaDiagnostics)' == 'true'" />
<PackageReference Include="Octokit" Version="14.0.0" />
</ItemGroup>

Expand Down
24 changes: 21 additions & 3 deletions src/UniGetUI.Core.LanguageEngine/LanguageEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ public LanguageEngine(string ForceLanguage = "")
if (LangName is "default" or "")
{
LangName = CultureInfo.CurrentUICulture.ToString().Replace("-", "_");
if (string.IsNullOrWhiteSpace(LangName))
{
LangName = "en";
}
}
LoadLanguage((ForceLanguage != "") ? ForceLanguage : LangName);
}
Expand All @@ -37,14 +41,20 @@ public void LoadLanguage(string lang)
{
try
{
lang = (lang ?? string.Empty).Trim();

Locale = "en";
if (LanguageData.LanguageReference.ContainsKey(lang))
{
Locale = lang;
}
else if (LanguageData.LanguageReference.ContainsKey(lang[0..2].Replace("uk", "ua")))
else if (lang.Length >= 2)
{
Locale = lang[0..2].Replace("uk", "ua");
string prefix = lang[0..2].Replace("uk", "ua");
if (LanguageData.LanguageReference.ContainsKey(prefix))
{
Locale = prefix;
}
}

MainLangDict = LoadLanguageFile(Locale);
Expand All @@ -58,6 +68,13 @@ public void LoadLanguage(string lang)
{
Logger.Error($"Could not load language file \"{lang}\"");
Logger.Error(ex);

// Keep the app functional even if locale resolution fails.
Locale = "en";
MainLangDict = LoadLanguageFile(Locale);
Formatter = new() { Locale = "en" };
LoadStaticTranslation();
SelectedLocale = Locale;
}
}

Expand Down Expand Up @@ -251,7 +268,8 @@ public string Translate(string key)

public string Translate(string key, Dictionary<string, object?> dict)
{
return Formatter!.FormatMessage(Translate(key), dict);
Formatter ??= new() { Locale = (Locale ?? "en").Replace('_', '-') };
return Formatter.FormatMessage(Translate(key), dict);
}
}
}
Loading