mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 01:36:31 +02:00
[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:
@@ -7,6 +7,7 @@
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <filesystem>
|
||||
#include <cwctype>
|
||||
#include <Shlobj.h>
|
||||
|
||||
SettingsLoader::SettingsLoader()
|
||||
@@ -180,3 +181,723 @@ std::wstring SettingsLoader::LoadSettings(const std::wstring& moduleName, const
|
||||
|
||||
return L"";
|
||||
}
|
||||
|
||||
std::wstring SettingsLoader::FindSettingsFilePath(const std::wstring& moduleName, const std::wstring& moduleDllPath)
|
||||
{
|
||||
const std::wstring powerToysPrefix = L"PowerToys.";
|
||||
|
||||
std::vector<std::wstring> moduleNameVariants;
|
||||
moduleNameVariants.push_back(moduleName);
|
||||
|
||||
if (moduleName.find(powerToysPrefix) != 0)
|
||||
{
|
||||
moduleNameVariants.push_back(powerToysPrefix + moduleName);
|
||||
}
|
||||
else
|
||||
{
|
||||
moduleNameVariants.push_back(moduleName.substr(powerToysPrefix.length()));
|
||||
}
|
||||
|
||||
// Try module directory first
|
||||
if (!moduleDllPath.empty())
|
||||
{
|
||||
std::filesystem::path dllPath(moduleDllPath);
|
||||
std::filesystem::path dllDirectory = dllPath.parent_path();
|
||||
std::wstring localSettingsPath = (dllDirectory / L"settings.json").wstring();
|
||||
|
||||
if (std::filesystem::exists(localSettingsPath))
|
||||
{
|
||||
return localSettingsPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Try standard locations
|
||||
for (const auto& variant : moduleNameVariants)
|
||||
{
|
||||
std::wstring settingsPath = GetSettingsPath(variant);
|
||||
|
||||
if (std::filesystem::exists(settingsPath))
|
||||
{
|
||||
return settingsPath;
|
||||
}
|
||||
|
||||
// Case-insensitive search
|
||||
std::wstring root = GetPowerToysSettingsRoot();
|
||||
if (!root.empty() && std::filesystem::exists(root))
|
||||
{
|
||||
try
|
||||
{
|
||||
for (const auto& entry : std::filesystem::directory_iterator(root))
|
||||
{
|
||||
if (entry.is_directory())
|
||||
{
|
||||
std::wstring dirName = entry.path().filename().wstring();
|
||||
if (_wcsicmp(dirName.c_str(), variant.c_str()) == 0)
|
||||
{
|
||||
std::wstring actualSettingsPath = entry.path().wstring() + L"\\settings.json";
|
||||
if (std::filesystem::exists(actualSettingsPath))
|
||||
{
|
||||
return actualSettingsPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...) {}
|
||||
}
|
||||
}
|
||||
|
||||
return L"";
|
||||
}
|
||||
|
||||
void SettingsLoader::DisplaySettingsInfo(const std::wstring& moduleName, const std::wstring& moduleDllPath)
|
||||
{
|
||||
std::wcout << L"\n";
|
||||
std::wcout << L"\033[1;36m"; // Cyan bold
|
||||
std::wcout << L"+----------------------------------------------------------------+\n";
|
||||
std::wcout << L"| MODULE SETTINGS INFO |\n";
|
||||
std::wcout << L"+----------------------------------------------------------------+\n";
|
||||
std::wcout << L"\033[0m";
|
||||
|
||||
std::wcout << L"\n\033[1mModule:\033[0m " << moduleName << L"\n";
|
||||
|
||||
std::wstring settingsPath = FindSettingsFilePath(moduleName, moduleDllPath);
|
||||
|
||||
if (settingsPath.empty())
|
||||
{
|
||||
std::wcout << L"\033[1;33mSettings file:\033[0m Not found\n";
|
||||
std::wcout << L"\nNo settings file found for this module.\n";
|
||||
return;
|
||||
}
|
||||
|
||||
std::wcout << L"\033[1mSettings file:\033[0m " << settingsPath << L"\n\n";
|
||||
|
||||
std::wstring settingsJson = ReadFileContents(settingsPath);
|
||||
if (settingsJson.empty())
|
||||
{
|
||||
std::wcout << L"Unable to read settings file.\n";
|
||||
return;
|
||||
}
|
||||
|
||||
std::wcout << L"\033[1;32mCurrent Settings:\033[0m\n";
|
||||
std::wcout << L"-----------------------------------------------------------------\n";
|
||||
|
||||
DisplayJsonProperties(settingsJson, 0);
|
||||
|
||||
std::wcout << L"-----------------------------------------------------------------\n\n";
|
||||
}
|
||||
|
||||
void SettingsLoader::DisplayJsonProperties(const std::wstring& settingsJson, int indent)
|
||||
{
|
||||
// Simple JSON parser for display - handles the PowerToys settings format
|
||||
// Format: { "properties": { "key": { "value": ... }, ... } }
|
||||
// Also handles hotkey settings: { "key": { "win": true, "alt": true, "code": 85 } }
|
||||
|
||||
std::string json(settingsJson.begin(), settingsJson.end());
|
||||
|
||||
// Find "properties" section
|
||||
size_t propsStart = json.find("\"properties\"");
|
||||
if (propsStart == std::string::npos)
|
||||
{
|
||||
// If no properties section, just display the raw JSON
|
||||
std::wcout << settingsJson << L"\n";
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the opening brace after "properties":
|
||||
size_t braceStart = json.find('{', propsStart + 12);
|
||||
if (braceStart == std::string::npos) return;
|
||||
|
||||
// Parse each property
|
||||
size_t pos = braceStart + 1;
|
||||
int braceCount = 1;
|
||||
|
||||
while (pos < json.size() && braceCount > 0)
|
||||
{
|
||||
// Skip whitespace
|
||||
while (pos < json.size() && std::isspace(json[pos])) pos++;
|
||||
|
||||
// Look for property name
|
||||
if (json[pos] == '"')
|
||||
{
|
||||
size_t nameStart = pos + 1;
|
||||
size_t nameEnd = json.find('"', nameStart);
|
||||
if (nameEnd == std::string::npos) break;
|
||||
|
||||
std::string propName = json.substr(nameStart, nameEnd - nameStart);
|
||||
|
||||
// Skip to the value object
|
||||
pos = json.find('{', nameEnd);
|
||||
if (pos == std::string::npos) break;
|
||||
|
||||
size_t objStart = pos;
|
||||
|
||||
// Check if this is a hotkey object (has "win", "code" etc. but no "value")
|
||||
if (IsHotkeyObject(json, objStart))
|
||||
{
|
||||
// Parse hotkey and display
|
||||
size_t objEnd;
|
||||
std::string hotkeyStr = ParseHotkeyObject(json, objStart, objEnd);
|
||||
|
||||
std::wstring wPropName(propName.begin(), propName.end());
|
||||
std::wstring wHotkeyStr(hotkeyStr.begin(), hotkeyStr.end());
|
||||
|
||||
std::wcout << L" \033[1;34m" << wPropName << L"\033[0m: ";
|
||||
std::wcout << L"\033[1;36m" << wHotkeyStr << L"\033[0m\n";
|
||||
|
||||
pos = objEnd + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular property with "value" key
|
||||
int innerBraceCount = 1;
|
||||
pos++;
|
||||
|
||||
std::string valueStr = "";
|
||||
bool foundValue = false;
|
||||
|
||||
while (pos < json.size() && innerBraceCount > 0)
|
||||
{
|
||||
if (json[pos] == '{') innerBraceCount++;
|
||||
else if (json[pos] == '}') innerBraceCount--;
|
||||
else if (json[pos] == '"' && !foundValue)
|
||||
{
|
||||
size_t keyStart = pos + 1;
|
||||
size_t keyEnd = json.find('"', keyStart);
|
||||
if (keyEnd != std::string::npos)
|
||||
{
|
||||
std::string key = json.substr(keyStart, keyEnd - keyStart);
|
||||
if (key == "value")
|
||||
{
|
||||
// Find the colon and then the value
|
||||
size_t colonPos = json.find(':', keyEnd);
|
||||
if (colonPos != std::string::npos)
|
||||
{
|
||||
size_t valStart = colonPos + 1;
|
||||
while (valStart < json.size() && std::isspace(json[valStart])) valStart++;
|
||||
|
||||
// Determine value type and extract
|
||||
if (json[valStart] == '"')
|
||||
{
|
||||
size_t valEnd = json.find('"', valStart + 1);
|
||||
if (valEnd != std::string::npos)
|
||||
{
|
||||
valueStr = json.substr(valStart + 1, valEnd - valStart - 1);
|
||||
foundValue = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Number or boolean
|
||||
size_t valEnd = valStart;
|
||||
while (valEnd < json.size() && json[valEnd] != ',' && json[valEnd] != '}' && !std::isspace(json[valEnd]))
|
||||
{
|
||||
valEnd++;
|
||||
}
|
||||
valueStr = json.substr(valStart, valEnd - valStart);
|
||||
foundValue = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pos = keyEnd + 1;
|
||||
continue;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
|
||||
// Print the property
|
||||
std::wstring wPropName(propName.begin(), propName.end());
|
||||
std::wstring wValueStr(valueStr.begin(), valueStr.end());
|
||||
|
||||
std::wcout << L" \033[1;34m" << wPropName << L"\033[0m: ";
|
||||
|
||||
// Color-code based on value type
|
||||
if (valueStr == "true")
|
||||
{
|
||||
std::wcout << L"\033[1;32mtrue\033[0m";
|
||||
}
|
||||
else if (valueStr == "false")
|
||||
{
|
||||
std::wcout << L"\033[1;31mfalse\033[0m";
|
||||
}
|
||||
else if (!valueStr.empty() && (std::isdigit(valueStr[0]) || valueStr[0] == '-'))
|
||||
{
|
||||
std::wcout << L"\033[1;33m" << wValueStr << L"\033[0m";
|
||||
}
|
||||
else
|
||||
{
|
||||
std::wcout << L"\033[1;35m\"" << wValueStr << L"\"\033[0m";
|
||||
}
|
||||
std::wcout << L"\n";
|
||||
}
|
||||
else if (json[pos] == '{')
|
||||
{
|
||||
braceCount++;
|
||||
pos++;
|
||||
}
|
||||
else if (json[pos] == '}')
|
||||
{
|
||||
braceCount--;
|
||||
pos++;
|
||||
}
|
||||
else
|
||||
{
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring SettingsLoader::GetSettingValue(const std::wstring& moduleName, const std::wstring& moduleDllPath, const std::wstring& key)
|
||||
{
|
||||
std::wstring settingsPath = FindSettingsFilePath(moduleName, moduleDllPath);
|
||||
if (settingsPath.empty()) return L"";
|
||||
|
||||
std::wstring settingsJson = ReadFileContents(settingsPath);
|
||||
if (settingsJson.empty()) return L"";
|
||||
|
||||
// Simple JSON parser to find the specific key
|
||||
std::string json(settingsJson.begin(), settingsJson.end());
|
||||
std::string searchKey(key.begin(), key.end());
|
||||
|
||||
// Look for "properties" -> key -> "value"
|
||||
std::string searchPattern = "\"" + searchKey + "\"";
|
||||
size_t keyPos = json.find(searchPattern);
|
||||
if (keyPos == std::string::npos) return L"";
|
||||
|
||||
// Find "value" within this property's object
|
||||
size_t objStart = json.find('{', keyPos);
|
||||
if (objStart == std::string::npos) return L"";
|
||||
|
||||
size_t valueKeyPos = json.find("\"value\"", objStart);
|
||||
if (valueKeyPos == std::string::npos) return L"";
|
||||
|
||||
// Find the colon and extract value
|
||||
size_t colonPos = json.find(':', valueKeyPos);
|
||||
if (colonPos == std::string::npos) return L"";
|
||||
|
||||
size_t valStart = colonPos + 1;
|
||||
while (valStart < json.size() && std::isspace(json[valStart])) valStart++;
|
||||
|
||||
std::string valueStr;
|
||||
if (json[valStart] == '"')
|
||||
{
|
||||
size_t valEnd = json.find('"', valStart + 1);
|
||||
if (valEnd != std::string::npos)
|
||||
{
|
||||
valueStr = json.substr(valStart + 1, valEnd - valStart - 1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
size_t valEnd = valStart;
|
||||
while (valEnd < json.size() && json[valEnd] != ',' && json[valEnd] != '}' && !std::isspace(json[valEnd]))
|
||||
{
|
||||
valEnd++;
|
||||
}
|
||||
valueStr = json.substr(valStart, valEnd - valStart);
|
||||
}
|
||||
|
||||
return std::wstring(valueStr.begin(), valueStr.end());
|
||||
}
|
||||
|
||||
bool SettingsLoader::SetSettingValue(const std::wstring& moduleName, const std::wstring& moduleDllPath, const std::wstring& key, const std::wstring& value)
|
||||
{
|
||||
std::wstring settingsPath = FindSettingsFilePath(moduleName, moduleDllPath);
|
||||
if (settingsPath.empty())
|
||||
{
|
||||
std::wcerr << L"Error: Settings file not found\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring settingsJson = ReadFileContents(settingsPath);
|
||||
if (settingsJson.empty())
|
||||
{
|
||||
std::wcerr << L"Error: Unable to read settings file\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string json(settingsJson.begin(), settingsJson.end());
|
||||
std::string searchKey(key.begin(), key.end());
|
||||
std::string newValue(value.begin(), value.end());
|
||||
|
||||
// Find the property
|
||||
std::string searchPattern = "\"" + searchKey + "\"";
|
||||
size_t keyPos = json.find(searchPattern);
|
||||
if (keyPos == std::string::npos)
|
||||
{
|
||||
// Setting not found - prompt user to add it
|
||||
std::wcout << L"\033[1;33mWarning:\033[0m Setting '" << key << L"' not found in settings file.\n";
|
||||
std::wcout << L"This could be a new setting or a typo.\n\n";
|
||||
|
||||
if (PromptYesNo(L"Do you want to add this as a new setting?"))
|
||||
{
|
||||
std::string modifiedJson = AddNewProperty(json, searchKey, newValue);
|
||||
if (modifiedJson.empty())
|
||||
{
|
||||
std::wcerr << L"Error: Failed to add new property to settings file\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring newJson(modifiedJson.begin(), modifiedJson.end());
|
||||
if (WriteFileContents(settingsPath, newJson))
|
||||
{
|
||||
std::wcout << L"\033[1;32m+\033[0m New setting '" << key << L"' added with value: " << value << L"\n";
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::wcerr << L"Error: Failed to write settings file\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::wcout << L"Operation cancelled.\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Find "value" within this property's object
|
||||
size_t objStart = json.find('{', keyPos);
|
||||
if (objStart == std::string::npos) return false;
|
||||
|
||||
size_t valueKeyPos = json.find("\"value\"", objStart);
|
||||
if (valueKeyPos == std::string::npos) return false;
|
||||
|
||||
// Find the colon and the existing value
|
||||
size_t colonPos = json.find(':', valueKeyPos);
|
||||
if (colonPos == std::string::npos) return false;
|
||||
|
||||
size_t valStart = colonPos + 1;
|
||||
while (valStart < json.size() && std::isspace(json[valStart])) valStart++;
|
||||
|
||||
size_t valEnd;
|
||||
bool isString = (json[valStart] == '"');
|
||||
|
||||
if (isString)
|
||||
{
|
||||
valEnd = json.find('"', valStart + 1);
|
||||
if (valEnd != std::string::npos) valEnd++; // Include closing quote
|
||||
}
|
||||
else
|
||||
{
|
||||
valEnd = valStart;
|
||||
while (valEnd < json.size() && json[valEnd] != ',' && json[valEnd] != '}' && !std::isspace(json[valEnd]))
|
||||
{
|
||||
valEnd++;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if new value should be quoted
|
||||
bool newValueNeedsQuotes = false;
|
||||
if (newValue != "true" && newValue != "false")
|
||||
{
|
||||
// Check if it's a number
|
||||
bool isNumber = !newValue.empty();
|
||||
for (char c : newValue)
|
||||
{
|
||||
if (!std::isdigit(c) && c != '.' && c != '-')
|
||||
{
|
||||
isNumber = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
newValueNeedsQuotes = !isNumber;
|
||||
}
|
||||
|
||||
std::string replacement;
|
||||
if (newValueNeedsQuotes)
|
||||
{
|
||||
replacement = "\"" + newValue + "\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
replacement = newValue;
|
||||
}
|
||||
|
||||
// Replace the value
|
||||
json = json.substr(0, valStart) + replacement + json.substr(valEnd);
|
||||
|
||||
// Write back
|
||||
std::wstring newJson(json.begin(), json.end());
|
||||
if (WriteFileContents(settingsPath, newJson))
|
||||
{
|
||||
std::wcout << L"\033[1;32m?\033[0m Setting '" << key << L"' updated to: " << value << L"\n";
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::wcerr << L"Error: Failed to write settings file\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool SettingsLoader::WriteFileContents(const std::wstring& filePath, const std::wstring& contents) const
|
||||
{
|
||||
std::ofstream file(filePath, std::ios::binary);
|
||||
if (!file.is_open())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string utf8Contents(contents.begin(), contents.end());
|
||||
file << utf8Contents;
|
||||
file.close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SettingsLoader::PromptYesNo(const std::wstring& prompt)
|
||||
{
|
||||
std::wcout << prompt << L" [y/N]: ";
|
||||
std::wcout.flush();
|
||||
|
||||
std::wstring input;
|
||||
std::getline(std::wcin, input);
|
||||
|
||||
// Trim whitespace
|
||||
while (!input.empty() && iswspace(input.front())) input.erase(input.begin());
|
||||
while (!input.empty() && iswspace(input.back())) input.pop_back();
|
||||
|
||||
// Check for yes responses
|
||||
return !input.empty() && (input[0] == L'y' || input[0] == L'Y');
|
||||
}
|
||||
|
||||
std::string SettingsLoader::AddNewProperty(const std::string& json, const std::string& key, const std::string& value)
|
||||
{
|
||||
// Find the "properties" section
|
||||
size_t propsPos = json.find("\"properties\"");
|
||||
if (propsPos == std::string::npos)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
// Find the opening brace of properties object
|
||||
size_t propsStart = json.find('{', propsPos);
|
||||
if (propsStart == std::string::npos)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
// Find the closing brace of properties object
|
||||
int braceCount = 1;
|
||||
size_t pos = propsStart + 1;
|
||||
size_t propsEnd = std::string::npos;
|
||||
|
||||
while (pos < json.size() && braceCount > 0)
|
||||
{
|
||||
if (json[pos] == '{') braceCount++;
|
||||
else if (json[pos] == '}')
|
||||
{
|
||||
braceCount--;
|
||||
if (braceCount == 0) propsEnd = pos;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (propsEnd == std::string::npos)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
// Determine if new value should be quoted
|
||||
bool needsQuotes = false;
|
||||
if (value != "true" && value != "false")
|
||||
{
|
||||
bool isNumber = !value.empty();
|
||||
for (char c : value)
|
||||
{
|
||||
if (!std::isdigit(c) && c != '.' && c != '-')
|
||||
{
|
||||
isNumber = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
needsQuotes = !isNumber;
|
||||
}
|
||||
|
||||
// Build the new property JSON
|
||||
// Format: "key": { "value": ... }
|
||||
std::string valueJson = needsQuotes ? ("\"" + value + "\"") : value;
|
||||
std::string newProperty = ",\n \"" + key + "\": {\n \"value\": " + valueJson + "\n }";
|
||||
|
||||
// Check if properties object is empty (only whitespace between braces)
|
||||
std::string propsContent = json.substr(propsStart + 1, propsEnd - propsStart - 1);
|
||||
bool isEmpty = true;
|
||||
for (char c : propsContent)
|
||||
{
|
||||
if (!std::isspace(c))
|
||||
{
|
||||
isEmpty = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the new property before the closing brace of properties
|
||||
std::string result;
|
||||
if (isEmpty)
|
||||
{
|
||||
// No leading comma for empty properties
|
||||
newProperty = "\n \"" + key + "\": {\n \"value\": " + valueJson + "\n }\n ";
|
||||
}
|
||||
|
||||
result = json.substr(0, propsEnd) + newProperty + json.substr(propsEnd);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool SettingsLoader::IsHotkeyObject(const std::string& json, size_t objStart)
|
||||
{
|
||||
// A hotkey object has "win", "alt", "ctrl", "shift", and "code" fields
|
||||
// Find the end of this object
|
||||
int braceCount = 1;
|
||||
size_t pos = objStart + 1;
|
||||
size_t objEnd = objStart;
|
||||
|
||||
while (pos < json.size() && braceCount > 0)
|
||||
{
|
||||
if (json[pos] == '{') braceCount++;
|
||||
else if (json[pos] == '}')
|
||||
{
|
||||
braceCount--;
|
||||
if (braceCount == 0) objEnd = pos;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (objEnd <= objStart) return false;
|
||||
|
||||
std::string objContent = json.substr(objStart, objEnd - objStart + 1);
|
||||
|
||||
// Check for hotkey-specific fields
|
||||
return (objContent.find("\"win\"") != std::string::npos ||
|
||||
objContent.find("\"code\"") != std::string::npos) &&
|
||||
objContent.find("\"value\"") == std::string::npos;
|
||||
}
|
||||
|
||||
std::string SettingsLoader::ParseHotkeyObject(const std::string& json, size_t objStart, size_t& objEnd)
|
||||
{
|
||||
// Find the end of this object
|
||||
int braceCount = 1;
|
||||
size_t pos = objStart + 1;
|
||||
objEnd = objStart;
|
||||
|
||||
while (pos < json.size() && braceCount > 0)
|
||||
{
|
||||
if (json[pos] == '{') braceCount++;
|
||||
else if (json[pos] == '}')
|
||||
{
|
||||
braceCount--;
|
||||
if (braceCount == 0) objEnd = pos;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (objEnd <= objStart) return "";
|
||||
|
||||
std::string objContent = json.substr(objStart, objEnd - objStart + 1);
|
||||
|
||||
// Parse hotkey fields
|
||||
bool win = false, ctrl = false, alt = false, shift = false;
|
||||
int code = 0;
|
||||
|
||||
// Helper to find boolean value
|
||||
auto findBool = [&objContent](const std::string& key) -> bool {
|
||||
size_t keyPos = objContent.find("\"" + key + "\"");
|
||||
if (keyPos == std::string::npos) return false;
|
||||
size_t colonPos = objContent.find(':', keyPos);
|
||||
if (colonPos == std::string::npos) return false;
|
||||
size_t valStart = colonPos + 1;
|
||||
while (valStart < objContent.size() && std::isspace(objContent[valStart])) valStart++;
|
||||
return objContent.substr(valStart, 4) == "true";
|
||||
};
|
||||
|
||||
// Helper to find integer value
|
||||
auto findInt = [&objContent](const std::string& key) -> int {
|
||||
size_t keyPos = objContent.find("\"" + key + "\"");
|
||||
if (keyPos == std::string::npos) return 0;
|
||||
size_t colonPos = objContent.find(':', keyPos);
|
||||
if (colonPos == std::string::npos) return 0;
|
||||
size_t valStart = colonPos + 1;
|
||||
while (valStart < objContent.size() && std::isspace(objContent[valStart])) valStart++;
|
||||
size_t valEnd = valStart;
|
||||
while (valEnd < objContent.size() && (std::isdigit(objContent[valEnd]) || objContent[valEnd] == '-'))
|
||||
valEnd++;
|
||||
if (valEnd > valStart)
|
||||
return std::stoi(objContent.substr(valStart, valEnd - valStart));
|
||||
return 0;
|
||||
};
|
||||
|
||||
win = findBool("win");
|
||||
ctrl = findBool("ctrl");
|
||||
alt = findBool("alt");
|
||||
shift = findBool("shift");
|
||||
code = findInt("code");
|
||||
|
||||
// Build hotkey string
|
||||
std::string result;
|
||||
if (win) result += "Win+";
|
||||
if (ctrl) result += "Ctrl+";
|
||||
if (alt) result += "Alt+";
|
||||
if (shift) result += "Shift+";
|
||||
|
||||
// Convert virtual key code to key name
|
||||
if (code > 0)
|
||||
{
|
||||
if (code >= 'A' && code <= 'Z')
|
||||
{
|
||||
result += static_cast<char>(code);
|
||||
}
|
||||
else if (code >= '0' && code <= '9')
|
||||
{
|
||||
result += static_cast<char>(code);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Common VK codes
|
||||
switch (code)
|
||||
{
|
||||
case 0x20: result += "Space"; break;
|
||||
case 0x0D: result += "Enter"; break;
|
||||
case 0x1B: result += "Escape"; break;
|
||||
case 0x09: result += "Tab"; break;
|
||||
case 0x08: result += "Backspace"; break;
|
||||
case 0x2E: result += "Delete"; break;
|
||||
case 0x24: result += "Home"; break;
|
||||
case 0x23: result += "End"; break;
|
||||
case 0x21: result += "PageUp"; break;
|
||||
case 0x22: result += "PageDown"; break;
|
||||
case 0x25: result += "Left"; break;
|
||||
case 0x26: result += "Up"; break;
|
||||
case 0x27: result += "Right"; break;
|
||||
case 0x28: result += "Down"; break;
|
||||
case 0x70: case 0x71: case 0x72: case 0x73: case 0x74: case 0x75:
|
||||
case 0x76: case 0x77: case 0x78: case 0x79: case 0x7A: case 0x7B:
|
||||
result += "F" + std::to_string(code - 0x70 + 1);
|
||||
break;
|
||||
case 0xC0: result += "`"; break;
|
||||
case 0xBD: result += "-"; break;
|
||||
case 0xBB: result += "="; break;
|
||||
case 0xDB: result += "["; break;
|
||||
case 0xDD: result += "]"; break;
|
||||
case 0xDC: result += "\\"; break;
|
||||
case 0xBA: result += ";"; break;
|
||||
case 0xDE: result += "'"; break;
|
||||
case 0xBC: result += ","; break;
|
||||
case 0xBE: result += "."; break;
|
||||
case 0xBF: result += "/"; break;
|
||||
default:
|
||||
result += "VK_0x" + std::to_string(code);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove trailing + if no key code
|
||||
if (!result.empty() && result.back() == '+')
|
||||
{
|
||||
result.pop_back();
|
||||
}
|
||||
|
||||
return result.empty() ? "(not set)" : result;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
#include <Windows.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
/// <summary>
|
||||
/// Utility class for discovering and loading PowerToy module settings
|
||||
@@ -31,6 +33,40 @@ public:
|
||||
/// <returns>Full path to the settings.json file</returns>
|
||||
std::wstring GetSettingsPath(const std::wstring& moduleName) const;
|
||||
|
||||
/// <summary>
|
||||
/// Display settings information for a module
|
||||
/// </summary>
|
||||
/// <param name="moduleName">Name of the module</param>
|
||||
/// <param name="moduleDllPath">Path to the module DLL</param>
|
||||
void DisplaySettingsInfo(const std::wstring& moduleName, const std::wstring& moduleDllPath);
|
||||
|
||||
/// <summary>
|
||||
/// Get a specific setting value
|
||||
/// </summary>
|
||||
/// <param name="moduleName">Name of the module</param>
|
||||
/// <param name="moduleDllPath">Path to the module DLL</param>
|
||||
/// <param name="key">Setting key to retrieve</param>
|
||||
/// <returns>Value as string, or empty if not found</returns>
|
||||
std::wstring GetSettingValue(const std::wstring& moduleName, const std::wstring& moduleDllPath, const std::wstring& key);
|
||||
|
||||
/// <summary>
|
||||
/// Set a specific setting value
|
||||
/// </summary>
|
||||
/// <param name="moduleName">Name of the module</param>
|
||||
/// <param name="moduleDllPath">Path to the module DLL</param>
|
||||
/// <param name="key">Setting key to set</param>
|
||||
/// <param name="value">Value to set</param>
|
||||
/// <returns>True if successful</returns>
|
||||
bool SetSettingValue(const std::wstring& moduleName, const std::wstring& moduleDllPath, const std::wstring& key, const std::wstring& value);
|
||||
|
||||
/// <summary>
|
||||
/// Find the actual settings file path (handles case-insensitivity)
|
||||
/// </summary>
|
||||
/// <param name="moduleName">Name of the module</param>
|
||||
/// <param name="moduleDllPath">Path to the module DLL</param>
|
||||
/// <returns>Actual path to settings.json, or empty if not found</returns>
|
||||
std::wstring FindSettingsFilePath(const std::wstring& moduleName, const std::wstring& moduleDllPath);
|
||||
|
||||
private:
|
||||
/// <summary>
|
||||
/// Get the PowerToys root settings directory
|
||||
@@ -44,4 +80,52 @@ private:
|
||||
/// <param name="filePath">Path to the file</param>
|
||||
/// <returns>File contents as a string</returns>
|
||||
std::wstring ReadFileContents(const std::wstring& filePath) const;
|
||||
|
||||
/// <summary>
|
||||
/// Write a string to a text file
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to the file</param>
|
||||
/// <param name="contents">Contents to write</param>
|
||||
/// <returns>True if successful</returns>
|
||||
bool WriteFileContents(const std::wstring& filePath, const std::wstring& contents) const;
|
||||
|
||||
/// <summary>
|
||||
/// Parse settings properties from JSON and display them
|
||||
/// </summary>
|
||||
/// <param name="settingsJson">JSON string containing settings</param>
|
||||
/// <param name="indent">Indentation level</param>
|
||||
void DisplayJsonProperties(const std::wstring& settingsJson, int indent = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Parse a hotkey object from JSON and format it as a string (e.g., "Win+Alt+U")
|
||||
/// </summary>
|
||||
/// <param name="json">JSON string</param>
|
||||
/// <param name="objStart">Start position of the hotkey object</param>
|
||||
/// <param name="objEnd">Output: end position of the hotkey object</param>
|
||||
/// <returns>Formatted hotkey string, or empty if not a valid hotkey</returns>
|
||||
std::string ParseHotkeyObject(const std::string& json, size_t objStart, size_t& objEnd);
|
||||
|
||||
/// <summary>
|
||||
/// Check if a JSON object appears to be a hotkey settings object
|
||||
/// </summary>
|
||||
/// <param name="json">JSON string</param>
|
||||
/// <param name="objStart">Start position of the object</param>
|
||||
/// <returns>True if this looks like a hotkey object</returns>
|
||||
bool IsHotkeyObject(const std::string& json, size_t objStart);
|
||||
|
||||
/// <summary>
|
||||
/// Prompt user for yes/no confirmation
|
||||
/// </summary>
|
||||
/// <param name="prompt">The question to ask</param>
|
||||
/// <returns>True if user answered yes</returns>
|
||||
bool PromptYesNo(const std::wstring& prompt);
|
||||
|
||||
/// <summary>
|
||||
/// Add a new property to the JSON settings file
|
||||
/// </summary>
|
||||
/// <param name="json">The JSON string to modify</param>
|
||||
/// <param name="key">The property key to add</param>
|
||||
/// <param name="value">The value to set</param>
|
||||
/// <returns>Modified JSON string, or empty if failed</returns>
|
||||
std::string AddNewProperty(const std::string& json, const std::string& key, const std::string& value);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user