From ab76dd1255281f06a3a513fe31c84344c31e81a2 Mon Sep 17 00:00:00 2001 From: Davide Giacometti <25966642+davidegiacometti@users.noreply.github.com> Date: Wed, 13 Aug 2025 20:42:40 +0200 Subject: [PATCH] [CmdPal] Search PATH starting with ~ / \ (#40887) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request Love that we have now environment variables expanding! 🚀 Porting also few small features I implemented in Run Folder plugin a long time ago and I love: - https://github.com/microsoft/PowerToys/pull/7711 - https://github.com/microsoft/PowerToys/pull/9579 Threat `/` and `\` as root of system drive (typically `C:\`) Threat `~` as user home directory `%USERPROFILE%` ## 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 image image image ## Validation Steps Performed - Tested search starting with `~` `/` `\` - Tested UNC network path starting with `\\...` and `//...` --- .../FallbackExecuteItem.cs | 64 +++++++++++++++++-- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/FallbackExecuteItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/FallbackExecuteItem.cs index 167956c166..79be63cd65 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/FallbackExecuteItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/FallbackExecuteItem.cs @@ -4,6 +4,7 @@ using System; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CmdPal.Ext.Shell.Helpers; @@ -15,6 +16,8 @@ namespace Microsoft.CmdPal.Ext.Shell; internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDisposable { + private static readonly char[] _systemDirectoryRoots = ['\\', '/']; + private readonly Action? _addToHistory; private CancellationTokenSource? _cancellationTokenSource; private Task? _currentUpdateTask; @@ -80,8 +83,8 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos cancellationToken.ThrowIfCancellationRequested(); var searchText = query.Trim(); - var expanded = Environment.ExpandEnvironmentVariables(searchText); - searchText = expanded; + Expand(ref searchText); + if (string.IsNullOrEmpty(searchText) || string.IsNullOrWhiteSpace(searchText)) { Command = null; @@ -184,8 +187,8 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos internal static bool SuppressFileFallbackIf(string query) { var searchText = query.Trim(); - var expanded = Environment.ExpandEnvironmentVariables(searchText); - searchText = expanded; + Expand(ref searchText); + if (string.IsNullOrEmpty(searchText) || string.IsNullOrWhiteSpace(searchText)) { return false; @@ -197,4 +200,57 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos return exeExists || pathIsDir; } + + private static void Expand(ref string searchText) + { + if (searchText.Length == 0) + { + return; + } + + var singleCharQuery = searchText.Length == 1; + + searchText = Environment.ExpandEnvironmentVariables(searchText); + + if (!TryExpandHome(ref searchText)) + { + TryExpandRoot(ref searchText); + } + } + + private static bool TryExpandHome(ref string searchText) + { + if (searchText[0] == '~') + { + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + + if (searchText.Length == 1) + { + searchText = home; + } + else if (_systemDirectoryRoots.Contains(searchText[1])) + { + searchText = Path.Combine(home, searchText[2..]); + } + + return true; + } + + return false; + } + + private static bool TryExpandRoot(ref string searchText) + { + if (_systemDirectoryRoots.Contains(searchText[0]) && (searchText.Length == 1 || !_systemDirectoryRoots.Contains(searchText[1]))) + { + var root = Path.GetPathRoot(Environment.SystemDirectory); + if (root != null) + { + searchText = searchText.Length == 1 ? root : Path.Combine(root, searchText[1..]); + return true; + } + } + + return false; + } }