Files
PowerToys/tools/module_loader/src/SettingsLoader.cpp
Niels Laute 86860df314 [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>
2026-03-04 13:56:32 +00:00

904 lines
30 KiB
C++

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