CmdPal: Add low-level keyboard hook for global hotkey (#38250)
Some checks are pending
Spell checking / Check Spelling (push) Waiting to run
Spell checking / Report (Push) (push) Blocked by required conditions
Spell checking / Report (PR) (push) Blocked by required conditions
Spell checking / Update PR (push) Waiting to run

Adds the ability to make the global CmdPal hotkey a low-level hook. This is needed for `win+space`, `win+r`, et al. 

I've only added this to the root hotkey. Other hotkeys will still use the normal `RegisterHotkey` ones. We can re-evaluate this for 0.2+.



Originally: https://github.com/zadjii-msft/PowerToys/issues/349

Solves: https://github.com/microsoft/PowerToys/issues/38297

---------

Co-authored-by: Mike Griese <migrie@microsoft.com>
This commit is contained in:
Stefan Markovic
2025-04-16 12:36:01 +02:00
committed by GitHub
parent e700f86ace
commit c7789abf04
19 changed files with 667 additions and 31 deletions

View File

@@ -706,6 +706,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RegistryPreview.FuzzTests",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.System", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.System\Microsoft.CmdPal.Ext.System.csproj", "{64B88F02-CD88-4ED8-9624-989A800230F9}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CmdPalKeyboardService", "src\modules\cmdpal\CmdPalKeyboardService\CmdPalKeyboardService.vcxproj", "{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2574,14 +2576,18 @@ Global
{64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|ARM64.Build.0 = Debug|ARM64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x64.ActiveCfg = Debug|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x64.Build.0 = Debug|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x86.ActiveCfg = Debug|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x86.Build.0 = Debug|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|ARM64.ActiveCfg = Release|ARM64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|ARM64.Build.0 = Release|ARM64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x64.ActiveCfg = Release|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x64.Build.0 = Release|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x86.ActiveCfg = Release|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x86.Build.0 = Release|x64
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Debug|ARM64.ActiveCfg = Debug|ARM64
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Debug|ARM64.Build.0 = Debug|ARM64
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Debug|x64.ActiveCfg = Debug|x64
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Debug|x64.Build.0 = Debug|x64
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|ARM64.ActiveCfg = Release|ARM64
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|ARM64.Build.0 = Release|ARM64
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|x64.ActiveCfg = Release|x64
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2852,6 +2858,7 @@ Global
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
{5702B3CC-8575-48D5-83D8-15BB42269CD3} = {929C1324-22E8-4412-A9A8-80E85F3985A5}
{64B88F02-CD88-4ED8-9624-989A800230F9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2} = {3846508C-77EB-4034-A702-F8BB263C4F79}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@@ -0,0 +1,3 @@
EXPORTS
DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE
DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE

View File

@@ -0,0 +1,40 @@
#include <windows.h>
#include "resource.h"
#include "../../../common/version/version.h"
#define APSTUDIO_READONLY_SYMBOLS
#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

View File

@@ -0,0 +1,187 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<CppWinRTRootNamespaceAutoMerge>true</CppWinRTRootNamespaceAutoMerge>
<CppWinRTGenerateWindowsMetadata>true</CppWinRTGenerateWindowsMetadata>
<ProjectGuid>{5f63c743-f6ce-4dba-a200-2b3f8a14e8c2}</ProjectGuid>
<ProjectName>CmdPalKeyboardService</ProjectName>
<RootNamespace>CmdPalKeyboardService</RootNamespace>
<AppxPackage>false</AppxPackage>
</PropertyGroup>
<!-- BEGIN common.build.pre.props -->
<PropertyGroup Label="Configuration">
<EnableHybridCRT>true</EnableHybridCRT>
<UseCrtSDKReferenceStaticWarning Condition="'$(EnableHybridCRT)'=='true'">false</UseCrtSDKReferenceStaticWarning>
</PropertyGroup>
<!-- END common.build.pre.props -->
<!-- BEGIN cppwinrt.build.pre.props -->
<PropertyGroup Label="Globals">
<CppWinRTEnabled>true</CppWinRTEnabled>
<CppWinRTOptimized>true</CppWinRTOptimized>
<DefaultLanguage>en-US</DefaultLanguage>
<MinimumVisualStudioVersion>17.0</MinimumVisualStudioVersion>
<ApplicationTypeRevision>10.0</ApplicationTypeRevision>
</PropertyGroup>
<PropertyGroup>
<MinimalCoreWin>true</MinimalCoreWin>
<AppContainerApplication>true</AppContainerApplication>
<WindowsStoreApp>true</WindowsStoreApp>
<ApplicationType>Windows Store</ApplicationType>
<UseCrtSDKReference Condition="'$(EnableHybridCRT)'=='true'">false</UseCrtSDKReference> <!-- The SDK reference breaks the Hybrid CRT -->
</PropertyGroup>
<PropertyGroup>
<!-- We have to use the Desktop platform for Hybrid CRT to work. -->
<_VC_Target_Library_Platform>Desktop</_VC_Target_Library_Platform>
<_NoWinAPIFamilyApp>true</_NoWinAPIFamilyApp>
</PropertyGroup>
<!-- END cppwinrt.build.pre.props -->
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
<GenerateManifest>false</GenerateManifest>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<UseDebugLibraries>true</UseDebugLibraries>
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="PropertySheet.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<TargetName>CmdPalKeyboardService</TargetName>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<PrecompiledHeaderOutputFile>$(IntDir)pch.pch</PrecompiledHeaderOutputFile>
<WarningLevel>Level4</WarningLevel>
<AdditionalOptions>%(AdditionalOptions) /bigobj</AdditionalOptions>
<PreprocessorDefinitions>_WINRT_DLL;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>../../..;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalUsingDirectories>$(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories)</AdditionalUsingDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateWindowsMetadata>false</GenerateWindowsMetadata>
<ModuleDefinitionFile>CmdPalKeyboardService.def</ModuleDefinitionFile>
<AdditionalDependencies>Shell32.lib;user32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="KeyboardListener.h">
<DependentUpon>KeyboardListener.idl</DependentUpon>
</ClInclude>
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="KeyboardListener.cpp">
<DependentUpon>KeyboardListener.idl</DependentUpon>
</ClCompile>
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
</ItemGroup>
<ItemGroup>
<Midl Include="KeyboardListener.idl" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<None Include="CmdPalKeyboardService.def" />
</ItemGroup>
<ItemGroup>
<None Include="PropertySheet.props" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="CmdPalKeyboardService.rc" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
<!-- BEGIN common.build.post.props -->
<!--
The Hybrid CRT model statically links the runtime and STL and dynamically
links the UCRT instead of the VC++ CRT. The UCRT ships with Windows.
WinAppSDK asserts that this is "supported according to the CRT maintainer."
This must come before Microsoft.Cpp.targets because it manipulates ClCompile.RuntimeLibrary.
-->
<ItemDefinitionGroup Condition="'$(EnableHybridCRT)'=='true' and '$(Configuration)'=='Debug'">
<ClCompile>
<!-- We use MultiThreadedDebug, rather than MultiThreadedDebugDLL, to avoid DLL dependencies on VCRUNTIME140d.dll and MSVCP140d.dll. -->
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrtd.lib</IgnoreSpecificDefaultLibraries>
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrtd.lib</AdditionalOptions>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(EnableHybridCRT)'=='true' and ('$(Configuration)'=='Release' or '$(Configuration)'=='AuditMode')">
<ClCompile>
<!-- We use MultiThreaded, rather than MultiThreadedDLL, to avoid DLL dependencies on VCRUNTIME140.dll and MSVCP140.dll. -->
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrt.lib</IgnoreSpecificDefaultLibraries>
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrt.lib</AdditionalOptions>
</Link>
</ItemDefinitionGroup>
<!-- END common.build.post.props -->
</Project>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Resources">
<UniqueIdentifier>accd3aa8-1ba0-4223-9bbe-0c431709210b</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tga;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
<Filter Include="Generated Files">
<UniqueIdentifier>{926ab91d-31b4-48c3-b9a4-e681349f27f0}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp" />
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<Midl Include="KeyboardListener.idl" />
</ItemGroup>
<ItemGroup>
<None Include="CmdPalKeyboardService.def" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<None Include="PropertySheet.props" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="CmdPalKeyboardService.rc">
<Filter>Resources</Filter>
</ResourceCompile>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,165 @@
#include "pch.h"
#include "KeyboardListener.h"
#include "KeyboardListener.g.cpp"
// #include <common/logger/logger.h>
// #include <common/utils/logger_helper.h>
#include <common/utils/winapi_error.h>
namespace
{
}
namespace winrt::CmdPalKeyboardService::implementation
{
KeyboardListener::KeyboardListener()
{
s_instance = this;
}
void KeyboardListener::Start()
{
#if defined(DISABLE_LOWLEVEL_HOOKS_WHEN_DEBUGGED)
const bool hook_disabled = IsDebuggerPresent();
#else
const bool hook_disabled = false;
#endif
if (!hook_disabled)
{
if (!s_llKeyboardHook)
{
s_llKeyboardHook = SetWindowsHookExW(WH_KEYBOARD_LL, LowLevelKeyboardProc, NULL, NULL);
if (!s_llKeyboardHook)
{
DWORD errorCode = GetLastError();
show_last_error_message(L"SetWindowsHookEx", errorCode, L"CmdPalKeyboardService");
}
}
}
}
void KeyboardListener::Stop()
{
if (s_llKeyboardHook && UnhookWindowsHookEx(s_llKeyboardHook))
{
s_llKeyboardHook = NULL;
}
}
void KeyboardListener::SetHotkeyAction(bool win, bool ctrl, bool shift, bool alt, uint8_t key, hstring const& id)
{
Hotkey hotkey = { .win = win, .ctrl = ctrl, .shift = shift, .alt = alt, .key = key };
std::unique_lock lock{ mutex };
HotkeyDescriptor desc = { .hotkey = hotkey, .id = std::wstring(id) };
hotkeyDescriptors.insert(desc);
}
void KeyboardListener::ClearHotkey(hstring const& id)
{
{
std::unique_lock lock{ mutex };
auto it = hotkeyDescriptors.begin();
while (it != hotkeyDescriptors.end())
{
if (it->id == id)
{
it = hotkeyDescriptors.erase(it);
}
else
{
++it;
}
}
}
}
void KeyboardListener::ClearHotkeys()
{
{
std::unique_lock lock{ mutex };
auto it = hotkeyDescriptors.begin();
while (it != hotkeyDescriptors.end())
{
it = hotkeyDescriptors.erase(it);
}
}
}
void KeyboardListener::SetProcessCommand(ProcessCommand processCommand)
{
m_processCommandCb = [trigger = std::move(processCommand)](hstring const& id) {
trigger(id);
};
}
LRESULT KeyboardListener::DoLowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
const auto& keyPressInfo = *reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
if ((wParam != WM_KEYDOWN) && (wParam != WM_SYSKEYDOWN))
{
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
Hotkey hotkey{
.win = (GetAsyncKeyState(VK_LWIN) & 0x8000) || (GetAsyncKeyState(VK_RWIN) & 0x8000),
.ctrl = static_cast<bool>(GetAsyncKeyState(VK_CONTROL) & 0x8000),
.shift = static_cast<bool>(GetAsyncKeyState(VK_SHIFT) & 0x8000),
.alt = static_cast<bool>(GetAsyncKeyState(VK_MENU) & 0x8000),
.key = static_cast<unsigned char>(keyPressInfo.vkCode)
};
if (hotkey == Hotkey{})
{
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
bool do_action = false;
std::wstring actionId{};
{
// Hold the lock for the shortest possible duration
std::unique_lock lock{ mutex };
HotkeyDescriptor dummy{ .hotkey = hotkey };
auto it = hotkeyDescriptors.find(dummy);
if (it != hotkeyDescriptors.end())
{
do_action = true;
actionId = it->id;
}
}
if (do_action)
{
m_processCommandCb(hstring{ actionId });
// After invoking the hotkey send a dummy key to prevent Start Menu from activating
INPUT dummyEvent[1] = {};
dummyEvent[0].type = INPUT_KEYBOARD;
dummyEvent[0].ki.wVk = 0xFF;
dummyEvent[0].ki.dwFlags = KEYEVENTF_KEYUP;
SendInput(1, dummyEvent, sizeof(INPUT));
// Swallow the key press
return 1;
}
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
LRESULT KeyboardListener::LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (s_instance == nullptr)
{
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
if (nCode < 0)
{
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
return s_instance->DoLowLevelKeyboardProc(nCode, wParam, lParam);
}
}

View File

@@ -0,0 +1,67 @@
#pragma once
#include "KeyboardListener.g.h"
#include <mutex>
#include <spdlog/stopwatch.h>
#include <set>
namespace winrt::CmdPalKeyboardService::implementation
{
struct KeyboardListener : KeyboardListenerT<KeyboardListener>
{
struct Hotkey
{
bool win = false;
bool ctrl = false;
bool shift = false;
bool alt = false;
unsigned char key = 0;
std::strong_ordering operator<=>(const Hotkey&) const = default;
};
struct HotkeyDescriptor
{
Hotkey hotkey;
std::wstring id;
bool operator<(const HotkeyDescriptor& other) const
{
return hotkey < other.hotkey;
};
};
KeyboardListener();
void Start();
void Stop();
void SetHotkeyAction(bool win, bool ctrl, bool shift, bool alt, uint8_t key, hstring const& id);
void ClearHotkey(hstring const& id);
void ClearHotkeys();
void SetProcessCommand(ProcessCommand processCommand);
static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
private:
LRESULT CALLBACK DoLowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
static inline KeyboardListener* s_instance;
HHOOK s_llKeyboardHook = nullptr;
// Max DWORD for key code to disable keys.
const DWORD VK_DISABLED = 0x100;
DWORD vkCodePressed = VK_DISABLED;
std::multiset<HotkeyDescriptor> hotkeyDescriptors;
std::mutex mutex;
std::function<void(hstring const&)> m_processCommandCb;
};
}
namespace winrt::CmdPalKeyboardService::factory_implementation
{
struct KeyboardListener : KeyboardListenerT<KeyboardListener, implementation::KeyboardListener>
{
};
}

View File

@@ -0,0 +1,16 @@
namespace CmdPalKeyboardService
{
[version(1.0), uuid(78ab07cd-e128-4e73-86aa-e48e6b6d01ff)] delegate void ProcessCommand(String id);
[default_interface] runtimeclass KeyboardListener {
KeyboardListener();
void Start();
void Stop();
void SetHotkeyAction(Boolean win, Boolean ctrl, Boolean shift, Boolean alt, UInt8 key, String id);
void ClearHotkey(String id);
void ClearHotkeys();
void SetProcessCommand(ProcessCommand processCommand);
}
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Label="PropertySheets" />
<PropertyGroup Label="UserMacros" />
<!--
To customize common C++/WinRT project properties:
* right-click the project node
* expand the Common Properties item
* select the C++/WinRT property page
For more advanced scenarios, and complete documentation, please see:
https://github.com/Microsoft/cppwinrt/tree/master/nuget
-->
<PropertyGroup />
<ItemDefinitionGroup />
</Project>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
</packages>

View File

@@ -0,0 +1 @@
#include "pch.h"

View File

@@ -0,0 +1,4 @@
#pragma once
#include <Unknwn.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>

View File

@@ -0,0 +1,13 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by PowerToys.MeasureToolCore.rc
//////////////////////////////
// Non-localizable
#define FILE_DESCRIPTION "CmdPalKeyboardService"
#define INTERNAL_NAME "CmdPalKeyboardService"
#define ORIGINAL_FILENAME "CmdPalKeyboardService.dll"
// Non-localizable
//////////////////////////////

View File

@@ -26,6 +26,8 @@ public partial class SettingsModel : ObservableObject
public HotkeySettings? Hotkey { get; set; } = DefaultActivationShortcut;
public bool UseLowLevelGlobalHotkey { get; set; }
public bool ShowAppDetails { get; set; }
public bool HotkeyGoesHome { get; set; }

View File

@@ -27,6 +27,17 @@ public partial class SettingsViewModel : INotifyPropertyChanged
}
}
public bool UseLowLevelGlobalHotkey
{
get => _settings.UseLowLevelGlobalHotkey;
set
{
_settings.UseLowLevelGlobalHotkey = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Hotkey)));
Save();
}
}
public bool ShowAppDetails
{
get => _settings.ShowAppDetails;

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using CmdPalKeyboardService;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CmdPal.Common.Messages;
@@ -52,6 +53,8 @@ public sealed partial class MainWindow : Window,
#pragma warning restore SA1306 // Field names should begin with lower-case letter
#pragma warning restore SA1310 // Field names should not contain underscore
private readonly KeyboardListener _keyboardListener;
// Notification Area ("Tray") icon data
private NOTIFYICONDATAW? _trayIconData;
private DestroyIconSafeHandle? _largeIcon;
@@ -66,6 +69,11 @@ public sealed partial class MainWindow : Window,
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
CommandPaletteHost.SetHostHwnd((ulong)_hwnd.Value);
_keyboardListener = new KeyboardListener();
_keyboardListener.Start();
_keyboardListener.SetProcessCommand(new CmdPalKeyboardService.ProcessCommand(HandleSummon));
// TaskbarCreated is the message that's broadcast when explorer.exe
// restarts. We need to know when that happens to be able to bring our
// notification area icon back
@@ -304,6 +312,8 @@ public sealed partial class MainWindow : Window,
// Workaround by turning it off before shutdown.
App.Current.DebugSettings.FailFastOnErrors = false;
DisposeAcrylic();
_keyboardListener.Stop();
}
private void DisposeAcrylic()
@@ -398,6 +408,8 @@ public sealed partial class MainWindow : Window,
private void UnregisterHotkeys()
{
_keyboardListener.ClearHotkeys();
while (_hotkeys.Count > 0)
{
PInvoke.UnregisterHotKey(_hwnd, _hotkeys.Count - 1);
@@ -412,19 +424,28 @@ public sealed partial class MainWindow : Window,
var globalHotkey = settings.Hotkey;
if (globalHotkey != null)
{
var vk = globalHotkey.Code;
var modifiers =
(globalHotkey.Alt ? HOT_KEY_MODIFIERS.MOD_ALT : 0) |
(globalHotkey.Ctrl ? HOT_KEY_MODIFIERS.MOD_CONTROL : 0) |
(globalHotkey.Shift ? HOT_KEY_MODIFIERS.MOD_SHIFT : 0) |
(globalHotkey.Win ? HOT_KEY_MODIFIERS.MOD_WIN : 0)
;
var success = PInvoke.RegisterHotKey(_hwnd, _hotkeys.Count, modifiers, (uint)vk);
if (success)
if (settings.UseLowLevelGlobalHotkey)
{
_keyboardListener.SetHotkeyAction(globalHotkey.Win, globalHotkey.Ctrl, globalHotkey.Shift, globalHotkey.Alt, (byte)globalHotkey.Code, string.Empty);
_hotkeys.Add(new(globalHotkey, string.Empty));
}
else
{
var vk = globalHotkey.Code;
var modifiers =
(globalHotkey.Alt ? HOT_KEY_MODIFIERS.MOD_ALT : 0) |
(globalHotkey.Ctrl ? HOT_KEY_MODIFIERS.MOD_CONTROL : 0) |
(globalHotkey.Shift ? HOT_KEY_MODIFIERS.MOD_SHIFT : 0) |
(globalHotkey.Win ? HOT_KEY_MODIFIERS.MOD_WIN : 0)
;
var success = PInvoke.RegisterHotKey(_hwnd, _hotkeys.Count, modifiers, (uint)vk);
if (success)
{
_hotkeys.Add(new(globalHotkey, string.Empty));
}
}
}
foreach (var commandHotkey in settings.CommandHotkeys)
@@ -450,6 +471,26 @@ public sealed partial class MainWindow : Window,
}
}
private void HandleSummon(string commandId)
{
var isRootHotkey = string.IsNullOrEmpty(commandId);
PowerToysTelemetry.Log.WriteEvent(new CmdPalHotkeySummoned(isRootHotkey));
// Note to future us: the wParam will have the index of the hotkey we registered.
// We can use that in the future to differentiate the hotkeys we've pressed
// so that we can bind hotkeys to individual commands
if (!this.Visible || !isRootHotkey)
{
Activate();
Summon(commandId);
}
else if (isRootHotkey)
{
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
}
}
private LRESULT HotKeyPrc(
HWND hwnd,
uint uMsg,
@@ -464,22 +505,23 @@ public sealed partial class MainWindow : Window,
if (hotkeyIndex < _hotkeys.Count)
{
var hotkey = _hotkeys[hotkeyIndex];
var isRootHotkey = string.IsNullOrEmpty(hotkey.CommandId);
PowerToysTelemetry.Log.WriteEvent(new CmdPalHotkeySummoned(isRootHotkey));
HandleSummon(hotkey.CommandId);
// Note to future us: the wParam will have the index of the hotkey we registered.
// We can use that in the future to differentiate the hotkeys we've pressed
// so that we can bind hotkeys to individual commands
if (!this.Visible || !isRootHotkey)
{
Activate();
// var isRootHotkey = string.IsNullOrEmpty(hotkey.CommandId);
Summon(hotkey.CommandId);
}
else if (isRootHotkey)
{
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_HIDE);
}
// // Note to future us: the wParam will have the index of the hotkey we registered.
// // We can use that in the future to differentiate the hotkeys we've pressed
// // so that we can bind hotkeys to individual commands
// if (!this.Visible || !isRootHotkey)
// {
// Activate();
// Summon(hotkey.CommandId);
// }
// else if (isRootHotkey)
// {
// PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_HIDE);
// }
}
return (LRESULT)IntPtr.Zero;

View File

@@ -29,7 +29,7 @@
<PropertyGroup>
<!-- This lets us actually reference types from Microsoft.Terminal.UI -->
<CsWinRTIncludes>Microsoft.Terminal.UI</CsWinRTIncludes>
<CsWinRTIncludes>Microsoft.Terminal.UI;CmdPalKeyboardService</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
</PropertyGroup>
@@ -123,6 +123,12 @@
<Private>True</Private>
<CopyLocalSatelliteAssemblies>True</CopyLocalSatelliteAssemblies>
</ProjectReference>
<ProjectReference Include="..\CmdPalKeyboardService\CmdPalKeyboardService.vcxproj">
<ReferenceOutputAssembly>True</ReferenceOutputAssembly>
<Private>True</Private>
<CopyLocalSatelliteAssemblies>True</CopyLocalSatelliteAssemblies>
</ProjectReference>
</ItemGroup>
<ItemGroup>

View File

@@ -36,9 +36,18 @@
<TextBlock x:Uid="ActivationSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="Settings_GeneralPage_ActivationKey_SettingsExpander" HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}">
<controls:SettingsExpander
x:Uid="Settings_GeneralPage_ActivationKey_SettingsExpander"
HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}"
IsExpanded="True">
<ptControls:ShortcutControl HotkeySettings="{x:Bind viewModel.Hotkey, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="Settings_GeneralPage_LowLevelHook_SettingsCard">
<ToggleSwitch IsOn="{x:Bind viewModel.UseLowLevelGlobalHotkey, Mode=TwoWay}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<controls:SettingsCard x:Uid="Settings_GeneralPage_GoHome_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE80F;}">
<ToggleSwitch IsOn="{x:Bind viewModel.HotkeyGoesHome, Mode=TwoWay}" />
</controls:SettingsCard>

View File

@@ -322,6 +322,12 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Settings_GeneralPage_ActivationKey_SettingsExpander.Description" xml:space="preserve">
<value>This key will open the Command Palette.</value>
</data>
<data name="Settings_GeneralPage_LowLevelHook_SettingsCard.Header" xml:space="preserve">
<value>Use low-level keyboard hook</value>
</data>
<data name="Settings_GeneralPage_LowLevelHook_SettingsCard.Description" xml:space="preserve">
<value>Try this if there are issues with the shortcut (Command Palette might not get focus when triggered from an elevated window)</value>
</data>
<data name="Settings_GeneralPage_GoHome_SettingsCard.Header" xml:space="preserve">
<value>Go home when activated</value>
</data>