Last active
May 23, 2025 17:36
-
-
Save m417z/3b2dafc3fc2f7e88dffe04883f72c8cf to your computer and use it in GitHub Desktop.
"Tool mod" implementation. Context: https://github.com/ramensoftware/windhawk-mods/pull/1916#issuecomment-2889149966
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==WindhawkMod== | |
// @id auto-theme-switcher | |
// @name Auto Theme Switcher | |
// @description Automatically changes between light and dark mode/themes based on a set schedule | |
// @version 1.0 | |
// @author tinodin | |
// @github https://github.com/tinodin | |
// @include windhawk.exe | |
// @compilerOptions -lole32 -loleaut32 | |
// ==/WindhawkMod== | |
// ==WindhawkModSettings== | |
/* | |
- Light: 07:00 | |
$name: Light mode time | |
- Dark: 19:00 | |
$name: Dark mode time | |
- LightThemePath: "C:\\Windows\\Resources\\Themes\\aero.theme" | |
$name: Light mode theme path (.theme) | |
- DarkThemePath: "C:\\Windows\\Resources\\Themes\\dark.theme" | |
$name: Dark mode theme path (.theme) | |
- LockScreen: true | |
$name: Apply Wallpaper to Lock screen | |
*/ | |
// ==/WindhawkModSettings== | |
#include <fstream> | |
#include <combaseapi.h> | |
#include <comdef.h> | |
#include <winrt/base.h> | |
HANDLE g_timerThread = nullptr; | |
HANDLE g_wakeEvent = nullptr; | |
bool g_exitFlag = false; | |
SYSTEMTIME g_lightTime, g_darkTime; | |
std::wstring g_lightThemePath, g_darkThemePath; | |
enum Appearance { | |
light, | |
dark | |
}; | |
void ApplyLockScreen() { | |
std::wstring wallpaperPath; | |
wchar_t currentWallpaper[MAX_PATH] = {0}; | |
DWORD size = sizeof(currentWallpaper); | |
if (RegGetValueW(HKEY_CURRENT_USER, L"Control Panel\\Desktop", L"WallPaper", RRF_RT_REG_SZ, nullptr, currentWallpaper, &size) != ERROR_SUCCESS) | |
{ | |
return; | |
} | |
wallpaperPath = currentWallpaper; | |
HKEY hKey; | |
if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Policies\\Microsoft\\Windows\\Personalization", 0, nullptr, 0, KEY_SET_VALUE, nullptr, &hKey, nullptr) == ERROR_SUCCESS) | |
{ | |
RegSetValueExW(hKey, L"LockScreenImage", 0, REG_SZ, (const BYTE*)wallpaperPath.c_str(), (DWORD)((wallpaperPath.size() + 1) * sizeof(wchar_t))); | |
RegCloseKey(hKey); | |
} | |
if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\PersonalizationCSP", 0, nullptr, 0, KEY_SET_VALUE, nullptr, &hKey, nullptr) == ERROR_SUCCESS) | |
{ | |
for (PCWSTR valueName : { L"LockScreenImagePath", L"LockScreenImageUrl" }) { | |
RegSetValueExW(hKey, valueName, 0, REG_SZ, (const BYTE*)wallpaperPath.c_str(), (DWORD)((wallpaperPath.size() + 1) * sizeof(wchar_t))); | |
} | |
RegCloseKey(hKey); | |
} | |
Wh_Log(L"[Theme] Applied as Lock Screen"); | |
} | |
bool IsAppearanceApplied(Appearance appearance) { | |
DWORD val = (appearance == light) ? 1 : 0, current = 1, size = sizeof(DWORD); | |
RegGetValueW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", L"AppsUseLightTheme", RRF_RT_REG_DWORD, nullptr, ¤t, &size); | |
if (current != val) return false; | |
RegGetValueW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", L"SystemUsesLightTheme", RRF_RT_REG_DWORD, nullptr, ¤t, &size); | |
return current == val; | |
} | |
bool IsThemeApplied(PCWSTR themePath) { | |
wchar_t currentTheme[MAX_PATH] = {0}; | |
DWORD size = sizeof(currentTheme); | |
if (RegGetValueW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes", L"CurrentTheme", RRF_RT_REG_SZ, nullptr, currentTheme, &size) != ERROR_SUCCESS) | |
return false; | |
if (_wcsicmp(currentTheme, themePath) != 0) | |
return false; | |
DWORD appsLight = 1, systemLight = 1, dataSize = sizeof(DWORD); | |
if (RegGetValueW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", L"AppsUseLightTheme", RRF_RT_REG_DWORD, nullptr, &appsLight, &dataSize) != ERROR_SUCCESS) | |
return false; | |
if (RegGetValueW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", L"SystemUsesLightTheme", RRF_RT_REG_DWORD, nullptr, &systemLight, &dataSize) != ERROR_SUCCESS) | |
return false; | |
std::wifstream file(themePath); | |
if (!file) | |
return false; | |
bool inVisualStyles = false; | |
std::wstring line, systemMode, appMode; | |
while (std::getline(file, line)) { | |
if (line.empty() || line[0] == L';') continue; | |
if (line[0] == L'[') { | |
inVisualStyles = (line == L"[VisualStyles]"); | |
continue; | |
} | |
if (inVisualStyles) { | |
if (line.find(L"SystemMode=") == 0) | |
systemMode = line.substr(11); | |
else if (line.find(L"AppMode=") == 0) | |
appMode = line.substr(8); | |
if (!systemMode.empty() && !appMode.empty()) | |
break; | |
} | |
} | |
auto isLight = [](const std::wstring& s) { return _wcsicmp(s.c_str(), L"Light") == 0; }; | |
bool themeLight = isLight(systemMode) && isLight(appMode); | |
return (appsLight == (themeLight ? 1 : 0)) && (systemLight == (themeLight ? 1 : 0)); | |
} | |
void ApplyAppearance(Appearance appearance) { | |
DWORD val = (appearance == light) ? 1 : 0; | |
// change appearance | |
RegSetKeyValueW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", L"AppsUseLightTheme", REG_DWORD, &val, sizeof(val)); | |
RegSetKeyValueW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", L"SystemUsesLightTheme", REG_DWORD, &val, sizeof(val)); | |
// broadcast the change | |
SendMessageTimeoutW(HWND_BROADCAST, WM_SETTINGCHANGE, 0, (LPARAM)L"ImmersiveColorSet", SMTO_ABORTIFHUNG, 100, nullptr); | |
Wh_Log(L"[Theme] Applied %s Mode.", appearance == light ? L"Light" : L"Dark"); | |
// apply wallpaper to lock screen | |
if (Wh_GetIntSetting(L"LockScreen")) | |
ApplyLockScreen(); | |
} | |
// Based on: | |
// https://github.com/qwerty12/AutoHotkeyScripts/blob/a9423f59c945a3a031cb38b25cf461a34de9a6d3/SetThemeFromdotThemeFile.ahk | |
void ApplyTheme(PCWSTR themePath) { | |
std::thread([=]() { | |
Wh_Log(L"[Theme] Waiting for explorer to load..."); | |
for (;;) { | |
HWND progman = FindWindowW(L"Progman", nullptr); | |
HWND tray = FindWindowW(L"Shell_TrayWnd", nullptr); | |
if (progman && tray && IsWindowVisible(tray)) | |
break; | |
Sleep(500); | |
} | |
Wh_Log(L"[Theme] Explorer loaded"); | |
if (!themePath || !*themePath) { | |
Wh_Log(L"[Theme] No theme path specified."); | |
return; | |
} | |
CoInitialize(nullptr); | |
// {C04B329E-5823-4415-9C93-BA44688947B0} | |
constexpr winrt::guid CLSID_IThemeManager{ | |
0xC04B329E, | |
0x5823, | |
0x4415, | |
{0x9C, 0x93, 0xBA, 0x44, 0x68, 0x89, 0x47, 0xB0}}; | |
// {0646EBBE-C1B7-4045-8FD0-FFD65D3FC792} | |
constexpr winrt::guid IID_IThemeManager{ | |
0x0646EBBE, | |
0xC1B7, | |
0x4045, | |
{0x8F, 0xD0, 0xFF, 0xD6, 0x5D, 0x3F, 0xC7, 0x92}}; | |
winrt::com_ptr<IUnknown> pThemeManager; | |
HRESULT hr = | |
CoCreateInstance(CLSID_IThemeManager, nullptr, CLSCTX_INPROC_SERVER, | |
IID_IThemeManager, pThemeManager.put_void()); | |
if (FAILED(hr) || !pThemeManager) { | |
Wh_Log(L"[Theme] Failed to apply theme."); | |
} | |
_bstr_t bstrTheme(themePath); | |
void** vtable = *(void***)pThemeManager.get(); | |
using ApplyThemeFunc = HRESULT(WINAPI*)(IUnknown*, BSTR); | |
ApplyThemeFunc ApplyThemeMethod = (ApplyThemeFunc)vtable[4]; | |
hr = ApplyThemeMethod(pThemeManager.get(), bstrTheme); | |
if (FAILED(hr)) { | |
Wh_Log(L"[Theme] Failed to apply theme."); | |
} | |
Wh_Log(L"[Theme] Successfully applied theme"); | |
CoUninitialize(); | |
if (Wh_GetIntSetting(L"LockScreen")) | |
ApplyLockScreen(); | |
}).detach(); | |
} | |
SYSTEMTIME ParseScheduleTime(PCWSTR timeStr) { | |
SYSTEMTIME st = {}; | |
swscanf_s(timeStr, L"%hu:%hu", &st.wHour, &st.wMinute); | |
return st; | |
} | |
time_t GetNextSwitch(const SYSTEMTIME& light, const SYSTEMTIME& dark, bool& nextLight) { | |
time_t now = time(nullptr); | |
struct tm local; | |
localtime_s(&local, &now); | |
auto makeTime = [&](const SYSTEMTIME& st) { | |
struct tm t = local; | |
t.tm_hour = st.wHour; t.tm_min = st.wMinute; t.tm_sec = 0; | |
return mktime(&t); | |
}; | |
time_t lightT = makeTime(light); | |
time_t darkT = makeTime(dark); | |
bool isLightNow; | |
if (lightT < darkT) | |
isLightNow = now >= lightT && now < darkT; | |
else | |
isLightNow = now >= lightT || now < darkT; | |
if (isLightNow) { | |
nextLight = false; | |
if (darkT <= now) darkT += 86400; | |
return darkT; | |
} else { | |
nextLight = true; | |
if (lightT <= now) lightT += 86400; | |
return lightT; | |
} | |
} | |
DWORD WINAPI ThemeScheduler(LPVOID) { | |
while (!g_exitFlag) { | |
bool nextLight; | |
time_t now = time(nullptr); | |
time_t nextSwitch = GetNextSwitch(g_lightTime, g_darkTime, nextLight); | |
int waitTime = (int)(nextSwitch - now); | |
DWORD res = WaitForSingleObject(g_wakeEvent, waitTime * 1000); | |
if (res == WAIT_OBJECT_0) { | |
if (g_exitFlag) break; | |
continue; | |
} | |
PCWSTR themePath = nextLight ? g_lightThemePath.c_str() : g_darkThemePath.c_str(); | |
if (*themePath) { | |
if (IsThemeApplied(themePath)) { | |
Wh_Log(L"[Theme] Theme already applied."); | |
continue; | |
} | |
ApplyTheme(themePath); | |
} else { | |
if (IsAppearanceApplied(nextLight ? light : dark)) { | |
Wh_Log(L"[Theme] Appearance already applied."); | |
continue; | |
} | |
ApplyAppearance(nextLight ? light : dark); | |
} | |
} | |
return 0; | |
} | |
void StartScheduler() { | |
if (g_timerThread) { | |
SetEvent(g_wakeEvent); | |
return; | |
} | |
g_wakeEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr); | |
g_timerThread = CreateThread(nullptr, 0, ThemeScheduler, nullptr, 0, nullptr); | |
} | |
std::wstring TrimQuotes(const std::wstring& str) { | |
size_t start = 0; | |
size_t end = str.length(); | |
if (!str.empty() && str.front() == L'"') start = 1; | |
if (end > start && str[end - 1] == L'"') end--; | |
return str.substr(start, end - start); | |
} | |
void LoadSettings() { | |
g_lightTime = ParseScheduleTime(Wh_GetStringSetting(L"Light")); | |
g_darkTime = ParseScheduleTime(Wh_GetStringSetting(L"Dark")); | |
auto rawLightPath = Wh_GetStringSetting(L"LightThemePath"); | |
g_lightThemePath = rawLightPath ? TrimQuotes(rawLightPath) : L""; | |
auto rawDarkPath = Wh_GetStringSetting(L"DarkThemePath"); | |
g_darkThemePath = rawDarkPath ? TrimQuotes(rawDarkPath) : L""; | |
time_t now = time(nullptr); | |
struct tm local; | |
localtime_s(&local, &now); | |
auto makeTime = [&](const SYSTEMTIME& st) { | |
struct tm t = local; | |
t.tm_hour = st.wHour; t.tm_min = st.wMinute; t.tm_sec = 0; | |
return mktime(&t); | |
}; | |
time_t lightT = makeTime(g_lightTime); | |
time_t darkT = makeTime(g_darkTime); | |
bool isLightNow; | |
if (lightT < darkT) | |
isLightNow = now >= lightT && now < darkT; | |
else | |
isLightNow = now >= lightT || now < darkT; | |
PCWSTR themePath = isLightNow ? g_lightThemePath.c_str() : g_darkThemePath.c_str(); | |
if (*themePath) { | |
if (IsThemeApplied(themePath)) { | |
Wh_Log(L"[Theme] Theme already applied."); | |
return; | |
} | |
ApplyTheme(themePath); | |
} else { | |
if (IsAppearanceApplied(isLightNow ? light : dark)) { | |
Wh_Log(L"[Theme] Appearance already applied."); | |
return; | |
} | |
ApplyAppearance(isLightNow ? light : dark); | |
} | |
StartScheduler(); | |
} | |
BOOL WhTool_ModInit() { | |
LoadSettings(); | |
return TRUE; | |
} | |
void WhTool_ModSettingsChanged() { | |
LoadSettings(); | |
} | |
void WhTool_ModUninit() { | |
g_exitFlag = true; | |
if (g_wakeEvent) SetEvent(g_wakeEvent); | |
if (g_timerThread) { | |
WaitForSingleObject(g_timerThread, INFINITE); | |
CloseHandle(g_timerThread); | |
} | |
if (g_wakeEvent) CloseHandle(g_wakeEvent); | |
} | |
//////////////////////////////////////////////////////////////////////////////// | |
// Windhawk tool mod implementation for mods which don't need to inject to other | |
// processes or hook other functions. Context: | |
// https://github.com/ramensoftware/windhawk-mods/pull/1916 | |
// | |
// The mod will load and run in a dedicated windhawk.exe process. | |
// | |
// Paste the code below as part of the mod code, and use these callbacks: | |
// * WhTool_ModInit | |
// * WhTool_ModSettingsChanged | |
// * WhTool_ModUninit | |
// | |
// Currently, other callbacks are not supported. | |
bool g_isToolModProcessLauncher; | |
HANDLE g_toolModProcessMutex; | |
void WINAPI EntryPoint_Hook() { | |
Wh_Log(L">"); | |
ExitThread(0); | |
} | |
BOOL Wh_ModInit() { | |
bool isService = false; | |
bool isToolModProcess = false; | |
bool isCurrentToolModProcess = false; | |
int argc; | |
LPWSTR* argv = CommandLineToArgvW(GetCommandLine(), &argc); | |
if (!argv) { | |
Wh_Log(L"CommandLineToArgvW failed"); | |
return FALSE; | |
} | |
for (int i = 1; i < argc; i++) { | |
if (wcscmp(argv[i], L"-service") == 0) { | |
isService = true; | |
break; | |
} | |
} | |
for (int i = 1; i < argc - 1; i++) { | |
if (wcscmp(argv[i], L"-tool-mod") == 0) { | |
isToolModProcess = true; | |
if (wcscmp(argv[i + 1], WH_MOD_ID) == 0) { | |
isCurrentToolModProcess = true; | |
} | |
break; | |
} | |
} | |
LocalFree(argv); | |
if (isService) { | |
return FALSE; | |
} | |
if (isCurrentToolModProcess) { | |
g_toolModProcessMutex = | |
CreateMutex(nullptr, TRUE, L"windhawk-tool-mod_" WH_MOD_ID); | |
if (!g_toolModProcessMutex) { | |
Wh_Log(L"CreateMutex failed"); | |
ExitProcess(1); | |
} | |
if (GetLastError() == ERROR_ALREADY_EXISTS) { | |
Wh_Log(L"Tool mod already running (%s)", WH_MOD_ID); | |
ExitProcess(1); | |
} | |
if (!WhTool_ModInit()) { | |
ExitProcess(1); | |
} | |
IMAGE_DOS_HEADER* dosHeader = | |
(IMAGE_DOS_HEADER*)GetModuleHandle(nullptr); | |
IMAGE_NT_HEADERS* ntHeaders = | |
(IMAGE_NT_HEADERS*)((BYTE*)dosHeader + dosHeader->e_lfanew); | |
DWORD entryPointRVA = ntHeaders->OptionalHeader.AddressOfEntryPoint; | |
void* entryPoint = (BYTE*)dosHeader + entryPointRVA; | |
Wh_SetFunctionHook(entryPoint, (void*)EntryPoint_Hook, nullptr); | |
return TRUE; | |
} | |
if (isToolModProcess) { | |
return FALSE; | |
} | |
g_isToolModProcessLauncher = true; | |
return TRUE; | |
} | |
void Wh_ModAfterInit() { | |
if (!g_isToolModProcessLauncher) { | |
return; | |
} | |
WCHAR currentProcessPath[MAX_PATH]; | |
switch (GetModuleFileName(nullptr, currentProcessPath, | |
ARRAYSIZE(currentProcessPath))) { | |
case 0: | |
case ARRAYSIZE(currentProcessPath): | |
Wh_Log(L"GetModuleFileName failed"); | |
return; | |
} | |
WCHAR | |
commandLine[MAX_PATH + 2 + | |
(sizeof(L" -tool-mod \"" WH_MOD_ID "\"") / sizeof(WCHAR)) - 1]; | |
swprintf_s(commandLine, L"\"%s\" -tool-mod \"%s\"", currentProcessPath, | |
WH_MOD_ID); | |
HMODULE kernelModule = GetModuleHandle(L"kernelbase.dll"); | |
if (!kernelModule) { | |
kernelModule = GetModuleHandle(L"kernel32.dll"); | |
if (!kernelModule) { | |
Wh_Log(L"No kernelbase.dll/kernel32.dll"); | |
return; | |
} | |
} | |
using CreateProcessInternalW_t = BOOL(WINAPI*)( | |
HANDLE hUserToken, LPCWSTR lpApplicationName, LPWSTR lpCommandLine, | |
LPSECURITY_ATTRIBUTES lpProcessAttributes, | |
LPSECURITY_ATTRIBUTES lpThreadAttributes, WINBOOL bInheritHandles, | |
DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, | |
LPSTARTUPINFOW lpStartupInfo, | |
LPPROCESS_INFORMATION lpProcessInformation, | |
PHANDLE hRestrictedUserToken); | |
CreateProcessInternalW_t pCreateProcessInternalW = | |
(CreateProcessInternalW_t)GetProcAddress(kernelModule, | |
"CreateProcessInternalW"); | |
if (!pCreateProcessInternalW) { | |
Wh_Log(L"No CreateProcessInternalW"); | |
return; | |
} | |
STARTUPINFO si{ | |
.cb = sizeof(STARTUPINFO), | |
.dwFlags = STARTF_FORCEOFFFEEDBACK, | |
}; | |
PROCESS_INFORMATION pi; | |
if (!pCreateProcessInternalW(nullptr, currentProcessPath, commandLine, | |
nullptr, nullptr, FALSE, NORMAL_PRIORITY_CLASS, | |
nullptr, nullptr, &si, &pi, nullptr)) { | |
Wh_Log(L"CreateProcess failed"); | |
return; | |
} | |
CloseHandle(pi.hProcess); | |
CloseHandle(pi.hThread); | |
} | |
void Wh_ModSettingsChanged() { | |
if (g_isToolModProcessLauncher) { | |
return; | |
} | |
WhTool_ModSettingsChanged(); | |
} | |
void Wh_ModUninit() { | |
if (g_isToolModProcessLauncher) { | |
return; | |
} | |
WhTool_ModUninit(); | |
ExitProcess(0); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment