From eeb84cb621afbc124529783b1dc0321fecb11a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Mon, 29 Sep 2025 15:43:57 +0200 Subject: [PATCH 01/11] Dependencies: Upgrade WinUIEx to 2.8.0 (#40639) ## Summary of the Pull Request This change upgrades the WinUIEx NuGet package from version 2.2.0 to 2.8.0. - Prevents the window itself from taking focus when it should not. - Removes dead code from Settings.UI (the code triggered error [WinUIEX1001](https://dotmorten.github.io/WinUIEx/rules/WinUIEx1001.html) -- Window.Current is always null). ## PR Checklist - [x] Closes: #40637 - [x] Closes: #7647 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed I've built and run [projects utilizing WinUIEx](https://github.com/search?q=repo%3Amicrosoft%2FPowerToys+WinUIEx+path%3A*.*proj&type=code): - Microsoft.CmdPal.UI - MeasureToolUI - FileLocksmithUI - EnvironmentVariables - AdvancedPaste - Peek.UI - RegistryPreview - PowerToys.Settings - Hosts --- Directory.Packages.props | 2 +- .../Activation/ActivationHandler.cs | 44 -------- .../Activation/DefaultActivationHandler.cs | 42 ------- .../Settings.UI/Services/ActivationService.cs | 106 ------------------ .../Settings.UI/Services/NavigationService.cs | 6 - 5 files changed, 1 insertion(+), 199 deletions(-) delete mode 100644 src/settings-ui/Settings.UI/Activation/ActivationHandler.cs delete mode 100644 src/settings-ui/Settings.UI/Activation/DefaultActivationHandler.cs delete mode 100644 src/settings-ui/Settings.UI/Services/ActivationService.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index eabda4151d..c77a8898fc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -108,7 +108,7 @@ - + diff --git a/src/settings-ui/Settings.UI/Activation/ActivationHandler.cs b/src/settings-ui/Settings.UI/Activation/ActivationHandler.cs deleted file mode 100644 index aabe2aff53..0000000000 --- a/src/settings-ui/Settings.UI/Activation/ActivationHandler.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; - -namespace Microsoft.PowerToys.Settings.UI.Activation -{ - // For more information on understanding and extending activation flow see - // https://github.com/Microsoft/WindowsTemplateStudio/blob/master/docs/activation.md - internal abstract class ActivationHandler - { - public abstract bool CanHandle(object args); - - public abstract Task HandleAsync(object args); - } - - [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "abstract T and abstract")] - internal abstract class ActivationHandler : ActivationHandler - where T : class - { - public override async Task HandleAsync(object args) - { - await HandleInternalAsync(args as T).ConfigureAwait(false); - } - - public override bool CanHandle(object args) - { - // CanHandle checks the args is of type you have configured - return args is T && CanHandleInternal(args as T); - } - - // Override this method to add the activation logic in your activation handler - protected abstract Task HandleInternalAsync(T args); - - // You can override this method to add extra validation on activation args - // to determine if your ActivationHandler should handle this activation args - protected virtual bool CanHandleInternal(T args) - { - return true; - } - } -} diff --git a/src/settings-ui/Settings.UI/Activation/DefaultActivationHandler.cs b/src/settings-ui/Settings.UI/Activation/DefaultActivationHandler.cs deleted file mode 100644 index 946fab205c..0000000000 --- a/src/settings-ui/Settings.UI/Activation/DefaultActivationHandler.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Threading.Tasks; - -using Microsoft.PowerToys.Settings.UI.Services; -using Windows.ApplicationModel.Activation; - -namespace Microsoft.PowerToys.Settings.UI.Activation -{ - internal sealed class DefaultActivationHandler : ActivationHandler - { - private readonly Type navElement; - - public DefaultActivationHandler(Type navElement) - { - this.navElement = navElement; - } - - protected override async Task HandleInternalAsync(IActivatedEventArgs args) - { - // When the navigation stack isn't restored, navigate to the first page and configure - // the new page by passing required information in the navigation parameter - object arguments = null; - if (args is LaunchActivatedEventArgs launchArgs) - { - arguments = launchArgs.Arguments; - } - - NavigationService.Navigate(navElement, arguments); - await Task.CompletedTask.ConfigureAwait(false); - } - - protected override bool CanHandleInternal(IActivatedEventArgs args) - { - // None of the ActivationHandlers has handled the app activation - return NavigationService.Frame.Content == null && navElement != null; - } - } -} diff --git a/src/settings-ui/Settings.UI/Services/ActivationService.cs b/src/settings-ui/Settings.UI/Services/ActivationService.cs deleted file mode 100644 index 86ad2e4d7c..0000000000 --- a/src/settings-ui/Settings.UI/Services/ActivationService.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -using Microsoft.PowerToys.Settings.UI.Activation; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Windows.ApplicationModel.Activation; - -namespace Microsoft.PowerToys.Settings.UI.Services -{ - // For more information on understanding and extending activation flow see - // https://github.com/Microsoft/WindowsTemplateStudio/blob/master/docs/activation.md - internal sealed class ActivationService - { - private readonly App app; - private readonly Type defaultNavItem; - private Lazy shell; - - private object lastActivationArgs; - - public ActivationService(App app, Type defaultNavItem, Lazy shell = null) - { - this.app = app; - this.shell = shell; - this.defaultNavItem = defaultNavItem; - } - - public async Task ActivateAsync(object activationArgs) - { - if (IsInteractive(activationArgs)) - { - // Initialize services that you need before app activation - // take into account that the splash screen is shown while this code runs. - await InitializeAsync().ConfigureAwait(false); - - // Do not repeat app initialization when the Window already has content, - // just ensure that the window is active - if (Window.Current.Content == null) - { - // Create a Shell or Frame to act as the navigation context - Window.Current.Content = shell?.Value ?? new Frame(); - } - } - - // Depending on activationArgs one of ActivationHandlers or DefaultActivationHandler - // will navigate to the first page - await HandleActivationAsync(activationArgs).ConfigureAwait(false); - lastActivationArgs = activationArgs; - - if (IsInteractive(activationArgs)) - { - // Ensure the current window is active - Window.Current.Activate(); - - // Tasks after activation - await StartupAsync().ConfigureAwait(false); - } - } - - private static async Task InitializeAsync() - { - await Task.CompletedTask.ConfigureAwait(false); - } - - private async Task HandleActivationAsync(object activationArgs) - { - var activationHandler = GetActivationHandlers() - .FirstOrDefault(h => h.CanHandle(activationArgs)); - - if (activationHandler != null) - { - await activationHandler.HandleAsync(activationArgs).ConfigureAwait(false); - } - - if (IsInteractive(activationArgs)) - { - var defaultHandler = new DefaultActivationHandler(defaultNavItem); - if (defaultHandler.CanHandle(activationArgs)) - { - await defaultHandler.HandleAsync(activationArgs).ConfigureAwait(false); - } - } - } - - private static async Task StartupAsync() - { - await Task.CompletedTask.ConfigureAwait(false); - } - - private static IEnumerable GetActivationHandlers() - { - yield break; - } - - private static bool IsInteractive(object args) - { - return args is IActivatedEventArgs; - } - } -} diff --git a/src/settings-ui/Settings.UI/Services/NavigationService.cs b/src/settings-ui/Settings.UI/Services/NavigationService.cs index b70976bd01..d7c408208b 100644 --- a/src/settings-ui/Settings.UI/Services/NavigationService.cs +++ b/src/settings-ui/Settings.UI/Services/NavigationService.cs @@ -24,12 +24,6 @@ namespace Microsoft.PowerToys.Settings.UI.Services { get { - if (frame == null) - { - frame = Window.Current.Content as Frame; - RegisterFrameEvents(); - } - return frame; } From 08dc3fbcefb98e742a19aa06dae365bd59c5da10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Mon, 29 Sep 2025 17:09:42 +0200 Subject: [PATCH 02/11] CmdPal: Fix desynced resmanager files (#42038) ## Summary of the Pull Request This PR fixes desynced resource manager files introduced by previous commits. While Visual Studio would regenerate and correct these files automatically, applying this fix preemptively reduces unnecessary churn in unrelated commits and avoids redundant file changes. ## PR Checklist - [ ] Closes: #xxx - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --- .../Properties/Resources.resx | 59 +++++++++++++++++++ .../Properties/Resources.Designer.cs | 9 --- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Properties/Resources.resx b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Properties/Resources.resx index 0486f9b68b..560907942b 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Properties/Resources.resx +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Properties/Resources.resx @@ -1,5 +1,64 @@ + diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Properties/Resources.Designer.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Properties/Resources.Designer.cs index 0ef003b4fe..e2fd310a60 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Properties/Resources.Designer.cs @@ -159,15 +159,6 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit.Properties { } } - /// - /// Looks up a localized string similar to Open. - /// - internal static string Page_Name { - get { - return ResourceManager.GetString("Page_Name", resourceCulture); - } - } - /// /// Looks up a localized string similar to Settings. /// From 48b70e086183584f63f07eb47184b05312202f3f Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Mon, 29 Sep 2025 17:26:24 +0200 Subject: [PATCH 03/11] [CmdPal Settings] Improved NavView behavior (#42044) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request The NavView behavior (e.g. when showing the panebutton, collapsing the menu etc.) was inconsistent with other Settings experiences (like PT Settings and W11 Settings). This PR makes use of the TitleBar's PaneToggleButton. ## PR Checklist - [ ] Closes: #xxx - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --------- Co-authored-by: Jiří Polášek --- .../Settings/SettingsWindow.xaml | 54 +++++++++---------- .../Settings/SettingsWindow.xaml.cs | 13 +++-- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml index 7798b5588b..e4acb05ae1 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml @@ -24,10 +24,11 @@ - + @@ -36,18 +37,19 @@ + Loaded="NavView_Loaded"> 15,0,0,0 - - - - - - - - - - - - 28 - 7,4,8,0 - SemiBold - 16 - - - - + + + + + + + + + + 28 + 7,4,8,0 + SemiBold + 16 + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml.cs index b3e1647294..ecd6d3564d 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml.cs @@ -34,7 +34,7 @@ public sealed partial class SettingsWindow : WindowEx, var title = RS_.GetString("SettingsWindowTitle"); this.AppWindow.Title = title; this.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall; - this.TitleBar.Title = title; + this.AppTitleBar.Title = title; PositionCentered(); WeakReferenceMessenger.Default.Register(this); @@ -142,11 +142,13 @@ public sealed partial class SettingsWindow : WindowEx, { if (args.DisplayMode == NavigationViewDisplayMode.Compact || args.DisplayMode == NavigationViewDisplayMode.Minimal) { - NavView.IsPaneToggleButtonVisible = false; + AppTitleBar.IsPaneToggleButtonVisible = true; + WorkAroundIcon.Margin = new Thickness(8, 0, 16, 0); // Required for workaround, see XAML comment } else { - NavView.IsPaneToggleButtonVisible = true; + AppTitleBar.IsPaneToggleButtonVisible = false; + WorkAroundIcon.Margin = new Thickness(16, 0, 0, 0); // Required for workaround, see XAML comment } } @@ -155,6 +157,11 @@ public sealed partial class SettingsWindow : WindowEx, // This might come in on a background thread DispatcherQueue.TryEnqueue(() => Close()); } + + private void AppTitleBar_PaneToggleRequested(TitleBar sender, object args) + { + NavView.IsPaneOpen = !NavView.IsPaneOpen; + } } public readonly struct Crumb From 05c700a4cdfe680287c0fcd621467c7506b42016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Mon, 29 Sep 2025 23:01:32 +0200 Subject: [PATCH 04/11] CmdPal: Fix NavView merge (#42096) ## Summary of the Pull Request Regression: #42044 ## PR Checklist - [ ] Closes: #xxx - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --- .../cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml.cs index ecd6d3564d..5d042a09e3 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml.cs @@ -14,6 +14,7 @@ using Microsoft.UI.Xaml.Automation.Peers; using Microsoft.UI.Xaml.Controls; using WinUIEx; using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance; +using TitleBar = Microsoft.UI.Xaml.Controls.TitleBar; namespace Microsoft.CmdPal.UI.Settings; From c8486087d85cb04456d067eddaae16b2680e86ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Tue, 30 Sep 2025 16:06:48 +0200 Subject: [PATCH 05/11] CmdPal: Update visual style of details panel elements (#42102) ## Summary of the Pull Request This PR updates the details panel formatting: - Hides the empty text block used as a separator when the key is empty. - Makes separators more subtle by adjusting the brush. - Reverses the typographical hierarchy of detail key/value items, making the value dominant and the key more subtle to help users focus on the content. - Defines new detail text styles derived from the base WinUI typographical styles. | Before | After | |--------|-------| | image | image | ## PR Checklist - [x] Closes: #42099 - [x] Closes: #41664 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --- .../Helpers/BindTransformers.cs | 8 +++ .../Microsoft.CmdPal.UI/Pages/ShellPage.xaml | 59 +++++++++++-------- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BindTransformers.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BindTransformers.cs index 66744b4c99..24d2ef47a6 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BindTransformers.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BindTransformers.cs @@ -2,9 +2,17 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.UI.Xaml; + namespace Microsoft.CmdPal.UI.Helpers; internal static class BindTransformers { public static bool Negate(bool value) => !value; + + public static Visibility EmptyToCollapsed(string? input) + => string.IsNullOrEmpty(input) ? Visibility.Collapsed : Visibility.Visible; + + public static Visibility EmptyOrWhitespaceToCollapsed(string? input) + => string.IsNullOrWhiteSpace(input) ? Visibility.Collapsed : Visibility.Visible; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml index af0eff2181..597072241a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml @@ -41,6 +41,31 @@ FalseValue="Visible" TrueValue="Collapsed" /> + + + + + + - + @@ -76,20 +101,13 @@ + - @@ -98,10 +116,7 @@ - + + Visibility="{x:Bind help:BindTransformers.EmptyOrWhitespaceToCollapsed(Key), FallbackValue=Collapsed}" /> - + Date: Tue, 30 Sep 2025 18:03:52 +0200 Subject: [PATCH 06/11] CmdPal: Properly quote arguments when rebuilding normalized path (#42071) ## Summary of the Pull Request This PR ensures proper quoting of arguments after normalization. When joining arguments back into a single string, any argument containing whitespace or double quotes must be quoted (because parsing unquoted them). Adjusts unit tests to reflect the correct expected results. Ref: 42016 ## PR Checklist - [ ] Closes: #xxx - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --- .../NormalizeCommandLineTests.cs | 3 +- .../Helpers/ShellListPageHelpers.cs | 93 ++++++++++++++++++- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Shell.UnitTests/NormalizeCommandLineTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Shell.UnitTests/NormalizeCommandLineTests.cs index 67c98f274b..919790f198 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Shell.UnitTests/NormalizeCommandLineTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Shell.UnitTests/NormalizeCommandLineTests.cs @@ -23,6 +23,7 @@ public class NormalizeCommandLineTests : CommandPaletteUnitTestBase [DataRow("ping bing.com", "c:\\Windows\\system32\\ping.exe", "bing.com")] [DataRow("curl bing.com", "c:\\Windows\\system32\\curl.exe", "bing.com")] [DataRow("ipconfig /all", "c:\\Windows\\system32\\ipconfig.exe", "/all")] + [DataRow("ipconfig a b \"c d\"", "c:\\Windows\\system32\\ipconfig.exe", "a b \"c d\"")] public void NormalizeCommandLineSimple(string input, string expectedExe, string expectedArgs = "") { NormalizeTestCore(input, expectedExe, expectedArgs); @@ -46,7 +47,7 @@ public class NormalizeCommandLineTests : CommandPaletteUnitTestBase [TestMethod] [DataRow("cmd --run --test", "C:\\Windows\\System32\\cmd.exe", "--run --test")] [DataRow("cmd --run --test ", "C:\\Windows\\System32\\cmd.exe", "--run --test")] - [DataRow("cmd \"--run --test\" --pass", "C:\\Windows\\System32\\cmd.exe", "--run --test --pass")] + [DataRow("cmd \"--run --test\" --pass", "C:\\Windows\\System32\\cmd.exe", "\"--run --test\" --pass")] public void NormalizeArgsWithSpaces(string input, string expectedExe, string expectedArgs = "") { NormalizeTestCore(input, expectedExe, expectedArgs); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs index 621a265b28..14d605f458 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs @@ -157,7 +157,98 @@ public class ShellListPageHelpers executable = segments[0]; if (segments.Length > 1) { - arguments = string.Join(' ', segments[1..]); + arguments = ArgumentBuilder.BuildArguments(segments[1..]); + } + } + + private static class ArgumentBuilder + { + internal static string BuildArguments(string[] arguments) + { + if (arguments.Length <= 0) + { + return string.Empty; + } + + var stringBuilder = new StringBuilder(); + foreach (var argument in arguments) + { + AppendArgument(stringBuilder, argument); + } + + return stringBuilder.ToString(); + } + + private static void AppendArgument(StringBuilder stringBuilder, string argument) + { + if (stringBuilder.Length > 0) + { + stringBuilder.Append(' '); + } + + if (argument.Length == 0 || ShouldBeQuoted(argument)) + { + stringBuilder.Append('\"'); + var index = 0; + while (index < argument.Length) + { + var c = argument[index++]; + if (c == '\\') + { + var numBackSlash = 1; + while (index < argument.Length && argument[index] == '\\') + { + index++; + numBackSlash++; + } + + if (index == argument.Length) + { + stringBuilder.Append('\\', numBackSlash * 2); + } + else if (argument[index] == '\"') + { + stringBuilder.Append('\\', (numBackSlash * 2) + 1); + stringBuilder.Append('\"'); + index++; + } + else + { + stringBuilder.Append('\\', numBackSlash); + } + + continue; + } + + if (c == '\"') + { + stringBuilder.Append('\\'); + stringBuilder.Append('\"'); + continue; + } + + stringBuilder.Append(c); + } + + stringBuilder.Append('\"'); + } + else + { + stringBuilder.Append(argument); + } + } + + private static bool ShouldBeQuoted(string s) + { + foreach (var c in s) + { + if (char.IsWhiteSpace(c) || c == '\"') + { + return true; + } + } + + return false; } } } From f1f00475d1d41d00d994652ab2377660921a12d4 Mon Sep 17 00:00:00 2001 From: ruslanlap <106077551+ruslanlap@users.noreply.github.com> Date: Tue, 30 Sep 2025 20:53:22 +0300 Subject: [PATCH 07/11] Add CheatSheets plugin to third-party Run plugins documentation (#41952) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds the CheatSheets plugin to the third-party plugins documentation in the General plugins section. ## CheatSheets Plugin 📚 CheatSheets for PowerToys Run - Find cheat sheets and command examples instantly always at your fingertips with PowerToys Run plugin ![CheatSheets Demo](https://github.com/ruslanlap/PowerToysRun-CheatSheets/blob/master/assets/demo-cheatsheets.gif) This plugin enables users to instantly find cheat sheets and command examples for various tools and programming languages without leaving PowerToys Run. ### Features - 🔍 Instant Search - Find commands and cheat sheets with fuzzy matching - 📚 Multiple Sources - Integrates with tldr, cheat.sh, and offline cheat sheets - ⭐ Favorites System - Save and quickly access your most-used commands - 📂 Categories - Browse commands by tool/language (git, docker, python, etc.) - 📊 Usage History - Tracks popular commands for quick access - 💾 Smart Caching - Fast offline access with configurable cache duration - 🎨 Modern UI - Beautiful WPF interface with theme adaptation - 🔧 Offline Mode - Works without internet connection using cached data ## Link to plugin - https://github.com/ruslanlap/PowerToysRun-CheatSheets --- .github/actions/spell-check/allow/names.txt | 2 ++ doc/thirdPartyRunPlugins.md | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/actions/spell-check/allow/names.txt b/.github/actions/spell-check/allow/names.txt index 475e68045b..f1216a159d 100644 --- a/.github/actions/spell-check/allow/names.txt +++ b/.github/actions/spell-check/allow/names.txt @@ -210,6 +210,7 @@ capturevideosample cmdow Controlz cortana +devhints dlnilsson fancymouse firefox @@ -229,6 +230,7 @@ regedit roslyn Skia Spotify +tldr Vanara wangyi WEX diff --git a/doc/thirdPartyRunPlugins.md b/doc/thirdPartyRunPlugins.md index eccdc3530e..a15cb542a8 100644 --- a/doc/thirdPartyRunPlugins.md +++ b/doc/thirdPartyRunPlugins.md @@ -50,6 +50,7 @@ Contact the developers of a plugin directly for assistance with a specific plugi | [Hotkeys](https://github.com/ruslanlap/PowerToysRun-Hotkeys) | [ruslanlap](https://github.com/ruslanlap) | Create, manage, and trigger custom keyboard shortcuts directly from PowerToys Run. | | [RandomGen](https://github.com/ruslanlap/PowerToysRun-RandomGen) | [ruslanlap](https://github.com/ruslanlap) | 🎲 Generate random data instantly with a single keystroke. Perfect for developers, testers, designers, and anyone who needs quick access to random data. Features include secure passwords, PINs, names, business data, dates, numbers, GUIDs, color codes, and more. Especially useful for designers who need random color codes and placeholder content. | | [Open With Cursor](https://github.com/VictorNoxx/PowerToys-Run-Cursor/) | [VictorNoxx](https://github.com/VictorNoxx) | Open Visual Studio, VS Code recents with Cursor AI | +| [CheatSheets](https://github.com/ruslanlap/PowerToysRun-CheatSheets) | [ruslanlap](https://github.com/ruslanlap) | 📚 Find cheat sheets and command examples instantly from tldr pages, cheat.sh, and devhints.io. Features include favorites system, categories, offline mode, and smart caching. | ## Extending software plugins From 8318a40dd44fcbab49ef021cfb6163c016471565 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 30 Sep 2025 12:55:06 -0500 Subject: [PATCH 08/11] CmdPal: Bump version to 0.6 (#42097) This bumps the CmdPal version to 0.6. It also moves the template project to consume the 0.5 SDK. It also removes the WASDK dependency, because we only need the MSIX tooling. --- src/CmdPalVersion.props | 3 +++ .../Directory.Packages.props | 3 ++- .../TemplateCmdPalExtension.csproj | 12 +++++++++--- .../Assets/template.zip | Bin 18987 -> 19144 bytes src/modules/cmdpal/custom.props | 2 +- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/CmdPalVersion.props b/src/CmdPalVersion.props index 2be9bc69d4..c3c5d7b608 100644 --- a/src/CmdPalVersion.props +++ b/src/CmdPalVersion.props @@ -2,7 +2,10 @@ $(XES_APPXMANIFESTVERSION) + + 0.0.1.0 + Local diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props index 75ffb75e31..4722a0974e 100644 --- a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props +++ b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props @@ -3,12 +3,13 @@ true - + + diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/TemplateCmdPalExtension.csproj b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/TemplateCmdPalExtension.csproj index ae3035a498..7d83967705 100644 --- a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/TemplateCmdPalExtension.csproj +++ b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/TemplateCmdPalExtension.csproj @@ -40,10 +40,13 @@ - - - + + + + all + runtime; build; native; contentfiles; analyzers + true + + + false diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Assets/template.zip b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Assets/template.zip index f026bbba8b238d8ec1ca59b5c009830328697522..7bfe8ce57b4a0f5fba1a55d1139e20bba9c3434e 100644 GIT binary patch delta 4292 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 delta 4036 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 2025 0 - 5 + 6 Microsoft Command Palette From de53c81d75e394dfcc57204a9222c10e966c8683 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Tue, 30 Sep 2025 16:09:56 -0500 Subject: [PATCH 09/11] build: switch Touchdown to Federated Identity (#42119) This is required as part of offboarding our non-user service account. --- .pipelines/loc/loc.yml | 4 ++-- .../v2/templates/steps-fetch-and-prepare-localizations.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pipelines/loc/loc.yml b/.pipelines/loc/loc.yml index cc4512c92e..2abc298652 100644 --- a/.pipelines/loc/loc.yml +++ b/.pipelines/loc/loc.yml @@ -29,8 +29,8 @@ steps: displayName: 'Touchdown Build - 37400, PRODEXT' inputs: teamId: 37400 - TDBuildServiceConnection: $(TouchdownServiceConnection) - authType: SubjectNameIssuer + FederatedIdentityTDBuildServiceConnection: $(TouchdownServiceConnection) + authType: FederatedIdentityTDBuild resourceFilePath: | src\**\Resources.resx src\**\Resource.resx diff --git a/.pipelines/v2/templates/steps-fetch-and-prepare-localizations.yml b/.pipelines/v2/templates/steps-fetch-and-prepare-localizations.yml index 44f8c4b6dc..58f2fe6c47 100644 --- a/.pipelines/v2/templates/steps-fetch-and-prepare-localizations.yml +++ b/.pipelines/v2/templates/steps-fetch-and-prepare-localizations.yml @@ -8,8 +8,8 @@ steps: displayName: 'Download Localization Files -- PowerToys 37400' inputs: teamId: 37400 - TDBuildServiceConnection: $(TouchdownServiceConnection) - authType: SubjectNameIssuer + FederatedIdentityTDBuildServiceConnection: $(TouchdownServiceConnection) + authType: FederatedIdentityTDBuild resourceFilePath: | **\Resources.resx **\Resource.resx From fae466887caa354247344e6cfee3cbe4ae8fa677 Mon Sep 17 00:00:00 2001 From: Gordon Lam <73506701+yeelam-gordon@users.noreply.github.com> Date: Tue, 30 Sep 2025 18:32:13 -0700 Subject: [PATCH 10/11] Add back build cache, which will use the nightly build one (#42106) ## Summary of the Pull Request Related to this PR https://github.com/microsoft/PowerToys/pull/41968. Now, we can enabled back the ci build with the nightly build cache (Just kick off one today, and it will run periodically daily). --- .pipelines/v2/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/v2/ci.yml b/.pipelines/v2/ci.yml index 297c268757..6b0105a38a 100644 --- a/.pipelines/v2/ci.yml +++ b/.pipelines/v2/ci.yml @@ -32,7 +32,7 @@ parameters: - name: enableMsBuildCaching type: boolean displayName: "Enable MSBuild Caching" - default: false + default: true - name: runTests type: boolean displayName: "Run Tests" From 0b9b91c060a517887b4311b35dce30f4dc58aed5 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 1 Oct 2025 05:50:53 -0500 Subject: [PATCH 11/11] CmdPal/Clipboard History: Ctrl+O to open links (#42115) Basically #42109, but with tests added, and no duplicated OpenUrl command. Closes #42108. Tests pass. Tested with both copy as default and paste as default, and things show up as expected. --- PowerToys.sln | 11 + ...dPal.Ext.ClipboardHistory.UnitTests.csproj | 19 ++ .../UrlHelperTests.cs | 274 ++++++++++++++++++ .../Helpers/UrlHelper.cs | 144 +++++++++ .../KeyChords.cs | 7 +- .../Pages/ClipboardListItem.cs | 50 +++- .../Properties/AssemblyInfo.cs | 7 + .../Properties/Resources.resx | 3 + 8 files changed, 498 insertions(+), 17 deletions(-) create mode 100644 src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj create mode 100644 src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests/UrlHelperTests.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/UrlHelper.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Properties/AssemblyInfo.cs diff --git a/PowerToys.sln b/PowerToys.sln index 4e5524c757..4c9c92ce47 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -811,6 +811,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSea EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Shell.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Shell.UnitTests\Microsoft.CmdPal.Ext.Shell.UnitTests.csproj", "{E816D7B4-4688-4ECB-97CC-3D8E798F3833}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj", "{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -2945,6 +2947,14 @@ Global {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|ARM64.Build.0 = Release|ARM64 {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.ActiveCfg = Release|x64 {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.Build.0 = Release|x64 + {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|ARM64.Build.0 = Debug|ARM64 + {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|x64.ActiveCfg = Debug|x64 + {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|x64.Build.0 = Debug|x64 + {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|ARM64.ActiveCfg = Release|ARM64 + {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|ARM64.Build.0 = Release|ARM64 + {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.ActiveCfg = Release|x64 + {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -3267,6 +3277,7 @@ Global {E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {E816D7B2-4688-4ECB-97CC-3D8E798F3831} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {E816D7B4-4688-4ECB-97CC-3D8E798F3833} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} + {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj new file mode 100644 index 0000000000..73abdbe772 --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj @@ -0,0 +1,19 @@ + + + + + + false + Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests + true + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests/UrlHelperTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests/UrlHelperTests.cs new file mode 100644 index 0000000000..8635a5e3c5 --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests/UrlHelperTests.cs @@ -0,0 +1,274 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests; + +[TestClass] +public class UrlHelperTests +{ + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + [DataRow("\t")] + [DataRow("\r\n")] + public void IsValidUrl_ReturnsFalse_WhenUrlIsNullOrWhitespace(string url) + { + // Act + var result = UrlHelper.IsValidUrl(url); + + // Assert + Assert.IsFalse(result); + } + + [TestMethod] + [DataRow("test\nurl")] + [DataRow("test\rurl")] + [DataRow("http://example.com\nmalicious")] + [DataRow("https://test.com\r\nheader")] + public void IsValidUrl_ReturnsFalse_WhenUrlContainsNewlines(string url) + { + // Act + var result = UrlHelper.IsValidUrl(url); + + // Assert + Assert.IsFalse(result); + } + + [TestMethod] + [DataRow("com")] + [DataRow("org")] + [DataRow("localhost")] + [DataRow("test")] + [DataRow("http")] + [DataRow("https")] + public void IsValidUrl_ReturnsFalse_WhenUrlDoesNotContainDot(string url) + { + // Act + var result = UrlHelper.IsValidUrl(url); + + // Assert + Assert.IsFalse(result); + } + + [TestMethod] + [DataRow("https://www.example.com")] + [DataRow("http://test.org")] + [DataRow("ftp://files.example.net")] + [DataRow("file://localhost/path/to/file.txt")] + [DataRow("https://subdomain.example.co.uk")] + [DataRow("http://192.168.1.1")] + [DataRow("https://example.com:8080/path")] + public void IsValidUrl_ReturnsTrue_WhenUrlIsWellFormedAbsolute(string url) + { + // Act + var result = UrlHelper.IsValidUrl(url); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod] + [DataRow("www.example.com")] + [DataRow("example.org")] + [DataRow("subdomain.test.net")] + [DataRow("github.com/user/repo")] + [DataRow("stackoverflow.com/questions/123")] + [DataRow("192.168.1.1")] + public void IsValidUrl_ReturnsTrue_WhenUrlIsValidWithoutProtocol(string url) + { + // Act + var result = UrlHelper.IsValidUrl(url); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod] + [DataRow("not a url")] + [DataRow("invalid..url")] + [DataRow("http://")] + [DataRow("https://")] + [DataRow("://example.com")] + [DataRow("ht tp://example.com")] + public void IsValidUrl_ReturnsFalse_WhenUrlIsInvalid(string url) + { + // Act + var result = UrlHelper.IsValidUrl(url); + + // Assert + Assert.IsFalse(result); + } + + [TestMethod] + [DataRow(" https://www.example.com ")] + [DataRow("\t\tgithub.com\t\t")] + [DataRow(" \r\n stackoverflow.com \r\n ")] + public void IsValidUrl_TrimsWhitespace_BeforeValidation(string url) + { + // Act + var result = UrlHelper.IsValidUrl(url); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod] + [DataRow("tel:+1234567890")] + [DataRow("javascript:alert('test')")] + public void IsValidUrl_ReturnsFalse_ForNonWebProtocols(string url) + { + // Act + var result = UrlHelper.IsValidUrl(url); + + // Assert + Assert.IsFalse(result); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public void NormalizeUrl_ReturnsInput_WhenUrlIsNullOrWhitespace(string url) + { + // Act + var result = UrlHelper.NormalizeUrl(url); + + // Assert + Assert.AreEqual(url, result); + } + + [TestMethod] + [DataRow("https://www.example.com")] + [DataRow("http://test.org")] + [DataRow("ftp://files.example.net")] + [DataRow("file://localhost/path/to/file.txt")] + public void NormalizeUrl_ReturnsUnchanged_WhenUrlIsAlreadyWellFormed(string url) + { + // Act + var result = UrlHelper.NormalizeUrl(url); + + // Assert + Assert.AreEqual(url, result); + } + + [TestMethod] + [DataRow("www.example.com", "https://www.example.com")] + [DataRow("example.org", "https://example.org")] + [DataRow("github.com/user/repo", "https://github.com/user/repo")] + [DataRow("stackoverflow.com/questions/123", "https://stackoverflow.com/questions/123")] + public void NormalizeUrl_AddsHttpsPrefix_WhenNoProtocolPresent(string input, string expected) + { + // Act + var result = UrlHelper.NormalizeUrl(input); + + // Assert + Assert.AreEqual(expected, result); + } + + [TestMethod] + [DataRow(" www.example.com ", "https://www.example.com")] + [DataRow("\t\tgithub.com\t\t", "https://github.com")] + [DataRow(" \r\n stackoverflow.com \r\n ", "https://stackoverflow.com")] + public void NormalizeUrl_TrimsWhitespace_BeforeNormalizing(string input, string expected) + { + // Act + var result = UrlHelper.NormalizeUrl(input); + + // Assert + Assert.AreEqual(expected, result); + } + + [TestMethod] + [DataRow(@"C:\Users\Test\Documents\file.txt")] + [DataRow(@"D:\Projects\MyProject\readme.md")] + [DataRow(@"E:\")] + [DataRow(@"F:")] + [DataRow(@"G:\folder\subfolder")] + public void IsValidUrl_ReturnsTrue_ForValidLocalPaths(string path) + { + // Act + var result = UrlHelper.IsValidUrl(path); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod] + [DataRow(@"\\server\share")] + [DataRow(@"\\server\share\folder")] + [DataRow(@"\\192.168.1.100\public")] + [DataRow(@"\\myserver\documents\file.docx")] + [DataRow(@"\\domain.com\share\folder\file.pdf")] + public void IsValidUrl_ReturnsTrue_ForValidNetworkPaths(string path) + { + // Act + var result = UrlHelper.IsValidUrl(path); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod] + [DataRow(@"\\")] + [DataRow(@":")] + [DataRow(@"Z")] + [DataRow(@"folder")] + [DataRow(@"folder\file.txt")] + [DataRow(@"documents\project\readme.md")] + [DataRow(@"./config/settings.json")] + [DataRow(@"../data/input.csv")] + public void IsValidUrl_ReturnsFalse_ForInvalidPathsAndRelativePaths(string path) + { + // Act + var result = UrlHelper.IsValidUrl(path); + + // Assert + Assert.IsFalse(result); + } + + [TestMethod] + [DataRow(@"C:\Users\Test\Documents\file.txt")] + [DataRow(@"D:\Projects\MyProject")] + [DataRow(@"E:\")] + public void NormalizeUrl_ConvertsLocalPathToFileUri_WhenValidLocalPath(string path) + { + // Act + var result = UrlHelper.NormalizeUrl(path); + + // Assert + Assert.IsTrue(result.StartsWith("file:///", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(result.Contains(path.Replace('\\', '/'))); + } + + [TestMethod] + [DataRow(@"\\server\share")] + [DataRow(@"\\192.168.1.100\public")] + [DataRow(@"\\myserver\documents")] + public void NormalizeUrl_ConvertsNetworkPathToFileUri_WhenValidNetworkPath(string path) + { + // Act + var result = UrlHelper.NormalizeUrl(path); + + // Assert + Assert.IsTrue(result.StartsWith("file://", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(result.Contains(path.Replace('\\', '/'))); + } + + [TestMethod] + [DataRow("file:///C:/Users/Test/file.txt")] + [DataRow("file://server/share/folder")] + public void NormalizeUrl_ReturnsUnchanged_WhenAlreadyFileUri(string fileUri) + { + // Act + var result = UrlHelper.NormalizeUrl(fileUri); + + // Assert + Assert.AreEqual(fileUri, result); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/UrlHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/UrlHelper.cs new file mode 100644 index 0000000000..60e7851761 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/UrlHelper.cs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; + +namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers; + +internal static class UrlHelper +{ + /// + /// Validates if a string is a valid URL or file path + /// + /// The string to validate + /// True if the string is a valid URL or file path, false otherwise + internal static bool IsValidUrl(string url) + { + if (string.IsNullOrWhiteSpace(url)) + { + return false; + } + + // Trim whitespace for validation + url = url.Trim(); + + // URLs should not contain newlines + if (url.Contains('\n', StringComparison.Ordinal) || url.Contains('\r', StringComparison.Ordinal)) + { + return false; + } + + // Check if it's a valid file path (local or network) + if (IsValidFilePath(url)) + { + return true; + } + + if (!url.Contains('.', StringComparison.OrdinalIgnoreCase)) + { + // eg: 'com', 'org'. We don't think it's a valid url. + // This can simplify the logic of checking if the url is valid. + return false; + } + + if (Uri.IsWellFormedUriString(url, UriKind.Absolute)) + { + return true; + } + + if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && + !url.StartsWith("https://", StringComparison.OrdinalIgnoreCase) && + !url.StartsWith("ftp://", StringComparison.OrdinalIgnoreCase) && + !url.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) + { + if (Uri.IsWellFormedUriString("https://" + url, UriKind.Absolute)) + { + return true; + } + } + + return false; + } + + /// + /// Normalizes a URL or file path by adding appropriate schema if none is present + /// + /// The URL or file path to normalize + /// Normalized URL or file path with schema + internal static string NormalizeUrl(string url) + { + if (string.IsNullOrWhiteSpace(url)) + { + return url; + } + + // Trim whitespace + url = url.Trim(); + + // If it's a valid file path, convert to file:// URI + if (IsValidFilePath(url) && !url.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) + { + try + { + // Convert to file URI (path is already absolute since we only accept absolute paths) + return new Uri(url).ToString(); + } + catch + { + // If conversion fails, return original + return url; + } + } + + if (!Uri.IsWellFormedUriString(url, UriKind.Absolute)) + { + if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && + !url.StartsWith("https://", StringComparison.OrdinalIgnoreCase) && + !url.StartsWith("ftp://", StringComparison.OrdinalIgnoreCase) && + !url.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) + { + url = "https://" + url; + } + } + + return url; + } + + /// + /// Checks if a string represents a valid file path (local or network) + /// + /// The string to check + /// True if the string is a valid file path, false otherwise + private static bool IsValidFilePath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return false; + } + + try + { + // Check for UNC paths (network paths starting with \\) + if (path.StartsWith(@"\\", StringComparison.Ordinal)) + { + // Basic UNC path validation: \\server\share or \\server\share\path + var parts = path.Substring(2).Split('\\', StringSplitOptions.RemoveEmptyEntries); + return parts.Length >= 2; // At minimum: server and share + } + + // Check for drive letters (C:\ or C:) + if (path.Length >= 2 && char.IsLetter(path[0]) && path[1] == ':') + { + return true; + } + + return false; + } + catch + { + return false; + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/KeyChords.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/KeyChords.cs index 5d59d0d1f2..e30969b56c 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/KeyChords.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/KeyChords.cs @@ -2,11 +2,6 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; using Windows.System; @@ -16,4 +11,6 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory; internal static class KeyChords { internal static KeyChord DeleteEntry { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Delete); + + internal static KeyChord OpenUrl { get; } = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.O); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardListItem.cs index ac19335bfa..9b5aae6f7d 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardListItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardListItem.cs @@ -22,6 +22,7 @@ internal sealed partial class ClipboardListItem : ListItem private readonly CommandContextItem _deleteContextMenuItem; private readonly CommandContextItem? _pasteCommand; private readonly CommandContextItem? _copyCommand; + private readonly CommandContextItem? _openUrlCommand; private readonly Lazy
_lazyDetails; public override IDetails? Details @@ -72,11 +73,26 @@ internal sealed partial class ClipboardListItem : ListItem _pasteCommand = new CommandContextItem(new PasteCommand(_item, ClipboardFormat.Text, _settingsManager)); _copyCommand = new CommandContextItem(new CopyCommand(_item, ClipboardFormat.Text)); + + // Check if the text content is a valid URL and add OpenUrl command + if (UrlHelper.IsValidUrl(_item.Content ?? string.Empty)) + { + var normalizedUrl = UrlHelper.NormalizeUrl(_item.Content ?? string.Empty); + _openUrlCommand = new CommandContextItem(new OpenUrlCommand(normalizedUrl)) + { + RequestedShortcut = KeyChords.OpenUrl, + }; + } + else + { + _openUrlCommand = null; + } } else { _pasteCommand = null; _copyCommand = null; + _openUrlCommand = null; } RefreshCommands(); @@ -99,12 +115,7 @@ internal sealed partial class ClipboardListItem : ListItem { case PrimaryAction.Paste: Command = _pasteCommand?.Command; - MoreCommands = - [ - _copyCommand!, - new Separator(), - _deleteContextMenuItem, - ]; + MoreCommands = BuildMoreCommands(_copyCommand); if (_item.IsText) { @@ -124,12 +135,7 @@ internal sealed partial class ClipboardListItem : ListItem case PrimaryAction.Copy: default: Command = _copyCommand?.Command; - MoreCommands = - [ - _pasteCommand!, - new Separator(), - _deleteContextMenuItem, - ]; + MoreCommands = BuildMoreCommands(_pasteCommand); if (_item.IsText) { @@ -148,6 +154,26 @@ internal sealed partial class ClipboardListItem : ListItem } } + private IContextItem[] BuildMoreCommands(CommandContextItem? firstCommand) + { + var commands = new List(); + + if (firstCommand != null) + { + commands.Add(firstCommand); + } + + if (_openUrlCommand != null) + { + commands.Add(_openUrlCommand); + } + + commands.Add(new Separator()); + commands.Add(_deleteContextMenuItem); + + return commands.ToArray(); + } + private Details CreateDetails() { IDetailsElement[] metadata = diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Properties/AssemblyInfo.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..fbc2b32860 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests")] diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Properties/Resources.resx index 70226f7292..0af6ee4cfc 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Properties/Resources.resx @@ -183,4 +183,7 @@ Copy to Clipboard + + Open URL + \ No newline at end of file