[Cursor Wrap] Update edge wrap model, update simulator, add cursor logging, add settings support to ModuleLoader (#45915)

This PR adds new options for disabling wrap, updates the wrapping model,
extends the simulator and cursor logging.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #45116 
- [ ] Closes: #44955 
- [ ] Closes: #44827 
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **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

The PR adds a new option for disabling cursor wrapping, exposing three
options: None - wrapping is not disabled, Ctrl key - if this is pressed
then wrapping is disabled, Shift key - if this is pressed then wrapping
is disabled, this would enable a user to temporarily disable wrapping if
they wanted to get close to a monitor edge without wrapping (auto-hide
status bar for example).

The cursor wrap edge model has been updated to mirror Windows
monitor-to-monitor cursor movement, this should ensure there aren't any
non-wrappable edges.

A new test tool has been added 'CursorLog' this is a monitor aware,
dpi/scaling aware Win32 application that captures mouse movement across
monitors to a log file, the log contains one line per mouse movement
which includes: Monitor, x, y, scale, dpi.

The wrapping simulator has been updated to include the new wrapping
model and support mouse cursor log playback.

## Validation Steps Performed
The updated CursorWrap has been tested on a single monitor (laptop) and
multi-monitor desktop PC with monitors being offset to test
edge/wrapping behavior.

---------

Co-authored-by: Mike Hall <mikehall@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: vanzue <vanzue@outlook.com>
This commit is contained in:
Niels Laute
2026-03-04 14:56:32 +01:00
committed by GitHub
parent d28f312b81
commit 86860df314
23 changed files with 4658 additions and 67 deletions

View File

@@ -7,6 +7,8 @@
#include <iostream>
#include <string>
#include <filesystem>
#include <vector>
#include <utility>
#include "ModuleLoader.h"
#include "SettingsLoader.h"
#include "HotkeyManager.h"
@@ -17,9 +19,15 @@ 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"Usage: ModuleLoader.exe <module_dll_path> [options]\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"Options:\n";
std::wcout << L" --info Display current module settings and exit\n";
std::wcout << L" --get <key> Get a specific setting value and exit\n";
std::wcout << L" --set <key>=<val> Set a setting value (can be used multiple times)\n";
std::wcout << L" --no-run Apply settings changes without running the module\n";
std::wcout << L" --help Show this help message\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";
@@ -27,10 +35,12 @@ namespace
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" ModuleLoader.exe CursorWrap.dll --info\n";
std::wcout << L" ModuleLoader.exe CursorWrap.dll --get wrap_mode\n";
std::wcout << L" ModuleLoader.exe CursorWrap.dll --set wrap_mode=1\n";
std::wcout << L" ModuleLoader.exe CursorWrap.dll --set auto_activate=true --no-run\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";
}
@@ -68,13 +78,151 @@ namespace
return filename;
}
struct CommandLineOptions
{
std::wstring dllPath;
bool showInfo = false;
bool showHelp = false;
bool noRun = false;
std::wstring getKey;
std::vector<std::pair<std::wstring, std::wstring>> setValues;
};
CommandLineOptions ParseCommandLine(int argc, wchar_t* argv[])
{
CommandLineOptions options;
for (int i = 1; i < argc; i++)
{
std::wstring arg = argv[i];
if (arg == L"--help" || arg == L"-h" || arg == L"/?")
{
options.showHelp = true;
}
else if (arg == L"--info")
{
options.showInfo = true;
}
else if (arg == L"--no-run")
{
options.noRun = true;
}
else if (arg == L"--get" && i + 1 < argc)
{
options.getKey = argv[++i];
}
else if (arg == L"--set" && i + 1 < argc)
{
std::wstring setValue = argv[++i];
size_t eqPos = setValue.find(L'=');
if (eqPos != std::wstring::npos)
{
std::wstring key = setValue.substr(0, eqPos);
std::wstring value = setValue.substr(eqPos + 1);
options.setValues.push_back({key, value});
}
else
{
std::wcerr << L"Warning: Invalid --set format. Use --set key=value\n";
}
}
else if (arg[0] != L'-' && options.dllPath.empty())
{
options.dllPath = arg;
}
}
return options;
}
}
int wmain(int argc, wchar_t* argv[])
{
std::wcout << L"PowerToys Module Loader v1.0\n";
// Enable UTF-8 console output for box-drawing characters
SetConsoleOutputCP(CP_UTF8);
// Enable virtual terminal processing for ANSI escape codes (colors)
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD dwMode = 0;
if (GetConsoleMode(hOut, &dwMode))
{
SetConsoleMode(hOut, dwMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
}
std::wcout << L"PowerToys Module Loader v1.1\n";
std::wcout << L"=============================\n\n";
// Parse command-line arguments
auto options = ParseCommandLine(argc, argv);
if (options.showHelp)
{
PrintUsage();
return 0;
}
if (options.dllPath.empty())
{
std::wcerr << L"Error: Missing required argument <module_dll_path>\n\n";
PrintUsage();
return 1;
}
// Validate DLL exists
if (!std::filesystem::exists(options.dllPath))
{
std::wcerr << L"Error: Module DLL not found: " << options.dllPath << L"\n";
return 1;
}
// Extract module name from DLL path
std::wstring moduleName = ExtractModuleName(options.dllPath);
// Create settings loader
SettingsLoader settingsLoader;
// Handle --info option
if (options.showInfo)
{
settingsLoader.DisplaySettingsInfo(moduleName, options.dllPath);
return 0;
}
// Handle --get option
if (!options.getKey.empty())
{
std::wstring value = settingsLoader.GetSettingValue(moduleName, options.dllPath, options.getKey);
if (value.empty())
{
std::wcerr << L"Setting '" << options.getKey << L"' not found.\n";
return 1;
}
std::wcout << options.getKey << L"=" << value << L"\n";
return 0;
}
// Handle --set options
if (!options.setValues.empty())
{
bool allSuccess = true;
for (const auto& [key, value] : options.setValues)
{
if (!settingsLoader.SetSettingValue(moduleName, options.dllPath, key, value))
{
allSuccess = false;
}
}
if (options.noRun)
{
return allSuccess ? 0 : 1;
}
std::wcout << L"\n";
}
// Check if PowerToys.exe is running
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot != INVALID_HANDLE_VALUE)
@@ -99,26 +247,22 @@ int wmain(int argc, wchar_t* argv[])
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
std::wcout << L"\033[0m\n";
// 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
std::wcout << L"\033[0m\n";
// 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
std::wcout << L"\033[0m";
wchar_t response = L'\0';
std::wcin >> response;
@@ -133,35 +277,14 @@ int wmain(int argc, wchar_t* argv[])
}
}
// 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"Loading module: " << options.dllPath << L"\n";
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);
std::wstring settingsJson = settingsLoader.LoadSettings(moduleName, options.dllPath);
if (settingsJson.empty())
{
@@ -175,7 +298,7 @@ int wmain(int argc, wchar_t* argv[])
// Load the module DLL
std::wcout << L"Loading module DLL...\n";
ModuleLoader moduleLoader;
if (!moduleLoader.Load(dllPath))
if (!moduleLoader.Load(options.dllPath))
{
std::wcerr << L"Error: Failed to load module DLL\n";
return 1;