From 534a764b964a704a0cd332f0829830e391203620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Thu, 4 Jun 2026 02:42:33 -0400 Subject: [PATCH] Allow custom PinGet executable paths Honor existing custom executable paths even when they are not part of the detected PATH candidates, and add Browse support to the WinUI and Avalonia package-manager settings pages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SettingsPages/PackageManagerPage.axaml.cs | 70 ++++++++++++++++++- .../Manager/PackageManager.cs | 38 +++++----- .../PackageManagerTests.cs | 19 ++++- .../ManagersPages/PackageManager.xaml | 13 +++- .../ManagersPages/PackageManager.xaml.cs | 54 ++++++++++++-- 5 files changed, 165 insertions(+), 29 deletions(-) diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs index 1228cf75ff..5f0c54e948 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs @@ -3,6 +3,7 @@ using Avalonia.Input.Platform; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.Platform.Storage; using UniGetUI.Avalonia.Infrastructure; using UniGetUI.Avalonia.ViewModels; using UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; @@ -78,7 +79,7 @@ private void BuildPage() var execHint = new TextBlock { - Text = CoreTools.Translate("Not finding the file you are looking for? Make sure it has been added to path."), + Text = CoreTools.Translate("Not finding the file you are looking for? Browse to it or make sure it has been added to PATH."), FontSize = 12, FontWeight = FontWeight.SemiBold, Opacity = 0.7, @@ -90,14 +91,21 @@ private void BuildPage() var execCombo = new ComboBox { HorizontalAlignment = HorizontalAlignment.Stretch }; AutomationProperties.SetName(execCombo, CoreTools.Translate("Select the executable to be used. The following list shows the executables found by UniGetUI")); foreach (var path in manager.FindCandidateExecutableFiles()) - execCombo.Items.Add(path); + AddExecutablePathItem(execCombo, path); string savedPath = CoreSettings.GetDictionaryItem(CoreSettings.K.ManagerPaths, manager.Name) ?? ""; + if (!string.IsNullOrEmpty(savedPath) && File.Exists(savedPath)) + AddExecutablePathItem(execCombo, savedPath); if (string.IsNullOrEmpty(savedPath)) { var (found, path) = manager.GetExecutableFile(); savedPath = found ? path : ""; } + else if (!File.Exists(savedPath)) + { + var (found, path) = manager.GetExecutableFile(); + savedPath = found ? path : ""; + } execCombo.SelectedItem = savedPath; execCombo.IsEnabled = customPathsAllowed; execCombo.SelectionChanged += (s, _) => @@ -106,9 +114,37 @@ private void BuildPage() ViewModel.OnExecutableSelected(selected); }; Grid.SetRow(execCombo, 1); - Grid.SetColumnSpan(execCombo, 2); + Grid.SetColumn(execCombo, 0); execGrid.Children.Add(execCombo); + var browseExecutableButton = new Button + { + Content = CoreTools.Translate("Browse..."), + IsEnabled = customPathsAllowed, + Margin = new Thickness(8, 0, 0, 0), + }; + browseExecutableButton.Click += async (_, _) => + { + if (TopLevel.GetTopLevel(this) is not { } topLevel) return; + var files = await topLevel.StorageProvider.OpenFilePickerAsync( + new FilePickerOpenOptions + { + AllowMultiple = false, + Title = CoreTools.Translate("Select executable"), + FileTypeFilter = GetExecutableFileTypeFilter(), + }); + if (files is not [{ } file]) return; + + string? path = file.TryGetLocalPath(); + if (string.IsNullOrWhiteSpace(path)) return; + + AddExecutablePathItem(execCombo, path); + execCombo.SelectedItem = path; + }; + Grid.SetRow(browseExecutableButton, 1); + Grid.SetColumn(browseExecutableButton, 1); + execGrid.Children.Add(browseExecutableButton); + if (!customPathsAllowed) { var securityWarning = new TextBlock @@ -342,6 +378,34 @@ private void BuildPage() } } + private static IReadOnlyList GetExecutableFileTypeFilter() + { + if (!OperatingSystem.IsWindows()) + { + return [new FilePickerFileType(CoreTools.Translate("All files")) { Patterns = ["*"] }]; + } + + return + [ + new FilePickerFileType(CoreTools.Translate("Executable")) { Patterns = ["*.exe"] }, + new FilePickerFileType(CoreTools.Translate("All files")) { Patterns = ["*"] }, + ]; + } + + private static void AddExecutablePathItem(ComboBox comboBox, string path) + { + if (string.IsNullOrWhiteSpace(path)) + return; + + foreach (object? item in comboBox.Items) + { + if (string.Equals(item?.ToString(), path, StringComparison.OrdinalIgnoreCase)) + return; + } + + comboBox.Items.Add(path); + } + private void BuildExtraControls(CheckboxCard_Dict disableNotifsCard) { ExtraControls.Children.Clear(); diff --git a/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs b/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs index 40b861ef77..c08ef9a594 100644 --- a/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs +++ b/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs @@ -194,15 +194,16 @@ private void _logManagerInfo() public Tuple GetExecutableFile() { var candidates = FindCandidateExecutableFiles(); - if (candidates.Count == 0) - { - // No paths were found - return new(false, ""); - } // If custom package manager paths are DISABLED, get the first one (as old UniGetUI did) and return it. if (!SecureSettings.Get(SecureSettings.K.AllowCustomManagerPaths)) { + if (candidates.Count == 0) + { + // No paths were found + return new(false, ""); + } + return new(true, candidates[0]); } else @@ -214,32 +215,33 @@ public Tuple GetExecutableFile() // If there is no executable selection for this package manager if (string.IsNullOrEmpty(exeSelection)) { + if (candidates.Count == 0) + { + // No paths were found + return new(false, ""); + } + return new(true, candidates[0]); } else if (!File.Exists(exeSelection)) { Logger.Error( - $"The selected executable path {exeSelection} for manager {Name} does not exist, the default one will be used..." + $"The selected executable path {exeSelection} for manager {Name} does not exist, the default one will be used if available..." ); - return new(true, candidates[0]); } - // While technically executables that are not in the path should work, - // since detection of executables will be performed on the found paths, it is more consistent - // to throw an error when a non-found executable is used. Furthermore, doing this we can filter out - // any invalid paths or files. - if (candidates.Select(x => x.ToLower()).Contains(exeSelection.ToLower())) + else { return new(true, exeSelection); } - else + + if (candidates.Count == 0) { - Logger.Error( - $"The selected executable path {exeSelection} for manager {Name} was not found among the candidates " - + $"(executables found are [{string.Join(',', candidates)}]), the default will be used..." - ); - return new(true, candidates[0]); + // No paths were found + return new(false, ""); } + + return new(true, candidates[0]); } } diff --git a/src/UniGetUI.PackageEngine.Tests/PackageManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/PackageManagerTests.cs index b9d8a7a249..0aab8e4b23 100644 --- a/src/UniGetUI.PackageEngine.Tests/PackageManagerTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/PackageManagerTests.cs @@ -145,6 +145,21 @@ public void GetExecutableFileReturnsFalseWhenNoCandidatesExist() Assert.Equal(string.Empty, executable.Item2); } + [Fact] + public void GetExecutableFileUsesSavedPathWhenNoCandidatesExistAndCustomPathsAreEnabled() + { + var manager = CreateManager(); + var customPath = CreateExecutable("custom-only.exe"); + manager.SetCandidateExecutableFiles(); + EnableCustomManagerPaths(); + Settings.SetDictionaryItem(Settings.K.ManagerPaths, manager.Name, customPath); + + var executable = manager.GetExecutableFile(); + + Assert.True(executable.Item1); + Assert.Equal(customPath, executable.Item2); + } + [Fact] public void GetExecutableFileIgnoresSavedPathWhenCustomPathsAreDisabled() { @@ -197,7 +212,7 @@ public void GetExecutableFileFallsBackWhenSavedPathDoesNotExist() } [Fact] - public void GetExecutableFileFallsBackWhenSavedPathIsNotACandidate() + public void GetExecutableFileUsesSavedPathWhenSavedPathIsNotACandidate() { var manager = CreateManager(); var first = CreateExecutable("outside-a.exe"); @@ -214,7 +229,7 @@ public void GetExecutableFileFallsBackWhenSavedPathIsNotACandidate() var executable = manager.GetExecutableFile(); Assert.True(executable.Item1); - Assert.Equal(first, executable.Item2); + Assert.Equal(outsideCandidateList, executable.Item2); } [Fact] diff --git a/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml b/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml index 3a79e68dfe..3bebba72bd 100644 --- a/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml +++ b/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml @@ -75,14 +75,23 @@ FontSize="12" FontWeight="SemiBold" Opacity="0.7" - Text="Not finding the file you are looking for? Make sure it has been added to path." + Text="Not finding the file you are looking for? Browse to it or make sure it has been added to PATH." /> + (Settings.K.ManagerPaths, Manager.Name) ?? ""; + string selectedValue = configuredValue; + if (!string.IsNullOrEmpty(configuredValue) && File.Exists(configuredValue)) + { + AddExecutableComboBoxItem(configuredValue); + } if (string.IsNullOrEmpty(selectedValue)) { var exe = Manager.GetExecutableFile(); selectedValue = exe.Item1 ? exe.Item2 : ""; } + else if (!File.Exists(selectedValue)) + { + var exe = Manager.GetExecutableFile(); + selectedValue = exe.Item1 ? exe.Item2 : ""; + } ExecutableComboBox.SelectedValue = selectedValue; ExecutableComboBox.SelectionChanged += ExecutableComboBox_SelectionChanged; @@ -699,17 +712,50 @@ private void ManagerLogs_Click(object sender, RoutedEventArgs e) private void ExecutableComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { - if (string.IsNullOrEmpty(ExecutableComboBox.SelectedValue.ToString())) + if (ExecutableComboBox.SelectedValue?.ToString() is not { Length: > 0 } selectedValue) return; Settings.SetDictionaryItem( Settings.K.ManagerPaths, Manager!.Name, - ExecutableComboBox.SelectedValue.ToString() + selectedValue ); _ = ReloadPackageManager(); } + private void BrowseExecutableButton_Click(object sender, RoutedEventArgs e) => + _ = _browseExecutableButton_Click(); + + private async Task _browseExecutableButton_Click() + { + if (Manager is null) + return; + + ExternalLibraries.Pickers.FileOpenPicker picker = new( + MainApp.Instance.MainWindow.GetWindowHandle() + ); + string file = picker.Show(["*.exe"]); + if (file == string.Empty) + return; + + Settings.SetDictionaryItem(Settings.K.ManagerPaths, Manager.Name, file); + await ReloadPackageManager(); + } + + private void AddExecutableComboBoxItem(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return; + + foreach (object? item in ExecutableComboBox.Items) + { + if (string.Equals(item?.ToString(), path, StringComparison.OrdinalIgnoreCase)) + return; + } + + ExecutableComboBox.Items.Add(path); + } + private void GoToSecureSettingsBtn_Click(object sender, RoutedEventArgs e) { MainApp.Instance.MainWindow.NavigationPage.OpenSettingsPage(typeof(Administrator));