Skip to content

Commit acfafad

Browse files
Create theme-toggler-tray.cpp (#4022)
* Create theme-toggler-tray.cpp This mod adds a customizable system tray icon to quickly toggle between Light and Dark mode in Windows. * Rename theme-toggler-tray.cpp to theme-toggler-tray.wh.cpp * Update theme-toggler-tray.wh.cpp convert to standalone tool and fix startup icon issue * Update theme-toggler-tray.wh.cpp * fix: restore standard tool mod boilerplate and fix formatting Restored the official Windhawk tool mod boilerplate at the end of the file to match the reference implementation exactly. Changes: - Reverted all custom formatting and logic simplifications in the boilerplate section. - Restored original Wh_Log statements and comment blocks as prescribed in the Wiki. - Ensured the mod remains compliant with the "Mods as tools" dedicated process architecture for better shell stability.
1 parent baa1a76 commit acfafad

1 file changed

Lines changed: 327 additions & 0 deletions

File tree

mods/theme-toggler-tray.wh.cpp

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
// ==WindhawkMod==
2+
// @id theme-toggler-tray
3+
// @name Theme Toggler Tray
4+
// @description Add a system tray button to toggle between Light and Dark mode instantly.
5+
// @version 1.2.1
6+
// @author Husam-Abdulraheem
7+
// @github https://github.com/Husam-Abdulraheem
8+
// @include windhawk.exe
9+
// ==/WindhawkMod==
10+
11+
// ==WindhawkModReadme==
12+
/*
13+
# Theme Toggler Tray (Standalone Mode)
14+
15+
This mod now runs in a dedicated Windhawk process for better stability.
16+
Click the tray icon to instantly toggle between Dark and Light mode.
17+
*/
18+
// ==/WindhawkModReadme==
19+
20+
// ==WindhawkModSettings==
21+
/*
22+
- icon_file: shell32.dll
23+
$name: Icon File (DLL or EXE)
24+
- icon_index: 25
25+
$name: Icon Index
26+
- tooltip_text: Toggle Light/Dark Theme
27+
$name: Tooltip Text
28+
*/
29+
// ==/WindhawkModSettings==
30+
31+
#include <windows.h>
32+
#include <shellapi.h>
33+
#include <stdio.h>
34+
35+
#define WM_USER_TRAYICON (WM_USER + 1)
36+
#define WM_USER_UPDATESETTINGS (WM_USER + 2)
37+
38+
// --- Global Variables ---
39+
HWND g_hWnd = NULL;
40+
NOTIFYICONDATAW g_nid = {0};
41+
HANDLE g_hThread = NULL;
42+
UINT g_uMsgTaskbarCreated = 0;
43+
44+
// --- Functional Logic ---
45+
46+
void ToggleTheme() {
47+
HKEY hKey;
48+
LPCWSTR path = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
49+
if (RegOpenKeyExW(HKEY_CURRENT_USER, path, 0, KEY_READ | KEY_WRITE, &hKey) == ERROR_SUCCESS) {
50+
DWORD value = 0;
51+
DWORD size = sizeof(DWORD);
52+
if (RegQueryValueExW(hKey, L"SystemUsesLightTheme", NULL, NULL, (LPBYTE)&value, &size) == ERROR_SUCCESS) {
53+
DWORD newValue = (value == 0) ? 1 : 0;
54+
RegSetValueExW(hKey, L"SystemUsesLightTheme", 0, REG_DWORD, (const BYTE*)&newValue, sizeof(newValue));
55+
RegSetValueExW(hKey, L"AppsUseLightTheme", 0, REG_DWORD, (const BYTE*)&newValue, sizeof(newValue));
56+
SendMessageTimeoutW(HWND_BROADCAST, WM_SETTINGCHANGE, 0, (LPARAM)L"ImmersiveColorSet", SMTO_ABORTIFHUNG, 5000, NULL);
57+
}
58+
RegCloseKey(hKey);
59+
}
60+
}
61+
62+
void ApplySettingsToTray() {
63+
PCWSTR iconFile = Wh_GetStringSetting(L"icon_file");
64+
int iconIndex = Wh_GetIntSetting(L"icon_index");
65+
PCWSTR tooltipText = Wh_GetStringSetting(L"tooltip_text");
66+
67+
HICON hOldIcon = g_nid.hIcon;
68+
ExtractIconExW(iconFile, iconIndex, NULL, &g_nid.hIcon, 1);
69+
if (!g_nid.hIcon) g_nid.hIcon = (HICON)LoadImageW(NULL, iconFile, IMAGE_ICON, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE);
70+
if (!g_nid.hIcon) g_nid.hIcon = LoadIcon(NULL, IDI_INFORMATION);
71+
72+
wcscpy_s(g_nid.szTip, tooltipText);
73+
if (!Shell_NotifyIconW(NIM_MODIFY, &g_nid)) Shell_NotifyIconW(NIM_ADD, &g_nid);
74+
75+
if (hOldIcon && hOldIcon != g_nid.hIcon) DestroyIcon(hOldIcon);
76+
Wh_FreeStringSetting(iconFile);
77+
Wh_FreeStringSetting(tooltipText);
78+
}
79+
80+
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
81+
if (g_uMsgTaskbarCreated != 0 && uMsg == g_uMsgTaskbarCreated) {
82+
Shell_NotifyIconW(NIM_ADD, &g_nid);
83+
ApplySettingsToTray();
84+
return 0;
85+
}
86+
switch (uMsg) {
87+
case WM_USER_TRAYICON:
88+
if (LOWORD(lParam) == WM_LBUTTONUP) ToggleTheme();
89+
return 0;
90+
case WM_USER_UPDATESETTINGS:
91+
ApplySettingsToTray();
92+
return 0;
93+
case WM_DESTROY:
94+
Shell_NotifyIconW(NIM_DELETE, &g_nid);
95+
if (g_nid.hIcon) DestroyIcon(g_nid.hIcon);
96+
PostQuitMessage(0);
97+
return 0;
98+
}
99+
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
100+
}
101+
102+
DWORD WINAPI TrayThread(LPVOID lpParam) {
103+
g_uMsgTaskbarCreated = RegisterWindowMessageW(L"TaskbarCreated");
104+
WNDCLASSW wc = {0};
105+
wc.lpfnWndProc = WindowProc;
106+
wc.hInstance = GetModuleHandleW(NULL);
107+
wc.lpszClassName = L"ThemeTogglerToolWindow";
108+
RegisterClassW(&wc);
109+
110+
g_hWnd = CreateWindowExW(0, wc.lpszClassName, L"", 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, wc.hInstance, NULL);
111+
if (g_hWnd) {
112+
g_nid.cbSize = sizeof(g_nid);
113+
g_nid.hWnd = g_hWnd;
114+
g_nid.uID = 1001;
115+
g_nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
116+
g_nid.uCallbackMessage = WM_USER_TRAYICON;
117+
Shell_NotifyIconW(NIM_ADD, &g_nid);
118+
ApplySettingsToTray();
119+
120+
MSG msg;
121+
while (GetMessage(&msg, NULL, 0, 0)) {
122+
TranslateMessage(&msg);
123+
DispatchMessage(&msg);
124+
}
125+
}
126+
return 0;
127+
}
128+
129+
// --- WhTool Callbacks ---
130+
131+
BOOL WhTool_ModInit() {
132+
g_hThread = CreateThread(NULL, 0, TrayThread, NULL, 0, NULL);
133+
return (g_hThread != NULL);
134+
}
135+
136+
void WhTool_ModSettingsChanged() {
137+
if (g_hWnd) PostMessage(g_hWnd, WM_USER_UPDATESETTINGS, 0, 0);
138+
}
139+
140+
void WhTool_ModUninit() {
141+
if (g_hWnd) SendMessageW(g_hWnd, WM_CLOSE, 0, 0);
142+
if (g_hThread) {
143+
WaitForSingleObject(g_hThread, 2000);
144+
CloseHandle(g_hThread);
145+
}
146+
}
147+
148+
// --- Windhawk Tool Mod Boilerplate (Do not modify) ---
149+
150+
////////////////////////////////////////////////////////////////////////////////
151+
// Windhawk tool mod implementation for mods which don't need to inject to other
152+
// processes or hook other functions. Context:
153+
// https://github.com/ramensoftware/windhawk/wiki/Mods-as-tools:-Running-mods-in-a-dedicated-process
154+
//
155+
// The mod will load and run in a dedicated windhawk.exe process.
156+
//
157+
// Paste the code below as part of the mod code, and use these callbacks:
158+
// * WhTool_ModInit
159+
// * WhTool_ModSettingsChanged
160+
// * WhTool_ModUninit
161+
//
162+
// Currently, other callbacks are not supported.
163+
164+
bool g_isToolModProcessLauncher;
165+
HANDLE g_toolModProcessMutex;
166+
167+
void WINAPI EntryPoint_Hook() {
168+
Wh_Log(L">");
169+
ExitThread(0);
170+
}
171+
172+
BOOL Wh_ModInit() {
173+
DWORD sessionId;
174+
if (ProcessIdToSessionId(GetCurrentProcessId(), &sessionId) &&
175+
sessionId == 0) {
176+
return FALSE;
177+
}
178+
179+
bool isExcluded = false;
180+
bool isToolModProcess = false;
181+
bool isCurrentToolModProcess = false;
182+
int argc;
183+
LPWSTR* argv = CommandLineToArgvW(GetCommandLine(), &argc);
184+
if (!argv) {
185+
Wh_Log(L"CommandLineToArgvW failed");
186+
return FALSE;
187+
}
188+
189+
for (int i = 1; i < argc; i++) {
190+
if (wcscmp(argv[i], L"-service") == 0 ||
191+
wcscmp(argv[i], L"-service-start") == 0 ||
192+
wcscmp(argv[i], L"-service-stop") == 0) {
193+
isExcluded = true;
194+
break;
195+
}
196+
}
197+
198+
for (int i = 1; i < argc - 1; i++) {
199+
if (wcscmp(argv[i], L"-tool-mod") == 0) {
200+
isToolModProcess = true;
201+
if (wcscmp(argv[i + 1], WH_MOD_ID) == 0) {
202+
isCurrentToolModProcess = true;
203+
}
204+
break;
205+
}
206+
}
207+
208+
LocalFree(argv);
209+
210+
if (isExcluded) {
211+
return FALSE;
212+
}
213+
214+
if (isCurrentToolModProcess) {
215+
g_toolModProcessMutex =
216+
CreateMutex(nullptr, TRUE, L"windhawk-tool-mod_" WH_MOD_ID);
217+
if (!g_toolModProcessMutex) {
218+
Wh_Log(L"CreateMutex failed");
219+
ExitProcess(1);
220+
}
221+
222+
if (GetLastError() == ERROR_ALREADY_EXISTS) {
223+
Wh_Log(L"Tool mod already running (%s)", WH_MOD_ID);
224+
ExitProcess(1);
225+
}
226+
227+
if (!WhTool_ModInit()) {
228+
ExitProcess(1);
229+
}
230+
231+
IMAGE_DOS_HEADER* dosHeader =
232+
(IMAGE_DOS_HEADER*)GetModuleHandle(nullptr);
233+
IMAGE_NT_HEADERS* ntHeaders =
234+
(IMAGE_NT_HEADERS*)((BYTE*)dosHeader + dosHeader->e_lfanew);
235+
236+
DWORD entryPointRVA = ntHeaders->OptionalHeader.AddressOfEntryPoint;
237+
void* entryPoint = (BYTE*)dosHeader + entryPointRVA;
238+
239+
Wh_SetFunctionHook(entryPoint, (void*)EntryPoint_Hook, nullptr);
240+
return TRUE;
241+
}
242+
243+
if (isToolModProcess) {
244+
return FALSE;
245+
}
246+
247+
g_isToolModProcessLauncher = true;
248+
return TRUE;
249+
}
250+
251+
void Wh_ModAfterInit() {
252+
if (!g_isToolModProcessLauncher) {
253+
return;
254+
}
255+
256+
WCHAR currentProcessPath[MAX_PATH];
257+
switch (GetModuleFileName(nullptr, currentProcessPath,
258+
ARRAYSIZE(currentProcessPath))) {
259+
case 0:
260+
case ARRAYSIZE(currentProcessPath):
261+
Wh_Log(L"GetModuleFileName failed");
262+
return;
263+
}
264+
265+
WCHAR
266+
commandLine[MAX_PATH + 2 +
267+
(sizeof(L" -tool-mod \"" WH_MOD_ID "\"") / sizeof(WCHAR)) - 1];
268+
swprintf_s(commandLine, L"\"%s\" -tool-mod \"%s\"", currentProcessPath,
269+
WH_MOD_ID);
270+
271+
HMODULE kernelModule = GetModuleHandle(L"kernelbase.dll");
272+
if (!kernelModule) {
273+
kernelModule = GetModuleHandle(L"kernel32.dll");
274+
if (!kernelModule) {
275+
Wh_Log(L"No kernelbase.dll/kernel32.dll");
276+
return;
277+
}
278+
}
279+
280+
using CreateProcessInternalW_t = BOOL(WINAPI*)(
281+
HANDLE hUserToken, LPCWSTR lpApplicationName, LPWSTR lpCommandLine,
282+
LPSECURITY_ATTRIBUTES lpProcessAttributes,
283+
LPSECURITY_ATTRIBUTES lpThreadAttributes, WINBOOL bInheritHandles,
284+
DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory,
285+
LPSTARTUPINFOW lpStartupInfo,
286+
LPPROCESS_INFORMATION lpProcessInformation,
287+
PHANDLE hRestrictedUserToken);
288+
CreateProcessInternalW_t pCreateProcessInternalW =
289+
(CreateProcessInternalW_t)GetProcAddress(kernelModule,
290+
"CreateProcessInternalW");
291+
if (!pCreateProcessInternalW) {
292+
Wh_Log(L"No CreateProcessInternalW");
293+
return;
294+
}
295+
296+
STARTUPINFO si{
297+
.cb = sizeof(STARTUPINFO),
298+
.dwFlags = STARTF_FORCEOFFFEEDBACK,
299+
};
300+
PROCESS_INFORMATION pi;
301+
if (!pCreateProcessInternalW(nullptr, currentProcessPath, commandLine,
302+
nullptr, nullptr, FALSE, NORMAL_PRIORITY_CLASS,
303+
nullptr, nullptr, &si, &pi, nullptr)) {
304+
Wh_Log(L"CreateProcess failed");
305+
return;
306+
}
307+
308+
CloseHandle(pi.hProcess);
309+
CloseHandle(pi.hThread);
310+
}
311+
312+
void Wh_ModSettingsChanged() {
313+
if (g_isToolModProcessLauncher) {
314+
return;
315+
}
316+
317+
WhTool_ModSettingsChanged();
318+
}
319+
320+
void Wh_ModUninit() {
321+
if (g_isToolModProcessLauncher) {
322+
return;
323+
}
324+
325+
WhTool_ModUninit();
326+
ExitProcess(0);
327+
}

0 commit comments

Comments
 (0)