Skip to content

Commit c63edfb

Browse files
Improve Avalonia diagnostics and WSL native tool support (#4491)
* Improve Avalonia diagnostics and WSL native tool support - add build-time and runtime controls for Avalonia DevTools and document the developer-only workflow in AGENTS.md - harden language loading by falling back to English when locale values are empty or incomplete - filter Windows host PATH entries under WSL and prefer native cargo, dotnet, and npm executables - align Cargo dependency detection and installation commands with Linux executable names * Fix Cargo helper import ordering
1 parent 3f960a0 commit c63edfb

File tree

10 files changed

+334
-39
lines changed

10 files changed

+334
-39
lines changed

AGENTS.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
## Project Overview
44

5-
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`.
5+
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).
6+
7+
Solution entry points:
8+
- `src/UniGetUI.sln` - official Windows application based on WinUI 3
9+
- `src/UniGetUI.Avalonia.slnx` - experimental cross-platform Avalonia port
610

711
## Architecture
812

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

56+
## Avalonia DevTools (Developer-Only)
57+
58+
Use these rules when changing Avalonia diagnostics/devtools behavior:
59+
60+
- Build-time switch is `EnableAvaloniaDiagnostics` in `src/Directory.Build.props`.
61+
- Default policy: enabled in `Debug`, disabled in `Release`.
62+
- `src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj` must condition `AvaloniaUI.DiagnosticsSupport` on `$(EnableAvaloniaDiagnostics)`.
63+
- Compile-time diagnostics code in `src/UniGetUI.Avalonia/Program.cs` must be gated by `#if AVALONIA_DIAGNOSTICS_ENABLED` (not `#if DEBUG`).
64+
- Runtime controls are developer-only and intentionally not listed in `cli-arguments.md`.
65+
- Runtime precedence in `Program.cs`: CLI flags > `UNIGETUI_AVALONIA_DEVTOOLS` environment variable > `Auto` default.
66+
- Accepted runtime env/CLI values for mode parsing: `auto`, `enabled`, `disabled`, `on`, `off`, `true`, `false`, `1`, `0`.
67+
- `Auto` mode must remain WSL-safe (DevTools disabled by default on WSL).
68+
- If diagnostics were excluded at build time, runtime toggle requests should log a no-op warning.
69+
5270
## Key Patterns & Conventions
5371

5472
### Settings
@@ -77,6 +95,7 @@ Use `CoreTools.Translate("text")` for all user-facing strings. Parameterized: `C
7795
| Purpose | Path |
7896
|---|---|
7997
| Solution | `src/UniGetUI.sln` |
98+
| Experimental cross-platform solution | `src/UniGetUI.Avalonia.slnx` |
8099
| Shared build props | `src/Directory.Build.props` |
81100
| Version info | `src/SharedAssemblyInfo.cs` |
82101
| Manager interface | `src/UniGetUI.PAckageEngine.Interfaces/IPackageManager.cs` |

src/Directory.Build.props

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@
4545
<Configurations>Debug;Release</Configurations>
4646
</PropertyGroup>
4747

48+
<PropertyGroup>
49+
<EnableAvaloniaDiagnostics>false</EnableAvaloniaDiagnostics>
50+
<EnableAvaloniaDiagnostics Condition="'$(Configuration)' == 'Debug'">true</EnableAvaloniaDiagnostics>
51+
</PropertyGroup>
52+
4853
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
4954
<Optimize>true</Optimize>
5055
<WholeProgramOptimization>true</WholeProgramOptimization>

src/UniGetUI.Avalonia/Program.cs

Lines changed: 176 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System.Threading;
22
using Avalonia;
3+
#if AVALONIA_DIAGNOSTICS_ENABLED
34
using AvaloniaUI.DiagnosticsProtocol;
5+
#endif
46
using UniGetUI.Avalonia.Infrastructure;
57
using UniGetUI.Core.Data;
68
using UniGetUI.Core.Logging;
@@ -11,10 +13,22 @@ namespace UniGetUI.Avalonia;
1113

1214
internal sealed class Program
1315
{
16+
private const string DevToolsEnvVar = "UNIGETUI_AVALONIA_DEVTOOLS";
17+
18+
private enum DevToolsRuntimeMode
19+
{
20+
Auto,
21+
Enabled,
22+
Disabled,
23+
}
24+
1425
// Kept alive for the lifetime of the process to enforce single-instance
1526
// ReSharper disable once NotAccessedField.Local
1627
private static Mutex? _singleInstanceMutex;
1728

29+
private static DevToolsRuntimeMode _devToolsRuntimeMode = DevToolsRuntimeMode.Auto;
30+
private static string _devToolsRuntimeModeSource = "default";
31+
1832
[STAThread]
1933
public static void Main(string[] args)
2034
{
@@ -61,6 +75,10 @@ public static void Main(string[] args)
6175
return;
6276
}
6377

78+
(_devToolsRuntimeMode, _devToolsRuntimeModeSource) = ResolveDevToolsRuntimeMode(args);
79+
Logger.Info(
80+
$"Avalonia DevTools runtime mode: {_devToolsRuntimeMode} (source: {_devToolsRuntimeModeSource})");
81+
6482
// ── Single-instance enforcement ────────────────────────────────────
6583
_singleInstanceMutex = new Mutex(
6684
initiallyOwned: true,
@@ -263,22 +281,173 @@ private static void ReportFatalException(Exception ex)
263281
catch { /* best-effort */ }
264282
}
265283

284+
private static (DevToolsRuntimeMode Mode, string Source) ResolveDevToolsRuntimeMode(
285+
string[] args)
286+
{
287+
if (TryGetDevToolsModeFromCli(args, out DevToolsRuntimeMode cliMode))
288+
{
289+
return (cliMode, "cli");
290+
}
291+
292+
string? envValue = Environment.GetEnvironmentVariable(DevToolsEnvVar);
293+
if (TryParseDevToolsRuntimeMode(envValue, out DevToolsRuntimeMode envMode))
294+
{
295+
return (envMode, "env");
296+
}
297+
298+
if (!string.IsNullOrWhiteSpace(envValue))
299+
{
300+
Logger.Warn(
301+
$"Ignoring invalid {DevToolsEnvVar} value '{envValue}'. Expected one of: auto, on, off, true, false, 1, 0.");
302+
}
303+
304+
return (DevToolsRuntimeMode.Auto, "default");
305+
}
306+
307+
private static bool TryGetDevToolsModeFromCli(
308+
IEnumerable<string> args,
309+
out DevToolsRuntimeMode mode)
310+
{
311+
bool found = false;
312+
mode = DevToolsRuntimeMode.Auto;
313+
314+
foreach (string rawArg in args)
315+
{
316+
string arg = rawArg.Trim();
317+
318+
if (arg.Equals("--enable-devtools", StringComparison.OrdinalIgnoreCase))
319+
{
320+
mode = DevToolsRuntimeMode.Enabled;
321+
found = true;
322+
continue;
323+
}
324+
325+
if (arg.Equals("--disable-devtools", StringComparison.OrdinalIgnoreCase))
326+
{
327+
mode = DevToolsRuntimeMode.Disabled;
328+
found = true;
329+
continue;
330+
}
331+
332+
const string modePrefix = "--devtools-mode=";
333+
if (!arg.StartsWith(modePrefix, StringComparison.OrdinalIgnoreCase))
334+
{
335+
continue;
336+
}
337+
338+
string modeValue = arg[modePrefix.Length..].Trim();
339+
if (TryParseDevToolsRuntimeMode(modeValue, out DevToolsRuntimeMode parsedMode))
340+
{
341+
mode = parsedMode;
342+
found = true;
343+
}
344+
else
345+
{
346+
Logger.Warn(
347+
$"Ignoring invalid --devtools-mode value '{modeValue}'. Expected one of: auto, enabled, disabled.");
348+
}
349+
}
350+
351+
return found;
352+
}
353+
354+
private static bool TryParseDevToolsRuntimeMode(
355+
string? value,
356+
out DevToolsRuntimeMode mode)
357+
{
358+
mode = DevToolsRuntimeMode.Auto;
359+
if (string.IsNullOrWhiteSpace(value))
360+
{
361+
return false;
362+
}
363+
364+
switch (value.Trim().ToLowerInvariant())
365+
{
366+
case "auto":
367+
mode = DevToolsRuntimeMode.Auto;
368+
return true;
369+
case "enabled":
370+
case "enable":
371+
case "on":
372+
case "true":
373+
case "1":
374+
mode = DevToolsRuntimeMode.Enabled;
375+
return true;
376+
case "disabled":
377+
case "disable":
378+
case "off":
379+
case "false":
380+
case "0":
381+
mode = DevToolsRuntimeMode.Disabled;
382+
return true;
383+
default:
384+
return false;
385+
}
386+
}
387+
266388
public static AppBuilder BuildAvaloniaApp()
267389
{
268390
var builder = AppBuilder.Configure<App>()
269391
.UsePlatformDetect()
270392
.LogToTrace();
271393

272-
#if DEBUG
273-
builder = builder.WithDeveloperTools(options =>
394+
#if AVALONIA_DIAGNOSTICS_ENABLED
395+
bool isWsl = IsRunningInWsl();
396+
bool shouldEnableDevTools = _devToolsRuntimeMode switch
397+
{
398+
DevToolsRuntimeMode.Enabled => true,
399+
DevToolsRuntimeMode.Disabled => false,
400+
_ => !isWsl,
401+
};
402+
403+
if (_devToolsRuntimeMode == DevToolsRuntimeMode.Auto && isWsl)
404+
{
405+
Logger.Warn("Avalonia DevTools auto mode disabled on WSL to avoid avdt runner crashes.");
406+
}
407+
408+
if (_devToolsRuntimeMode == DevToolsRuntimeMode.Enabled && isWsl)
409+
{
410+
Logger.Warn("Avalonia DevTools explicitly enabled on WSL. This configuration may be unstable.");
411+
}
412+
413+
if (shouldEnableDevTools)
274414
{
275-
options.ApplicationName = "UniGetUI.Avalonia";
276-
options.ConnectOnStartup = true;
277-
options.EnableDiscovery = true;
278-
options.DiagnosticLogger = DiagnosticLogger.CreateConsole();
279-
});
415+
builder = builder.WithDeveloperTools(options =>
416+
{
417+
options.ApplicationName = "UniGetUI.Avalonia";
418+
options.ConnectOnStartup = true;
419+
options.EnableDiscovery = true;
420+
options.DiagnosticLogger = DiagnosticLogger.CreateConsole();
421+
});
422+
Logger.Info(
423+
$"Avalonia DevTools enabled (mode: {_devToolsRuntimeMode}, source: {_devToolsRuntimeModeSource}).");
424+
}
425+
else
426+
{
427+
Logger.Info(
428+
$"Avalonia DevTools disabled (mode: {_devToolsRuntimeMode}, source: {_devToolsRuntimeModeSource}).");
429+
}
430+
#else
431+
if (_devToolsRuntimeMode != DevToolsRuntimeMode.Auto)
432+
{
433+
Logger.Warn(
434+
"Avalonia DevTools runtime toggle was requested, but diagnostics support is not included in this build.");
435+
}
280436
#endif
281437

282438
return builder;
283439
}
440+
441+
#if AVALONIA_DIAGNOSTICS_ENABLED
442+
private static bool IsRunningInWsl()
443+
{
444+
if (!OperatingSystem.IsLinux())
445+
return false;
446+
447+
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("WSL_DISTRO_NAME")))
448+
return true;
449+
450+
return !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("WSL_INTEROP"));
451+
}
452+
#endif
284453
}

src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@
88
<ApplicationIcon>..\UniGetUI\icon.ico</ApplicationIcon>
99
</PropertyGroup>
1010

11+
<PropertyGroup Condition="'$(EnableAvaloniaDiagnostics)' == 'true'">
12+
<DefineConstants>$(DefineConstants);AVALONIA_DIAGNOSTICS_ENABLED</DefineConstants>
13+
</PropertyGroup>
14+
1115
<ItemGroup>
1216
<PackageReference Include="Avalonia" Version="11.3.7" />
1317
<PackageReference Include="Avalonia.Desktop" Version="11.3.7" />
1418
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.7" />
1519
<PackageReference Include="Devolutions.AvaloniaTheme.DevExpress" Version="2026.3.13" />
1620
<PackageReference Include="Devolutions.AvaloniaTheme.MacOS" Version="2026.3.13" />
1721
<PackageReference Include="Devolutions.AvaloniaTheme.Linux" Version="2026.3.11" />
18-
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.0-beta3" Condition="'$(Configuration)' == 'Debug'" />
22+
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.0-beta3" Condition="'$(EnableAvaloniaDiagnostics)' == 'true'" />
1923
<PackageReference Include="Octokit" Version="14.0.0" />
2024
</ItemGroup>
2125

src/UniGetUI.Core.LanguageEngine/LanguageEngine.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ public LanguageEngine(string ForceLanguage = "")
2525
if (LangName is "default" or "")
2626
{
2727
LangName = CultureInfo.CurrentUICulture.ToString().Replace("-", "_");
28+
if (string.IsNullOrWhiteSpace(LangName))
29+
{
30+
LangName = "en";
31+
}
2832
}
2933
LoadLanguage((ForceLanguage != "") ? ForceLanguage : LangName);
3034
}
@@ -37,14 +41,20 @@ public void LoadLanguage(string lang)
3741
{
3842
try
3943
{
44+
lang = (lang ?? string.Empty).Trim();
45+
4046
Locale = "en";
4147
if (LanguageData.LanguageReference.ContainsKey(lang))
4248
{
4349
Locale = lang;
4450
}
45-
else if (LanguageData.LanguageReference.ContainsKey(lang[0..2].Replace("uk", "ua")))
51+
else if (lang.Length >= 2)
4652
{
47-
Locale = lang[0..2].Replace("uk", "ua");
53+
string prefix = lang[0..2].Replace("uk", "ua");
54+
if (LanguageData.LanguageReference.ContainsKey(prefix))
55+
{
56+
Locale = prefix;
57+
}
4858
}
4959

5060
MainLangDict = LoadLanguageFile(Locale);
@@ -58,6 +68,13 @@ public void LoadLanguage(string lang)
5868
{
5969
Logger.Error($"Could not load language file \"{lang}\"");
6070
Logger.Error(ex);
71+
72+
// Keep the app functional even if locale resolution fails.
73+
Locale = "en";
74+
MainLangDict = LoadLanguageFile(Locale);
75+
Formatter = new() { Locale = "en" };
76+
LoadStaticTranslation();
77+
SelectedLocale = Locale;
6178
}
6279
}
6380

@@ -251,7 +268,8 @@ public string Translate(string key)
251268

252269
public string Translate(string key, Dictionary<string, object?> dict)
253270
{
254-
return Formatter!.FormatMessage(Translate(key), dict);
271+
Formatter ??= new() { Locale = (Locale ?? "en").Replace('_', '-') };
272+
return Formatter.FormatMessage(Translate(key), dict);
255273
}
256274
}
257275
}

0 commit comments

Comments
 (0)