diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index a36b157274..2091f78ed3 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -161,7 +161,6 @@ CHANGECBCHAIN
changecursor
CHILDACTIVATE
CHILDWINDOW
-CHT
cidl
cim
CImage
@@ -182,6 +181,7 @@ clrcall
CLSCTX
Clusion
cmder
+CMDNOTFOUNDMODULEINTERFACE
Cmds
CMIC
CMINVOKECOMMANDINFO
@@ -219,7 +219,6 @@ CONFIGW
CONFLICTINGMODIFIERKEY
CONFLICTINGMODIFIERSHORTCUT
CONOUT
-constexpr
consts
contentdialog
contentfiles
@@ -319,7 +318,6 @@ DESKTOPABSOLUTEEDITING
DESKTOPABSOLUTEPARSING
desktopshorcutinstalled
desktopwindowxamlsource
-DEU
devblogs
devdocs
devenum
@@ -508,6 +506,7 @@ GETCLIENTAREAANIMATION
GETDESKWALLPAPER
GETDLGCODE
GETDPISCALEDSIZE
+getfilesiginforedist
GETICON
GETMINMAXINFO
GETPROPERTYSTOREFLAGS
@@ -538,7 +537,6 @@ Hanzi
Hardlines
hardlinks
HARDWAREINPUT
-hashcode
Hashset
hashtag
HASHVAL
@@ -557,7 +555,6 @@ hcwhite
hdc
hdrop
hdwwiz
-HEB
Helpline
helptext
HGFE
@@ -720,7 +717,6 @@ jif
jjw
jobject
jpe
-JPN
jpnime
Jsons
jsonval
@@ -746,10 +742,7 @@ killrunner
Knownfolders
KSPROPERTY
Kybd
-LAlt
-Lambson
languagesjson
-langword
lastcodeanalysissucceeded
Lastdevice
LAYOUTRTL
@@ -907,6 +900,8 @@ MOUSEHWHEEL
MOUSEINPUT
MOVESIZEEND
MOVESIZESTART
+MOZILLAPL
+MOZPL
mpmc
MRM
MRT
@@ -972,6 +967,7 @@ newdev
newitem
newpath
newrow
+newsgroups
NIF
NLD
NLog
@@ -984,7 +980,6 @@ NOCLOSEPROCESS
NOCOALESCE
NOCOPYBITS
nodeca
-nodiscard
nodoc
NODRAWCAPTION
NODRAWICON
@@ -1093,7 +1088,6 @@ pcch
pcelt
pch
PCIDLIST
-pcs
PCWSTR
pdisp
pdo
@@ -1304,7 +1298,6 @@ RKey
RNumber
roadmap
rop
-roundf
ROUNDSMALL
rpcrt
RRF
@@ -1320,12 +1313,11 @@ rundll
rungameid
RUNLEVEL
runsettings
+runspace
runtimeclass
runtimeobject
runtimepack
runtimes
-RUS
-RValue
rvm
rwin
rwl
@@ -1506,7 +1498,6 @@ subquery
Superbar
sut
svchost
-SVE
SVGIn
SVGIO
svgz
@@ -1579,7 +1570,6 @@ tlbimp
TMPVAR
TNP
toggleswitch
-tonos
toolkitcontrols
toolkitconverters
Toolset
@@ -1715,7 +1705,6 @@ wcsnicmp
WDA
wdp
wdupenv
-weakme
webbrowsers
webcam
webpage
@@ -1724,7 +1713,6 @@ wekyb
Wevtapi
wgpocpl
WIC
-wifi
wil
winapi
winappdriver
@@ -1852,9 +1840,6 @@ zonable
zoneset
Zoneszonabletester
zzz
-newsgroups
-MOZILLAPL
-MOZPL
# FALSE POSITIVES
diff --git a/.github/actions/spell-check/patterns.txt b/.github/actions/spell-check/patterns.txt
index daf0220efe..d48cec9abd 100644
--- a/.github/actions/spell-check/patterns.txt
+++ b/.github/actions/spell-check/patterns.txt
@@ -118,6 +118,9 @@ aka\.ms/[a-zA-Z0-9]+
# YouTube url
\b(?:(?:www\.|)youtube\.com|youtu.be)/(?:channel/|embed/|user/|playlist\?list=|watch\?v=|v/|)[-a-zA-Z0-9?&=_%]*
+# power shell gallery website
+\bpowershellgallery.com/[-_a-zA-Z0-9()=./%]*
+
# uuid: (or CompGUIDPrefix)
L?(["']|[-<({>]|\b)[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{10,12}(?:\g{-1}|[<})>])
diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json
index 73bc45ed03..a6350ec764 100644
--- a/.pipelines/ESRPSigning_core.json
+++ b/.pipelines/ESRPSigning_core.json
@@ -28,7 +28,10 @@
"PowerToys.AlwaysOnTop.exe",
"PowerToys.AlwaysOnTopModuleInterface.dll",
-
+
+ "PowerToys.CmdNotFoundModuleInterface.dll",
+ "PowerToys.CmdNotFound.dll",
+
"PowerToys.ColorPicker.dll",
"PowerToys.ColorPickerUI.dll",
"PowerToys.ColorPickerUI.exe",
@@ -251,6 +254,7 @@
"Mages.Core.dll",
"JetBrains.Annotations.dll",
"NLog.Extensions.Logging.dll",
+ "getfilesiginforedist.dll",
"concrt140_app.dll",
"msvcp140_1_app.dll",
"msvcp140_2_app.dll",
diff --git a/.pipelines/versionAndSignCheck.ps1 b/.pipelines/versionAndSignCheck.ps1
index 42b66ed8bc..d5960efb64 100644
--- a/.pipelines/versionAndSignCheck.ps1
+++ b/.pipelines/versionAndSignCheck.ps1
@@ -24,6 +24,7 @@ $versionExceptions = @(
$nullVersionExceptions = @(
"codicon.ttf",
"e_sqlite3.dll",
+ "getfilesiginforedist.dll",
"vcamp140_app.dll",
"vcruntime140_app.dll",
"vcruntime140_1_app.dll",
diff --git a/Directory.Packages.props b/Directory.Packages.props
index cb36c3beaf..61ca429ea1 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -33,6 +33,7 @@
+
@@ -66,6 +67,7 @@
+
diff --git a/NOTICE.md b/NOTICE.md
index 59adbebaed..47d5e68545 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -1321,6 +1321,7 @@ EXHIBIT A -Mozilla Public License.
- Microsoft.Extensions.Hosting.WindowsServices 8.0.0
- Microsoft.Extensions.Logging 8.0.0
- Microsoft.Extensions.Logging.Abstractions 8.0.0
+- Microsoft.Extensions.ObjectPool 5.0.17
- Microsoft.NET.Test.Sdk 17.6.3
- Microsoft.Toolkit.Uwp.Notifications 7.1.2
- Microsoft.Web.WebView2 1.0.2088.41
@@ -1350,6 +1351,7 @@ EXHIBIT A -Mozilla Public License.
- System.IO.Abstractions 17.2.3
- System.IO.Abstractions.TestingHelpers 17.2.3
- System.Management 8.0.0
+- System.Management.Automation 7.4.0
- System.Reactive 6.0.0-preview.9
- System.Runtime.Caching 8.0.0
- System.ServiceProcess.ServiceController 8.0.0
diff --git a/PowerToys.sln b/PowerToys.sln
index c3a17c1804..6c37839b19 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -538,6 +538,10 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CropAndLock", "src\modules\
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CropAndLockModuleInterface", "src\modules\CropAndLock\CropAndLockModuleInterface\CropAndLockModuleInterface.vcxproj", "{3157FA75-86CF-4EE2-8F62-C43F776493C6}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CmdNotFound", "src\modules\cmdNotFound\CmdNotFound\CmdNotFound.csproj", "{A37865FE-2881-449F-8ADB-B8CD373D6D79}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cmdNotFound", "cmdNotFound", "{4C0D0746-BE5B-49EE-BD5D-A7811628AE8B}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests-FancyZonesEditor", "src\modules\fancyzones\UnitTests-FancyZonesEditor\UnitTests-FancyZonesEditor.csproj", "{FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EnvironmentVariables", "EnvironmentVariables", "{538ED0BB-B863-4B20-98CC-BCDF7FA0B68A}"
@@ -558,6 +562,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests-QoiPreviewHandler
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests-QoiThumbnailProvider", "src\modules\previewpane\UnitTests-QoiThumbnailProvider\UnitTests-QoiThumbnailProvider.csproj", "{F8FFFC12-A31A-4AFA-B3DF-14DCF42B5E38}"
EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CmdNotFoundModuleInterface", "src\modules\cmdNotFound\CmdNotFoundModuleInterface\CmdNotFoundModuleInterface.vcxproj", "{0014D652-901F-4456-8D65-06FC5F997FB0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2328,6 +2334,18 @@ Global
{3157FA75-86CF-4EE2-8F62-C43F776493C6}.Release|x64.Build.0 = Release|x64
{3157FA75-86CF-4EE2-8F62-C43F776493C6}.Release|x86.ActiveCfg = Release|x64
{3157FA75-86CF-4EE2-8F62-C43F776493C6}.Release|x86.Build.0 = Release|x64
+ {A37865FE-2881-449F-8ADB-B8CD373D6D79}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {A37865FE-2881-449F-8ADB-B8CD373D6D79}.Debug|ARM64.Build.0 = Debug|ARM64
+ {A37865FE-2881-449F-8ADB-B8CD373D6D79}.Debug|x64.ActiveCfg = Debug|x64
+ {A37865FE-2881-449F-8ADB-B8CD373D6D79}.Debug|x64.Build.0 = Debug|x64
+ {A37865FE-2881-449F-8ADB-B8CD373D6D79}.Debug|x86.ActiveCfg = Debug|x64
+ {A37865FE-2881-449F-8ADB-B8CD373D6D79}.Debug|x86.Build.0 = Debug|x64
+ {A37865FE-2881-449F-8ADB-B8CD373D6D79}.Release|ARM64.ActiveCfg = Release|ARM64
+ {A37865FE-2881-449F-8ADB-B8CD373D6D79}.Release|ARM64.Build.0 = Release|ARM64
+ {A37865FE-2881-449F-8ADB-B8CD373D6D79}.Release|x64.ActiveCfg = Release|x64
+ {A37865FE-2881-449F-8ADB-B8CD373D6D79}.Release|x64.Build.0 = Release|x64
+ {A37865FE-2881-449F-8ADB-B8CD373D6D79}.Release|x86.ActiveCfg = Release|x64
+ {A37865FE-2881-449F-8ADB-B8CD373D6D79}.Release|x86.Build.0 = Release|x64
{FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}.Debug|ARM64.ActiveCfg = Debug|ARM64
{FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}.Debug|ARM64.Build.0 = Debug|ARM64
{FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}.Debug|x64.ActiveCfg = Debug|x64
@@ -2436,6 +2454,18 @@ Global
{F8FFFC12-A31A-4AFA-B3DF-14DCF42B5E38}.Release|x64.Build.0 = Release|x64
{F8FFFC12-A31A-4AFA-B3DF-14DCF42B5E38}.Release|x86.ActiveCfg = Release|x64
{F8FFFC12-A31A-4AFA-B3DF-14DCF42B5E38}.Release|x86.Build.0 = Release|x64
+ {0014D652-901F-4456-8D65-06FC5F997FB0}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {0014D652-901F-4456-8D65-06FC5F997FB0}.Debug|ARM64.Build.0 = Debug|ARM64
+ {0014D652-901F-4456-8D65-06FC5F997FB0}.Debug|x64.ActiveCfg = Debug|x64
+ {0014D652-901F-4456-8D65-06FC5F997FB0}.Debug|x64.Build.0 = Debug|x64
+ {0014D652-901F-4456-8D65-06FC5F997FB0}.Debug|x86.ActiveCfg = Debug|Win32
+ {0014D652-901F-4456-8D65-06FC5F997FB0}.Debug|x86.Build.0 = Debug|Win32
+ {0014D652-901F-4456-8D65-06FC5F997FB0}.Release|ARM64.ActiveCfg = Release|ARM64
+ {0014D652-901F-4456-8D65-06FC5F997FB0}.Release|ARM64.Build.0 = Release|ARM64
+ {0014D652-901F-4456-8D65-06FC5F997FB0}.Release|x64.ActiveCfg = Release|x64
+ {0014D652-901F-4456-8D65-06FC5F997FB0}.Release|x64.Build.0 = Release|x64
+ {0014D652-901F-4456-8D65-06FC5F997FB0}.Release|x86.ActiveCfg = Release|Win32
+ {0014D652-901F-4456-8D65-06FC5F997FB0}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2630,6 +2660,8 @@ Global
{3B227528-4BA6-4CAF-B44A-A10C78A64849} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
{F5E1146E-B7B3-4E11-85FD-270A500BD78C} = {3B227528-4BA6-4CAF-B44A-A10C78A64849}
{3157FA75-86CF-4EE2-8F62-C43F776493C6} = {3B227528-4BA6-4CAF-B44A-A10C78A64849}
+ {A37865FE-2881-449F-8ADB-B8CD373D6D79} = {4C0D0746-BE5B-49EE-BD5D-A7811628AE8B}
+ {4C0D0746-BE5B-49EE-BD5D-A7811628AE8B} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
{FC8EB78F-F061-4BD9-A3F6-507BEA965E2B} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
{538ED0BB-B863-4B20-98CC-BCDF7FA0B68A} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
{51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA} = {538ED0BB-B863-4B20-98CC-BCDF7FA0B68A}
@@ -2640,6 +2672,7 @@ Global
{6B04803D-B418-4833-A67E-B0FC966636A5} = {2F305555-C296-497E-AC20-5FA1B237996A}
{3940AD4D-F748-4BE4-9083-85769CD553EF} = {2F305555-C296-497E-AC20-5FA1B237996A}
{F8FFFC12-A31A-4AFA-B3DF-14DCF42B5E38} = {2F305555-C296-497E-AC20-5FA1B237996A}
+ {0014D652-901F-4456-8D65-06FC5F997FB0} = {4C0D0746-BE5B-49EE-BD5D-A7811628AE8B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
diff --git a/README.md b/README.md
index 575abe4c54..110865b35c 100644
--- a/README.md
+++ b/README.md
@@ -17,14 +17,14 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline
| | Current utilities: | |
|--------------|--------------------|--------------|
-| [Always on Top](https://aka.ms/PowerToysOverview_AoT) | [PowerToys Awake](https://aka.ms/PowerToysOverview_Awake) | [Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) |
-| [Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) | [FancyZones](https://aka.ms/PowerToysOverview_FancyZones) |
-| [File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | [Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) |
-| [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [Mouse utilities](https://aka.ms/PowerToysOverview_MouseUtilities) |
-| [Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [Peek](https://aka.ms/PowerToysOverview_Peek) | [Paste as Plain Text](https://aka.ms/PowerToysOverview_PastePlain) |
-| [PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) | [Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) |
-| [Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) | [Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) |
-| [Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [Video Conference Mute](https://aka.ms/PowerToysOverview_VideoConference) |
+| [Always on Top](https://aka.ms/PowerToysOverview_AoT) | [PowerToys Awake](https://aka.ms/PowerToysOverview_Awake) | [Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) |
+| [Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) |
+| [FancyZones](https://aka.ms/PowerToysOverview_FancyZones) | [File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) |
+| [Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) | [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) |
+| [Mouse utilities](https://aka.ms/PowerToysOverview_MouseUtilities) | [Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [Peek](https://aka.ms/PowerToysOverview_Peek) |
+| [Paste as Plain Text](https://aka.ms/PowerToysOverview_PastePlain) | [PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) |
+| [Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) | [Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) |
+| [Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) | [Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [Video Conference Mute](https://aka.ms/PowerToysOverview_VideoConference) |
## Installing and running Microsoft PowerToys
diff --git a/doc/images/icons/Command Not Found.png b/doc/images/icons/Command Not Found.png
new file mode 100644
index 0000000000..8243b9f8c8
Binary files /dev/null and b/doc/images/icons/Command Not Found.png differ
diff --git a/installer/PowerToysSetup/CmdNotFound.wxs b/installer/PowerToysSetup/CmdNotFound.wxs
new file mode 100644
index 0000000000..3ac8445d88
--- /dev/null
+++ b/installer/PowerToysSetup/CmdNotFound.wxs
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/installer/PowerToysSetup/Common.wxi b/installer/PowerToysSetup/Common.wxi
index 4a1c93fc8e..6e68ce1986 100644
--- a/installer/PowerToysSetup/Common.wxi
+++ b/installer/PowerToysSetup/Common.wxi
@@ -18,6 +18,7 @@
+
diff --git a/installer/PowerToysSetup/PowerToysInstaller.wixproj b/installer/PowerToysSetup/PowerToysInstaller.wixproj
index 6a4f3b80cb..8fb186ebb6 100644
--- a/installer/PowerToysSetup/PowerToysInstaller.wixproj
+++ b/installer/PowerToysSetup/PowerToysInstaller.wixproj
@@ -1,5 +1,6 @@
-
+
@@ -14,7 +15,7 @@ SET PTRoot=$(SolutionDir)\..
call "..\..\..\publish.cmd" x64
)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs"
-
+
@@ -25,7 +26,7 @@ SET PTRoot=$(SolutionDir)\..
call "..\..\..\publish.cmd" arm64
)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs"
-
+
Always
@@ -67,7 +68,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
- Release
+ Release
$(Platform)
3.10
022a9d30-7c4f-416d-a9df-5ff2661cc0ad
@@ -103,6 +104,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
+
@@ -180,17 +182,6 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
-->
-
+
\ No newline at end of file
diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs
index 2dea1fab55..ee15ec090a 100644
--- a/installer/PowerToysSetup/Product.wxs
+++ b/installer/PowerToysSetup/Product.wxs
@@ -72,6 +72,7 @@
+
@@ -133,6 +134,7 @@
+
@@ -159,6 +161,9 @@
Installed AND (REMOVE="ALL")
+
+ Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")
+
Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")
@@ -200,6 +205,10 @@
Property="UnApplyModulesRegistryChangeSets"
Value="[INSTALLFOLDER]" />
+
+
@@ -252,7 +261,15 @@
BinaryKey="PTCustomActions"
DllEntry="UninstallServicesCA"
/>
-
+
+
+
+
+
@@ -45,6 +47,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -55,7 +67,9 @@
+
+
diff --git a/installer/PowerToysSetupCustomActions/CustomAction.cpp b/installer/PowerToysSetupCustomActions/CustomAction.cpp
index c76e2203f8..290df09898 100644
--- a/installer/PowerToysSetupCustomActions/CustomAction.cpp
+++ b/installer/PowerToysSetupCustomActions/CustomAction.cpp
@@ -433,7 +433,29 @@ UINT __stdcall RemoveWindowsServiceByName(std::wstring serviceName)
return ERROR_SUCCESS;
}
+UINT __stdcall UninstallCommandNotFoundModuleCA(MSIHANDLE hInstall)
+{
+ HRESULT hr = S_OK;
+ UINT er = ERROR_SUCCESS;
+ std::wstring installationFolder;
+ std::string command;
+ hr = WcaInitialize(hInstall, "UninstallCommandNotFoundModule");
+ ExitOnFailure(hr, "Failed to initialize");
+
+ hr = getInstallFolder(hInstall, installationFolder);
+ ExitOnFailure(hr, "Failed to get installFolder.");
+
+ command = "pwsh.exe";
+ command += " ";
+ command += "-NoProfile -NonInteractive -NoLogo -WindowStyle Hidden -ExecutionPolicy Unrestricted -File \"" + winrt::to_string(installationFolder) + "\\WinUI3Apps\\Assets\\Settings\\Scripts\\DisableModule.ps1" + "\"";
+
+ system(command.c_str());
+
+LExit:
+ er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
+ return WcaFinalize(er);
+}
UINT __stdcall UninstallServicesCA(MSIHANDLE hInstall)
{
diff --git a/installer/PowerToysSetupCustomActions/CustomAction.def b/installer/PowerToysSetupCustomActions/CustomAction.def
index 7c3c058d9c..6a503da797 100644
--- a/installer/PowerToysSetupCustomActions/CustomAction.def
+++ b/installer/PowerToysSetupCustomActions/CustomAction.def
@@ -22,4 +22,5 @@ EXPORTS
UninstallVirtualCameraDriverCA
UnRegisterContextMenuPackagesCA
UninstallEmbeddedMSIXCA
- UninstallServicesCA
\ No newline at end of file
+ UninstallServicesCA
+ UninstallCommandNotFoundModuleCA
\ No newline at end of file
diff --git a/src/common/GPOWrapper/GPOWrapper.cpp b/src/common/GPOWrapper/GPOWrapper.cpp
index 77d81e8516..c3177d60db 100644
--- a/src/common/GPOWrapper/GPOWrapper.cpp
+++ b/src/common/GPOWrapper/GPOWrapper.cpp
@@ -12,6 +12,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{
return static_cast(powertoys_gpo::getConfiguredAwakeEnabledValue());
}
+ GpoRuleConfigured GPOWrapper::GetConfiguredCmdNotFoundEnabledValue()
+ {
+ return static_cast(powertoys_gpo::getConfiguredCmdNotFoundEnabledValue());
+ }
GpoRuleConfigured GPOWrapper::GetConfiguredColorPickerEnabledValue()
{
return static_cast(powertoys_gpo::getConfiguredColorPickerEnabledValue());
diff --git a/src/common/GPOWrapper/GPOWrapper.h b/src/common/GPOWrapper/GPOWrapper.h
index c29fac95b6..bd153ed258 100644
--- a/src/common/GPOWrapper/GPOWrapper.h
+++ b/src/common/GPOWrapper/GPOWrapper.h
@@ -9,6 +9,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation
GPOWrapper() = default;
static GpoRuleConfigured GetConfiguredAlwaysOnTopEnabledValue();
static GpoRuleConfigured GetConfiguredAwakeEnabledValue();
+ static GpoRuleConfigured GetConfiguredCmdNotFoundEnabledValue();
static GpoRuleConfigured GetConfiguredColorPickerEnabledValue();
static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue();
static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue();
diff --git a/src/common/GPOWrapper/GPOWrapper.idl b/src/common/GPOWrapper/GPOWrapper.idl
index d0e3ebbd4f..3b4c0eca28 100644
--- a/src/common/GPOWrapper/GPOWrapper.idl
+++ b/src/common/GPOWrapper/GPOWrapper.idl
@@ -13,6 +13,7 @@ namespace PowerToys
[default_interface] static runtimeclass GPOWrapper {
static GpoRuleConfigured GetConfiguredAlwaysOnTopEnabledValue();
static GpoRuleConfigured GetConfiguredAwakeEnabledValue();
+ static GpoRuleConfigured GetConfiguredCmdNotFoundEnabledValue();
static GpoRuleConfigured GetConfiguredColorPickerEnabledValue();
static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue();
static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue();
diff --git a/src/common/GPOWrapperProjection/GPOWrapper.cs b/src/common/GPOWrapperProjection/GPOWrapper.cs
index 92196323e5..5e1ef14748 100644
--- a/src/common/GPOWrapperProjection/GPOWrapper.cs
+++ b/src/common/GPOWrapperProjection/GPOWrapper.cs
@@ -27,6 +27,11 @@ namespace PowerToys.GPOWrapperProjection
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredFancyZonesEnabledValue();
}
+ public static GpoRuleConfigured GetConfiguredCmdNotFoundEnabledValue()
+ {
+ return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredCmdNotFoundEnabledValue();
+ }
+
public static GpoRuleConfigured GetConfiguredColorPickerEnabledValue()
{
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredColorPickerEnabledValue();
diff --git a/src/common/ManagedCommon/ManagedCommon.csproj b/src/common/ManagedCommon/ManagedCommon.csproj
index 53ca704109..dbe6eca3f7 100644
--- a/src/common/ManagedCommon/ManagedCommon.csproj
+++ b/src/common/ManagedCommon/ManagedCommon.csproj
@@ -4,7 +4,7 @@
net8.0-windows
- win-x64;win-arm64
+ win-x64;win-arm64
$(Version).0
Microsoft Corporation
PowerToys
diff --git a/src/common/ManagedCommon/ModuleType.cs b/src/common/ManagedCommon/ModuleType.cs
index 3a489711b0..0bbfaeeda3 100644
--- a/src/common/ManagedCommon/ModuleType.cs
+++ b/src/common/ManagedCommon/ModuleType.cs
@@ -9,6 +9,7 @@ namespace ManagedCommon
AlwaysOnTop,
Awake,
ColorPicker,
+ CmdNotFound,
CropAndLock,
EnvironmentVariables,
FancyZones,
diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h
index 91dd5faae8..cc1b3825ce 100644
--- a/src/common/logger/logger_settings.h
+++ b/src/common/logger/logger_settings.h
@@ -67,6 +67,8 @@ struct LogSettings
inline const static std::string cropAndLockLoggerName = "crop-and-lock";
inline const static std::wstring registryPreviewLogPath = L"Logs\\registryPreview-log.txt";
inline const static std::string environmentVariablesLoggerName = "environment-variables";
+ inline const static std::wstring cmdNotFoundLogPath = L"Logs\\cmd-not-found-log.txt";
+ inline const static std::string cmdNotFoundLoggerName = "cmd-not-found";
inline const static int retention = 30;
std::wstring logLevel;
LogSettings();
diff --git a/src/common/utils/gpo.h b/src/common/utils/gpo.h
index be88e0d22d..016bd744cc 100644
--- a/src/common/utils/gpo.h
+++ b/src/common/utils/gpo.h
@@ -24,6 +24,7 @@ namespace powertoys_gpo {
const std::wstring POLICY_CONFIGURE_ENABLED_GLOBAL_ALL_UTILITIES = L"ConfigureGlobalUtilityEnabledState";
const std::wstring POLICY_CONFIGURE_ENABLED_ALWAYS_ON_TOP = L"ConfigureEnabledUtilityAlwaysOnTop";
const std::wstring POLICY_CONFIGURE_ENABLED_AWAKE = L"ConfigureEnabledUtilityAwake";
+ const std::wstring POLICY_CONFIGURE_ENABLED_CMD_NOT_FOUND = L"ConfigureEnabledUtilityCmdNotFound";
const std::wstring POLICY_CONFIGURE_ENABLED_COLOR_PICKER = L"ConfigureEnabledUtilityColorPicker";
const std::wstring POLICY_CONFIGURE_ENABLED_CROP_AND_LOCK = L"ConfigureEnabledUtilityCropAndLock";
const std::wstring POLICY_CONFIGURE_ENABLED_FANCYZONES = L"ConfigureEnabledUtilityFancyZones";
@@ -224,6 +225,11 @@ namespace powertoys_gpo {
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_AWAKE);
}
+ inline gpo_rule_configured_t getConfiguredCmdNotFoundEnabledValue()
+ {
+ return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_CMD_NOT_FOUND);
+ }
+
inline gpo_rule_configured_t getConfiguredColorPickerEnabledValue()
{
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_COLOR_PICKER);
diff --git a/src/gpo/assets/PowerToys.admx b/src/gpo/assets/PowerToys.admx
index 0b135f665e..59a0973dcf 100644
--- a/src/gpo/assets/PowerToys.admx
+++ b/src/gpo/assets/PowerToys.admx
@@ -15,6 +15,7 @@
+
@@ -59,6 +60,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/gpo/assets/en-US/PowerToys.adml b/src/gpo/assets/en-US/PowerToys.adml
index de04d77c8b..2f69d63986 100644
--- a/src/gpo/assets/en-US/PowerToys.adml
+++ b/src/gpo/assets/en-US/PowerToys.adml
@@ -17,6 +17,7 @@
PowerToys version 0.73.0 or later
PowerToys version 0.75.0 or later
PowerToys version 0.76.0 or later
+ PowerToys version 0.77.0 or later
This policy configures the enabled state for all PowerToys utilities.
@@ -112,6 +113,7 @@ Note: Changes require a restart of PowerToys Run.
Always On Top: Configure enabled state
Awake: Configure enabled state
Color Picker: Configure enabled state
+ Command Not Found: Configure enabled state
Crop And Lock: Configure enabled state
Environment Variables: Configure enabled state
FancyZones: Configure enabled state
diff --git a/src/modules/cmdNotFound/CmdNotFound/CmdNotFound.csproj b/src/modules/cmdNotFound/CmdNotFound/CmdNotFound.csproj
new file mode 100644
index 0000000000..0d4f12d5b4
--- /dev/null
+++ b/src/modules/cmdNotFound/CmdNotFound/CmdNotFound.csproj
@@ -0,0 +1,67 @@
+
+
+
+
+ net8.0-windows10.0.22621.0
+ 10.0.19041.0
+ 10.0.19041.0
+ win-x64;win-arm64
+ enable
+ Microsoft Corporation
+ PowerToys
+ enable
+ PowerToys CommandNotFound
+ PowerToys.CmdNotFound
+ false
+ true
+ ..\..\..\..\$(Platform)\$(Configuration)
+ false
+ false
+ true
+ true
+
+
+
+
+ win-x64
+
+
+ win-arm64
+
+
+
+ DEBUG;TRACE
+ full
+ prompt
+ 4
+ false
+
+
+
+ TRACE;RELEASE
+ true
+ pdbonly
+ prompt
+ 4
+
+
+
+
+ contentFiles
+ all
+
+
+ contentFiles
+ all
+
+
+ PreserveNewest
+ PreserveNewest
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdNotFound/CmdNotFound/Init.cs b/src/modules/cmdNotFound/CmdNotFound/Init.cs
new file mode 100644
index 0000000000..4d4ecb6d60
--- /dev/null
+++ b/src/modules/cmdNotFound/CmdNotFound/Init.cs
@@ -0,0 +1,57 @@
+// 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.Management.Automation;
+using System.Management.Automation.Subsystem;
+using System.Management.Automation.Subsystem.Feedback;
+using System.Management.Automation.Subsystem.Prediction;
+
+namespace WinGetCommandNotFound
+{
+ public sealed class Init : IModuleAssemblyInitializer, IModuleAssemblyCleanup
+ {
+ internal const string Id = "e5351aa4-dfde-4d4d-bf0f-1a2f5a37d8d6";
+
+ public void OnImport()
+ {
+ if (!Platform.IsWindows || !IsWinGetInstalled())
+ {
+ return;
+ }
+
+ SubsystemManager.RegisterSubsystem(SubsystemKind.FeedbackProvider, WinGetCommandNotFoundFeedbackPredictor.Singleton);
+ SubsystemManager.RegisterSubsystem(SubsystemKind.CommandPredictor, WinGetCommandNotFoundFeedbackPredictor.Singleton);
+ }
+
+ public void OnRemove(PSModuleInfo psModuleInfo)
+ {
+ if (!IsWinGetInstalled())
+ {
+ return;
+ }
+
+ SubsystemManager.UnregisterSubsystem(new Guid(Id));
+ SubsystemManager.UnregisterSubsystem(new Guid(Id));
+ }
+
+ private bool IsWinGetInstalled()
+ {
+ // Ensure WinGet is installed
+ using (var pwsh = PowerShell.Create(RunspaceMode.CurrentRunspace))
+ {
+ var results = pwsh.AddCommand("Get-Command")
+ .AddParameter("Name", "winget")
+ .AddParameter("CommandType", "Application")
+ .Invoke();
+
+ if (results.Count is 0)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/modules/cmdNotFound/CmdNotFound/PooledPowerShellObjectPolicy.cs b/src/modules/cmdNotFound/CmdNotFound/PooledPowerShellObjectPolicy.cs
new file mode 100644
index 0000000000..75d2ba8922
--- /dev/null
+++ b/src/modules/cmdNotFound/CmdNotFound/PooledPowerShellObjectPolicy.cs
@@ -0,0 +1,34 @@
+// 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.Management.Automation;
+using System.Management.Automation.Runspaces;
+using Microsoft.Extensions.ObjectPool;
+
+namespace WinGetCommandNotFound
+{
+ public sealed class PooledPowerShellObjectPolicy : IPooledObjectPolicy
+ {
+ private static readonly string[] WingetClientModuleName = new[] { "Microsoft.WinGet.Client" };
+
+ public PowerShell Create()
+ {
+ var iss = InitialSessionState.CreateDefault2();
+ iss.ImportPSModule(WingetClientModuleName);
+ return PowerShell.Create(iss);
+ }
+
+ public bool Return(PowerShell obj)
+ {
+ if (obj != null)
+ {
+ obj.Commands.Clear();
+ obj.Streams.ClearStreams();
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/modules/cmdNotFound/CmdNotFound/Telemetry/CmdNotFoundFeedbackProvidedEvent.cs b/src/modules/cmdNotFound/CmdNotFound/Telemetry/CmdNotFoundFeedbackProvidedEvent.cs
new file mode 100644
index 0000000000..83563bbe35
--- /dev/null
+++ b/src/modules/cmdNotFound/CmdNotFound/Telemetry/CmdNotFoundFeedbackProvidedEvent.cs
@@ -0,0 +1,16 @@
+// 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.Tracing;
+using Microsoft.PowerToys.Telemetry;
+using Microsoft.PowerToys.Telemetry.Events;
+
+namespace WinGetCommandNotFound.Telemetry
+{
+ [EventData]
+ public class CmdNotFoundFeedbackProvidedEvent : EventBase, IEvent
+ {
+ public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
+ }
+}
diff --git a/src/modules/cmdNotFound/CmdNotFound/Telemetry/CmdNotFoundSuggestionProvidedEvent.cs b/src/modules/cmdNotFound/CmdNotFound/Telemetry/CmdNotFoundSuggestionProvidedEvent.cs
new file mode 100644
index 0000000000..2f55dbbb39
--- /dev/null
+++ b/src/modules/cmdNotFound/CmdNotFound/Telemetry/CmdNotFoundSuggestionProvidedEvent.cs
@@ -0,0 +1,16 @@
+// 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.Tracing;
+using Microsoft.PowerToys.Telemetry;
+using Microsoft.PowerToys.Telemetry.Events;
+
+namespace WinGetCommandNotFound.Telemetry
+{
+ [EventData]
+ public class CmdNotFoundSuggestionProvidedEvent : EventBase, IEvent
+ {
+ public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
+ }
+}
diff --git a/src/modules/cmdNotFound/CmdNotFound/WinGetCommandNotFound.psd1 b/src/modules/cmdNotFound/CmdNotFound/WinGetCommandNotFound.psd1
new file mode 100644
index 0000000000..c7a1118eac
--- /dev/null
+++ b/src/modules/cmdNotFound/CmdNotFound/WinGetCommandNotFound.psd1
@@ -0,0 +1,11 @@
+@{
+ ModuleVersion = '0.1.0'
+ GUID = '28c9afa2-92e5-413e-8e53-44b2d7a83ac6'
+ Author = 'Carlos Zamora'
+ CompanyName = "Microsoft Corporation"
+ Copyright = "Copyright (c) Microsoft Corporation."
+ Description = 'Enable suggestions on how to install missing commands via winget'
+ PowerShellVersion = '7.4'
+ NestedModules = @('PowerToys.CmdNotFound.dll')
+ RequiredModules = @(@{ModuleName = 'Microsoft.WinGet.Client'; ModuleVersion = "0.2.1"; })
+}
diff --git a/src/modules/cmdNotFound/CmdNotFound/WinGetCommandNotFoundFeedbackPredictor.cs b/src/modules/cmdNotFound/CmdNotFound/WinGetCommandNotFoundFeedbackPredictor.cs
new file mode 100644
index 0000000000..681b073bad
--- /dev/null
+++ b/src/modules/cmdNotFound/CmdNotFound/WinGetCommandNotFoundFeedbackPredictor.cs
@@ -0,0 +1,206 @@
+// 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.Collections;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Management.Automation;
+using System.Management.Automation.Subsystem.Feedback;
+using System.Management.Automation.Subsystem.Prediction;
+using Microsoft.Extensions.ObjectPool;
+using Microsoft.PowerToys.Telemetry;
+
+namespace WinGetCommandNotFound
+{
+ public sealed class WinGetCommandNotFoundFeedbackPredictor : IFeedbackProvider, ICommandPredictor
+ {
+ private readonly Guid _guid;
+
+ private readonly ObjectPool _pool;
+
+ private const int _maxSuggestions = 20;
+
+ private List? _candidates;
+
+ private bool _warmedUp;
+
+ public static WinGetCommandNotFoundFeedbackPredictor Singleton { get; } = new WinGetCommandNotFoundFeedbackPredictor(Init.Id);
+
+ private WinGetCommandNotFoundFeedbackPredictor(string guid)
+ {
+ _guid = new Guid(guid);
+
+ var provider = new DefaultObjectPoolProvider();
+ _pool = provider.Create(new PooledPowerShellObjectPolicy());
+ _pool.Return(_pool.Get());
+ Task.Run(() => WarmUp());
+ }
+
+ public Guid Id => _guid;
+
+ public string Name => "Windows Package Manager - WinGet";
+
+ public string Description => "Finds missing commands that can be installed via WinGet.";
+
+ public Dictionary? FunctionsToDefine => null;
+
+ private void WarmUp()
+ {
+ var ps = _pool.Get();
+ try
+ {
+ ps.AddCommand("Find-WinGetPackage")
+ .AddParameter("Count", 1)
+ .Invoke();
+ }
+ finally
+ {
+ _pool.Return(ps);
+ _warmedUp = true;
+ }
+ }
+
+ ///
+ /// Gets feedback based on the given commandline and error record.
+ ///
+ public FeedbackItem? GetFeedback(FeedbackContext context, CancellationToken token)
+ {
+ var target = (string)context.LastError!.TargetObject;
+ if (target is not null)
+ {
+ bool tooManySuggestions = false;
+ string packageMatchFilterField = "command";
+ var pkgList = FindPackages(target, ref tooManySuggestions, ref packageMatchFilterField);
+ if (pkgList.Count == 0)
+ {
+ return null;
+ }
+
+ // Build list of suggestions
+ _candidates = new List();
+ foreach (var pkg in pkgList)
+ {
+ _candidates.Add(string.Format(CultureInfo.InvariantCulture, "winget install --id {0}", pkg.Members["Id"].Value.ToString()));
+ }
+
+ // Build footer message
+ var footerMessage = tooManySuggestions ?
+ string.Format(CultureInfo.InvariantCulture, "Additional results can be found using \"winget search --{0} {1}\"", packageMatchFilterField, target) :
+ null;
+
+ PowerToysTelemetry.Log.WriteEvent(new Telemetry.CmdNotFoundFeedbackProvidedEvent());
+
+ return new FeedbackItem(
+ "Try installing this package using winget:",
+ _candidates,
+ footerMessage,
+ FeedbackDisplayLayout.Portrait);
+ }
+
+ return null;
+ }
+
+ private Collection FindPackages(string query, ref bool tooManySuggestions, ref string packageMatchFilterField)
+ {
+ if (!_warmedUp)
+ {
+ return new Collection();
+ }
+
+ var ps = _pool.Get();
+ try
+ {
+ var common = new Hashtable()
+ {
+ ["Source"] = "winget",
+ };
+
+ // 1) Search by command
+ var pkgList = ps.AddCommand("Find-WinGetPackage")
+ .AddParameter("Command", query)
+ .AddParameter("MatchOption", "StartsWithCaseInsensitive")
+ .AddParameters(common)
+ .Invoke();
+ if (pkgList.Count > 0)
+ {
+ tooManySuggestions = pkgList.Count > _maxSuggestions;
+ packageMatchFilterField = "command";
+ return pkgList;
+ }
+
+ // 2) No matches found,
+ // search by name
+ ps.Commands.Clear();
+ pkgList = ps.AddCommand("Find-WinGetPackage")
+ .AddParameter("Name", query)
+ .AddParameter("MatchOption", "ContainsCaseInsensitive")
+ .AddParameters(common)
+ .Invoke();
+ if (pkgList.Count > 0)
+ {
+ tooManySuggestions = pkgList.Count > _maxSuggestions;
+ packageMatchFilterField = "name";
+ return pkgList;
+ }
+
+ // 3) No matches found,
+ // search by moniker
+ ps.Commands.Clear();
+ pkgList = ps.AddCommand("Find-WinGetPackage")
+ .AddParameter("Moniker", query)
+ .AddParameter("MatchOption", "ContainsCaseInsensitive")
+ .AddParameters(common)
+ .Invoke();
+ tooManySuggestions = pkgList.Count > _maxSuggestions;
+ packageMatchFilterField = "moniker";
+ return pkgList;
+ }
+ finally
+ {
+ _pool.Return(ps);
+ }
+ }
+
+ public bool CanAcceptFeedback(PredictionClient client, PredictorFeedbackKind feedback)
+ {
+ return feedback switch
+ {
+ PredictorFeedbackKind.CommandLineAccepted => true,
+ _ => false,
+ };
+ }
+
+ public SuggestionPackage GetSuggestion(PredictionClient client, PredictionContext context, CancellationToken cancellationToken)
+ {
+ if (_candidates is not null)
+ {
+ string input = context.InputAst.Extent.Text;
+ List? result = null;
+
+ foreach (string c in _candidates)
+ {
+ if (c.StartsWith(input, StringComparison.OrdinalIgnoreCase))
+ {
+ result ??= new List(_candidates.Count);
+ result.Add(new PredictiveSuggestion(c));
+ }
+ }
+
+ if (result is not null)
+ {
+ PowerToysTelemetry.Log.WriteEvent(new Telemetry.CmdNotFoundSuggestionProvidedEvent());
+ return new SuggestionPackage(result);
+ }
+ }
+
+ return default;
+ }
+
+ public void OnCommandLineAccepted(PredictionClient client, IReadOnlyList history)
+ {
+ // Reset the candidate state.
+ _candidates = null;
+ }
+ }
+}
diff --git a/src/modules/cmdNotFound/CmdNotFoundModuleInterface/CmdNotFoundModuleInterface.rc b/src/modules/cmdNotFound/CmdNotFoundModuleInterface/CmdNotFoundModuleInterface.rc
new file mode 100644
index 0000000000..e9ac022fc5
--- /dev/null
+++ b/src/modules/cmdNotFound/CmdNotFoundModuleInterface/CmdNotFoundModuleInterface.rc
@@ -0,0 +1,108 @@
+// Microsoft Visual C++ generated resource script.
+//
+#include
+#include "resource.h"
+#include "../../../common/version/version.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "winres.h"
+
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+1 VERSIONINFO
+FILEVERSION FILE_VERSION
+PRODUCTVERSION PRODUCT_VERSION
+FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+FILEFLAGS VS_FF_DEBUG
+#else
+FILEFLAGS 0x0L
+#endif
+FILEOS VOS_NT_WINDOWS32
+FILETYPE VFT_DLL
+FILESUBTYPE VFT2_UNKNOWN
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
+ BEGIN
+ VALUE "CompanyName", COMPANY_NAME
+ VALUE "FileDescription", FILE_DESCRIPTION
+ VALUE "FileVersion", FILE_VERSION_STRING
+ VALUE "InternalName", INTERNAL_NAME
+ VALUE "LegalCopyright", COPYRIGHT_NOTE
+ VALUE "OriginalFilename", ORIGINAL_FILENAME
+ VALUE "ProductName", PRODUCT_NAME
+ VALUE "ProductVersion", PRODUCT_VERSION_STRING
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
+ END
+END
+
+
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+#pragma code_page(1252)
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+ "resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+ "#include ""winres.h""\r\n"
+ "\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+ "\r\n"
+ "\0"
+END
+
+#endif // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// String Table
+//
+
+STRINGTABLE
+BEGIN
+ IDS_CMD_NOT_FOUND_NAME "Command Not Found"
+END
+
+#endif // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif // not APSTUDIO_INVOKED
+
diff --git a/src/modules/cmdNotFound/CmdNotFoundModuleInterface/CmdNotFoundModuleInterface.vcxproj b/src/modules/cmdNotFound/CmdNotFoundModuleInterface/CmdNotFoundModuleInterface.vcxproj
new file mode 100644
index 0000000000..a7fd427c2a
--- /dev/null
+++ b/src/modules/cmdNotFound/CmdNotFoundModuleInterface/CmdNotFoundModuleInterface.vcxproj
@@ -0,0 +1,107 @@
+
+
+
+ 17.0
+ Win32Proj
+ {0014d652-901f-4456-8d65-06fc5f997fb0}
+ CmdNotFoundModuleInterface
+ PowerToys.CmdNotFoundModuleInterface
+ v143
+ CmdNotFoundModuleInterface
+
+
+
+ DynamicLibrary
+ true
+ Unicode
+
+
+ DynamicLibrary
+ false
+ true
+ Unicode
+
+
+
+
+
+
+
+
+
+
+
+ ..\..\..\..\$(Platform)\$(Configuration)\
+
+
+
+ Level3
+ true
+ WIN32;_DEBUG;CMDNOTFOUNDMODULEINTERFACE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+ true
+ Use
+
+
+ Windows
+ true
+ false
+ Shlwapi.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)
+
+
+
+
+ Level3
+ true
+ true
+ true
+ WIN32;NDEBUG;CMDNOTFOUNDMODULEINTERFACE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+ true
+ Use
+
+
+ Windows
+ true
+ true
+ true
+ false
+ Shlwapi.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)
+
+
+
+
+ ..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)
+
+
+
+
+
+ Header Files
+
+
+
+
+
+ Source Files
+
+
+
+ Create
+
+
+
+
+ {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}
+
+
+ {6955446d-23f7-4023-9bb3-8657f904af99}
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/cmdNotFound/CmdNotFoundModuleInterface/CmdNotFoundModuleInterface.vcxproj.filters b/src/modules/cmdNotFound/CmdNotFoundModuleInterface/CmdNotFoundModuleInterface.vcxproj.filters
new file mode 100644
index 0000000000..1834d3ae88
--- /dev/null
+++ b/src/modules/cmdNotFound/CmdNotFoundModuleInterface/CmdNotFoundModuleInterface.vcxproj.filters
@@ -0,0 +1,45 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd
+
+
+ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
+ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
+
+
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Resource Files
+
+
+
\ No newline at end of file
diff --git a/src/modules/cmdNotFound/CmdNotFoundModuleInterface/dllmain.cpp b/src/modules/cmdNotFound/CmdNotFoundModuleInterface/dllmain.cpp
new file mode 100644
index 0000000000..fed395d45d
--- /dev/null
+++ b/src/modules/cmdNotFound/CmdNotFoundModuleInterface/dllmain.cpp
@@ -0,0 +1,161 @@
+// dllmain.cpp : Defines the entry point for the DLL application.
+#include "pch.h"
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "resource.h"
+#include "trace.h"
+
+BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
+{
+ switch (ul_reason_for_call)
+ {
+ case DLL_PROCESS_ATTACH:
+ Trace::RegisterProvider();
+ break;
+ case DLL_THREAD_ATTACH:
+ case DLL_THREAD_DETACH:
+ break;
+ case DLL_PROCESS_DETACH:
+ Trace::UnregisterProvider();
+ break;
+ }
+ return TRUE;
+}
+
+const static wchar_t* MODULE_NAME = L"Command Not Found";
+const static wchar_t* MODULE_DESC = L"A module that detects an error thrown by a command in PowerShell and suggests a relevant WinGet package to install, if available.";
+
+inline const std::wstring ModuleKey = L"CmdNotFound";
+
+class CmdNotFound : public PowertoyModuleIface
+{
+ std::wstring app_name;
+ std::wstring app_key;
+
+private:
+ bool m_enabled = false;
+
+ void install_module()
+ {
+ auto module_path = get_module_folderpath();
+
+ std::string command = "pwsh.exe";
+ command += " ";
+ command += "-NoProfile -NonInteractive -NoLogo -WindowStyle Hidden -ExecutionPolicy Unrestricted -File \"" + winrt::to_string(module_path) + "\\WinUI3Apps\\Assets\\Settings\\Scripts\\EnableModule.ps1" + "\"" + " -scriptPath \"" + winrt::to_string(module_path) + "\"";
+
+ int ret = system(command.c_str());
+
+ if (ret != 0)
+ {
+ Logger::error("Running EnableModule.ps1 script failed.");
+ }
+ else
+ {
+ Logger::info("Module installed successfully.");
+ Trace::EnableCmdNotFoundGpo(true);
+ }
+ }
+
+ void uninstall_module()
+ {
+ auto module_path = get_module_folderpath();
+
+ std::string command = "pwsh.exe";
+ command += " ";
+ command += "-NoProfile -NonInteractive -NoLogo -WindowStyle Hidden -ExecutionPolicy Unrestricted -File \"" + winrt::to_string(module_path) + "\\WinUI3Apps\\Assets\\Settings\\Scripts\\DisableModule.ps1" + "\"";
+
+ int ret = system(command.c_str());
+
+ if (ret != 0)
+ {
+ Logger::error("Running EnableModule.ps1 script failed.");
+ }
+ else
+ {
+ Logger::info("Module uninstalled successfully.");
+ Trace::EnableCmdNotFoundGpo(false);
+ }
+ }
+
+public:
+ CmdNotFound()
+ {
+ app_name = GET_RESOURCE_STRING(IDS_CMD_NOT_FOUND_NAME);
+ app_key = ModuleKey;
+
+ std::filesystem::path logFilePath(PTSettingsHelper::get_module_save_folder_location(this->app_key));
+ logFilePath.append(LogSettings::cmdNotFoundLogPath);
+ Logger::init(LogSettings::cmdNotFoundLoggerName, logFilePath.wstring(), PTSettingsHelper::get_log_settings_file_location());
+ Logger::info("CmdNotFound object is constructing");
+
+ powertoys_gpo::gpo_rule_configured_t gpo_rule_configured_value = gpo_policy_enabled_configuration();
+ if (gpo_rule_configured_value == powertoys_gpo::gpo_rule_configured_t::gpo_rule_configured_enabled)
+ {
+ install_module();
+ m_enabled = true;
+ }
+ else if (gpo_rule_configured_value == powertoys_gpo::gpo_rule_configured_t::gpo_rule_configured_disabled)
+ {
+ uninstall_module();
+ m_enabled = false;
+ }
+ }
+
+ virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
+ {
+ return powertoys_gpo::getConfiguredCmdNotFoundEnabledValue();
+ }
+
+ virtual void destroy() override
+ {
+ delete this;
+ }
+
+ virtual const wchar_t* get_name() override
+ {
+ return MODULE_NAME;
+ }
+
+ virtual const wchar_t* get_key() override
+ {
+ return app_key.c_str();
+ }
+
+ virtual bool get_config(wchar_t* /*buffer*/, int* /*buffer_size*/) override
+ {
+ return false;
+ }
+
+ virtual void set_config(const wchar_t* config) override
+ {
+ }
+
+ virtual void enable()
+ {
+ }
+
+ virtual void disable()
+ {
+ }
+
+ virtual bool is_enabled() override
+ {
+ return m_enabled;
+ }
+};
+
+extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
+{
+ return new CmdNotFound();
+}
diff --git a/src/modules/cmdNotFound/CmdNotFoundModuleInterface/pch.cpp b/src/modules/cmdNotFound/CmdNotFoundModuleInterface/pch.cpp
new file mode 100644
index 0000000000..64b7eef6d6
--- /dev/null
+++ b/src/modules/cmdNotFound/CmdNotFoundModuleInterface/pch.cpp
@@ -0,0 +1,5 @@
+// pch.cpp: source file corresponding to the pre-compiled header
+
+#include "pch.h"
+
+// When you are using pre-compiled headers, this source file is necessary for compilation to succeed.
diff --git a/src/modules/cmdNotFound/CmdNotFoundModuleInterface/pch.h b/src/modules/cmdNotFound/CmdNotFoundModuleInterface/pch.h
new file mode 100644
index 0000000000..96a774ab2a
--- /dev/null
+++ b/src/modules/cmdNotFound/CmdNotFoundModuleInterface/pch.h
@@ -0,0 +1,16 @@
+// pch.h: This is a precompiled header file.
+// Files listed below are compiled only once, improving build performance for future builds.
+// This also affects IntelliSense performance, including code completion and many code browsing features.
+// However, files listed here are ALL re-compiled if any one of them is updated between builds.
+// Do not add files here that you will be updating frequently as this negates the performance advantage.
+
+#ifndef PCH_H
+#define PCH_H
+
+#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
+// Windows Header Files
+#include
+
+#include
+
+#endif //PCH_H
diff --git a/src/modules/cmdNotFound/CmdNotFoundModuleInterface/resource.h b/src/modules/cmdNotFound/CmdNotFoundModuleInterface/resource.h
new file mode 100644
index 0000000000..9b16533a8a
--- /dev/null
+++ b/src/modules/cmdNotFound/CmdNotFoundModuleInterface/resource.h
@@ -0,0 +1,21 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by Awake.rc
+//
+#define IDS_CMD_NOT_FOUND_NAME 101
+
+
+#define FILE_DESCRIPTION "PowerToys Command Not Found"
+#define INTERNAL_NAME "PowerToys.CmdNotFoundModuleInterface"
+#define ORIGINAL_FILENAME "PowerToys.CmdNotFoundModuleInterface.dll"
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE 102
+#define _APS_NEXT_COMMAND_VALUE 40001
+#define _APS_NEXT_CONTROL_VALUE 1001
+#define _APS_NEXT_SYMED_VALUE 101
+#endif
+#endif
diff --git a/src/modules/cmdNotFound/CmdNotFoundModuleInterface/trace.cpp b/src/modules/cmdNotFound/CmdNotFoundModuleInterface/trace.cpp
new file mode 100644
index 0000000000..255c46ea99
--- /dev/null
+++ b/src/modules/cmdNotFound/CmdNotFoundModuleInterface/trace.cpp
@@ -0,0 +1,30 @@
+#include "pch.h"
+#include "trace.h"
+
+TRACELOGGING_DEFINE_PROVIDER(
+ g_hProvider,
+ "Microsoft.PowerToys",
+ // {38e8889b-9731-53f5-e901-e8a7c1753074}
+ (0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
+ TraceLoggingOptionProjectTelemetry());
+
+void Trace::RegisterProvider()
+{
+ TraceLoggingRegister(g_hProvider);
+}
+
+void Trace::UnregisterProvider()
+{
+ TraceLoggingUnregister(g_hProvider);
+}
+
+// Log if the user has CmdNotFound enabled or disabled
+void Trace::EnableCmdNotFoundGpo(const bool enabled) noexcept
+{
+ TraceLoggingWrite(
+ g_hProvider,
+ "CmdNotFound_EnableCmdNotFound",
+ ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
+ TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
+ TraceLoggingBoolean(enabled, "Enabled"));
+}
diff --git a/src/modules/cmdNotFound/CmdNotFoundModuleInterface/trace.h b/src/modules/cmdNotFound/CmdNotFoundModuleInterface/trace.h
new file mode 100644
index 0000000000..4294c510a6
--- /dev/null
+++ b/src/modules/cmdNotFound/CmdNotFoundModuleInterface/trace.h
@@ -0,0 +1,11 @@
+#pragma once
+
+class Trace
+{
+public:
+ static void RegisterProvider();
+ static void UnregisterProvider();
+
+ // Log if the user has CmdNotFound enabled or disabled
+ static void EnableCmdNotFoundGpo(const bool enabled) noexcept;
+};
diff --git a/src/runner/main.cpp b/src/runner/main.cpp
index 59a455b379..a16197655e 100644
--- a/src/runner/main.cpp
+++ b/src/runner/main.cpp
@@ -158,6 +158,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
L"WinUI3Apps/PowerToys.EnvironmentVariablesModuleInterface.dll",
L"PowerToys.MouseWithoutBordersModuleInterface.dll",
L"PowerToys.CropAndLockModuleInterface.dll",
+ L"PowerToys.CmdNotFoundModuleInterface.dll",
};
const auto VCM_PATH = L"PowerToys.VideoConferenceModule.dll";
if (const auto mf = LoadLibraryA("mf.dll"))
diff --git a/src/settings-ui/Settings.UI.Library/CmdNotFoundSettings.cs b/src/settings-ui/Settings.UI.Library/CmdNotFoundSettings.cs
new file mode 100644
index 0000000000..4353e5a188
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/CmdNotFoundSettings.cs
@@ -0,0 +1,42 @@
+// 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.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
+
+namespace Microsoft.PowerToys.Settings.UI.Library
+{
+ public class CmdNotFoundSettings : BasePTModuleSettings, ISettingsConfig
+ {
+ private static readonly JsonSerializerOptions SerializerOptions = new()
+ {
+ WriteIndented = true,
+ };
+
+ public const string ModuleName = "CmdNotFound";
+
+ public CmdNotFoundSettings()
+ {
+ Version = "1";
+ Name = ModuleName;
+ }
+
+ public virtual void Save(ISettingsUtils settingsUtils)
+ {
+ // Save settings to file
+ ArgumentNullException.ThrowIfNull(settingsUtils);
+
+ settingsUtils.SaveSettings(JsonSerializer.Serialize(this, SerializerOptions), ModuleName);
+ }
+
+ public string GetModuleName()
+ => Name;
+
+ // This can be utilized in the future if the settings.json file is to be modified/deleted.
+ public bool UpgradeSettingsConfiguration()
+ => false;
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/EnabledModules.cs b/src/settings-ui/Settings.UI.Library/EnabledModules.cs
index 27af49c4eb..a9ef81c1a8 100644
--- a/src/settings-ui/Settings.UI.Library/EnabledModules.cs
+++ b/src/settings-ui/Settings.UI.Library/EnabledModules.cs
@@ -427,6 +427,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
+ private bool cmdNotFound = true;
+
+ [JsonPropertyName("CmdNotFound")]
+ public bool CmdNotFound
+ {
+ get => cmdNotFound;
+ set
+ {
+ if (cmdNotFound != value)
+ {
+ LogTelemetryEvent(value);
+ cmdNotFound = value;
+ NotifyChange();
+ }
+ }
+ }
+
private bool environmentVariables = true;
[JsonPropertyName("EnvironmentVariables")]
diff --git a/src/settings-ui/Settings.UI.Library/Telemetry/Events/CmdNotFoundInstallEvent.cs b/src/settings-ui/Settings.UI.Library/Telemetry/Events/CmdNotFoundInstallEvent.cs
new file mode 100644
index 0000000000..97d700a8e5
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/Telemetry/Events/CmdNotFoundInstallEvent.cs
@@ -0,0 +1,16 @@
+// 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.Tracing;
+using Microsoft.PowerToys.Telemetry;
+using Microsoft.PowerToys.Telemetry.Events;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events
+{
+ [EventData]
+ public class CmdNotFoundInstallEvent : EventBase, IEvent
+ {
+ public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/Telemetry/Events/CmdNotFoundUninstallEvent.cs b/src/settings-ui/Settings.UI.Library/Telemetry/Events/CmdNotFoundUninstallEvent.cs
new file mode 100644
index 0000000000..ae469903ee
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/Telemetry/Events/CmdNotFoundUninstallEvent.cs
@@ -0,0 +1,16 @@
+// 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.Tracing;
+using Microsoft.PowerToys.Telemetry;
+using Microsoft.PowerToys.Telemetry.Events;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events
+{
+ [EventData]
+ public class CmdNotFoundUninstallEvent : EventBase, IEvent
+ {
+ public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
+ }
+}
diff --git a/src/settings-ui/Settings.UI/Assets/Settings/FluentIcons/FluentIconsCmdNotFound.png b/src/settings-ui/Settings.UI/Assets/Settings/FluentIcons/FluentIconsCmdNotFound.png
new file mode 100644
index 0000000000..c4de6573a5
Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/FluentIcons/FluentIconsCmdNotFound.png differ
diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Modules/CmdNotFound.png b/src/settings-ui/Settings.UI/Assets/Settings/Modules/CmdNotFound.png
new file mode 100644
index 0000000000..4b06c4de36
Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/Modules/CmdNotFound.png differ
diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Modules/OOBE/CmdNotFound.png b/src/settings-ui/Settings.UI/Assets/Settings/Modules/OOBE/CmdNotFound.png
new file mode 100644
index 0000000000..f40d8ae08c
Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/Modules/OOBE/CmdNotFound.png differ
diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Scripts/DisableModule.ps1 b/src/settings-ui/Settings.UI/Assets/Settings/Scripts/DisableModule.ps1
new file mode 100644
index 0000000000..fef86f4935
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Assets/Settings/Scripts/DisableModule.ps1
@@ -0,0 +1,35 @@
+$profileContent = Get-Content $PROFILE
+
+$newContent = ""
+$linesToDeleteFound = $False
+$atLeastOneInstanceFound = $False
+
+$profileContent | ForEach-Object {
+ if ($_.Contains("34de4b3d-13a8-4540-b76d-b9e8d3851756") -and !$linesToDeleteFound)
+ {
+ $linesToDeleteFound = $True
+ $atLeastOneInstanceFound = $True
+ return
+ }
+
+ if ($_.Contains("34de4b3d-13a8-4540-b76d-b9e8d3851756") -and $linesToDeleteFound)
+ {
+ $linesToDeleteFound = $False
+ return
+ }
+
+ if($linesToDeleteFound)
+ {
+ return
+ }
+
+ $newContent += $_ + "`r`n"
+}
+
+if($atLeastOneInstanceFound)
+{
+ Set-Content -Path $PROFILE -Value $newContent
+ Write-Host "Removed the Command Not Found reference from the profile file."
+} else {
+ Write-Host "No instance of Command Not Found was found in the profile file."
+}
diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Scripts/EnableModule.ps1 b/src/settings-ui/Settings.UI/Assets/Settings/Scripts/EnableModule.ps1
new file mode 100644
index 0000000000..844613a2be
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Assets/Settings/Scripts/EnableModule.ps1
@@ -0,0 +1,38 @@
+[CmdletBinding()]
+Param(
+ [Parameter(Mandatory=$True,Position=1)]
+ [string]$scriptPath
+)
+
+Write-Host "Enabling experimental feature: PSFeedbackProvider"
+Enable-ExperimentalFeature PSFeedbackProvider
+Write-Host "Enabling experimental feature: PSCommandNotFoundSuggestion"
+Enable-ExperimentalFeature PSCommandNotFoundSuggestion
+
+if (Get-Module -ListAvailable -Name Microsoft.WinGet.Client) {
+ Write-Host "WinGet Client module detected"
+}
+else {
+ Write-Host "WinGet module was not found. Installation instructions can be found on https://www.powershellgallery.com/packages/Microsoft.WinGet.Client `r`n"
+}
+
+if (!(Test-Path $PROFILE))
+{
+ Write-Host "Profile file $PROFILE not found".
+ New-Item -Path $PROFILE -ItemType File
+ Write-Host "Created profile file $PROFILE".
+}
+
+$profileContent = Get-Content -Path $PROFILE -Raw
+
+if ((-not [string]::IsNullOrEmpty($profileContent)) -and ($profileContent.Contains("34de4b3d-13a8-4540-b76d-b9e8d3851756")))
+{
+ Write-Host "Module is already registered in the profile file."
+}
+else
+{
+ Add-Content -Path $PROFILE -Value "`r`n#34de4b3d-13a8-4540-b76d-b9e8d3851756 PowerToys CommandNotFound module"
+ Add-Content -Path $PROFILE -Value "`r`nImport-Module `"$scriptPath\WinGetCommandNotFound.psd1`""
+ Add-Content -Path $PROFILE -Value "#34de4b3d-13a8-4540-b76d-b9e8d3851756"
+ Write-Host "Module was successfully registered in the profile file."
+}
diff --git a/src/settings-ui/Settings.UI/Helpers/ModuleHelper.cs b/src/settings-ui/Settings.UI/Helpers/ModuleHelper.cs
index 25d5fb4f59..7daa62ae0d 100644
--- a/src/settings-ui/Settings.UI/Helpers/ModuleHelper.cs
+++ b/src/settings-ui/Settings.UI/Helpers/ModuleHelper.cs
@@ -44,6 +44,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
case ModuleType.AlwaysOnTop: return generalSettingsConfig.Enabled.AlwaysOnTop;
case ModuleType.Awake: return generalSettingsConfig.Enabled.Awake;
case ModuleType.ColorPicker: return generalSettingsConfig.Enabled.ColorPicker;
+ case ModuleType.CmdNotFound: return generalSettingsConfig.Enabled.CmdNotFound;
case ModuleType.CropAndLock: return generalSettingsConfig.Enabled.CropAndLock;
case ModuleType.EnvironmentVariables: return generalSettingsConfig.Enabled.EnvironmentVariables;
case ModuleType.FancyZones: return generalSettingsConfig.Enabled.FancyZones;
@@ -76,6 +77,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
case ModuleType.AlwaysOnTop: generalSettingsConfig.Enabled.AlwaysOnTop = isEnabled; break;
case ModuleType.Awake: generalSettingsConfig.Enabled.Awake = isEnabled; break;
case ModuleType.ColorPicker: generalSettingsConfig.Enabled.ColorPicker = isEnabled; break;
+ case ModuleType.CmdNotFound: generalSettingsConfig.Enabled.CmdNotFound = isEnabled; break;
case ModuleType.CropAndLock: generalSettingsConfig.Enabled.CropAndLock = isEnabled; break;
case ModuleType.EnvironmentVariables: generalSettingsConfig.Enabled.EnvironmentVariables = isEnabled; break;
case ModuleType.FancyZones: generalSettingsConfig.Enabled.FancyZones = isEnabled; break;
@@ -107,6 +109,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
case ModuleType.AlwaysOnTop: return GPOWrapper.GetConfiguredAlwaysOnTopEnabledValue();
case ModuleType.Awake: return GPOWrapper.GetConfiguredAwakeEnabledValue();
case ModuleType.ColorPicker: return GPOWrapper.GetConfiguredColorPickerEnabledValue();
+ case ModuleType.CmdNotFound: return GPOWrapper.GetConfiguredCmdNotFoundEnabledValue();
case ModuleType.CropAndLock: return GPOWrapper.GetConfiguredCropAndLockEnabledValue();
case ModuleType.EnvironmentVariables: return GPOWrapper.GetConfiguredEnvironmentVariablesEnabledValue();
case ModuleType.FancyZones: return GPOWrapper.GetConfiguredFancyZonesEnabledValue();
@@ -139,6 +142,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
ModuleType.AlwaysOnTop => Color.FromArgb(255, 74, 196, 242), // #4ac4f2
ModuleType.Awake => Color.FromArgb(255, 40, 177, 233), // #28b1e9
ModuleType.ColorPicker => Color.FromArgb(255, 7, 129, 211), // #0781d3
+ ModuleType.CmdNotFound => Color.FromArgb(255, 31, 164, 227), // #1fa4e3
ModuleType.CropAndLock => Color.FromArgb(255, 32, 166, 228), // #20a6e4
ModuleType.EnvironmentVariables => Color.FromArgb(255, 16, 132, 208), // #1084d0
ModuleType.FancyZones => Color.FromArgb(255, 65, 209, 247), // #41d1f7
@@ -171,6 +175,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
ModuleType.AlwaysOnTop => typeof(AlwaysOnTopPage),
ModuleType.Awake => typeof(AwakePage),
ModuleType.ColorPicker => typeof(ColorPickerPage),
+ ModuleType.CmdNotFound => typeof(CmdNotFoundPage),
ModuleType.CropAndLock => typeof(CropAndLockPage),
ModuleType.EnvironmentVariables => typeof(EnvironmentVariablesPage),
ModuleType.FancyZones => typeof(FancyZonesPage),
diff --git a/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs b/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs
index 1a5091f5f4..111b0f669a 100644
--- a/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs
+++ b/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs
@@ -9,6 +9,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums
Overview = 0,
AlwaysOnTop,
Awake,
+ CmdNotFound,
ColorPicker,
CropAndLock,
EnvironmentVariables,
diff --git a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
index bffa7cc353..16c118872a 100644
--- a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
+++ b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
@@ -53,7 +53,7 @@
CA1720
true
-
+
PowerToys.GPOWrapper
@@ -61,10 +61,10 @@
false
-
-
- https://pkgs.dev.azure.com/dotnet/CommunityToolkit/_packaging/CommunityToolkit-Labs/nuget/v3/index.json
-
+
+
+ https://pkgs.dev.azure.com/dotnet/CommunityToolkit/_packaging/CommunityToolkit-Labs/nuget/v3/index.json
+
@@ -96,11 +96,11 @@
-
+
-
+
@@ -111,7 +111,7 @@
-
+
@@ -119,10 +119,19 @@
VSTHRD002;VSTHRD110;VSTHRD100;VSTHRD200;VSTHRD101
-
-
- Always
-
-
-
+
+
+ Always
+
+
+
+
+
+ Always
+
+
+ Always
+
+
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs
index 46cd4d7e0e..9d3b030ef0 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs
@@ -395,6 +395,7 @@ namespace Microsoft.PowerToys.Settings.UI
case "Overview": return typeof(GeneralPage);
case "AlwaysOnTop": return typeof(AlwaysOnTopPage);
case "Awake": return typeof(AwakePage);
+ case "CmdNotFound": return typeof(CmdNotFoundPage);
case "ColorPicker": return typeof(ColorPickerPage);
case "FancyZones": return typeof(FancyZonesPage);
case "FileLocksmith": return typeof(FileLocksmithPage);
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCmdNotFound.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCmdNotFound.xaml
new file mode 100644
index 0000000000..d08cea71aa
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCmdNotFound.xaml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCmdNotFound.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCmdNotFound.xaml.cs
new file mode 100644
index 0000000000..1dea42fa74
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCmdNotFound.xaml.cs
@@ -0,0 +1,45 @@
+// 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 Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
+using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
+using Microsoft.PowerToys.Settings.UI.Views;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Navigation;
+
+namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
+{
+ public sealed partial class OobeCmdNotFound : Page
+ {
+ public OobePowerToysModule ViewModel { get; set; }
+
+ public OobeCmdNotFound()
+ {
+ this.InitializeComponent();
+ ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.CmdNotFound]);
+ DataContext = ViewModel;
+ }
+
+ private void SettingsLaunchButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ if (OobeShellPage.OpenMainWindowCallback != null)
+ {
+ OobeShellPage.OpenMainWindowCallback(typeof(CmdNotFoundPage));
+ }
+
+ ViewModel.LogOpeningSettingsEvent();
+ }
+
+ protected override void OnNavigatedTo(NavigationEventArgs e)
+ {
+ ViewModel.LogOpeningModuleEvent();
+ }
+
+ protected override void OnNavigatedFrom(NavigationEventArgs e)
+ {
+ ViewModel.LogClosingModuleEvent();
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShellPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShellPage.xaml
index 85e765091d..8122520df8 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShellPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShellPage.xaml
@@ -69,6 +69,10 @@
x:Uid="Shell_Awake"
Icon="{ui:BitmapIcon Source=/Assets/Settings/FluentIcons/FluentIconsAwake.png}"
Tag="Awake" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdNotFoundPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdNotFoundPage.xaml.cs
new file mode 100644
index 0000000000..8afa34700e
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdNotFoundPage.xaml.cs
@@ -0,0 +1,21 @@
+// 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 Microsoft.PowerToys.Settings.UI.ViewModels;
+using Microsoft.UI.Xaml.Controls;
+
+namespace Microsoft.PowerToys.Settings.UI.Views
+{
+ public sealed partial class CmdNotFoundPage : Page
+ {
+ private CmdNotFoundViewModel ViewModel { get; set; }
+
+ public CmdNotFoundPage()
+ {
+ ViewModel = new CmdNotFoundViewModel();
+ DataContext = ViewModel;
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml
index de1e86f55c..3fc595eb1f 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml
@@ -115,6 +115,11 @@
helpers:NavHelper.NavigateTo="views:AwakePage"
Icon="{ui:BitmapIcon Source=/Assets/Settings/FluentIcons/FluentIconsAwake.png}" />
+
+
Learn more about Awake
Awake is a product name, do not loc
+
+ Learn more about Command Not Found
+ Command Not Found is the name of the module.
+
Den Delimarsky's work on creating Awake
Awake is a product name, do not loc
@@ -3655,6 +3659,51 @@ Activate by holding the key for the character you want to add an accent to, then
to paste your clipboard data as plain text. Note: this will replace the formatted text in your clipboard with the plain text.
+
+ Command Not Found
+ Product name: Navigation view item name for Command Not Found
+
+
+ A PowerShell module that detects an error thrown by a command and suggests a relevant WinGet package to install, if available.
+ "Command Not Found" is a product name
+
+
+ Command Not Found
+ "Command Not Found" is a product name
+
+
+ Command Not Found
+ "Command Not Found" is a product name
+
+
+ Add this module to the PowerShell 7 profile script so that it is enabled with every new session
+
+
+ Installation logs
+
+
+ PowerShell 7 is required to use this module
+
+
+ Check if your PowerShell configuration is compatible
+
+
+ Install
+
+
+ Uninstall
+
+
+ Command Not Found detects an error thrown by a command in PowerShell and suggests a relevant WinGet package to install, if available.
+ "Command Not Found" is a product name
+
+
+ Command Not Found
+ "Command Not Found" is a product name
+
+
+ If a command returns an error, the module will suggest a WinGet package that may provide the relevant executable.
+
All apps
diff --git a/src/settings-ui/Settings.UI/ViewModels/CmdNotFoundViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/CmdNotFoundViewModel.cs
new file mode 100644
index 0000000000..e814354788
--- /dev/null
+++ b/src/settings-ui/Settings.UI/ViewModels/CmdNotFoundViewModel.cs
@@ -0,0 +1,123 @@
+// 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.Diagnostics;
+using System.IO;
+using System.Reflection;
+using global::PowerToys.GPOWrapper;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
+using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events;
+using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands;
+using Microsoft.PowerToys.Telemetry;
+
+namespace Microsoft.PowerToys.Settings.UI.ViewModels
+{
+ public class CmdNotFoundViewModel : Observable
+ {
+ public ButtonClickCommand CheckPowershellVersionEventHandler => new ButtonClickCommand(CheckPowershellVersion);
+
+ public ButtonClickCommand InstallModuleEventHandler => new ButtonClickCommand(InstallModule);
+
+ public ButtonClickCommand UninstallModuleEventHandler => new ButtonClickCommand(UninstallModule);
+
+ private GpoRuleConfigured _enabledGpoRuleConfiguration;
+ private bool _enabledStateIsGPOConfigured;
+
+ public static string AssemblyDirectory
+ {
+ get
+ {
+ string codeBase = Assembly.GetExecutingAssembly().Location;
+ UriBuilder uri = new UriBuilder(codeBase);
+ string path = Uri.UnescapeDataString(uri.Path);
+ return Path.GetDirectoryName(path);
+ }
+ }
+
+ public CmdNotFoundViewModel()
+ {
+ InitializeEnabledValue();
+ }
+
+ private void InitializeEnabledValue()
+ {
+ _enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredCmdNotFoundEnabledValue();
+ if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
+ {
+ // Get the enabled state from GPO.
+ _enabledStateIsGPOConfigured = true;
+ }
+ }
+
+ private string _commandOutputLog;
+
+ public string CommandOutputLog
+ {
+ get => _commandOutputLog;
+ set
+ {
+ if (_commandOutputLog != value)
+ {
+ _commandOutputLog = value;
+ OnPropertyChanged(nameof(CommandOutputLog));
+ }
+ }
+ }
+
+ public bool IsEnabledGpoConfigured
+ {
+ get => _enabledStateIsGPOConfigured;
+ }
+
+ public void RunPowerShellScript(string powershellArguments)
+ {
+ string outputLog = string.Empty;
+ try
+ {
+ var startInfo = new ProcessStartInfo()
+ {
+ FileName = "pwsh.exe",
+ Arguments = powershellArguments,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ };
+ startInfo.EnvironmentVariables["NO_COLOR"] = "1";
+ var process = Process.Start(startInfo);
+ while (!process.StandardOutput.EndOfStream)
+ {
+ outputLog += process.StandardOutput.ReadLine() + "\r\n"; // Weirdly, PowerShell 7 won't give us new lines.
+ }
+ }
+ catch (Exception ex)
+ {
+ outputLog = ex.ToString();
+ }
+
+ CommandOutputLog = outputLog;
+ }
+
+ public void CheckPowershellVersion()
+ {
+ var arguments = $"-NoProfile -NonInteractive -Command $PSVersionTable";
+ RunPowerShellScript(arguments);
+ }
+
+ public void InstallModule()
+ {
+ var ps1File = AssemblyDirectory + "\\Assets\\Settings\\Scripts\\EnableModule.ps1";
+ var arguments = $"-NoProfile -ExecutionPolicy Unrestricted -File \"{ps1File}\" -scriptPath \"{AssemblyDirectory}\\..\"";
+ RunPowerShellScript(arguments);
+ PowerToysTelemetry.Log.WriteEvent(new CmdNotFoundInstallEvent());
+ }
+
+ public void UninstallModule()
+ {
+ var ps1File = AssemblyDirectory + "\\Assets\\Settings\\Scripts\\DisableModule.ps1";
+ var arguments = $"-NoProfile -ExecutionPolicy Unrestricted -File \"{ps1File}\"";
+ RunPowerShellScript(arguments);
+ PowerToysTelemetry.Log.WriteEvent(new CmdNotFoundUninstallEvent());
+ }
+ }
+}