Module Loader tool for rapid testing of modules (#43813)

## Summary of the Pull Request
ModuleLoader tool, a stand-alone Win32 executable for testing of
PowerToy modules without needing branch builds.

sample output from running the tool is below:

.\ModuleLoader.exe .\powertoys.cursorwrap.dll
PowerToys Module Loader v1.0
=============================

Loading module: .\powertoys.cursorwrap.dll
Detected module name: cursorwrap

Loading settings...
Trying settings path:
C:\Users\mikehall\AppData\Local\Microsoft\PowerToys\cursorwrap\settings.json
Settings file loaded (315 characters)
Settings loaded successfully.

Loading module DLL...
Module instance created successfully
Module DLL loaded successfully.
Module key: CursorWrap
Module name: CursorWrap

Applying settings to module...
Settings applied.

Registering module hotkeys...
Module reports 1 legacy hotkey(s)
  Registering hotkey 0: Win+Alt+U - OK
Hotkeys registered: 1

Enabling module...
Module enabled.

=============================
Module is now running!
=============================

Module Status:
  - Name: CursorWrap
  - Key: CursorWrap
  - Enabled: Yes
  - Hotkeys: 1 registered

Registered Hotkeys:
  Win+Alt+U

Press Ctrl+C to exit.
You can press the module's hotkey to toggle its functionality.

Note that this doesn't integrate with Powertoys settings UI - this is
purely to test Powertoys module functionality.

## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

## Detailed Description of the Pull Request / Additional comments
See details above.

## Validation Steps Performed
ModuleLoader tested on Windows 11, Surface Laptop 7 Pro.
This commit is contained in:
Mike Hall
2025-11-26 14:08:34 +00:00
committed by GitHub
parent 2c9a9e9fca
commit 452e0dcf51
13 changed files with 2017 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="*"
name="Microsoft.PowerToys.ModuleLoader"
type="win32"
/>
<description>PowerToys Module Loader - Standalone module testing utility</description>
<!-- Per-Monitor DPI Awareness V2 -->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<!-- Request administrator execution level if needed -->
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<!-- Windows 10+ compatibility -->
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<!-- Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9b}"/>
</application>
</compatibility>
</assembly>

View File

@@ -0,0 +1,205 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}</ProjectGuid>
<RootNamespace>ModuleLoader</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<ProjectName>ModuleLoader</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\</IntDir>
<TargetName>ModuleLoader</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\</IntDir>
<TargetName>ModuleLoader</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\</IntDir>
<TargetName>ModuleLoader</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\</IntDir>
<TargetName>ModuleLoader</TargetName>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<TreatWarningAsError>false</TreatWarningAsError>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
</Link>
<Manifest>
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<TreatWarningAsError>false</TreatWarningAsError>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
</Link>
<Manifest>
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<TreatWarningAsError>false</TreatWarningAsError>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
</Link>
<Manifest>
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<TreatWarningAsError>false</TreatWarningAsError>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
</Link>
<Manifest>
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
</Manifest>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="src\main.cpp" />
<ClCompile Include="src\ModuleLoader.cpp" />
<ClCompile Include="src\SettingsLoader.cpp" />
<ClCompile Include="src\HotkeyManager.cpp" />
<ClCompile Include="src\ConsoleHost.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="src\ModuleLoader.h" />
<ClInclude Include="src\SettingsLoader.h" />
<ClInclude Include="src\HotkeyManager.h" />
<ClInclude Include="src\ConsoleHost.h" />
</ItemGroup>
<ItemGroup>
<None Include="README.md" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="src\main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\ModuleLoader.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\SettingsLoader.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\HotkeyManager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\ConsoleHost.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="src\ModuleLoader.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\SettingsLoader.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\HotkeyManager.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\ConsoleHost.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<None Include="README.md" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,483 @@
# Sharing ModuleLoader and Modules
This guide explains how to share the ModuleLoader tool and PowerToy modules with others for testing purposes.
## Overview
The ModuleLoader is designed to be a **portable, standalone testing tool** that can be shared with module developers and testers. It has minimal dependencies and can work with any compatible PowerToy module DLL.
---
## What You Need to Share
### For Testing a Module (e.g., CursorWrap)
#### **Minimum Package** (Recommended for Quick Testing)
1. **ModuleLoader.exe** - The standalone loader application
- Location: `x64\Debug\ModuleLoader.exe` or `x64\Release\ModuleLoader.exe`
- No additional DLLs required (uses only Windows system libraries)
2. **The Module DLL** - The PowerToy module to test
- Example: `CursorWrap.dll` from `x64\Debug\` or `x64\Release\`
- Location varies by module (see module-specific locations below)
3. **settings.json** - Module configuration (place in same folder as the DLL)
- **NEW**: Settings can be placed alongside the module DLL for portable testing
- Location: Same directory as the module DLL (e.g., `settings.json` next to `CursorWrap.dll`)
- Falls back to: `%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\settings.json` if not found locally
#### **Complete Standalone Package** (For Users Without PowerToys Installed)
1. **ModuleLoader.exe**
2. **Module DLL**
3. **Sample settings.json** - Pre-configured settings file
4. **Installation instructions** - See "Standalone Package Setup" section below
---
### Debug Builds
If you build the module in Debug configuration:
- The module will output debug messages via `OutputDebugString()`
- View these with [DebugView](https://learn.microsoft.com/sysinternals/downloads/debugview) or Visual Studio Output window
- Example: CursorWrap outputs detailed topology and cursor wrapping debug info
---
## Module-Specific File Locations
### CursorWrap
```
Files to share:
- x64\Debug\CursorWrap.dll (or Release)
- %LOCALAPPDATA%\Microsoft\PowerToys\CursorWrap\settings.json
Size: ~100KB
```
### MouseHighlighter
```
Files to share:
- x64\Debug\MouseHighlighter.dll (or Release)
- %LOCALAPPDATA%\Microsoft\PowerToys\MouseHighlighter\settings.json
Size: ~150KB
```
### FindMyMouse
```
Files to share:
- x64\Debug\FindMyMouse.dll (or Release)
- %LOCALAPPDATA%\Microsoft\PowerToys\FindMyMouse\settings.json
Size: ~120KB
```
### MousePointerCrosshairs
```
Files to share:
- x64\Debug\MousePointerCrosshairs.dll (or Release)
- %LOCALAPPDATA%\Microsoft\PowerToys\MousePointerCrosshairs\settings.json
Size: ~140KB
```
### MouseJump
```
Files to share:
- x64\Debug\MouseJump.dll (or Release)
- %LOCALAPPDATA%\Microsoft\PowerToys\MouseJump\settings.json
Note: MouseJump is a UI-based module and may not work fully with ModuleLoader
Size: ~200KB
```
### AlwaysOnTop
```
Files to share:
- x64\Debug\AlwaysOnTop.dll (or Release)
- %LOCALAPPDATA%\Microsoft\PowerToys\AlwaysOnTop\settings.json
Size: ~100KB
```
---
## Dependency Analysis
### ModuleLoader.exe Dependencies
**Windows System Libraries Only** (automatically available on all Windows systems):
- `KERNEL32.dll` - Core Windows API
- `USER32.dll` - User interface functions
- `SHELL32.dll` - Shell functions
- `ole32.dll` - COM library
**No PowerToys dependencies required!** The ModuleLoader is completely standalone.
### Module DLL Dependencies (Typical)
Most PowerToy modules depend on:
- Windows system DLLs (automatically available)
- PowerToys common libraries (if any, they're typically statically linked)
- **Module settings** - Must be present in `%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\`
**Important**: Modules are generally **self-contained** and statically link most dependencies. You typically only need the module DLL itself.
---
## Creating a Standalone Package
### Step 1: Prepare the Files
Create a folder structure like this:
```
ModuleLoaderPackage\
??? ModuleLoader.exe
??? CursorWrap.dll (or other module)
??? settings.json (module settings - placed locally!)
```
**NEW Simplified Structure**: You can now place `settings.json` directly alongside the module DLL! The ModuleLoader will check this location first before looking in the standard PowerToys settings directories.
### Step 2: Extract Settings from Your Machine
```powershell
# Copy settings from your development machine
$moduleName = "CursorWrap" # Change as needed
$settingsPath = "$env:LOCALAPPDATA\Microsoft\PowerToys\$moduleName\settings.json"
Copy-Item $settingsPath ".\settings\$moduleName\settings.json"
```
### Step 3: Create Installation Instructions (README.txt)
```text
PowerToys Module Testing Package
=================================
This package contains the ModuleLoader tool for testing PowerToy modules.
Contents:
- ModuleLoader.exe : Standalone module loader
- modules\*.dll : PowerToy module(s) to test
- settings\*\*.json : Module configuration files
Setup (First Time):
-------------------
1. Create settings directory:
%LOCALAPPDATA%\Microsoft\PowerToys\
2. Copy settings:
Copy the entire "settings\<ModuleName>" folder to:
%LOCALAPPDATA%\Microsoft\PowerToys\
Example for CursorWrap:
Copy "settings\CursorWrap" to:
%LOCALAPPDATA%\Microsoft\PowerToys\CursorWrap\
Usage:
------
ModuleLoader.exe modules\CursorWrap.dll
The tool will:
- Load the module DLL
- Read settings from %LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\
- Register hotkeys
- Enable the module
Press Ctrl+C to exit.
Press the module's hotkey to toggle functionality.
Requirements:
-------------
- Windows 10 1803 or later
- No PowerToys installation required!
Troubleshooting:
----------------
If you see "Settings file not found":
1. Make sure you copied the settings folder correctly
2. Check that the path is:
%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\settings.json
3. You can also run PowerToys once to generate default settings
Debug Logs:
-----------
Module logs are written to:
%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\Logs\
For debug builds, use DebugView to see real-time output.
```
---
## Quick Distribution Methods
### Method 1: ZIP Archive
```powershell
# Create a complete package
$moduleName = "CursorWrap"
$packageName = "ModuleLoader-$moduleName-Package"
# Collect files
New-Item $packageName -ItemType Directory
Copy-Item "x64\Debug\ModuleLoader.exe" "$packageName\"
New-Item "$packageName\modules" -ItemType Directory
Copy-Item "x64\Debug\$moduleName.dll" "$packageName\modules\"
New-Item "$packageName\settings\$moduleName" -ItemType Directory -Force
Copy-Item "$env:LOCALAPPDATA\Microsoft\PowerToys\$moduleName\settings.json" "$packageName\settings\$moduleName\"
# Create README
@"
See README in the tools\module_loader folder for instructions
"@ | Out-File "$packageName\README.txt"
# Zip it
Compress-Archive -Path $packageName -DestinationPath "$packageName.zip"
```
### Method 2: Direct Share (Advanced Users)
For developers who already have PowerToys installed:
```powershell
# Just share the executables
Copy-Item "x64\Debug\ModuleLoader.exe" "\\ShareLocation\"
Copy-Item "x64\Debug\CursorWrap.dll" "\\ShareLocation\"
```
They can run: `ModuleLoader.exe CursorWrap.dll`
(Settings will be loaded from their existing PowerToys installation)
---
## Platform-Specific Notes
### x64 vs ARM64
**Important**: Match architectures!
- `x64\Debug\ModuleLoader.exe` ? Only works with `x64` module DLLs
- `ARM64\Debug\ModuleLoader.exe` ? Only works with `ARM64` module DLLs
**Distribution Tip**: Provide both architectures if targeting multiple platforms:
```
ModuleLoaderPackage\
??? x64\
? ??? ModuleLoader.exe
? ??? modules\CursorWrap.dll
??? ARM64\
? ??? ModuleLoader.exe
? ??? modules\CursorWrap.dll
??? settings\...
```
### Debug vs Release
**Debug builds**:
- Larger file size
- Include debug symbols
- Verbose logging via `OutputDebugString()`
- Recommended for testing/development
**Release builds**:
- Smaller file size
- Optimized performance
- Minimal logging
- Recommended for end-user testing
---
## Testing Checklist
Before sharing a module package:
- [ ] ModuleLoader.exe is included
- [ ] Module DLL is included (matching architecture)
- [ ] Sample settings.json is included
- [ ] README/instructions are included
- [ ] Tested on a clean machine (no PowerToys installed)
- [ ] Verified hotkeys work
- [ ] Verified Ctrl+C exits cleanly
- [ ] Confirmed settings path in documentation
---
## Advanced: Portable Package Script
Here's a complete PowerShell script to create a fully portable package:
```powershell
param(
[Parameter(Mandatory=$true)]
[string]$ModuleName,
[ValidateSet("Debug", "Release")]
[string]$Configuration = "Debug",
[ValidateSet("x64", "ARM64")]
[string]$Platform = "x64"
)
$packageName = "ModuleLoader-$ModuleName-$Platform-$Configuration"
$packagePath = ".\$packageName"
Write-Host "Creating portable package: $packageName" -ForegroundColor Green
# Create structure
New-Item $packagePath -ItemType Directory -Force | Out-Null
New-Item "$packagePath\modules" -ItemType Directory -Force | Out-Null
New-Item "$packagePath\settings\$ModuleName" -ItemType Directory -Force | Out-Null
# Copy ModuleLoader
$loaderPath = "$Platform\$Configuration\ModuleLoader.exe"
if (Test-Path $loaderPath) {
Copy-Item $loaderPath "$packagePath\"
Write-Host "? Copied ModuleLoader.exe" -ForegroundColor Green
} else {
Write-Host "? ModuleLoader.exe not found at $loaderPath" -ForegroundColor Red
exit 1
}
# Copy Module DLL
$modulePath = "$Platform\$Configuration\$ModuleName.dll"
if (Test-Path $modulePath) {
Copy-Item $modulePath "$packagePath\modules\"
Write-Host "? Copied $ModuleName.dll" -ForegroundColor Green
} else {
Write-Host "? $ModuleName.dll not found at $modulePath" -ForegroundColor Red
exit 1
}
# Copy Settings
$settingsPath = "$env:LOCALAPPDATA\Microsoft\PowerToys\$ModuleName\settings.json"
if (Test-Path $settingsPath) {
Copy-Item $settingsPath "$packagePath\settings\$ModuleName\"
Write-Host "? Copied settings.json" -ForegroundColor Green
} else {
Write-Host "? Settings not found at $settingsPath - creating placeholder" -ForegroundColor Yellow
@"
{
"name": "$ModuleName",
"version": "1.0"
}
"@ | Out-File "$packagePath\settings\$ModuleName\settings.json"
}
# Create README
@"
PowerToys $ModuleName Testing Package
======================================
Configuration: $Configuration
Platform: $Platform
Setup Instructions:
-------------------
1. Copy the 'settings\$ModuleName' folder to:
%LOCALAPPDATA%\Microsoft\PowerToys\
2. Run:
ModuleLoader.exe modules\$ModuleName.dll
3. Press Ctrl+C to exit
Logs are written to:
%LOCALAPPDATA%\Microsoft\PowerToys\$ModuleName\Logs\
For more information, see:
https://github.com/microsoft/PowerToys/tree/main/tools/module_loader
"@ | Out-File "$packagePath\README.txt"
# Create ZIP
$zipPath = "$packageName.zip"
Compress-Archive -Path $packagePath -DestinationPath $zipPath -Force
Write-Host "? Created $zipPath" -ForegroundColor Green
# Show summary
Write-Host "`nPackage Contents:" -ForegroundColor Cyan
Get-ChildItem $packagePath -Recurse | ForEach-Object {
Write-Host " $($_.FullName.Replace($packagePath, ''))"
}
Write-Host "`nPackage ready: $zipPath" -ForegroundColor Green
Write-Host "Size: $([math]::Round((Get-Item $zipPath).Length / 1KB, 2)) KB"
```
**Usage**:
```powershell
.\CreateModulePackage.ps1 -ModuleName "CursorWrap" -Configuration Release -Platform x64
```
---
## FAQ
### Q: Can I share just ModuleLoader.exe and the module DLL?
**A**: Yes, but the recipient must have PowerToys installed (or manually create the settings file).
### Q: Does the tester need PowerToys installed?
**A**: No, if you provide the complete package with settings. ModuleLoader is fully standalone.
### Q: What if settings.json doesn't exist?
**A**: ModuleLoader will show an error. Either:
1. Run PowerToys once with the module enabled to generate settings
2. Manually create a minimal settings.json file
3. Include a sample settings.json in your package
### Q: Can I test modules on a virtual machine?
**A**: Yes! This is a great use case. Just copy the package to the VM - no PowerToys installation needed.
### Q: Do I need to include PDB files?
**A**: Only for debugging. For normal testing, just the EXE and DLL are sufficient.
### Q: Can I distribute this to end users?
**A**: ModuleLoader is a **development/testing tool**, not intended for end-user distribution. For production use, direct users to install PowerToys.
---
## Security Considerations
When sharing module DLLs:
1. **Verify Source**: Only share modules you built from trusted source code
2. **Scan for Malware**: Run antivirus scans on the package before sharing
3. **HTTPS Only**: Use secure channels (HTTPS, OneDrive, SharePoint) for distribution
4. **Hash Verification**: Consider providing SHA256 hashes for file integrity:
```powershell
Get-FileHash ModuleLoader.exe -Algorithm SHA256
Get-FileHash modules\CursorWrap.dll -Algorithm SHA256
```
---
## Example Package (CursorWrap)
Here's what a complete CursorWrap testing package looks like:
```
ModuleLoader-CursorWrap-x64-Debug.zip (220 KB)
?
??? ModuleLoader-CursorWrap-x64-Debug\
??? ModuleLoader.exe (160 KB)
??? README.txt (2 KB)
??? modules\
? ??? CursorWrap.dll (55 KB)
??? settings\
??? CursorWrap\
??? settings.json (3 KB)
```
**Total package size**: ~220 KB (compressed)
---
## Support
For issues with ModuleLoader, see:
- [ModuleLoader README](./README.md)
- [PowerToys Documentation](https://aka.ms/PowerToysOverview)
- [PowerToys GitHub Issues](https://github.com/microsoft/PowerToys/issues)
---
## License
ModuleLoader is part of PowerToys and is licensed under the MIT License.
See the LICENSE file in the PowerToys repository root for details.

View File

@@ -0,0 +1,80 @@
// 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.
#include "ConsoleHost.h"
#include <iostream>
bool ConsoleHost::s_exitRequested = false;
ConsoleHost::ConsoleHost(ModuleLoader& moduleLoader, HotkeyManager& hotkeyManager)
: m_moduleLoader(moduleLoader)
, m_hotkeyManager(hotkeyManager)
{
}
ConsoleHost::~ConsoleHost()
{
}
BOOL WINAPI ConsoleHost::ConsoleCtrlHandler(DWORD ctrlType)
{
switch (ctrlType)
{
case CTRL_C_EVENT:
case CTRL_BREAK_EVENT:
case CTRL_CLOSE_EVENT:
std::wcout << L"\nCtrl+C received, shutting down...\n";
s_exitRequested = true;
// Post a quit message to break the message loop
PostQuitMessage(0);
return TRUE;
default:
return FALSE;
}
}
void ConsoleHost::Run()
{
// Install console control handler
if (!SetConsoleCtrlHandler(ConsoleCtrlHandler, TRUE))
{
std::wcerr << L"Warning: Failed to set console control handler\n";
}
s_exitRequested = false;
// Message loop
MSG msg;
while (!s_exitRequested)
{
// Wait for a message with a timeout so we can check s_exitRequested
DWORD result = MsgWaitForMultipleObjects(0, nullptr, FALSE, 100, QS_ALLINPUT);
if (result == WAIT_OBJECT_0)
{
// Process all pending messages
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT)
{
s_exitRequested = true;
break;
}
if (msg.message == WM_HOTKEY)
{
m_hotkeyManager.HandleHotkey(static_cast<int>(msg.wParam), m_moduleLoader);
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
// Remove console control handler
SetConsoleCtrlHandler(ConsoleCtrlHandler, FALSE);
}

View File

@@ -0,0 +1,38 @@
// 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.
#pragma once
#include <Windows.h>
#include "ModuleLoader.h"
#include "HotkeyManager.h"
/// <summary>
/// Console host that runs the message loop and handles Ctrl+C
/// </summary>
class ConsoleHost
{
public:
ConsoleHost(ModuleLoader& moduleLoader, HotkeyManager& hotkeyManager);
~ConsoleHost();
// Prevent copying
ConsoleHost(const ConsoleHost&) = delete;
ConsoleHost& operator=(const ConsoleHost&) = delete;
/// <summary>
/// Run the message loop until Ctrl+C is pressed
/// </summary>
void Run();
private:
ModuleLoader& m_moduleLoader;
HotkeyManager& m_hotkeyManager;
static bool s_exitRequested;
/// <summary>
/// Console control handler (for Ctrl+C)
/// </summary>
static BOOL WINAPI ConsoleCtrlHandler(DWORD ctrlType);
};

View File

@@ -0,0 +1,279 @@
// 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.
#include "HotkeyManager.h"
#include <iostream>
#include <sstream>
HotkeyManager::HotkeyManager()
: m_nextHotkeyId(1) // Start from 1
, m_hotkeyExRegistered(false)
, m_hotkeyExId(0)
{
}
HotkeyManager::~HotkeyManager()
{
UnregisterAll();
}
UINT HotkeyManager::ConvertModifiers(bool win, bool ctrl, bool alt, bool shift) const
{
UINT modifiers = MOD_NOREPEAT; // Prevent repeat events
if (win) modifiers |= MOD_WIN;
if (ctrl) modifiers |= MOD_CONTROL;
if (alt) modifiers |= MOD_ALT;
if (shift) modifiers |= MOD_SHIFT;
return modifiers;
}
bool HotkeyManager::RegisterModuleHotkeys(ModuleLoader& moduleLoader)
{
if (!moduleLoader.IsLoaded())
{
std::wcerr << L"Error: Module not loaded\n";
return false;
}
bool anyRegistered = false;
// First, try the newer GetHotkeyEx() API
auto hotkeyEx = moduleLoader.GetHotkeyEx();
if (hotkeyEx.has_value())
{
std::wcout << L"Module has HotkeyEx activation hotkey\n";
UINT modifiers = hotkeyEx->modifiersMask | MOD_NOREPEAT;
UINT vkCode = hotkeyEx->vkCode;
if (vkCode != 0)
{
int hotkeyId = m_nextHotkeyId++;
std::wcout << L" Registering HotkeyEx: ";
std::wcout << ModifiersToString(modifiers) << L"+" << VKeyToString(vkCode);
if (RegisterHotKey(nullptr, hotkeyId, modifiers, vkCode))
{
m_hotkeyExRegistered = true;
m_hotkeyExId = hotkeyId;
std::wcout << L" - OK (Activation/Toggle)\n";
anyRegistered = true;
}
else
{
DWORD error = GetLastError();
std::wcout << L" - FAILED (Error: " << error << L")\n";
if (error == ERROR_HOTKEY_ALREADY_REGISTERED)
{
std::wcout << L" (Hotkey is already registered by another application)\n";
}
}
}
}
// Also check the legacy get_hotkeys() API
size_t hotkeyCount = moduleLoader.GetHotkeys(nullptr, 0);
if (hotkeyCount > 0)
{
std::wcout << L"Module reports " << hotkeyCount << L" legacy hotkey(s)\n";
// Allocate buffer and get the hotkeys
std::vector<PowertoyModuleIface::Hotkey> hotkeys(hotkeyCount);
size_t actualCount = moduleLoader.GetHotkeys(hotkeys.data(), hotkeyCount);
// Register each hotkey
for (size_t i = 0; i < actualCount; i++)
{
const auto& hotkey = hotkeys[i];
UINT modifiers = ConvertModifiers(hotkey.win, hotkey.ctrl, hotkey.alt, hotkey.shift);
UINT vkCode = hotkey.key;
if (vkCode == 0)
{
std::wcout << L" Skipping hotkey " << i << L" (no key code)\n";
continue;
}
int hotkeyId = m_nextHotkeyId++;
std::wcout << L" Registering hotkey " << i << L": ";
std::wcout << ModifiersToString(modifiers) << L"+" << VKeyToString(vkCode);
if (RegisterHotKey(nullptr, hotkeyId, modifiers, vkCode))
{
HotkeyInfo info;
info.id = hotkeyId;
info.moduleHotkeyId = i;
info.modifiers = modifiers;
info.vkCode = vkCode;
info.description = ModifiersToString(modifiers) + L"+" + VKeyToString(vkCode);
m_registeredHotkeys.push_back(info);
std::wcout << L" - OK\n";
anyRegistered = true;
}
else
{
DWORD error = GetLastError();
std::wcout << L" - FAILED (Error: " << error << L")\n";
if (error == ERROR_HOTKEY_ALREADY_REGISTERED)
{
std::wcout << L" (Hotkey is already registered by another application)\n";
}
}
}
}
if (!anyRegistered && hotkeyCount == 0 && !hotkeyEx.has_value())
{
std::wcout << L"Module has no hotkeys\n";
}
return anyRegistered;
}
void HotkeyManager::UnregisterAll()
{
for (const auto& hotkey : m_registeredHotkeys)
{
UnregisterHotKey(nullptr, hotkey.id);
}
m_registeredHotkeys.clear();
if (m_hotkeyExRegistered)
{
UnregisterHotKey(nullptr, m_hotkeyExId);
m_hotkeyExRegistered = false;
m_hotkeyExId = 0;
}
}
bool HotkeyManager::HandleHotkey(int hotkeyId, ModuleLoader& moduleLoader)
{
// Check if it's the HotkeyEx activation hotkey
if (m_hotkeyExRegistered && hotkeyId == m_hotkeyExId)
{
std::wcout << L"\nActivation hotkey triggered (HotkeyEx)\n";
moduleLoader.OnHotkeyEx();
std::wcout << L"Module toggled via activation hotkey\n";
std::wcout << L"Module enabled: " << (moduleLoader.IsEnabled() ? L"Yes" : L"No") << L"\n\n";
return true;
}
// Check legacy hotkeys
for (const auto& hotkey : m_registeredHotkeys)
{
if (hotkey.id == hotkeyId)
{
std::wcout << L"\nHotkey triggered: " << hotkey.description << L"\n";
bool result = moduleLoader.OnHotkey(hotkey.moduleHotkeyId);
std::wcout << L"Module handled hotkey: " << (result ? L"Swallowed" : L"Not swallowed") << L"\n";
std::wcout << L"Module enabled: " << (moduleLoader.IsEnabled() ? L"Yes" : L"No") << L"\n\n";
return true;
}
}
return false;
}
void HotkeyManager::PrintHotkeys() const
{
for (const auto& hotkey : m_registeredHotkeys)
{
std::wcout << L" " << hotkey.description << L"\n";
}
}
std::wstring HotkeyManager::ModifiersToString(UINT modifiers) const
{
std::wstringstream ss;
bool first = true;
if (modifiers & MOD_WIN)
{
if (!first) ss << L"+";
ss << L"Win";
first = false;
}
if (modifiers & MOD_CONTROL)
{
if (!first) ss << L"+";
ss << L"Ctrl";
first = false;
}
if (modifiers & MOD_ALT)
{
if (!first) ss << L"+";
ss << L"Alt";
first = false;
}
if (modifiers & MOD_SHIFT)
{
if (!first) ss << L"+";
ss << L"Shift";
first = false;
}
return ss.str();
}
std::wstring HotkeyManager::VKeyToString(UINT vkCode) const
{
// Handle special keys
switch (vkCode)
{
case VK_SPACE: return L"Space";
case VK_RETURN: return L"Enter";
case VK_ESCAPE: return L"Esc";
case VK_TAB: return L"Tab";
case VK_BACK: return L"Backspace";
case VK_DELETE: return L"Del";
case VK_INSERT: return L"Ins";
case VK_HOME: return L"Home";
case VK_END: return L"End";
case VK_PRIOR: return L"PgUp";
case VK_NEXT: return L"PgDn";
case VK_LEFT: return L"Left";
case VK_RIGHT: return L"Right";
case VK_UP: return L"Up";
case VK_DOWN: return L"Down";
case VK_F1: return L"F1";
case VK_F2: return L"F2";
case VK_F3: return L"F3";
case VK_F4: return L"F4";
case VK_F5: return L"F5";
case VK_F6: return L"F6";
case VK_F7: return L"F7";
case VK_F8: return L"F8";
case VK_F9: return L"F9";
case VK_F10: return L"F10";
case VK_F11: return L"F11";
case VK_F12: return L"F12";
}
// For alphanumeric keys, use MapVirtualKey
wchar_t keyName[256];
UINT scanCode = MapVirtualKeyW(vkCode, MAPVK_VK_TO_VSC);
if (GetKeyNameTextW(scanCode << 16, keyName, 256) > 0)
{
return keyName;
}
// Fallback to hex code
std::wstringstream ss;
ss << L"0x" << std::hex << vkCode;
return ss.str();
}

View File

@@ -0,0 +1,86 @@
// 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.
#pragma once
#include <Windows.h>
#include <string>
#include <vector>
#include <map>
#include "ModuleLoader.h"
/// <summary>
/// Manages hotkey registration using RegisterHotKey API
/// </summary>
class HotkeyManager
{
public:
HotkeyManager();
~HotkeyManager();
// Prevent copying
HotkeyManager(const HotkeyManager&) = delete;
HotkeyManager& operator=(const HotkeyManager&) = delete;
/// <summary>
/// Register all hotkeys from a module
/// </summary>
/// <param name="moduleLoader">Module to get hotkeys from</param>
/// <returns>True if at least one hotkey was registered</returns>
bool RegisterModuleHotkeys(ModuleLoader& moduleLoader);
/// <summary>
/// Unregister all hotkeys
/// </summary>
void UnregisterAll();
/// <summary>
/// Handle a WM_HOTKEY message
/// </summary>
/// <param name="hotkeyId">ID from the WM_HOTKEY message</param>
/// <param name="moduleLoader">Module to trigger the hotkey on</param>
/// <returns>True if the hotkey was handled</returns>
bool HandleHotkey(int hotkeyId, ModuleLoader& moduleLoader);
/// <summary>
/// Get the number of registered hotkeys
/// </summary>
/// <returns>Number of registered hotkeys</returns>
size_t GetRegisteredCount() const { return m_registeredHotkeys.size() + (m_hotkeyExRegistered ? 1 : 0); }
/// <summary>
/// Print registered hotkeys to console
/// </summary>
void PrintHotkeys() const;
private:
struct HotkeyInfo
{
int id = 0;
size_t moduleHotkeyId = 0;
UINT modifiers = 0;
UINT vkCode = 0;
std::wstring description;
};
std::vector<HotkeyInfo> m_registeredHotkeys;
int m_nextHotkeyId;
bool m_hotkeyExRegistered;
int m_hotkeyExId;
/// <summary>
/// Convert modifier bools to RegisterHotKey modifiers
/// </summary>
UINT ConvertModifiers(bool win, bool ctrl, bool alt, bool shift) const;
/// <summary>
/// Get a string representation of modifiers
/// </summary>
std::wstring ModifiersToString(UINT modifiers) const;
/// <summary>
/// Get a string representation of a virtual key code
/// </summary>
std::wstring VKeyToString(UINT vkCode) const;
};

View File

@@ -0,0 +1,183 @@
// 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.
#include "ModuleLoader.h"
#include <iostream>
#include <stdexcept>
ModuleLoader::ModuleLoader()
: m_hModule(nullptr)
, m_module(nullptr)
{
}
ModuleLoader::~ModuleLoader()
{
if (m_module)
{
try
{
m_module->destroy();
}
catch (...)
{
// Ignore exceptions during cleanup
}
m_module = nullptr;
}
if (m_hModule)
{
FreeLibrary(m_hModule);
m_hModule = nullptr;
}
}
bool ModuleLoader::Load(const std::wstring& dllPath)
{
if (m_hModule || m_module)
{
std::wcerr << L"Error: Module already loaded\n";
return false;
}
m_dllPath = dllPath;
// Load the DLL
m_hModule = LoadLibraryW(dllPath.c_str());
if (!m_hModule)
{
DWORD error = GetLastError();
std::wcerr << L"Error: Failed to load DLL. Error code: " << error << L"\n";
return false;
}
// Get the powertoy_create function
using powertoy_create_func = PowertoyModuleIface* (*)();
auto create_func = reinterpret_cast<powertoy_create_func>(
GetProcAddress(m_hModule, "powertoy_create"));
if (!create_func)
{
std::wcerr << L"Error: DLL does not export 'powertoy_create' function\n";
FreeLibrary(m_hModule);
m_hModule = nullptr;
return false;
}
// Create the module instance
m_module = create_func();
if (!m_module)
{
std::wcerr << L"Error: powertoy_create() returned nullptr\n";
FreeLibrary(m_hModule);
m_hModule = nullptr;
return false;
}
std::wcout << L"Module instance created successfully\n";
return true;
}
void ModuleLoader::Enable()
{
if (!m_module)
{
throw std::runtime_error("Module not loaded");
}
m_module->enable();
}
void ModuleLoader::Disable()
{
if (!m_module)
{
return;
}
m_module->disable();
}
bool ModuleLoader::IsEnabled() const
{
if (!m_module)
{
return false;
}
return m_module->is_enabled();
}
void ModuleLoader::SetConfig(const std::wstring& configJson)
{
if (!m_module)
{
throw std::runtime_error("Module not loaded");
}
m_module->set_config(configJson.c_str());
}
std::wstring ModuleLoader::GetModuleName() const
{
if (!m_module)
{
return L"<not loaded>";
}
const wchar_t* name = m_module->get_name();
return name ? name : L"<unknown>";
}
std::wstring ModuleLoader::GetModuleKey() const
{
if (!m_module)
{
return L"<not loaded>";
}
const wchar_t* key = m_module->get_key();
return key ? key : L"<unknown>";
}
size_t ModuleLoader::GetHotkeys(PowertoyModuleIface::Hotkey* buffer, size_t bufferSize)
{
if (!m_module)
{
return 0;
}
return m_module->get_hotkeys(buffer, bufferSize);
}
bool ModuleLoader::OnHotkey(size_t hotkeyId)
{
if (!m_module)
{
return false;
}
return m_module->on_hotkey(hotkeyId);
}
std::optional<PowertoyModuleIface::HotkeyEx> ModuleLoader::GetHotkeyEx()
{
if (!m_module)
{
return std::nullopt;
}
return m_module->GetHotkeyEx();
}
void ModuleLoader::OnHotkeyEx()
{
if (!m_module)
{
return;
}
m_module->OnHotkeyEx();
}

View File

@@ -0,0 +1,102 @@
// 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.
#pragma once
#include <Windows.h>
#include <string>
#include <vector>
#include <powertoy_module_interface.h>
/// <summary>
/// Wrapper class for loading and managing a PowerToy module DLL
/// </summary>
class ModuleLoader
{
public:
ModuleLoader();
~ModuleLoader();
// Prevent copying
ModuleLoader(const ModuleLoader&) = delete;
ModuleLoader& operator=(const ModuleLoader&) = delete;
/// <summary>
/// Load a PowerToy module DLL
/// </summary>
/// <param name="dllPath">Path to the module DLL</param>
/// <returns>True if successful, false otherwise</returns>
bool Load(const std::wstring& dllPath);
/// <summary>
/// Enable the loaded module
/// </summary>
void Enable();
/// <summary>
/// Disable the loaded module
/// </summary>
void Disable();
/// <summary>
/// Check if the module is enabled
/// </summary>
/// <returns>True if enabled, false otherwise</returns>
bool IsEnabled() const;
/// <summary>
/// Set configuration for the module
/// </summary>
/// <param name="configJson">JSON configuration string</param>
void SetConfig(const std::wstring& configJson);
/// <summary>
/// Get the module's localized name
/// </summary>
/// <returns>Module name</returns>
std::wstring GetModuleName() const;
/// <summary>
/// Get the module's non-localized key
/// </summary>
/// <returns>Module key</returns>
std::wstring GetModuleKey() const;
/// <summary>
/// Get the module's hotkeys
/// </summary>
/// <param name="buffer">Buffer to store hotkeys</param>
/// <param name="bufferSize">Size of the buffer</param>
/// <returns>Number of hotkeys returned</returns>
size_t GetHotkeys(PowertoyModuleIface::Hotkey* buffer, size_t bufferSize);
/// <summary>
/// Trigger a hotkey callback on the module
/// </summary>
/// <param name="hotkeyId">ID of the hotkey to trigger</param>
/// <returns>True if the key press should be swallowed</returns>
bool OnHotkey(size_t hotkeyId);
/// <summary>
/// Check if the module is loaded
/// </summary>
/// <returns>True if loaded, false otherwise</returns>
bool IsLoaded() const { return m_module != nullptr; }
/// <summary>
/// Get the module's activation hotkey (newer HotkeyEx API)
/// </summary>
/// <returns>Optional HotkeyEx struct</returns>
std::optional<PowertoyModuleIface::HotkeyEx> GetHotkeyEx();
/// <summary>
/// Trigger the newer-style hotkey callback on the module
/// </summary>
void OnHotkeyEx();
private:
HMODULE m_hModule;
PowertoyModuleIface* m_module;
std::wstring m_dllPath;
};

View File

@@ -0,0 +1,182 @@
// 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.
#include "SettingsLoader.h"
#include <iostream>
#include <fstream>
#include <sstream>
#include <filesystem>
#include <Shlobj.h>
SettingsLoader::SettingsLoader()
{
}
SettingsLoader::~SettingsLoader()
{
}
std::wstring SettingsLoader::GetPowerToysSettingsRoot() const
{
// Get %LOCALAPPDATA%
PWSTR localAppDataPath = nullptr;
HRESULT hr = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &localAppDataPath);
if (FAILED(hr) || !localAppDataPath)
{
std::wcerr << L"Error: Failed to get LOCALAPPDATA path\n";
return L"";
}
std::wstring result(localAppDataPath);
CoTaskMemFree(localAppDataPath);
// Append PowerToys directory
result += L"\\Microsoft\\PowerToys";
return result;
}
std::wstring SettingsLoader::GetSettingsPath(const std::wstring& moduleName) const
{
std::wstring root = GetPowerToysSettingsRoot();
if (root.empty())
{
return L"";
}
// Construct path: %LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\settings.json
std::wstring settingsPath = root + L"\\" + moduleName + L"\\settings.json";
return settingsPath;
}
std::wstring SettingsLoader::ReadFileContents(const std::wstring& filePath) const
{
std::wifstream file(filePath, std::ios::binary);
if (!file.is_open())
{
std::wcerr << L"Error: Could not open file: " << filePath << L"\n";
return L"";
}
// Read the entire file
std::wstringstream buffer;
buffer << file.rdbuf();
return buffer.str();
}
std::wstring SettingsLoader::LoadSettings(const std::wstring& moduleName, const std::wstring& moduleDllPath)
{
const std::wstring powerToysPrefix = L"PowerToys.";
// Build list of possible module name variations to try
std::vector<std::wstring> moduleNameVariants;
// Try exact name first
moduleNameVariants.push_back(moduleName);
// If doesn't start with "PowerToys.", try adding it
if (moduleName.find(powerToysPrefix) != 0)
{
moduleNameVariants.push_back(powerToysPrefix + moduleName);
}
// If starts with "PowerToys.", try without it
else
{
moduleNameVariants.push_back(moduleName.substr(powerToysPrefix.length()));
}
// FIRST: Try same directory as the module DLL
if (!moduleDllPath.empty())
{
std::filesystem::path dllPath(moduleDllPath);
std::filesystem::path dllDirectory = dllPath.parent_path();
std::wstring localSettingsPath = (dllDirectory / L"settings.json").wstring();
std::wcout << L"Trying settings path (module directory): " << localSettingsPath << L"\n";
if (std::filesystem::exists(localSettingsPath))
{
std::wstring contents = ReadFileContents(localSettingsPath);
if (!contents.empty())
{
std::wcout << L"Settings file loaded from module directory (" << contents.size() << L" characters)\n";
return contents;
}
}
}
// SECOND: Try standard PowerToys settings locations
for (const auto& variant : moduleNameVariants)
{
std::wstring settingsPath = GetSettingsPath(variant);
std::wcout << L"Trying settings path: " << settingsPath << L"\n";
// Check if file exists (case-sensitive path)
if (std::filesystem::exists(settingsPath))
{
std::wstring contents = ReadFileContents(settingsPath);
if (!contents.empty())
{
std::wcout << L"Settings file loaded (" << contents.size() << L" characters)\n";
return contents;
}
}
else
{
// Try case-insensitive search in the parent directory
std::wstring root = GetPowerToysSettingsRoot();
if (!root.empty() && std::filesystem::exists(root))
{
try
{
// Search for a directory that matches case-insensitively
for (const auto& entry : std::filesystem::directory_iterator(root))
{
if (entry.is_directory())
{
std::wstring dirName = entry.path().filename().wstring();
// Case-insensitive comparison
if (_wcsicmp(dirName.c_str(), variant.c_str()) == 0)
{
std::wstring actualSettingsPath = entry.path().wstring() + L"\\settings.json";
std::wcout << L"Found case-insensitive match: " << actualSettingsPath << L"\n";
if (std::filesystem::exists(actualSettingsPath))
{
std::wstring contents = ReadFileContents(actualSettingsPath);
if (!contents.empty())
{
std::wcout << L"Settings file loaded (" << contents.size() << L" characters)\n";
return contents;
}
}
}
}
}
}
catch (const std::filesystem::filesystem_error& e)
{
std::wcerr << L"Error searching directory: " << e.what() << L"\n";
}
}
}
}
std::wcerr << L"Error: Settings file not found in any expected location:\n";
if (!moduleDllPath.empty())
{
std::filesystem::path dllPath(moduleDllPath);
std::filesystem::path dllDirectory = dllPath.parent_path();
std::wcerr << L" - " << (dllDirectory / L"settings.json").wstring() << L" (module directory)\n";
}
for (const auto& variant : moduleNameVariants)
{
std::wcerr << L" - " << GetSettingsPath(variant) << L"\n";
}
return L"";
}

View File

@@ -0,0 +1,47 @@
// 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.
#pragma once
#include <Windows.h>
#include <string>
/// <summary>
/// Utility class for discovering and loading PowerToy module settings
/// </summary>
class SettingsLoader
{
public:
SettingsLoader();
~SettingsLoader();
/// <summary>
/// Load settings for a PowerToy module
/// </summary>
/// <param name="moduleName">Name of the module (e.g., "CursorWrap")</param>
/// <param name="moduleDllPath">Full path to the module DLL (for checking local settings.json)</param>
/// <returns>JSON settings string, or empty string if not found</returns>
std::wstring LoadSettings(const std::wstring& moduleName, const std::wstring& moduleDllPath);
/// <summary>
/// Get the settings file path for a module
/// </summary>
/// <param name="moduleName">Name of the module</param>
/// <returns>Full path to the settings.json file</returns>
std::wstring GetSettingsPath(const std::wstring& moduleName) const;
private:
/// <summary>
/// Get the PowerToys root settings directory
/// </summary>
/// <returns>Path to %LOCALAPPDATA%\Microsoft\PowerToys</returns>
std::wstring GetPowerToysSettingsRoot() const;
/// <summary>
/// Read a text file into a string
/// </summary>
/// <param name="filePath">Path to the file</param>
/// <returns>File contents as a string</returns>
std::wstring ReadFileContents(const std::wstring& filePath) const;
};

View File

@@ -0,0 +1,244 @@
// 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.
#include <Windows.h>
#include <Tlhelp32.h>
#include <iostream>
#include <string>
#include <filesystem>
#include "ModuleLoader.h"
#include "SettingsLoader.h"
#include "HotkeyManager.h"
#include "ConsoleHost.h"
namespace
{
void PrintUsage()
{
std::wcout << L"PowerToys Module Loader - Standalone utility for loading and testing PowerToy modules\n\n";
std::wcout << L"Usage: ModuleLoader.exe <module_dll_path>\n\n";
std::wcout << L"Arguments:\n";
std::wcout << L" module_dll_path Path to the PowerToy module DLL (e.g., CursorWrap.dll)\n\n";
std::wcout << L"Behavior:\n";
std::wcout << L" - Automatically discovers settings from %%LOCALAPPDATA%%\\Microsoft\\PowerToys\\<ModuleName>\\settings.json\n";
std::wcout << L" - Loads and enables the module\n";
std::wcout << L" - Registers module hotkeys\n";
std::wcout << L" - Runs until Ctrl+C is pressed\n\n";
std::wcout << L"Examples:\n";
std::wcout << L" ModuleLoader.exe x64\\Debug\\modules\\CursorWrap.dll\n";
std::wcout << L" ModuleLoader.exe \"C:\\Program Files\\PowerToys\\modules\\MouseHighlighter.dll\"\n\n";
std::wcout << L"Notes:\n";
std::wcout << L" - Only non-UI modules are supported\n";
std::wcout << L" - Module must have a valid settings.json file\n";
std::wcout << L" - Debug output is written to module's log directory\n";
}
std::wstring ExtractModuleName(const std::wstring& dllPath)
{
std::filesystem::path path(dllPath);
std::wstring filename = path.stem().wstring();
// Remove "PowerToys." prefix if present (case-insensitive)
const std::wstring powerToysPrefix = L"PowerToys.";
if (filename.length() >= powerToysPrefix.length())
{
// Check if filename starts with "PowerToys." (case-insensitive)
if (_wcsnicmp(filename.c_str(), powerToysPrefix.c_str(), powerToysPrefix.length()) == 0)
{
filename = filename.substr(powerToysPrefix.length());
}
}
// Common PowerToys module naming patterns
// Remove common suffixes if present
const std::wstring suffixes[] = { L"Module", L"ModuleInterface", L"Interface" };
for (const auto& suffix : suffixes)
{
if (filename.size() > suffix.size())
{
size_t pos = filename.rfind(suffix);
if (pos != std::wstring::npos && pos + suffix.size() == filename.size())
{
filename = filename.substr(0, pos);
break;
}
}
}
return filename;
}
}
int wmain(int argc, wchar_t* argv[])
{
std::wcout << L"PowerToys Module Loader v1.0\n";
std::wcout << L"=============================\n\n";
// Check if PowerToys.exe is running
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot != INVALID_HANDLE_VALUE)
{
PROCESSENTRY32W pe32;
pe32.dwSize = sizeof(PROCESSENTRY32W);
bool powerToysRunning = false;
if (Process32FirstW(hSnapshot, &pe32))
{
do
{
if (_wcsicmp(pe32.szExeFile, L"PowerToys.exe") == 0)
{
powerToysRunning = true;
break;
}
} while (Process32NextW(hSnapshot, &pe32));
}
CloseHandle(hSnapshot);
if (powerToysRunning)
{
// Display warning with VT100 colors
// Yellow background (43m), black text (30m), bold (1m)
std::wcout << L"\033[1;43;30m WARNING \033[0m PowerToys.exe is currently running!\n\n";
// Red text for important message
std::wcout << L"\033[1;31m";
std::wcout << L"Running ModuleLoader while PowerToys is active may cause conflicts:\n";
std::wcout << L" - Duplicate hotkey registrations\n";
std::wcout << L" - Conflicting module instances\n";
std::wcout << L" - Unexpected behavior\n";
std::wcout << L"\033[0m\n"; // Reset color
// Cyan text for recommendation
std::wcout << L"\033[1;36m";
std::wcout << L"RECOMMENDATION: Exit PowerToys before continuing.\n";
std::wcout << L"\033[0m\n"; // Reset color
// Yellow text for prompt
std::wcout << L"\033[1;33m";
std::wcout << L"Do you want to continue anyway? (y/N): ";
std::wcout << L"\033[0m"; // Reset color
wchar_t response = L'\0';
std::wcin >> response;
if (response != L'y' && response != L'Y')
{
std::wcout << L"\nExiting. Please close PowerToys and try again.\n";
return 1;
}
std::wcout << L"\n";
}
}
// Parse command-line arguments
if (argc < 2)
{
std::wcerr << L"Error: Missing required argument <module_dll_path>\n\n";
PrintUsage();
return 1;
}
const std::wstring dllPath = argv[1];
// Validate DLL exists
if (!std::filesystem::exists(dllPath))
{
std::wcerr << L"Error: Module DLL not found: " << dllPath << L"\n";
return 1;
}
std::wcout << L"Loading module: " << dllPath << L"\n";
// Extract module name from DLL path
std::wstring moduleName = ExtractModuleName(dllPath);
std::wcout << L"Detected module name: " << moduleName << L"\n\n";
try
{
// Load settings for the module
std::wcout << L"Loading settings...\n";
SettingsLoader settingsLoader;
std::wstring settingsJson = settingsLoader.LoadSettings(moduleName, dllPath);
if (settingsJson.empty())
{
std::wcerr << L"Error: Could not load settings for module '" << moduleName << L"'\n";
std::wcerr << L"Expected location: %LOCALAPPDATA%\\Microsoft\\PowerToys\\" << moduleName << L"\\settings.json\n";
return 1;
}
std::wcout << L"Settings loaded successfully.\n\n";
// Load the module DLL
std::wcout << L"Loading module DLL...\n";
ModuleLoader moduleLoader;
if (!moduleLoader.Load(dllPath))
{
std::wcerr << L"Error: Failed to load module DLL\n";
return 1;
}
std::wcout << L"Module DLL loaded successfully.\n";
std::wcout << L"Module key: " << moduleLoader.GetModuleKey() << L"\n";
std::wcout << L"Module name: " << moduleLoader.GetModuleName() << L"\n\n";
// Apply settings to the module
std::wcout << L"Applying settings to module...\n";
moduleLoader.SetConfig(settingsJson);
std::wcout << L"Settings applied.\n\n";
// Register hotkeys
std::wcout << L"Registering module hotkeys...\n";
HotkeyManager hotkeyManager;
if (!hotkeyManager.RegisterModuleHotkeys(moduleLoader))
{
std::wcerr << L"Warning: Failed to register some hotkeys\n";
}
std::wcout << L"Hotkeys registered: " << hotkeyManager.GetRegisteredCount() << L"\n\n";
// Enable the module
std::wcout << L"Enabling module...\n";
moduleLoader.Enable();
std::wcout << L"Module enabled.\n\n";
// Display status
std::wcout << L"=============================\n";
std::wcout << L"Module is now running!\n";
std::wcout << L"=============================\n\n";
std::wcout << L"Module Status:\n";
std::wcout << L" - Name: " << moduleLoader.GetModuleName() << L"\n";
std::wcout << L" - Key: " << moduleLoader.GetModuleKey() << L"\n";
std::wcout << L" - Enabled: " << (moduleLoader.IsEnabled() ? L"Yes" : L"No") << L"\n";
std::wcout << L" - Hotkeys: " << hotkeyManager.GetRegisteredCount() << L" registered\n\n";
if (hotkeyManager.GetRegisteredCount() > 0)
{
std::wcout << L"Registered Hotkeys:\n";
hotkeyManager.PrintHotkeys();
std::wcout << L"\n";
}
std::wcout << L"Press Ctrl+C to exit.\n";
std::wcout << L"You can press the module's hotkey to toggle its functionality.\n\n";
// Run the message loop
ConsoleHost consoleHost(moduleLoader, hotkeyManager);
consoleHost.Run();
// Cleanup
std::wcout << L"\nShutting down...\n";
moduleLoader.Disable();
hotkeyManager.UnregisterAll();
std::wcout << L"Module unloaded successfully.\n";
return 0;
}
catch (const std::exception& ex)
{
std::wcerr << L"Fatal error: " << ex.what() << L"\n";
return 1;
}
}