From 452e0dcf513fd2d8c09c5115a56ed5327c5d4b38 Mon Sep 17 00:00:00 2001 From: Mike Hall Date: Wed, 26 Nov 2025 14:08:34 +0000 Subject: [PATCH] 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 - [ ] **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. --- tools/module_loader/ModuleLoader.manifest | 37 ++ tools/module_loader/ModuleLoader.vcxproj | 205 ++++++++ .../ModuleLoader.vcxproj.filters | 51 ++ tools/module_loader/SHARING.md | 483 ++++++++++++++++++ tools/module_loader/src/ConsoleHost.cpp | 80 +++ tools/module_loader/src/ConsoleHost.h | 38 ++ tools/module_loader/src/HotkeyManager.cpp | 279 ++++++++++ tools/module_loader/src/HotkeyManager.h | 86 ++++ tools/module_loader/src/ModuleLoader.cpp | 183 +++++++ tools/module_loader/src/ModuleLoader.h | 102 ++++ tools/module_loader/src/SettingsLoader.cpp | 182 +++++++ tools/module_loader/src/SettingsLoader.h | 47 ++ tools/module_loader/src/main.cpp | 244 +++++++++ 13 files changed, 2017 insertions(+) create mode 100644 tools/module_loader/ModuleLoader.manifest create mode 100644 tools/module_loader/ModuleLoader.vcxproj create mode 100644 tools/module_loader/ModuleLoader.vcxproj.filters create mode 100644 tools/module_loader/SHARING.md create mode 100644 tools/module_loader/src/ConsoleHost.cpp create mode 100644 tools/module_loader/src/ConsoleHost.h create mode 100644 tools/module_loader/src/HotkeyManager.cpp create mode 100644 tools/module_loader/src/HotkeyManager.h create mode 100644 tools/module_loader/src/ModuleLoader.cpp create mode 100644 tools/module_loader/src/ModuleLoader.h create mode 100644 tools/module_loader/src/SettingsLoader.cpp create mode 100644 tools/module_loader/src/SettingsLoader.h create mode 100644 tools/module_loader/src/main.cpp diff --git a/tools/module_loader/ModuleLoader.manifest b/tools/module_loader/ModuleLoader.manifest new file mode 100644 index 0000000000..2607358482 --- /dev/null +++ b/tools/module_loader/ModuleLoader.manifest @@ -0,0 +1,37 @@ + + + + PowerToys Module Loader - Standalone module testing utility + + + + + true/PM + PerMonitorV2 + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/module_loader/ModuleLoader.vcxproj b/tools/module_loader/ModuleLoader.vcxproj new file mode 100644 index 0000000000..dd9c01c584 --- /dev/null +++ b/tools/module_loader/ModuleLoader.vcxproj @@ -0,0 +1,205 @@ + + + + + Debug + x64 + + + Debug + ARM64 + + + Release + x64 + + + Release + ARM64 + + + + 17.0 + Win32Proj + {8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942} + ModuleLoader + 10.0 + ModuleLoader + + + + Application + true + v143 + Unicode + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + Application + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + ModuleLoader + + + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + ModuleLoader + + + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + ModuleLoader + + + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + ModuleLoader + + + + Level4 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + $(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories) + NotUsing + false + + + Console + true + kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies) + type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*' + + + $(ProjectDir)ModuleLoader.manifest + + + + + Level4 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + $(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories) + NotUsing + false + + + Console + true + kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies) + type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*' + + + $(ProjectDir)ModuleLoader.manifest + + + + + Level4 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + $(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories) + NotUsing + false + + + Console + true + true + true + kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies) + type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*' + + + $(ProjectDir)ModuleLoader.manifest + + + + + Level4 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + $(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories) + NotUsing + false + + + Console + true + true + true + kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies) + type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*' + + + $(ProjectDir)ModuleLoader.manifest + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/module_loader/ModuleLoader.vcxproj.filters b/tools/module_loader/ModuleLoader.vcxproj.filters new file mode 100644 index 0000000000..823f1c4e60 --- /dev/null +++ b/tools/module_loader/ModuleLoader.vcxproj.filters @@ -0,0 +1,51 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + + diff --git a/tools/module_loader/SHARING.md b/tools/module_loader/SHARING.md new file mode 100644 index 0000000000..1923d31adf --- /dev/null +++ b/tools/module_loader/SHARING.md @@ -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\\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\\` + +**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\" 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\\ + - 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\\settings.json + 3. You can also run PowerToys once to generate default settings + +Debug Logs: +----------- +Module logs are written to: + %LOCALAPPDATA%\Microsoft\PowerToys\\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. diff --git a/tools/module_loader/src/ConsoleHost.cpp b/tools/module_loader/src/ConsoleHost.cpp new file mode 100644 index 0000000000..1ab2cdefa2 --- /dev/null +++ b/tools/module_loader/src/ConsoleHost.cpp @@ -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 + +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(msg.wParam), m_moduleLoader); + } + + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + } + + // Remove console control handler + SetConsoleCtrlHandler(ConsoleCtrlHandler, FALSE); +} diff --git a/tools/module_loader/src/ConsoleHost.h b/tools/module_loader/src/ConsoleHost.h new file mode 100644 index 0000000000..153fdaa0f0 --- /dev/null +++ b/tools/module_loader/src/ConsoleHost.h @@ -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 +#include "ModuleLoader.h" +#include "HotkeyManager.h" + +/// +/// Console host that runs the message loop and handles Ctrl+C +/// +class ConsoleHost +{ +public: + ConsoleHost(ModuleLoader& moduleLoader, HotkeyManager& hotkeyManager); + ~ConsoleHost(); + + // Prevent copying + ConsoleHost(const ConsoleHost&) = delete; + ConsoleHost& operator=(const ConsoleHost&) = delete; + + /// + /// Run the message loop until Ctrl+C is pressed + /// + void Run(); + +private: + ModuleLoader& m_moduleLoader; + HotkeyManager& m_hotkeyManager; + static bool s_exitRequested; + + /// + /// Console control handler (for Ctrl+C) + /// + static BOOL WINAPI ConsoleCtrlHandler(DWORD ctrlType); +}; diff --git a/tools/module_loader/src/HotkeyManager.cpp b/tools/module_loader/src/HotkeyManager.cpp new file mode 100644 index 0000000000..ce0ced5a03 --- /dev/null +++ b/tools/module_loader/src/HotkeyManager.cpp @@ -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 +#include + +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 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(); +} diff --git a/tools/module_loader/src/HotkeyManager.h b/tools/module_loader/src/HotkeyManager.h new file mode 100644 index 0000000000..714e5a0962 --- /dev/null +++ b/tools/module_loader/src/HotkeyManager.h @@ -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 +#include +#include +#include +#include "ModuleLoader.h" + +/// +/// Manages hotkey registration using RegisterHotKey API +/// +class HotkeyManager +{ +public: + HotkeyManager(); + ~HotkeyManager(); + + // Prevent copying + HotkeyManager(const HotkeyManager&) = delete; + HotkeyManager& operator=(const HotkeyManager&) = delete; + + /// + /// Register all hotkeys from a module + /// + /// Module to get hotkeys from + /// True if at least one hotkey was registered + bool RegisterModuleHotkeys(ModuleLoader& moduleLoader); + + /// + /// Unregister all hotkeys + /// + void UnregisterAll(); + + /// + /// Handle a WM_HOTKEY message + /// + /// ID from the WM_HOTKEY message + /// Module to trigger the hotkey on + /// True if the hotkey was handled + bool HandleHotkey(int hotkeyId, ModuleLoader& moduleLoader); + + /// + /// Get the number of registered hotkeys + /// + /// Number of registered hotkeys + size_t GetRegisteredCount() const { return m_registeredHotkeys.size() + (m_hotkeyExRegistered ? 1 : 0); } + + /// + /// Print registered hotkeys to console + /// + void PrintHotkeys() const; + +private: + struct HotkeyInfo + { + int id = 0; + size_t moduleHotkeyId = 0; + UINT modifiers = 0; + UINT vkCode = 0; + std::wstring description; + }; + + std::vector m_registeredHotkeys; + int m_nextHotkeyId; + bool m_hotkeyExRegistered; + int m_hotkeyExId; + + /// + /// Convert modifier bools to RegisterHotKey modifiers + /// + UINT ConvertModifiers(bool win, bool ctrl, bool alt, bool shift) const; + + /// + /// Get a string representation of modifiers + /// + std::wstring ModifiersToString(UINT modifiers) const; + + /// + /// Get a string representation of a virtual key code + /// + std::wstring VKeyToString(UINT vkCode) const; +}; diff --git a/tools/module_loader/src/ModuleLoader.cpp b/tools/module_loader/src/ModuleLoader.cpp new file mode 100644 index 0000000000..3334e2ab42 --- /dev/null +++ b/tools/module_loader/src/ModuleLoader.cpp @@ -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 +#include + +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( + 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""; + } + + const wchar_t* name = m_module->get_name(); + return name ? name : L""; +} + +std::wstring ModuleLoader::GetModuleKey() const +{ + if (!m_module) + { + return L""; + } + + const wchar_t* key = m_module->get_key(); + return key ? key : L""; +} + +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 ModuleLoader::GetHotkeyEx() +{ + if (!m_module) + { + return std::nullopt; + } + + return m_module->GetHotkeyEx(); +} + +void ModuleLoader::OnHotkeyEx() +{ + if (!m_module) + { + return; + } + + m_module->OnHotkeyEx(); +} diff --git a/tools/module_loader/src/ModuleLoader.h b/tools/module_loader/src/ModuleLoader.h new file mode 100644 index 0000000000..5c155913f4 --- /dev/null +++ b/tools/module_loader/src/ModuleLoader.h @@ -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 +#include +#include +#include + +/// +/// Wrapper class for loading and managing a PowerToy module DLL +/// +class ModuleLoader +{ +public: + ModuleLoader(); + ~ModuleLoader(); + + // Prevent copying + ModuleLoader(const ModuleLoader&) = delete; + ModuleLoader& operator=(const ModuleLoader&) = delete; + + /// + /// Load a PowerToy module DLL + /// + /// Path to the module DLL + /// True if successful, false otherwise + bool Load(const std::wstring& dllPath); + + /// + /// Enable the loaded module + /// + void Enable(); + + /// + /// Disable the loaded module + /// + void Disable(); + + /// + /// Check if the module is enabled + /// + /// True if enabled, false otherwise + bool IsEnabled() const; + + /// + /// Set configuration for the module + /// + /// JSON configuration string + void SetConfig(const std::wstring& configJson); + + /// + /// Get the module's localized name + /// + /// Module name + std::wstring GetModuleName() const; + + /// + /// Get the module's non-localized key + /// + /// Module key + std::wstring GetModuleKey() const; + + /// + /// Get the module's hotkeys + /// + /// Buffer to store hotkeys + /// Size of the buffer + /// Number of hotkeys returned + size_t GetHotkeys(PowertoyModuleIface::Hotkey* buffer, size_t bufferSize); + + /// + /// Trigger a hotkey callback on the module + /// + /// ID of the hotkey to trigger + /// True if the key press should be swallowed + bool OnHotkey(size_t hotkeyId); + + /// + /// Check if the module is loaded + /// + /// True if loaded, false otherwise + bool IsLoaded() const { return m_module != nullptr; } + + /// + /// Get the module's activation hotkey (newer HotkeyEx API) + /// + /// Optional HotkeyEx struct + std::optional GetHotkeyEx(); + + /// + /// Trigger the newer-style hotkey callback on the module + /// + void OnHotkeyEx(); + +private: + HMODULE m_hModule; + PowertoyModuleIface* m_module; + std::wstring m_dllPath; +}; diff --git a/tools/module_loader/src/SettingsLoader.cpp b/tools/module_loader/src/SettingsLoader.cpp new file mode 100644 index 0000000000..2d1c869ba1 --- /dev/null +++ b/tools/module_loader/src/SettingsLoader.cpp @@ -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 +#include +#include +#include +#include + +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\\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 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""; +} diff --git a/tools/module_loader/src/SettingsLoader.h b/tools/module_loader/src/SettingsLoader.h new file mode 100644 index 0000000000..e005fdd4b2 --- /dev/null +++ b/tools/module_loader/src/SettingsLoader.h @@ -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 +#include + +/// +/// Utility class for discovering and loading PowerToy module settings +/// +class SettingsLoader +{ +public: + SettingsLoader(); + ~SettingsLoader(); + + /// + /// Load settings for a PowerToy module + /// + /// Name of the module (e.g., "CursorWrap") + /// Full path to the module DLL (for checking local settings.json) + /// JSON settings string, or empty string if not found + std::wstring LoadSettings(const std::wstring& moduleName, const std::wstring& moduleDllPath); + + /// + /// Get the settings file path for a module + /// + /// Name of the module + /// Full path to the settings.json file + std::wstring GetSettingsPath(const std::wstring& moduleName) const; + +private: + /// + /// Get the PowerToys root settings directory + /// + /// Path to %LOCALAPPDATA%\Microsoft\PowerToys + std::wstring GetPowerToysSettingsRoot() const; + + /// + /// Read a text file into a string + /// + /// Path to the file + /// File contents as a string + std::wstring ReadFileContents(const std::wstring& filePath) const; +}; diff --git a/tools/module_loader/src/main.cpp b/tools/module_loader/src/main.cpp new file mode 100644 index 0000000000..fc9894e623 --- /dev/null +++ b/tools/module_loader/src/main.cpp @@ -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 +#include +#include +#include +#include +#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 \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\\\\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 \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; + } +}