Introduce Command Not Found module

This commit is contained in:
Carlos Zamora
2023-05-25 11:02:38 -07:00
parent 5e353640d7
commit 784e4e4d43
38 changed files with 928 additions and 9 deletions

View File

@@ -1631,6 +1631,7 @@ rundll
rungameid
RUNLEVEL
runsettings
runspace
runtimeclass
runtimeobject
runtimepack
@@ -2141,6 +2142,7 @@ winexe
winforms
winfx
winget
wingetprovider
wingetcreate
Winhook
winkey
@@ -2151,6 +2153,7 @@ winmm
winnt
winres
winrt
winrtact
winsdk
winsdkver
Winsock

View File

@@ -28,7 +28,9 @@
"PowerToys.AlwaysOnTop.exe",
"PowerToys.AlwaysOnTopModuleInterface.dll",
"PowerToys.CmdNotFoundModuleInterface.dll",
"PowerToys.ColorPicker.dll",
"PowerToys.ColorPickerUI.dll",
"PowerToys.ColorPickerUI.exe",

View File

@@ -537,6 +537,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}") = "WinGetCommandNotFound", "src\modules\cmdNotFound\CmdNotFoundModuleInterface\WinGetCommandNotFound.csproj", "{A37865FE-2881-449F-8ADB-B8CD373D6D79}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cmdNotFound", "cmdNotFound", "{4C0D0746-BE5B-49EE-BD5D-A7811628AE8B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2307,6 +2311,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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2501,6 +2517,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}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@@ -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) | [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) | [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

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension" >
<?include $(sys.CURRENTDIR)\Common.wxi?>
<Fragment>
<DirectoryRef Id="CmdNotFoundInstallFolder" FileSource="$(var.BinDir)modules\$(var.CmdNotFoundProjectName)">
<!-- !Warning! Make sure to change Component Guid if you update the file list -->
<Component Id="Module_CmdNotFound" Win64="yes" Guid="80F648F2-29F6-4685-AED4-04DC1B6EE176">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="Module_CmdNotFound" Value="" KeyPath="yes"/>
</RegistryKey>
<File Source="$(var.BinDir)modules\$(var.CmdNotFoundProjectName)\PowerToys.CmdNotFoundModuleInterface.dll" />
</Component>
</DirectoryRef>
<ComponentGroup Id="CmdNotFoundComponentGroup">
<Component Id="RemoveCmdNotFoundFolder" Guid="C32B0224-2965-4082-BC20-600AD93F872F" Directory="CmdNotFoundInstallFolder" >
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="RemoveCmdNotFoundFolder" Value="" KeyPath="yes"/>
</RegistryKey>
<RemoveFolder Id="RemoveFolderCmdNotFoundInstallFolder" Directory="CmdNotFoundInstallFolder" On="uninstall"/>
</Component>
<ComponentRef Id="Module_CmdNotFound" />
</ComponentGroup>
</Fragment>
</Wix>

View File

@@ -18,6 +18,7 @@
<?define PastePlainProjectName="PastePlain"?>
<?define RegistryPreviewProjectName="RegistryPreview"?>
<?define PeekProjectName="Peek"?>
<?define CmdNotFoundProjectName="CmdNotFound"?>
<?define RepoDir="$(var.ProjectDir)..\..\" ?>
<?if $(var.Platform) = x64?>

View File

@@ -103,6 +103,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
<Compile Include="Awake.wxs" />
<Compile Include="BaseApplications.wxs" />
<Compile Include="CmdNotFound.wxs" />
<Compile Include="ColorPicker.wxs" />
<Compile Include="FileExplorerPreview.wxs" />
<Compile Include="FileLocksmith.wxs" />

View File

@@ -57,6 +57,7 @@
<ComponentGroupRef Id="BaseApplicationsComponentGroup" />
<ComponentGroupRef Id="WinUI3ApplicationsComponentGroup" />
<ComponentGroupRef Id="AwakeComponentGroup" />
<ComponentGroupRef Id="CmdNotFoundComponentGroup" />
<ComponentGroupRef Id="ColorPickerComponentGroup" />
<ComponentGroupRef Id="FileExplorerPreviewComponentGroup" />
<ComponentGroupRef Id="FileLocksmithComponentGroup" />

View File

@@ -12,6 +12,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredAwakeEnabledValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredCmdNotFoundEnabledValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredCmdNotFoundEnabledValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredColorPickerEnabledValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredColorPickerEnabledValue());

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -21,6 +21,7 @@ namespace powertoys_gpo {
// The registry value names for PowerToys utilities enabled and disabled policies.
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";

View File

@@ -42,6 +42,16 @@
<decimal value="0" />
</disabledValue>
</policy>
<policy name="ConfigureEnabledUtilityCmdNotFound" class="Both" displayName="$(string.ConfigureEnabledUtilityCmdNotFound)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityCmdNotFound">
<parentCategory ref="PowerToys" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_71_0" />
<enabledValue>
<decimal value="1" />
</enabledValue>
<disabledValue>
<decimal value="0" />
</disabledValue>
</policy>
<policy name="ConfigureEnabledUtilityColorPicker" class="Both" displayName="$(string.ConfigureEnabledUtilityColorPicker)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityColorPicker">
<parentCategory ref="PowerToys" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_64_0" />

View File

@@ -67,6 +67,7 @@ If this setting is disabled, experimentation is not allowed.
</string>
<string id="ConfigureEnabledUtilityAlwaysOnTop">Always On Top: Configure enabled state</string>
<string id="ConfigureEnabledUtilityAwake">Awake: Configure enabled state</string>
<string id="ConfigureEnabledUtilityCmdNotFound">Command Not Found: Configure enabled state</string>
<string id="ConfigureEnabledUtilityColorPicker">Color Picker: Configure enabled state</string>
<string id="ConfigureEnabledUtilityCropAndLock">Crop And Lock: Configure enabled state</string>
<string id="ConfigureEnabledUtilityFancyZones">FancyZones: Configure enabled state</string>

View File

@@ -0,0 +1,303 @@
using System.Security.Principal;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Management.Automation.Subsystem;
using System.Management.Automation.Subsystem.Feedback;
using System.Runtime.InteropServices;
using Microsoft.Management.Deployment;
namespace wingetprovider
{
// Adapted from https://github.com/microsoft/winget-cli/blob/1898da0b657585d2e6399ef783ecb667eed280f9/src/PowerShell/Microsoft.WinGet.Client/Helpers/ComObjectFactory.cs
public class ComObjectFactory
{
private static readonly Guid PackageManagerClsid = Guid.Parse("C53A4F16-787E-42A4-B304-29EFFB4BF597");
private static readonly Guid FindPackagesOptionsClsid = Guid.Parse("572DED96-9C60-4526-8F92-EE7D91D38C1A");
private static readonly Guid PackageMatchFilterClsid = Guid.Parse("D02C9DAF-99DC-429C-B503-4E504E4AB000");
[System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")]
private static readonly Type PackageManagerType = Type.GetTypeFromCLSID(PackageManagerClsid);
[System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")]
private static readonly Type FindPackagesOptionsType = Type.GetTypeFromCLSID(FindPackagesOptionsClsid);
[System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")]
private static readonly Type PackageMatchFilterType = Type.GetTypeFromCLSID(PackageMatchFilterClsid);
private static readonly Guid PackageManagerIid = Guid.Parse("B375E3B9-F2E0-5C93-87A7-B67497F7E593");
private static readonly Guid FindPackagesOptionsIid = Guid.Parse("A5270EDD-7DA7-57A3-BACE-F2593553561F");
private static readonly Guid PackageMatchFilterIid = Guid.Parse("D981ECA3-4DE5-5AD7-967A-698C7D60FC3B");
public static bool IsAdministrator()
{
WindowsIdentity identity = WindowsIdentity.GetCurrent();
WindowsPrincipal principal = new WindowsPrincipal(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")]
private static T Create<T>(Type type, in Guid iid)
{
object instance = null;
if (IsAdministrator())
{
var hr = WinGetServerManualActivation_CreateInstance(type.GUID, iid, 0, out instance);
if (hr < 0)
{
throw new COMException($"Failed to create instance: {hr}", hr);
}
}
else
{
instance = Activator.CreateInstance(type);
}
IntPtr pointer = Marshal.GetIUnknownForObject(instance);
return WinRT.MarshalInterface<T>.FromAbi(pointer);
}
[DllImport("winrtact.dll", EntryPoint = "WinGetServerManualActivation_CreateInstance", ExactSpelling = true, PreserveSig = true)]
private static extern int WinGetServerManualActivation_CreateInstance(
[In, MarshalAs(UnmanagedType.LPStruct)] Guid clsid,
[In, MarshalAs(UnmanagedType.LPStruct)] Guid iid,
uint flags,
[Out, MarshalAs(UnmanagedType.IUnknown)] out object instance);
[DllImport("winrtact.dll", EntryPoint = "winrtact_Initialize", ExactSpelling = true, PreserveSig = true)]
public static extern void InitializeUndockedRegFreeWinRT();
public static PackageManager CreatePackageManager()
{
return Create<PackageManager>(PackageManagerType, PackageManagerIid);
}
public static FindPackagesOptions CreateFindPackagesOptions()
{
return Create<FindPackagesOptions>(FindPackagesOptionsType, FindPackagesOptionsIid);
}
public static PackageMatchFilter CreatePackageMatchFilter()
{
return Create<PackageMatchFilter>(PackageMatchFilterType, PackageMatchFilterIid);
}
}
public sealed class WinGetComObjects
{
public static WinGetComObjects Singleton { get; } = new WinGetComObjects();
private WinGetComObjects()
{
ComObjectFactory.InitializeUndockedRegFreeWinRT();
packageManager = ComObjectFactory.CreatePackageManager();
findPackagesOptions = ComObjectFactory.CreateFindPackagesOptions();
packageMatchFilter = ComObjectFactory.CreatePackageMatchFilter();
}
public PackageManager packageManager { get; }
public FindPackagesOptions findPackagesOptions { get; }
public PackageMatchFilter packageMatchFilter { get; }
}
public sealed class Init : IModuleAssemblyInitializer, IModuleAssemblyCleanup
{
internal const string id = "e5351aa4-dfde-4d4d-bf0f-1a2f5a37d8d6";
public void OnImport()
{
if (!Platform.IsWindows)
{
return;
}
// Ensure WinGet is installed
using (var rs = RunspaceFactory.CreateRunspace(InitialSessionState.CreateDefault()))
{
rs.Open();
var invocation = rs.SessionStateProxy.InvokeCommand;
var winget = invocation.GetCommand("winget", CommandTypes.Application);
if (winget is null)
{
return;
}
}
SubsystemManager.RegisterSubsystem<IFeedbackProvider, WinGetCommandNotFoundFeedbackPredictor>(WinGetCommandNotFoundFeedbackPredictor.Singleton);
}
public void OnRemove(PSModuleInfo psModuleInfo)
{
SubsystemManager.UnregisterSubsystem<IFeedbackProvider>(new Guid(id));
}
}
public sealed class WinGetCommandNotFoundFeedbackPredictor : IFeedbackProvider
{
private readonly Guid _guid;
private bool _tooManySuggestions;
private static readonly byte _maxSuggestions = 5;
public static WinGetCommandNotFoundFeedbackPredictor Singleton { get; } = new WinGetCommandNotFoundFeedbackPredictor(Init.id);
private WinGetCommandNotFoundFeedbackPredictor(string guid)
{
_guid = new Guid(guid);
_tooManySuggestions = false;
}
public void Dispose()
{
}
public Guid Id => _guid;
public string Name => "Windows Package Manager - WinGet";
public string Description => "Finds missing commands that can be installed via WinGet.";
/// <summary>
/// Gets feedback based on the given commandline and error record.
/// </summary>
public FeedbackItem? GetFeedback(FeedbackContext context, CancellationToken token)
{
var lastError = context.LastError;
if (lastError != null && lastError.FullyQualifiedErrorId == "CommandNotFoundException")
{
var target = (string)lastError.TargetObject;
var pkgList = _FindPackages(target);
if (pkgList.Count == 0)
{
return null;
}
// Build list of suggestions
var suggestionList = new List<string>();
foreach (var pkg in pkgList)
{
suggestionList.Add(String.Format("winget install --id {0}", pkg.Id));
}
// Build footer message
var filterFieldString = WinGetComObjects.Singleton.packageMatchFilter.Field == PackageMatchField.Command ? "command" : "name";
var footerMessage = _tooManySuggestions ?
String.Format("Additional results can be found using \"winget search --{0} {1}\"", filterFieldString, WinGetComObjects.Singleton.packageMatchFilter.Value) :
null;
return new FeedbackItem(
"Try installing this package using winget:",
suggestionList,
footerMessage,
FeedbackDisplayLayout.Portrait
);
}
return null;
}
private void _ApplyPackageMatchFilter(PackageMatchField field, PackageFieldMatchOption matchOption, string query)
{
// Configure filter
WinGetComObjects.Singleton.packageMatchFilter.Field = field;
WinGetComObjects.Singleton.packageMatchFilter.Option = matchOption;
WinGetComObjects.Singleton.packageMatchFilter.Value = query;
// Apply filter
WinGetComObjects.Singleton.findPackagesOptions.ResultLimit = _maxSuggestions + 1u;
WinGetComObjects.Singleton.findPackagesOptions.Filters.Clear();
WinGetComObjects.Singleton.findPackagesOptions.Filters.Add(WinGetComObjects.Singleton.packageMatchFilter);
}
private List<CatalogPackage> _TryGetBestMatchingPackage(IReadOnlyList<MatchResult> matches)
{
var results = new List<CatalogPackage>();
if (matches.Count == 1)
{
// One match --> return the package
results.Add(matches.First().CatalogPackage);
}
else if (matches.Count > 1)
{
// Multiple matches --> display top 5 matches (prioritize best matches first)
var bestExactMatches = new List<CatalogPackage>();
var secondaryMatches = new List<CatalogPackage>();
var tertiaryMatches = new List<CatalogPackage>();
for (int i = 0; i < matches.Count; i++)
{
var match = matches[i];
switch (match.MatchCriteria.Option)
{
case PackageFieldMatchOption.EqualsCaseInsensitive:
case PackageFieldMatchOption.Equals:
bestExactMatches.Add(match.CatalogPackage);
break;
case PackageFieldMatchOption.StartsWithCaseInsensitive:
secondaryMatches.Add(match.CatalogPackage);
break;
case PackageFieldMatchOption.ContainsCaseInsensitive:
tertiaryMatches.Add(match.CatalogPackage);
break;
}
}
// Now return the top _maxSuggestions
while (results.Count < _maxSuggestions)
{
if (bestExactMatches.Count > 0)
{
results.Add(bestExactMatches.First());
bestExactMatches.RemoveAt(0);
}
else if (secondaryMatches.Count > 0)
{
results.Add(secondaryMatches.First());
secondaryMatches.RemoveAt(0);
}
else if (tertiaryMatches.Count > 0)
{
results.Add(tertiaryMatches.First());
tertiaryMatches.RemoveAt(0);
}
else
{
break;
}
}
}
_tooManySuggestions = matches.Count > _maxSuggestions;
return results;
}
// Adapted from WinGet sample documentation: https://github.com/microsoft/winget-cli/blob/master/doc/specs/%23888%20-%20Com%20Api.md#32-search
private List<CatalogPackage> _FindPackages(string query)
{
// Get the package catalog
var catalogRef = WinGetComObjects.Singleton.packageManager.GetPredefinedPackageCatalog(PredefinedPackageCatalog.OpenWindowsCatalog);
var connectResult = catalogRef.Connect();
byte retryCount = 0;
while (connectResult.Status != ConnectResultStatus.Ok && retryCount < 3)
{
connectResult = catalogRef.Connect();
retryCount++;
}
var catalog = connectResult.PackageCatalog;
// Perform the query (search by command)
_ApplyPackageMatchFilter(PackageMatchField.Command, PackageFieldMatchOption.StartsWithCaseInsensitive, query);
var findPackagesResult = catalog.FindPackages(WinGetComObjects.Singleton.findPackagesOptions);
var matches = findPackagesResult.Matches;
var pkgList = _TryGetBestMatchingPackage(matches);
if (pkgList.Count > 0)
{
return pkgList;
}
// No matches found when searching by command,
// let's try again and search by name
_ApplyPackageMatchFilter(PackageMatchField.Name, PackageFieldMatchOption.ContainsCaseInsensitive, query);
// Perform the query (search by name)
findPackagesResult = catalog.FindPackages(WinGetComObjects.Singleton.findPackagesOptions);
matches = findPackagesResult.Matches;
return _TryGetBestMatchingPackage(matches);
}
}
}

View File

@@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
<Platform>AnyCPU</Platform>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDependencyFile>false</GenerateDependencyFile>
<PublishDir>..\bin\WinGetCommandNotFound</PublishDir>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<RuntimeIdentifiers>win-arm64;win-x64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup>
<CsWinRTIncludes>Microsoft.Management.Deployment</CsWinRTIncludes>
<CsWinRTWindowsMetadata>10.0.22621.0</CsWinRTWindowsMetadata>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
</PropertyGroup>
<ItemGroup>
<ReferencePath Include="Microsoft.Management.Deployment.winmd" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<!-- Disable PDB generation for the Release build -->
<DebugSymbols>false</DebugSymbols>
<DebugType>None</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Management.Automation" Version="7.4.0-preview.3">
<ExcludeAssets>contentFiles</ExcludeAssets>
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; compile; build; native; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.0.1" />
<PackageReference Include="Microsoft.WindowsPackageManager.Utils" Version="1.3.8" />
<Content Include="WinGetCommandNotFound.psd1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,10 @@
@{
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 = @('WinGetCommandNotFound.dll')
}

View File

@@ -157,6 +157,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
L"WinUI3Apps/PowerToys.Peek.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"))

View File

@@ -0,0 +1,18 @@
// 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.Text.Json;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class CmdNotFoundProperties
{
public CmdNotFoundProperties()
{
}
public override string ToString()
=> JsonSerializer.Serialize(this);
}
}

View File

@@ -0,0 +1,49 @@
// 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
{
public const string ModuleName = "CmdNotFound";
[JsonPropertyName("properties")]
public CmdNotFoundProperties Properties { get; set; }
public CmdNotFoundSettings()
{
Properties = new CmdNotFoundProperties();
Version = "1";
Name = ModuleName;
}
public virtual void Save(ISettingsUtils settingsUtils)
{
// Save settings to file
var options = new JsonSerializerOptions
{
WriteIndented = true,
};
if (settingsUtils == null)
{
throw new ArgumentNullException(nameof(settingsUtils));
}
settingsUtils.SaveSettings(JsonSerializer.Serialize(this, options), 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;
}
}

View File

@@ -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 void NotifyChange()
{
notifyEnabledChangedAction?.Invoke();

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -9,6 +9,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums
Overview = 0,
AlwaysOnTop,
Awake,
CmdNotFound,
ColorPicker,
CropAndLock,
FancyZones,

View File

@@ -383,6 +383,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);

View File

@@ -94,6 +94,9 @@ namespace Microsoft.PowerToys.Settings.UI
case "Awake":
needToUpdate = generalSettingsConfig.Enabled.Awake != isEnabled;
generalSettingsConfig.Enabled.Awake = isEnabled; break;
case "CmdNotFound":
needToUpdate = generalSettingsConfig.Enabled.CmdNotFound != isEnabled;
generalSettingsConfig.Enabled.CmdNotFound = isEnabled; break;
case "ColorPicker":
needToUpdate = generalSettingsConfig.Enabled.ColorPicker != isEnabled;
generalSettingsConfig.Enabled.ColorPicker = isEnabled; break;

View File

@@ -0,0 +1,44 @@
<Page
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.OobePastePlain"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.PowerToys.Settings.UI.OOBE.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:toolkitcontrols="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl
x:Uid="Oobe_CmdNotFound"
HeroImage="ms-appx:///Assets/Modules/OOBE/CmdNotFound.gif">
<controls:OOBEPageControl.PageContent>
<StackPanel Orientation="Vertical">
<TextBlock
x:Uid="Oobe_HowToUse"
Style="{ThemeResource OobeSubtitleStyle}" />
<controls:ShortcutWithTextLabelControl
x:Name="HotkeyControl"
x:Uid="Oobe_CmdNotFound_HowToUse" />
<StackPanel
Margin="0,24,0,0"
Orientation="Horizontal"
Spacing="12">
<Button
x:Uid="OOBE_Settings"
Click="SettingsLaunchButton_Click" />
<HyperlinkButton
NavigateUri="https://aka.ms/PowerToysOverview_CmdNotFound"
Style="{StaticResource TextButtonStyle}">
<TextBlock
x:Uid="LearnMore_CmdNotFound"
TextWrapping="Wrap" />
</HyperlinkButton>
</StackPanel>
</StackPanel>
</controls:OOBEPageControl.PageContent>
</controls:OOBEPageControl>
</Page>

View File

@@ -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();
}
}
}

View File

@@ -87,6 +87,10 @@
x:Uid="Shell_Awake"
Icon="{ui:BitmapIcon Source=/Assets/Settings/FluentIcons/FluentIconsAwake.png}"
Tag="Awake" />
<NavigationViewItem
x:Uid="Shell_CmdNotFound"
Icon="{ui:BitmapIcon Source=/Assets/FluentIcons/FluentIconsCmdNotFound.png}"
Tag="CmdNotFound" />
<NavigationViewItem
x:Uid="Shell_ColorPicker"
Icon="{ui:BitmapIcon Source=/Assets/Settings/FluentIcons/FluentIconsColorPicker.png}"

View File

@@ -79,6 +79,11 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
ModuleName = "Awake",
IsNew = false,
});
Modules.Insert((int)PowerToysModules.CmdNotFound, new OobePowerToysModule()
{
ModuleName = "CmdNotFound",
IsNew = true,
});
Modules.Insert((int)PowerToysModules.ColorPicker, new OobePowerToysModule()
{
ModuleName = "ColorPicker",
@@ -245,6 +250,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
case "WhatsNew": NavigationFrame.Navigate(typeof(OobeWhatsNew)); break;
case "AlwaysOnTop": NavigationFrame.Navigate(typeof(OobeAlwaysOnTop)); break;
case "Awake": NavigationFrame.Navigate(typeof(OobeAwake)); break;
case "CmdNotFound": NavigationFrame.Navigate(typeof(OobeCmdNotFound)); break;
case "ColorPicker": NavigationFrame.Navigate(typeof(OobeColorPicker)); break;
case "CropAndLock": NavigationFrame.Navigate(typeof(OobeCropAndLock)); break;
case "FancyZones": NavigationFrame.Navigate(typeof(OobeFancyZones)); break;

View File

@@ -0,0 +1,42 @@
<Page
x:Class="Microsoft.PowerToys.Settings.UI.Views.CmdNotFoundPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:labs="using:CommunityToolkit.Labs.WinUI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI.UI"
AutomationProperties.LandmarkType="Main"
mc:Ignorable="d">
<controls:SettingsPageControl
x:Uid="CmdNotFound"
ModuleImageSource="ms-appx:///Assets/Modules/CmdNotFound.png">
<controls:SettingsPageControl.ModuleContent>
<StackPanel ChildrenTransitions="{StaticResource SettingsCardsAnimations}"
Orientation="Vertical" Spacing="2">
<labs:SettingsCard
x:Uid="CmdNotFound_EnableToggleControl_HeaderText"
HeaderIcon="{ui:BitmapIcon Source=/Assets/FluentIcons/FluentIconsCmdNotFound.png}"
IsEnabled="{x:Bind Mode=OneWay, Path=ViewModel.IsEnabledGpoConfigured, Converter={StaticResource BoolNegationConverter}}">
<ToggleSwitch
x:Uid="ToggleSwitch"
IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
</labs:SettingsCard>
<InfoBar
x:Uid="GPO_IsSettingForced"
IsClosable="False"
IsOpen="{x:Bind Mode=OneWay, Path=ViewModel.IsEnabledGpoConfigured}"
IsTabStop="{x:Bind Mode=OneWay, Path=ViewModel.IsEnabledGpoConfigured}"
Severity="Informational" />
</StackPanel>
</controls:SettingsPageControl.ModuleContent>
<controls:SettingsPageControl.PrimaryLinks>
<controls:PageLink
x:Uid="LearnMore_CmdNotFound"
Link="https://aka.ms/PowerToysOverview_CmdNotFound" />
</controls:SettingsPageControl.PrimaryLinks>
</controls:SettingsPageControl>
</Page>

View File

@@ -0,0 +1,33 @@
// 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.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Views
{
public sealed partial class CmdNotFoundPage : Page, IRefreshablePage
{
private CmdNotFoundViewModel ViewModel { get; set; }
public CmdNotFoundPage()
{
var settingsUtils = new SettingsUtils();
ViewModel = new CmdNotFoundViewModel(
settingsUtils,
SettingsRepository<GeneralSettings>.GetInstance(settingsUtils),
SettingsRepository<CmdNotFoundSettings>.GetInstance(settingsUtils),
ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel;
InitializeComponent();
}
public void RefreshEnabledState()
{
ViewModel.RefreshEnabledState();
}
}
}

View File

@@ -101,6 +101,11 @@
helpers:NavHelper.NavigateTo="views:AwakePage"
Icon="{ui:BitmapIcon Source=/Assets/Settings/FluentIcons/FluentIconsAwake.png}" />
<NavigationViewItem
x:Uid="Shell_CmdNotFound"
helpers:NavHelper.NavigateTo="views:CmdNotFoundPage"
Icon="{ui:BitmapIcon Source=/Assets/FluentIcons/FluentIconsCmdNotFound.png}" />
<NavigationViewItem
x:Uid="Shell_ColorPicker"
helpers:NavHelper.NavigateTo="views:ColorPickerPage"

View File

@@ -2312,6 +2312,10 @@ From there, simply click on one of the supported files in the File Explorer and
<value>Learn more about Awake</value>
<comment>Awake is a product name, do not loc</comment>
</data>
<data name="LearnMore_CmdNotFound.Text" xml:space="preserve">
<value>Learn more about Command Not Found</value>
<comment> Command Not Found is the name of the module. </comment>
</data>
<data name="LearnMore_ColorPicker.Text" xml:space="preserve">
<value>Learn more about Color Picker</value>
<comment>Color Picker is a product name, do not loc</comment>
@@ -3462,6 +3466,31 @@ Activate by holding the key for the character you want to add an accent to, then
<data name="Oobe_PastePlain_HowToUse.Text" xml:space="preserve">
<value>to paste your clipboard data as plain text. Note: this will replace the formatted text in your clipboard with the plain text.</value>
</data>
<data name="Shell_CmdNotFound.Content" xml:space="preserve">
<value>Command Not Found</value>
<comment>Product name: Navigation view item name for Command Not Found</comment>
</data>
<data name="CmdNotFound.ModuleDescription" xml:space="preserve">
<value>Command Not Found is a PowerShell module that detects an error thrown by a command and suggests a relevant WinGet package to install, if available.</value>
</data>
<data name="CmdNotFound.ModuleTitle" xml:space="preserve">
<value>Command Not Found</value>
</data>
<data name="CmdNotFound_Cancel" xml:space="preserve">
<value>cancel</value>
</data>
<data name="CmdNotFound_EnableToggleControl_HeaderText.Header" xml:space="preserve">
<value>Enable Command Not Found</value>
</data>
<data name="Oobe_CmdNotFound.Description" xml:space="preserve">
<value>Command Not Found detects an error thrown by a command in PowerShell and suggests a relevant WinGet package to install, if available.</value>
</data>
<data name="Oobe_CmdNotFound.Title" xml:space="preserve">
<value>Command Not Found</value>
</data>
<data name="Oobe_CmdNotFound_HowToUse.Text" xml:space="preserve">
<value>If a command returns an error, the module will suggest a WinGet package that may provide the relevant executable.</value>
</data>
<data name="AllAppsTxt.Text" xml:space="preserve">
<value>All apps</value>
</data>

View File

@@ -0,0 +1,180 @@
// 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.Globalization;
using System.Text.Json;
using System.Timers;
using global::PowerToys.GPOWrapper;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
public class CmdNotFoundViewModel : Observable, IDisposable
{
private bool disposedValue;
// Delay saving of settings in order to avoid calling save multiple times and hitting file in use exception. If there is no other request to save settings in given interval, we proceed to save it, otherwise we schedule saving it after this interval
private const int SaveSettingsDelayInMs = 500;
private GeneralSettings GeneralSettingsConfig { get; set; }
private readonly ISettingsUtils _settingsUtils;
private readonly object _delayedActionLock = new object();
private readonly CmdNotFoundSettings _cmdNotFoundSettings;
private Timer _delayedTimer;
private GpoRuleConfigured _enabledGpoRuleConfiguration;
private bool _enabledStateIsGPOConfigured;
private bool _isEnabled;
private Func<string, int> SendConfigMSG { get; }
public CmdNotFoundViewModel(
ISettingsUtils settingsUtils,
ISettingsRepository<GeneralSettings> settingsRepository,
ISettingsRepository<CmdNotFoundSettings> cmdNotFoundSettingsRepository,
Func<string, int> ipcMSGCallBackFunc)
{
// To obtain the general settings configurations of PowerToys Settings.
if (settingsRepository == null)
{
throw new ArgumentNullException(nameof(settingsRepository));
}
GeneralSettingsConfig = settingsRepository.SettingsConfig;
// To obtain the settings configurations of Fancy zones.
if (settingsRepository == null)
{
throw new ArgumentNullException(nameof(settingsRepository));
}
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
if (cmdNotFoundSettingsRepository == null)
{
throw new ArgumentNullException(nameof(cmdNotFoundSettingsRepository));
}
_cmdNotFoundSettings = cmdNotFoundSettingsRepository.SettingsConfig;
InitializeEnabledValue();
// set the callback functions value to hangle outgoing IPC message.
SendConfigMSG = ipcMSGCallBackFunc;
_delayedTimer = new Timer();
_delayedTimer.Interval = SaveSettingsDelayInMs;
_delayedTimer.Elapsed += DelayedTimer_Tick;
_delayedTimer.AutoReset = false;
}
private void InitializeEnabledValue()
{
_enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredCmdNotFoundEnabledValue();
if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
{
// Get the enabled state from GPO.
_enabledStateIsGPOConfigured = true;
_isEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
}
else
{
_isEnabled = GeneralSettingsConfig.Enabled.CmdNotFound;
}
}
public bool IsEnabled
{
get => _isEnabled;
set
{
if (_enabledStateIsGPOConfigured)
{
// If it's GPO configured, shouldn't be able to change this state.
return;
}
if (_isEnabled != value)
{
_isEnabled = value;
OnPropertyChanged(nameof(IsEnabled));
// Set the status of CmdNotFound in the general settings
GeneralSettingsConfig.Enabled.CmdNotFound = value;
var outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
SendConfigMSG(outgoing.ToString());
}
}
}
public bool IsEnabledGpoConfigured
{
get => _enabledStateIsGPOConfigured;
}
private void ScheduleSavingOfSettings()
{
lock (_delayedActionLock)
{
if (_delayedTimer.Enabled)
{
_delayedTimer.Stop();
}
_delayedTimer.Start();
}
}
private void DelayedTimer_Tick(object sender, EventArgs e)
{
lock (_delayedActionLock)
{
_delayedTimer.Stop();
NotifySettingsChanged();
}
}
private void NotifySettingsChanged()
{
// Using InvariantCulture as this is an IPC message
SendConfigMSG(
string.Format(
CultureInfo.InvariantCulture,
"{{ \"powertoys\": {{ \"{0}\": {1} }} }}",
CmdNotFoundSettings.ModuleName,
JsonSerializer.Serialize(_cmdNotFoundSettings)));
}
public void RefreshEnabledState()
{
InitializeEnabledValue();
OnPropertyChanged(nameof(IsEnabled));
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
_delayedTimer.Dispose();
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -42,6 +42,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
FlyoutMenuItems.Add(new FlyoutMenuItem() { Label = resourceLoader.GetString("Awake/ModuleTitle"), IsEnabled = generalSettingsConfig.Enabled.Awake, Tag = "Awake", Icon = "ms-appx:///Assets/Settings/FluentIcons/FluentIconsAwake.png", EnabledChangedCallback = EnabledChangedOnUI });
}
if ((gpo = GPOWrapper.GetConfiguredCmdNotFoundEnabledValue()) != GpoRuleConfigured.Disabled && gpo != GpoRuleConfigured.Enabled)
{
FlyoutMenuItems.Add(new FlyoutMenuItem() { Label = resourceLoader.GetString("CmdNotFound/ModuleTitle"), IsEnabled = generalSettingsConfig.Enabled.CmdNotFound, Tag = "CmdNotFound", Icon = "ms-appx:///Assets/FluentIcons/FluentIconsCmdNotFound.png", EnabledChangedCallback = EnabledChangedOnUI });
}
if ((gpo = GPOWrapper.GetConfiguredColorPickerEnabledValue()) != GpoRuleConfigured.Disabled && gpo != GpoRuleConfigured.Enabled)
{
FlyoutMenuItems.Add(new FlyoutMenuItem() { Label = resourceLoader.GetString("ColorPicker/ModuleTitle"), IsEnabled = generalSettingsConfig.Enabled.ColorPicker, Tag = "ColorPicker", Icon = "ms-appx:///Assets/Settings/FluentIcons/FluentIconsColorPicker.png", EnabledChangedCallback = EnabledChangedOnUI });
@@ -169,6 +174,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
case "AlwaysOnTop": item.IsEnabled = generalSettingsConfig.Enabled.AlwaysOnTop; break;
case "Awake": item.IsEnabled = generalSettingsConfig.Enabled.Awake; break;
case "CmdNotFound": item.IsEnabled = generalSettingsConfig.Enabled.CmdNotFound; break;
case "ColorPicker": item.IsEnabled = generalSettingsConfig.Enabled.ColorPicker; break;
case "CropAndLock": item.IsEnabled = generalSettingsConfig.Enabled.CropAndLock; break;
case "FancyZones": item.IsEnabled = generalSettingsConfig.Enabled.FancyZones; break;