How to Add Global Hotkeys Using the Centralized Hotkey System in PowerToys
To add global hotkeys in PowerToys, implement the GetHotkeys or GetHotkeyEx method in your module DLL, handle the callback via OnHotkey or OnHotkeyEx, and the runner automatically registers them through the centralized HotkeyManager.
PowerToys uses a unified HotkeyManager located in the tools/module_loader directory to handle global hotkey registration for all modules. Instead of each PowerToy calling the Win32 RegisterHotKey API directly, they expose hotkey descriptors through a standardized interface, allowing the runner to manage conflicts and dispatch events centrally. This guide walks through implementing global hotkeys in the microsoft/PowerToys repository using the legacy and modern APIs.
Understanding the Centralized Hotkey Architecture
The HotkeyManager Component
The HotkeyManager class defined in tools/module_loader/src/HotkeyManager.h acts as the single authority for global hotkeys. It maintains an internal map of registered hotkeys and their associated module callbacks. When the runner loads a module, it invokes RegisterModuleHotkeys, which iterates over the module's declared hotkeys, converts modifier flags using ConvertModifiers, and registers each combination with the Windows RegisterHotKey API.
Module Interface Contracts
All PowerToy modules must implement the PowertoyModuleIface interface defined in src/common/interop/powertoy_module_interface.h. This interface exposes two distinct hotkey APIs:
- Legacy API:
GetHotkeysreturns an array ofHotkeystructs for simple action hotkeys. - Modern API:
GetHotkeyExreturns an optionalHotkeyExstruct for activation/toggle hotkeys.
Implementing Global Hotkeys in Your Module
Choosing Between Legacy and HotkeyEx APIs
Select the API based on your hotkey's purpose:
| API | Use Case | Interface Method |
|---|---|---|
| Legacy | Multiple independent action hotkeys | size_t GetHotkeys(Hotkey* buffer, size_t bufferSize) |
| HotkeyEx | Single activation/toggle hotkey (enable/disable module) | std::optional<HotkeyEx> GetHotkeyEx() |
Declaring Hotkeys with GetHotkeys (Legacy)
Implement GetHotkeys to expose multiple action hotkeys. The method uses a two-call pattern: first with bufferSize == 0 to query the count, then with an allocated buffer to receive the descriptors.
// MyToy.cpp
size_t MyToy::GetHotkeys(PowertoyModuleIface::Hotkey* buffer, size_t bufferSize)
{
if (bufferSize == 0) return 2; // Report total hotkey count
if (bufferSize < 2) return 0; // Buffer too small
// Hotkey 0: Ctrl+Win+Y
buffer[0] = {
.win = true,
.ctrl = true,
.alt = false,
.shift = false,
.key = 0x59 // 'Y' virtual key code
};
// Hotkey 1: Alt+F12
buffer[1] = {
.win = false,
.ctrl = false,
.alt = true,
.shift = false,
.key = VK_F12
};
return 2; // Return count written
}
Declaring Activation Hotkeys with GetHotkeyEx
For modules that require a single toggle hotkey to enable or disable functionality, implement GetHotkeyEx:
std::optional<PowertoyModuleIface::HotkeyEx> MyToy::GetHotkeyEx()
{
PowertoyModuleIface::HotkeyEx hk{};
hk.modifiersMask = MOD_CONTROL | MOD_WIN; // Ctrl+Win
hk.vkCode = 0x4B; // 'K' key
return hk; // Optional contains value
}
Handling Hotkey Events
Implement the corresponding callbacks to respond when the user presses the registered combination:
bool MyToy::OnHotkey(size_t hotkeyId)
{
if (hotkeyId == 0) {
ExecutePrimaryAction();
return true; // true = swallow key, false = propagate
}
if (hotkeyId == 1) {
ExecuteSecondaryAction();
return true;
}
return false;
}
void MyToy::OnHotkeyEx()
{
// Toggle enabled state
if (IsEnabled()) {
Disable();
} else {
Enable();
}
}
Runtime Registration and Dispatch
Automatic Registration via the Runner
When the PowerToys runner loads your module via ModuleLoader (defined in tools/module_loader/src/ModuleLoader.h), it automatically invokes HotkeyManager::RegisterModuleHotkeys. This method:
- Queries your module's
GetHotkeysandGetHotkeyEximplementations - Converts modifier flags using
ConvertModifiersto match Win32MOD_*constants - Calls
RegisterHotKeyfor each valid combination - Stores the mapping between Windows hotkey IDs and your module's callbacks
If registration fails due to a conflict with another application, the manager logs the error via Logger::error and continues loading the module without that specific hotkey.
The WM_HOTKEY Dispatch Flow
When Windows sends a WM_HOTKEY message to the PowerToys runner:
- The message loop forwards the event to
HotkeyManager::HandleHotkey - The manager looks up the internal ID in its registration map
- It invokes the corresponding module's
OnHotkeyorOnHotkeyExmethod - If
OnHotkeyreturnstrue, the manager prevents further propagation of the keystroke
Making Hotkeys Configurable in Settings UI
Implementing IHotkeyConfig
To allow users to customize hotkeys through the Settings interface, create a C# class implementing IHotkeyConfig in your module's UI library:
using Settings.UI.Library.Interfaces;
public class MyToyHotkeyConfig : IHotkeyConfig
{
public string ModuleKey => "MyToy";
public string Hotkey
{
get => Settings.Default.Hotkey;
set => Settings.Default.Hotkey = value;
}
}
Persisting User Preferences
The Settings UI uses HotkeySettingsControlHook (located in src/settings-ui/Settings.UI.Library/HotkeySettingsControlHook.cs) to capture keyboard input and serialize the shortcut string. The HotkeyAccessor helper (src/settings-ui/Settings.UI.Library/Helpers/HotkeyAccessor.cs) reads these values from the module's JSON configuration file when the runner starts, passing the user-defined hotkey back to your module's initialization code.
Complete Implementation Example
Below is a minimal, runnable example implementing a Ctrl+Win+M global hotkey that displays a notification when pressed.
C++ Module Implementation:
#include "powertoy_module_interface.h"
#include <optional>
class MyToy : public PowertoyModuleIface
{
public:
// Return the number of hotkeys we expose
size_t GetHotkeys(PowertoyModuleIface::Hotkey* buffer, size_t bufferSize) override
{
if (bufferSize == 0) return 1;
if (bufferSize < 1) return 0;
// Ctrl+Win+M (0x4D = 'M')
buffer[0] = {
.win = true,
.ctrl = true,
.alt = false,
.shift = false,
.key = 0x4D
};
return 1;
}
// Handle the hotkey press
bool OnHotkey(size_t hotkeyId) override
{
if (hotkeyId == 0) {
ShowActivationNotification();
return true; // Swallow the keystroke
}
return false;
}
// Boilerplate methods omitted for brevity
void Enable() override {}
void Disable() override {}
bool IsEnabled() override { return true; }
private:
void ShowActivationNotification() {
// Implementation to show toast or message
}
};
C# Settings UI Configuration:
using Settings.UI.Library.Interfaces;
public class MyToySettings : IHotkeyConfig
{
public string ModuleKey => "MyToy";
public string Hotkey
{
get => Properties.Settings.Default.ActivationShortcut;
set => Properties.Settings.Default.ActivationShortcut = value;
}
}
Summary
- Centralized Management: PowerToys routes all global hotkeys through
HotkeyManagerintools/module_loader/src/HotkeyManager.cpp, ensuring consistent conflict handling and dispatch. - Implementation Path: Expose hotkeys via
GetHotkeys(multiple actions) orGetHotkeyEx(single toggle), then handle them inOnHotkeyorOnHotkeyEx. - Automatic Registration: The runner calls
RegisterModuleHotkeysautomatically when loading your DLL, converting modifiers and invoking the Win32RegisterHotKeyAPI. - User Configuration: Implement
IHotkeyConfigin C# to expose customizable shortcuts through the Settings UI, usingHotkeySettingsControlHookfor input capture.
Frequently Asked Questions
What is the difference between GetHotkeys and GetHotkeyEx in PowerToys?
GetHotkeys is the legacy API that returns an array of Hotkey structures, allowing a module to expose multiple independent action hotkeys (such as Ctrl+Win+Y for one feature and Alt+F12 for another). GetHotkeyEx is the modern API designed for activation/toggle scenarios, returning a single HotkeyEx structure that the runner treats as the master enable/disable shortcut for the entire module. Choose GetHotkeys for multiple actions and GetHotkeyEx for a single on/off toggle.
How does PowerToys handle hotkey conflicts between modules?
The HotkeyManager in tools/module_loader/src/HotkeyManager.cpp attempts to register each hotkey with the Windows RegisterHotKey API. If the registration fails because another application (or another PowerToys module) has already claimed that specific key combination, the manager logs the error using Logger::error and continues loading the module without that hotkey. The module does not receive the callback for unregistered hotkeys, effectively silencing that shortcut until the conflict is resolved through the Settings UI.
Can users customize global hotkeys without editing code?
Yes, PowerToys provides a Settings UI framework that allows users to rebind hotkeys through the graphical interface. Module developers must implement the IHotkeyConfig interface in their C# settings library (located in src/settings-ui/Settings.UI.Library/Interfaces/IHotkeyConfig.cs). The UI uses HotkeySettingsControlHook (src/settings-ui/Settings.UI.Library/HotkeySettingsControlHook.cs) to capture keyboard input and persist the shortcut string to the module's JSON configuration file. When the runner restarts, HotkeyAccessor reads these values and passes them to your module's initialization logic.
What return value should OnHotkey provide to swallow keystrokes?
The OnHotkey method in your module implementation should return true to indicate that the hotkey press has been handled and should not propagate further to other applications or system handlers. Return false to allow the keystroke to continue through the Windows message chain. This mechanism allows PowerToys modules to act as exclusive handlers for specific shortcuts (such as overlay toggles) while avoiding interference with global system shortcuts when the module is merely monitoring for activation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →