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
## 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;
+ }
}