Skip to content

Instantly share code, notes, and snippets.

@m417z
Last active May 23, 2025 17:36
Show Gist options
  • Save m417z/3b2dafc3fc2f7e88dffe04883f72c8cf to your computer and use it in GitHub Desktop.
Save m417z/3b2dafc3fc2f7e88dffe04883f72c8cf to your computer and use it in GitHub Desktop.
// ==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, &current, &size);
if (current != val) return false;
RegGetValueW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", L"SystemUsesLightTheme", RRF_RT_REG_DWORD, nullptr, &current, &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