├── .gitattributes ├── dlgbmp.bmp ├── icon.ico ├── bannrbmp.bmp ├── screenshot.png ├── docs ├── screenshot.png └── index.html ├── update-docs.cmd ├── .gitignore ├── test.cmd ├── toggle.wixproj ├── .vscode ├── c_cpp_properties.json ├── launch.json ├── tasks.json └── settings.json ├── Makefile ├── LICENSE.txt ├── toggle.exe.manifest ├── CMakeLists.txt ├── vc.bat ├── toggle.rc ├── LICENSE.rtf ├── README.md ├── toggle.wxs └── toggle.c /.gitattributes: -------------------------------------------------------------------------------- 1 | Makefile eol=lf -------------------------------------------------------------------------------- /dlgbmp.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgjackson/toggle-dark-light/HEAD/dlgbmp.bmp -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgjackson/toggle-dark-light/HEAD/icon.ico -------------------------------------------------------------------------------- /bannrbmp.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgjackson/toggle-dark-light/HEAD/bannrbmp.bmp -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgjackson/toggle-dark-light/HEAD/screenshot.png -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgjackson/toggle-dark-light/HEAD/docs/screenshot.png -------------------------------------------------------------------------------- /update-docs.cmd: -------------------------------------------------------------------------------- 1 | @pushd %~dp0 2 | pandoc --shift-heading-level-by=-1 -s README.md -o docs/index.html 3 | copy /y screenshot.png docs 4 | @popd 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.exe 2 | /*.obj 3 | /*.res 4 | /*.o 5 | /*.pdb 6 | /*.ilk 7 | _local 8 | /*.wixobj 9 | /*.wixpdb 10 | /*.msi 11 | /build 12 | /obj 13 | /bin 14 | -------------------------------------------------------------------------------- /test.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | call .\build.cmd 3 | if errorlevel 1 ( 4 | echo ERROR: Not running, build failed. 5 | goto :eof 6 | ) 7 | .\toggle.exe /CONSOLE:ATTACH /TOGGLE /EXIT 8 | -------------------------------------------------------------------------------- /toggle.wixproj: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Win32", 5 | "includePath": [ 6 | "${workspaceFolder}/**" 7 | ], 8 | "defines": [ 9 | "_DEBUG", 10 | "UNICODE", 11 | "_UNICODE" 12 | ], 13 | "windowsSdkVersion": "10.0.18362.0", 14 | "compilerPath": "C:/Program Files (x86)/Microsoft Visual Studio/2019/Community/VC/Tools/MSVC/14.26.28801/bin/Hostx64/x64/cl.exe", 15 | "cStandard": "c11", 16 | "cppStandard": "c++17", 17 | "intelliSenseMode": "msvc-x64" 18 | } 19 | ], 20 | "version": 4 21 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # To build with native Windows toolchain use: build.cmd 2 | # 3 | # To cross-compile from WSL: 4 | # 5 | # wsl sudo apt install build-essential gcc-mingw-w64 && wsl make 6 | 7 | BIN_NAME = toggle.exe 8 | CC = x86_64-w64-mingw32-gcc 9 | CFLAGS = -m64 -O3 -Wall -municode -DUNICODE -D_UNICODE 10 | LIBS = -luser32 -lgdi32 -lcomctl32 -lshell32 -ladvapi32 -lcomdlg32 -lole32 -loleaut32 -lwbemuuid -ldxva2 -lversion 11 | 12 | RES = $(wildcard *.rc) 13 | SRC = $(wildcard *.c) 14 | INC = $(wildcard *.h) 15 | 16 | all: $(BIN_NAME) 17 | 18 | $(BIN_NAME): Makefile $(SRC) $(INC) $(RES) 19 | x86_64-w64-mingw32-windres -i $(RES) -o $(RES:.rc=_res.o) 20 | $(CC) -std=c99 -o $(BIN_NAME) $(CFLAGS) $(SRC) $(RES:.rc=_res.o) -I/usr/x86_64-w64-mingw32/include -I/usr/local/include -L/usr/x86_64-w64-mingw32/lib -L/usr/local/lib $(LIBS) 21 | 22 | clean: 23 | rm -f *.o $(BIN_NAME) 24 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "(Windows) Launch", 9 | "type": "cppvsdbg", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/toggle.exe", 12 | "args": [ 13 | //"/CONSOLE:DEBUG", 14 | "/CONSOLE:CREATE", 15 | //"/EXIT", 16 | ], 17 | "stopAtEntry": false, 18 | "cwd": "${workspaceFolder}", 19 | "environment": [], 20 | "externalConsole": false, 21 | "preLaunchTask": "Build Project", 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2021-2024 Daniel Jackson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /toggle.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | true 18 | PerMonitorV2 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Build Project", 6 | "dependsOn": ["Build Resource", "Build Code"], 7 | "group": { 8 | "kind": "build", 9 | "isDefault": true 10 | } 11 | }, 12 | { 13 | "type": "shell", 14 | "label": "Build Resource", 15 | "command": "./vc.bat", 16 | "args": [ 17 | "rc.exe", 18 | "/nologo", 19 | "${workspaceFolder}\\toggle.rc", 20 | ], 21 | "options": { 22 | "cwd": "${workspaceFolder}" 23 | }, 24 | "problemMatcher": [ 25 | "$msCompile" 26 | ] 27 | }, 28 | { 29 | "type": "shell", 30 | "label": "Build Code", 31 | "command": "./vc.bat", 32 | "args": [ 33 | "cl.exe", 34 | "/nologo", 35 | "/Zi", 36 | "/EHsc", 37 | "/DUNICODE", 38 | "/D_UNICODE", 39 | "${workspaceFolder}\\*.c", 40 | "/link", 41 | "/out:${workspaceFolder}\\toggle.exe", 42 | "${workspaceFolder}\\toggle.res", 43 | "/subsystem:windows" 44 | ], 45 | "options": { 46 | "cwd": "${workspaceFolder}" 47 | }, 48 | "problemMatcher": [ 49 | "$msCompile" 50 | ] 51 | } 52 | ] 53 | } -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.4) 2 | 3 | set(CMAKE_SYSTEM_NAME Windows) 4 | IF(WIN32) 5 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /MANIFEST:NO") 6 | ELSE() 7 | set(TOOLCHAIN_PREFIX x86_64-w64-mingw32) 8 | set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc) 9 | set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++) 10 | set(CMAKE_RC_COMPILER ${TOOLCHAIN_PREFIX}-windres) # https://gitlab.kitware.com/cmake/cmake/-/issues/20500 11 | set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX}) 12 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 13 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 14 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 15 | set(MINGW TRUE) 16 | ENDIF() 17 | 18 | project(toggle) 19 | 20 | add_executable(toggle WIN32 toggle.c) 21 | add_definitions(-DUNICODE -D_UNICODE) 22 | target_link_libraries(toggle user32 gdi32 comctl32 shell32 advapi32 comdlg32 ole32 oleaut32 wbemuuid dxva2 version) 23 | IF(MINGW) 24 | target_link_libraries(toggle "-municode") 25 | ENDIF() 26 | target_sources(toggle PRIVATE toggle.rc) 27 | #target_sources(toggle PRIVATE toggle.exe.manifest) 28 | # Release binary output at top level 29 | set_target_properties(toggle PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE "..") 30 | 31 | # cmake -S . -B build && cmake --build build --config Release 32 | -------------------------------------------------------------------------------- /vc.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | IF NOT DEFINED VCPARAMS SET VCPARAMS=x86 3 | SETLOCAL EnableDelayedExpansion 4 | 5 | SET FIND_CL= 6 | FOR %%p IN (cl.exe) DO SET "FIND_CL=%%~$PATH:p" 7 | IF DEFINED FIND_CL ( 8 | ENDLOCAL 9 | GOTO RUN 10 | ) 11 | 12 | SET VCVARSALL= 13 | FOR %%f IN (70 71 80 90 100 110 120 130 140) DO IF EXIST "!VS%%fCOMNTOOLS!\..\..\VC\vcvarsall.bat" SET VCVARSALL=!VS%%fCOMNTOOLS!\..\..\VC\vcvarsall.bat 14 | FOR /F "usebackq tokens=*" %%f IN (`DIR /B /ON "%ProgramFiles(x86)%\Microsoft Visual Studio\????"`) DO FOR %%g IN (Community Professional Enterprise) DO IF EXIST "%ProgramFiles(x86)%\Microsoft Visual Studio\%%f\%%g\VC\Auxiliary\Build\vcvarsall.bat" SET "VCVARSALL=%ProgramFiles(x86)%\Microsoft Visual Studio\%%f\%%g\VC\Auxiliary\Build\vcvarsall.bat" 15 | FOR /F "usebackq tokens=*" %%f IN (`DIR /B /ON "%ProgramFiles%\Microsoft Visual Studio\????"`) DO FOR %%g IN (Community Professional Enterprise) DO IF EXIST "%ProgramFiles%\Microsoft Visual Studio\%%f\%%g\VC\Auxiliary\Build\vcvarsall.bat" SET "VCVARSALL=%ProgramFiles%\Microsoft Visual Studio\%%f\%%g\VC\Auxiliary\Build\vcvarsall.bat" 16 | IF DEFINED VCVARSALL ( 17 | ENDLOCAL 18 | SET "VCVARSALL=%VCVARSALL%" 19 | GOTO VCVARS 20 | ) 21 | ECHO Error: Cannot locate C compiler environment 'vcvarsall.bat'. 22 | EXIT /B 1 23 | GOTO :EOF 24 | 25 | :VCVARS 26 | @ECHO ON 27 | CALL "%VCVARSALL%" %VCPARAMS% 28 | @ECHO OFF 29 | 30 | :RUN 31 | @ECHO ON 32 | %* -------------------------------------------------------------------------------- /toggle.rc: -------------------------------------------------------------------------------- 1 | #include 2 | //#include 3 | CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "toggle.exe.manifest" 4 | MAINICON ICON "icon.ico" 5 | 6 | // File and product versions will be identical 7 | #define STRINGIZE_HELPER(x) #x 8 | #define STRINGIZE(x) STRINGIZE_HELPER(x) 9 | #define VER_MAJOR 1 10 | #define VER_MINOR 0 11 | #define VER_BUILD 14 // Patch ('build' in MS version order) 12 | #define VER_REVISION 0 // Build ('revision' in MS version order) 13 | #define VER_STRING STRINGIZE(VER_MAJOR) "." STRINGIZE(VER_MINOR) "." STRINGIZE(VER_BUILD) "." STRINGIZE(VER_REVISION) 14 | 15 | VS_VERSION_INFO VERSIONINFO 16 | FILEVERSION VER_MAJOR,VER_MINOR,VER_BUILD,VER_REVISION 17 | PRODUCTVERSION VER_MAJOR,VER_MINOR,VER_BUILD,VER_REVISION 18 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 19 | FILEFLAGS 0 20 | FILEOS VOS_NT 21 | FILETYPE VFT_APP 22 | BEGIN 23 | BLOCK "StringFileInfo" 24 | BEGIN 25 | BLOCK "040904E4" 26 | BEGIN 27 | VALUE "Comments", "Toggle between Windows dark mode and light mode." 28 | VALUE "CompanyName", "danielgjackson" 29 | VALUE "FileDescription", "Toggle Dark-Light Mode" 30 | VALUE "FileVersion", VER_STRING 31 | VALUE "InternalName", "toggle" 32 | VALUE "LegalCopyright", "(C)2021-2024 Daniel Jackson (MIT License)" 33 | VALUE "OriginalFilename", "toggle.exe" 34 | VALUE "ProductName", "Toggle Dark-Light Mode" 35 | VALUE "ProductVersion", VER_STRING 36 | END 37 | END 38 | BLOCK "VarFileInfo" 39 | BEGIN 40 | VALUE "Translation", 0x0809,1200 41 | END 42 | END 43 | -------------------------------------------------------------------------------- /LICENSE.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang2057{\fonttbl{\f0\fswiss\fprq2\fcharset0 Calibri;}{\f1\fnil\fcharset0 Calibri;}} 2 | {\colortbl ;\red0\green0\blue255;} 3 | {\*\generator Riched20 10.0.19041}\viewkind4\uc1 4 | \pard\nowidctlpar\sa200\sl276\slmult1\f0\fs22\lang9 Toggle Dark-Light is Open Source under the MIT License: {{\field{\*\fldinst{HYPERLINK https://github.com/danielgjackson/toggle-dark-light }}{\fldrslt{https://github.com/danielgjackson/toggle-dark-light\ul0\cf0}}}}\f0\fs22\par 5 | Copyright 2021-2024 Daniel Jackson\par 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\par 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\par 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\par 9 | 10 | \pard\sa200\sl276\slmult1\f1\par 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Toggle Dark/Light Mode 2 | 3 | Single click toggle between dark mode and light mode using an icon in the Windows taskbar notification area (system tray). This may be useful for anyone working under significantly changing background or reflected light levels. 4 | 5 | ![Screenshot showing the application icon in the notification area](screenshot.png) 6 | 7 | This is an open source ([MIT License](https://github.com/danielgjackson/toggle-dark-light/blob/master/LICENSE.txt)) application, and the project repository is at: 8 | 9 | * [github.com/danielgjackson/toggle-dark-light](https://github.com/danielgjackson/toggle-dark-light) 10 | 11 | 12 | ## Installation 13 | 14 | 1. Download the installer `toggle.msi` from: 15 | 16 | * [Releases](https://github.com/danielgjackson/toggle-dark-light/releases/latest) 17 | 18 | 2. Double-click to run the downloaded installer. As Windows will not recognize the program, you may need to select *More info* / *Run anyway*. Follow the prompts to install the application. 19 | 20 | 3. The application will run after installation (by default), and an icon for *Toggle Dark/Light Mode* will appear in the taskbar notification area (system tray). It might be in the *Taskbar overflow* menu behind the **ᐱ** symbol (this can be changed in the *Taskbar Settings*). 21 | 22 | 23 | ## Use 24 | 25 | 1. Left-click the icon to immediately toggle dark/light mode. 26 | 27 | 2. Use the shortcut key combination Win+Shift+D to toggle between dark and light mode. 28 | 29 | 3. Right-click the icon for a menu: 30 | 31 | * *Toggle Dark/Light Mode* - Toggles between dark and light modes (same as left-clicking the icon). 32 | * *Auto-Start* - Toggles whether the executable will be automatically run when you log in. 33 | * *Save Debug Info* - This will prompt to save a text (`.txt`) file with debugging information. 34 | * *About* - Information about the program. 35 | * *Exit* - Stops the program and removes the icon. (If *Auto-Start* is enabled, it will start when you log-in again) 36 | 37 | 38 | ## Advanced Use 39 | 40 | You do not need to install the application, but instead just download and run the `toggle.exe` executable directly (i.e. as a *portable app*). 41 | 42 | The executable takes command-line options and can be used to directly set the mode and optionally exit without remaining as an icon in the taskbar notification area, for example: 43 | 44 | * `toggle /TOGGLE /EXIT` 45 | * `toggle /LIGHT /EXIT` 46 | * `toggle /DARK /EXIT` 47 | 48 | --- 49 | 50 | * [danielgjackson.github.io/toggle-dark-light](https://danielgjackson.github.io/toggle-dark-light) 51 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "ABORTIFHUNG", 4 | "advapi", 5 | "ALLEVENTS", 6 | "ALLOWDUPLICATE", 7 | "APIENTRY", 8 | "APPWINDOW", 9 | "ASSOCCHANGED", 10 | "BOTTOMALIGN", 11 | "BSTR", 12 | "CANCELTRYCONTINUE", 13 | "CENTERALIGN", 14 | "CIMTYPE", 15 | "CIMV", 16 | "CLIPCHILDREN", 17 | "CLIPSIBLINGS", 18 | "CLSID", 19 | "COINIT", 20 | "comctl", 21 | "Comdlg", 22 | "commctrl", 23 | "CONIN", 24 | "CONOUT", 25 | "CONTEXTMENU", 26 | "CONTROLPARENT", 27 | "CREATEPROCESS", 28 | "CTLCOLORSTATIC", 29 | "Cust", 30 | "danielgjackson", 31 | "DCOM", 32 | "DEFBUTTON", 33 | "DELA", 34 | "DWMCOMPOSITIONCHANGED", 35 | "Dxva", 36 | "ENDSESSION", 37 | "EOAC", 38 | "EXSTYLE", 39 | "FILEFLAGS", 40 | "FILEFLAGSMASK", 41 | "FILEOS", 42 | "FILEVERSION", 43 | "FIXEDFILEINFO", 44 | "ftprintf", 45 | "GETDEFID", 46 | "GETNONCLIENTMETRICS", 47 | "GETWORKAREA", 48 | "GWLP", 49 | "HBRUSH", 50 | "HCURSOR", 51 | "HFONT", 52 | "HICON", 53 | "HINSTANCE", 54 | "HINTERNET", 55 | "HKCU", 56 | "HKEY", 57 | "HMENU", 58 | "HREDRAW", 59 | "HRESULT", 60 | "HRGN", 61 | "HSCROLL", 62 | "HWND", 63 | "ICONERROR", 64 | "ICONINFORMATION", 65 | "ICONWARNING", 66 | "IDCANCEL", 67 | "IDCONTINUE", 68 | "IDLIST", 69 | "IDOK", 70 | "IDTRYAGAIN", 71 | "INITCOMMONCONTROLSEX", 72 | "INPROC", 73 | "LEFTALIGN", 74 | "LOWORD", 75 | "LPARAM", 76 | "LPEDITMENU", 77 | "lpfn", 78 | "LPMONITORINFO", 79 | "LPOFNHOOKPROC", 80 | "lpsapi", 81 | "lpstr", 82 | "lpsz", 83 | "LRESULT", 84 | "LSTATUS", 85 | "MAINICON", 86 | "MAKEINTRESOURCE", 87 | "MAKELPARAM", 88 | "manifestdependency", 89 | "MENUDROPALIGNMENT", 90 | "NIIF", 91 | "NOACTIVATE", 92 | "NOMIN", 93 | "NOMOVE", 94 | "NONCLIENTMETRICS", 95 | "NONOTIFY", 96 | "NOPORTABLE", 97 | "NOREPEAT", 98 | "NOSIZE", 99 | "NOSOUND", 100 | "NOTIFYCALLBACK", 101 | "NOTIFYICON", 102 | "NOTIFYICONDATA", 103 | "NOZORDER", 104 | "OKCANCEL", 105 | "oleaut", 106 | "OPENFILENAME", 107 | "OVERWRITEPROMPT", 108 | "POSTMESSAGE", 109 | "PRECONFIG", 110 | "PRODUCTVERSION", 111 | "psapi", 112 | "RBUTTONUP", 113 | "RIGHTALIGN", 114 | "RIGHTBUTTON", 115 | "SENDNOTIFYMESSAGE", 116 | "SETDEFID", 117 | "SETFONT", 118 | "SETTINGCHANGE", 119 | "SETVERSION", 120 | "SHCNE", 121 | "SHCNF", 122 | "SHOWHELP", 123 | "SHOWTIP", 124 | "SMTO", 125 | "sntprintf", 126 | "stprintf", 127 | "STRINGIZE", 128 | "SYSCOLORCHANGE", 129 | "TASKDIALOG", 130 | "TASKDIALOGCONFIG", 131 | "tcscmp", 132 | "tcscpy", 133 | "tcsicmp", 134 | "tcslen", 135 | "tcsnccmp", 136 | "tcsncmp", 137 | "TDCBF", 138 | "tfopen", 139 | "THEMECHANGED", 140 | "THICKFRAME", 141 | "tmain", 142 | "TOOLWINDOW", 143 | "Uninitialize", 144 | "USEDEFAULT", 145 | "VERSIONINFO", 146 | "VREDRAW", 147 | "Wbem", 148 | "wbemuuid", 149 | "wcex", 150 | "wininet", 151 | "winuser", 152 | "WMAPP", 153 | "WNDCLASSEX", 154 | "WORKAREA", 155 | "WPARAM" 156 | ] 157 | } -------------------------------------------------------------------------------- /toggle.wxs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Toggle Dark/Light Mode 8 | 157 | 160 | 161 | 162 |
163 |

Toggle Dark/Light Mode

164 |
165 |

Single click toggle between dark mode and light mode using an icon in 166 | the Windows taskbar notification area (system tray). This may be useful 167 | for anyone working under significantly changing background or reflected 168 | light levels.

169 |
170 | Screenshot showing the application icon in the notification area 172 | 174 |
175 |

This is an open source (MIT 177 | License) application, and the project repository is at:

178 | 182 |

Installation

183 |
    184 |
  1. Download the installer toggle.msi from:

    185 |
  2. 189 |
  3. Double-click to run the downloaded installer. As Windows will not 190 | recognize the program, you may need to select More info / 191 | Run anyway. Follow the prompts to install the 192 | application.

  4. 193 |
  5. The application will run after installation (by default), and an 194 | icon for Toggle Dark/Light Mode will appear in the taskbar 195 | notification area (system tray). It might be in the Taskbar 196 | overflow menu behind the symbol (this can be 197 | changed in the Taskbar Settings).

  6. 198 |
199 |

Use

200 |
    201 |
  1. Left-click the icon to immediately toggle dark/light 202 | mode.

  2. 203 |
  3. Use the shortcut key combination 204 | Win+Shift+D to toggle between dark and 205 | light mode.

  4. 206 |
  5. Right-click the icon for a menu:

    207 |
      208 |
    • Toggle Dark/Light Mode - Toggles between dark and light 209 | modes (same as left-clicking the icon).
    • 210 |
    • Auto-Start - Toggles whether the executable will be 211 | automatically run when you log in.
    • 212 |
    • Save Debug Info - This will prompt to save a text 213 | (.txt) file with debugging information.
    • 214 |
    • About - Information about the program.
    • 215 |
    • Exit - Stops the program and removes the icon. (If 216 | Auto-Start is enabled, it will start when you log-in 217 | again)
    • 218 |
  6. 219 |
220 |

Advanced Use

221 |

You do not need to install the application, but instead just download 222 | and run the toggle.exe executable directly (i.e. as a 223 | portable app).

224 |

The executable takes command-line options and can be used to directly 225 | set the mode and optionally exit without remaining as an icon in the 226 | taskbar notification area, for example:

227 | 232 |
233 | 237 | 238 | 239 | -------------------------------------------------------------------------------- /toggle.c: -------------------------------------------------------------------------------- 1 | // Toggle Dark/Light Mode -- icon in the taskbar notification area 2 | // Dan Jackson, 2021-2024. 3 | 4 | #define _WIN32_WINNT 0x0601 5 | #define _CRT_SECURE_NO_WARNINGS // TODO: Use more secure versions 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | 19 | // commctrl v6 for LoadIconMetric() 20 | #include 21 | 22 | // Notify icons 23 | #include 24 | 25 | // Internet (update check) 26 | #include 27 | 28 | // MSC-Specific Pragmas 29 | #ifdef _MSC_VER 30 | // Moved to external .manifest file linked through .rc file 31 | //#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") 32 | #pragma comment(lib, "comctl32.lib") // InitCommonControlsEx(), LoadIconMetric() 33 | #pragma comment(lib, "shell32.lib") // Shell_NotifyIcon() 34 | #pragma comment(lib, "advapi32.lib") // RegOpenKeyEx(), RegSetValueEx(), RegGetValue(), RegDeleteValue(), RegCloseKey() 35 | #pragma comment(lib, "Comdlg32.lib") // GetSaveFileName() 36 | #pragma comment(lib, "Version.lib") // GetFileVersionInfoSize(), GetFileVersionInfo(), VerQueryValue() 37 | #pragma comment(lib, "gdi32.lib") // CreateFontIndirect() 38 | #pragma comment(lib, "User32.lib") // Windows 39 | #pragma comment(lib, "ole32.lib") // CoInitialize(), etc. 40 | #pragma comment (lib, "wininet.lib") // WinInet functions: InternetOpenW / InternetConnectW / HttpOpenRequestW / HttpSendRequestW / InternetReadFile / InternetCloseHandle 41 | // #pragma comment(lib, "psapi.lib") // GetModuleFileName() // -lpsapi 42 | #endif 43 | 44 | // Defines 45 | #define COPYRIGHT_NAME "Daniel Jackson" 46 | #define COPYRIGHT_YEAR "2021-2024" 47 | #define TITLE TEXT("Toggle Dark/Light Mode") 48 | #define TITLE_SAFE TEXT("Toggle Dark-Light Mode") 49 | #define USER_AGENT_BASE TEXT("toggle-dark-light") 50 | #define UPDATE_URL TEXT("https://api.github.com/repos/danielgjackson/toggle-dark-light/releases?per_page=1") 51 | #define RELEASES_URL TEXT("https://github.com/danielgjackson/toggle-dark-light/releases/latest") 52 | #define HOT_KEY_ID 0x0000 53 | #define WMAPP_NOTIFYCALLBACK (WM_APP + 1) 54 | #define IDM_TOGGLE 101 55 | #define IDM_AUTOSTART 102 56 | #define IDM_DEBUG 103 57 | #define IDM_ABOUT 104 58 | #define IDM_EXIT 105 59 | 60 | // Hacky global state 61 | HINSTANCE ghInstance = NULL; 62 | HWND ghWndMain = NULL; 63 | UINT idMinimizeIcon = 1; 64 | BOOL gbHasNotifyIcon = FALSE; 65 | BOOL gbAutoStart = FALSE; 66 | BOOL gbNotify = TRUE; 67 | BOOL gbPortable = FALSE; 68 | BOOL gbAllowDuplicate = FALSE; 69 | BOOL gbSubsystemWindows = FALSE; 70 | BOOL gbHasConsole = FALSE; 71 | BOOL gbImmediatelyExit = FALSE; // Quit right away (mainly for testing) 72 | BOOL gbSetDark = FALSE; 73 | BOOL gbSetLight = FALSE; 74 | BOOL gbQuery = FALSE; 75 | BOOL gbExiting = FALSE; 76 | HANDLE ghStartEvent = NULL; // Event for single instance 77 | TCHAR gUserAgent[128] = USER_AGENT_BASE; 78 | int gVersion[4] = { 0, 0, 0, 0 }; 79 | 80 | NOTIFYICONDATA nid = {0}; 81 | 82 | // Redirect standard I/O to a console 83 | static BOOL RedirectIOToConsole(BOOL tryAttach, BOOL createIfRequired) 84 | { 85 | BOOL hasConsole = FALSE; 86 | if (tryAttach && AttachConsole(ATTACH_PARENT_PROCESS)) 87 | { 88 | hasConsole = TRUE; 89 | } 90 | if (createIfRequired && !hasConsole) 91 | { 92 | AllocConsole(); 93 | AttachConsole(GetCurrentProcessId()); 94 | hasConsole = TRUE; 95 | } 96 | if (hasConsole) 97 | { 98 | freopen("CONIN$", "r", stdin); 99 | freopen("CONOUT$", "w", stdout); 100 | freopen("CONOUT$", "w", stderr); 101 | } 102 | return hasConsole; 103 | } 104 | 105 | int HttpGet(const TCHAR *url, const TCHAR *headers, char **response, size_t *responseSize) 106 | { 107 | // Initialize WinInet 108 | HINTERNET hInternet = InternetOpen(gUserAgent, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); 109 | if (hInternet == NULL) 110 | { 111 | _ftprintf(stderr, TEXT("GET: InternetOpen() failed (%d)\n"), GetLastError()); 112 | return -1; 113 | } 114 | 115 | // Open the URL 116 | HINTERNET hResource = InternetOpenUrl(hInternet, url, headers, (DWORD)-1, INTERNET_FLAG_NO_UI | INTERNET_FLAG_NO_AUTH | INTERNET_FLAG_PRAGMA_NOCACHE | INTERNET_FLAG_NO_CACHE_WRITE, (DWORD_PTR)NULL); 117 | if (hResource == NULL) 118 | { 119 | _ftprintf(stderr, TEXT("GET: InternetOpenUrl() failed (%d)\n"), GetLastError()); 120 | InternetCloseHandle(hInternet); 121 | return -1; 122 | } 123 | 124 | // Read response 125 | char *buffer = NULL; 126 | size_t bufferSize = 0, bufferCapacity = 0; 127 | DWORD bytesRead; 128 | do 129 | { 130 | const size_t chunkSize = 4096; 131 | if (bufferSize + chunkSize > bufferCapacity) { 132 | bufferCapacity += chunkSize; 133 | buffer = (char *)realloc(buffer, bufferCapacity + 1); // null terminated for easier handling of string responses 134 | if (buffer == NULL) 135 | { 136 | _ftprintf(stderr, TEXT("GET: Memory allocation failed\n")); 137 | InternetCloseHandle(hResource); 138 | InternetCloseHandle(hInternet); 139 | return -1; 140 | } 141 | } 142 | bytesRead = 0; 143 | if (!InternetReadFile(hResource, (LPVOID)(buffer + bufferSize), bufferCapacity - bufferSize, &bytesRead)) 144 | { 145 | _ftprintf(stderr, TEXT("GET: InternetReadFile() failed (%d)\n"), GetLastError()); 146 | free(buffer); 147 | InternetCloseHandle(hResource); 148 | InternetCloseHandle(hInternet); 149 | return -1; 150 | } 151 | bufferSize += bytesRead; 152 | } while (bytesRead > 0); 153 | buffer[bufferSize] = '\0'; 154 | //_ftprintf(stderr, TEXT("GET: InternetReadFile() total read: %u\n"), (unsigned int)bufferSize); 155 | 156 | // Clean up 157 | InternetCloseHandle(hResource); 158 | InternetCloseHandle(hInternet); 159 | 160 | // Assign return values 161 | if (response) 162 | { 163 | *response = buffer; 164 | } else { 165 | free(buffer); 166 | } 167 | if (responseSize) 168 | { 169 | *responseSize = bufferSize; 170 | } 171 | return 0; 172 | } 173 | 174 | bool FindOnlineVersion(char *version, size_t versionSize) 175 | { 176 | sprintf(version, ""); 177 | 178 | // Check for updates 179 | const TCHAR *url = UPDATE_URL; 180 | const TCHAR *headers = NULL; // TEXT("Accept: application/json"); 181 | char *response = NULL; 182 | size_t responseSize = 0; 183 | int result = HttpGet(url, headers, &response, &responseSize); 184 | if (result < 0) 185 | { 186 | _ftprintf(stderr, TEXT("FindOnlineVersion() failed: %d\n"), result); 187 | return false; 188 | } 189 | if (responseSize < 2 || response[0] != '[' || response[1] != '{') 190 | { 191 | _ftprintf(stderr, TEXT("FindOnlineVersion() failed: unexpected response\n")); 192 | free(response); 193 | return false; 194 | } 195 | 196 | // Dump response 197 | //_ftprintf(stderr, TEXT("---\n%S\n---\n"), response); 198 | 199 | // Hackily Locate tag_name ("tag_name":"1.0.##") 200 | const char *prefix = "\"tag_name\":\""; 201 | const char *suffix = "\""; 202 | bool located = false; 203 | char *tagStart = strstr(response, prefix); 204 | if (tagStart) 205 | { 206 | tagStart += strlen(prefix); 207 | char *tagEnd = strstr(tagStart, suffix); 208 | if (tagEnd) 209 | { 210 | // Copy version string 211 | size_t tagLength = tagEnd - tagStart; 212 | if (tagLength < versionSize - 1) 213 | { 214 | memcpy(version, tagStart, tagLength); 215 | version[tagLength] = '\0'; 216 | located = true; 217 | } 218 | } 219 | } 220 | 221 | if (!located) 222 | { 223 | _ftprintf(stderr, TEXT("FindOnlineVersion() failed: tag_name not found in response\n")); 224 | free(response); 225 | return false; 226 | } 227 | 228 | //_ftprintf(stderr, TEXT("FindOnlineVersion() found version: %s\n"), version); 229 | return true; 230 | } 231 | 232 | const char *CheckForUpdatedVersion() { 233 | char currentVersion[32] = {0}; 234 | sprintf(currentVersion, "%u.%u.%u", (unsigned int)gVersion[0], (unsigned int)gVersion[1], (unsigned int)gVersion[2]); 235 | 236 | _ftprintf(stderr, TEXT("UPDATE: Inline version update check... (current=%S)\n"), currentVersion); 237 | 238 | // Caution: static buffer 239 | static char onlineVersion[32] = {0}; 240 | if (!FindOnlineVersion(onlineVersion, sizeof(onlineVersion) / sizeof(onlineVersion[0]))) 241 | { 242 | _ftprintf(stderr, TEXT("UPDATE: Online version check failed.\n")); 243 | return NULL; 244 | } 245 | else 246 | { 247 | _ftprintf(stderr, TEXT("UPDATE: Online version: %S\n"), onlineVersion); 248 | if (strcmp(currentVersion, onlineVersion) != 0) 249 | { 250 | _ftprintf(stderr, TEXT("UPDATE: A newer version is available online.\n")); 251 | return onlineVersion; 252 | } 253 | else 254 | { 255 | // Online version is the same as the current version 256 | sprintf(onlineVersion, ""); 257 | return onlineVersion; 258 | } 259 | } 260 | return NULL; 261 | } 262 | 263 | /* 264 | TODO: Check for updates: only in non-portable mode, at start-up, if auto-update enabled, 265 | if "last fetch attempt" time (from registry) is at least one day ago, store "last fetch attempt" time and 266 | spawn thread to attempt to fetch `https://api.github.com/repos/danielgjackson/toggle-dark-light/releases?per_page=1`, 267 | take online version as `[0].tag_name`. If successfully parsed online version, if different to "last prompted version" 268 | (from registry) and different to current software version information then prompt user with new version number and to 269 | visit releases page, and store as "last prompted version". 270 | */ 271 | // (From About dialog) Immediately check the online version, and prompt the user as necessary. 272 | void QueryUpdate() 273 | { 274 | char currentVersion[32] = {0}; 275 | sprintf(currentVersion, "%u.%u.%u", (unsigned int)gVersion[0], (unsigned int)gVersion[1], (unsigned int)gVersion[2]); 276 | 277 | // Caution: blocking online check for new version 278 | HCURSOR hPreviousCursor = SetCursor(LoadCursor(NULL, IDC_WAIT)); 279 | const char *updatedVersion = CheckForUpdatedVersion(); 280 | SetCursor(hPreviousCursor); 281 | 282 | if (updatedVersion != NULL && updatedVersion[0] == '\0') 283 | { 284 | MessageBox(NULL, TEXT("You are using the latest version."), TEXT("Updates"), MB_OK | MB_ICONINFORMATION); 285 | } 286 | else if (updatedVersion == NULL) 287 | { 288 | int result = MessageBox(NULL, TEXT("Unable to check for updates - visit the releases page?"), TEXT("Updates"), MB_OKCANCEL | MB_ICONERROR); 289 | if (result == IDOK) 290 | { 291 | ShellExecute(ghWndMain, TEXT("open"), RELEASES_URL, NULL, NULL, SW_SHOW); 292 | } 293 | } 294 | else 295 | { 296 | TCHAR szMessage[256] = TEXT(""); 297 | _sntprintf(szMessage, sizeof(szMessage) / sizeof(szMessage[0]), TEXT("You are on version %S, but a different version is available online: %S\nVisit the releases page?"), currentVersion, updatedVersion); 298 | int result = MessageBox(NULL, szMessage, TEXT("Updates"), MB_OKCANCEL | MB_ICONINFORMATION); 299 | if (result == IDOK) 300 | { 301 | ShellExecute(ghWndMain, TEXT("open"), RELEASES_URL, NULL, NULL, SW_SHOW); 302 | } 303 | } 304 | } 305 | 306 | void DeleteNotificationIcon(void) 307 | { 308 | if (gbHasNotifyIcon) 309 | { 310 | nid.uFlags = 0; 311 | memset(&nid.guidItem, 0, sizeof(nid.guidItem)); 312 | nid.uID = idMinimizeIcon; 313 | Shell_NotifyIcon(NIM_DELETE, &nid); 314 | gbHasNotifyIcon = FALSE; 315 | } 316 | } 317 | 318 | BOOL AddNotificationIcon(HWND hwnd) 319 | { 320 | DeleteNotificationIcon(); 321 | 322 | memset(&nid, 0, sizeof(nid)); 323 | nid.cbSize = sizeof(nid); 324 | nid.hWnd = hwnd; 325 | nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_SHOWTIP; 326 | nid.uFlags |= NIS_HIDDEN; 327 | nid.dwStateMask = NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_SHOWTIP; 328 | if (gbNotify) 329 | { 330 | nid.uFlags |= NIF_INFO | NIF_REALTIME; 331 | _tcscpy(nid.szInfoTitle, TITLE); 332 | _tcscpy(nid.szInfo, TEXT("Running in notification area.")); 333 | nid.dwInfoFlags = NIIF_INFO | NIIF_NOSOUND; 334 | nid.hBalloonIcon = LoadIcon(ghInstance, TEXT("MAINICON")); 335 | nid.dwInfoFlags |= NIIF_LARGE_ICON | NIIF_USER; 336 | } 337 | memset(&nid.guidItem, 0, sizeof(nid.guidItem)); 338 | nid.uID = idMinimizeIcon; 339 | nid.uCallbackMessage = WMAPP_NOTIFYCALLBACK; 340 | nid.hIcon = NULL; 341 | LoadIconMetric(ghInstance, L"MAINICON", LIM_SMALL, &nid.hIcon); // MAKEINTRESOURCE(IDI_APPLICATION) 342 | _tcscpy(nid.szTip, TITLE); 343 | Shell_NotifyIcon(NIM_ADD, &nid); 344 | nid.uVersion = NOTIFYICON_VERSION_4; 345 | Shell_NotifyIcon(NIM_SETVERSION, &nid); 346 | gbHasNotifyIcon = TRUE; 347 | return gbHasNotifyIcon; 348 | } 349 | 350 | BOOL Notify(TCHAR *message) 351 | { 352 | if (!gbHasNotifyIcon) return FALSE; 353 | nid.uFlags |= NIF_INFO | NIF_REALTIME; 354 | _tcscpy(nid.szInfoTitle, TITLE); 355 | _tcscpy(nid.szInfo, message); 356 | nid.dwInfoFlags = NIIF_INFO | NIIF_NOSOUND; 357 | nid.hBalloonIcon = LoadIcon(ghInstance, TEXT("MAINICON")); 358 | nid.dwInfoFlags |= NIIF_LARGE_ICON | NIIF_USER; 359 | Shell_NotifyIcon(NIM_MODIFY, &nid); 360 | return TRUE; 361 | } 362 | 363 | bool AutoStart(bool change, bool startup) 364 | { 365 | bool retVal = false; 366 | HKEY hKeyMain = HKEY_CURRENT_USER; 367 | TCHAR *subKey = TEXT("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"); 368 | TCHAR *value = TITLE_SAFE; 369 | 370 | TCHAR szModuleFileName[MAX_PATH]; 371 | GetModuleFileName(NULL, szModuleFileName, sizeof(szModuleFileName) / sizeof(szModuleFileName[0])); 372 | 373 | TCHAR szQuotedFileName[MAX_PATH + 2]; 374 | _sntprintf(szQuotedFileName, sizeof(szQuotedFileName) / sizeof(szQuotedFileName[0]), TEXT("\"%Ts\""), szModuleFileName); 375 | 376 | TCHAR szAutoStartValue[MAX_PATH + 32]; 377 | _sntprintf(szAutoStartValue, sizeof(szAutoStartValue) / sizeof(szAutoStartValue[0]), TEXT("%Ts /AUTOSTART"), szQuotedFileName); 378 | 379 | if (change) 380 | { 381 | // Set the key 382 | HKEY hKey = NULL; 383 | LSTATUS lErrorCode = RegOpenKeyEx(hKeyMain, subKey, 0, KEY_SET_VALUE, &hKey); 384 | if (lErrorCode == ERROR_SUCCESS) 385 | { 386 | if (startup) 387 | { 388 | // Setting to auto-start 389 | lErrorCode = RegSetValueEx(hKey, value, 0, REG_SZ, (const BYTE *)szAutoStartValue, (DWORD)((_tcslen(szAutoStartValue) + 1) * sizeof(TCHAR))); 390 | if (lErrorCode == ERROR_SUCCESS) 391 | { 392 | retVal = true; 393 | } 394 | } 395 | else 396 | { 397 | // Setting to not auto-start 398 | lErrorCode = RegDeleteValue(hKey, value); 399 | if (lErrorCode == ERROR_SUCCESS) 400 | { 401 | retVal = true; 402 | } 403 | } 404 | RegCloseKey(hKey); 405 | } 406 | } 407 | else 408 | { 409 | // Query that the key is set and has the correct value prefix 410 | TCHAR szData[MAX_PATH]; 411 | DWORD cbData = sizeof(szData); 412 | LSTATUS lErrorCode = RegGetValue(hKeyMain, subKey, value, RRF_RT_REG_SZ, NULL, &szData, &cbData); 413 | if (lErrorCode == ERROR_SUCCESS && _tcsncmp(szData, szQuotedFileName, _tcslen(szQuotedFileName)) == 0) { 414 | retVal = true; 415 | } 416 | } 417 | return retVal; 418 | } 419 | 420 | 421 | void ShowContextMenu(HWND hwnd, POINT pt) 422 | { 423 | UINT autoStartFlags; 424 | // Don't touch the registry if running as a portable app 425 | if (!gbPortable) { 426 | autoStartFlags = AutoStart(false, false) ? MF_CHECKED : MF_UNCHECKED; 427 | } else { 428 | autoStartFlags = MF_UNCHECKED | MF_GRAYED; 429 | } 430 | 431 | HMENU hMenu = CreatePopupMenu(); 432 | AppendMenu(hMenu, MF_STRING, IDM_TOGGLE, TEXT("&Toggle Dark/Light Mode\tWin+Shift+D")); 433 | AppendMenu(hMenu, MF_SEPARATOR, 0, TEXT("Separator")); 434 | AppendMenu(hMenu, MF_STRING | autoStartFlags, IDM_AUTOSTART, TEXT("Auto-&Start")); 435 | AppendMenu(hMenu, MF_STRING, IDM_DEBUG, TEXT("Save &Debug Info...")); 436 | AppendMenu(hMenu, MF_STRING, IDM_ABOUT, TEXT("&About")); 437 | AppendMenu(hMenu, MF_SEPARATOR, 0, TEXT("Separator")); 438 | AppendMenu(hMenu, MF_STRING, IDM_EXIT, TEXT("E&xit")); 439 | SetMenuDefaultItem(hMenu, IDM_TOGGLE, FALSE); 440 | SetForegroundWindow(hwnd); 441 | UINT uFlags = TPM_RIGHTBUTTON; 442 | if (GetSystemMetrics(SM_MENUDROPALIGNMENT) != 0) 443 | { 444 | uFlags |= TPM_RIGHTALIGN; 445 | } 446 | else 447 | { 448 | uFlags |= TPM_LEFTALIGN; 449 | } 450 | SetForegroundWindow(hwnd); // for correct popup tracking 451 | TrackPopupMenuEx(hMenu, uFlags, pt.x, pt.y, hwnd, NULL); 452 | DestroyMenu(hMenu); 453 | } 454 | 455 | BOOL HasExistingInstance(void) 456 | { 457 | if (ghStartEvent) 458 | { 459 | CloseHandle(ghStartEvent); 460 | ghStartEvent = NULL; 461 | } 462 | ghStartEvent = CreateEvent(NULL, FALSE, FALSE, TEXT("Local\\toggle")); // Session namespace is ok (does not need to be Global) 463 | DWORD lastError = GetLastError(); 464 | if (ghStartEvent && !lastError) 465 | { 466 | // We are the first instance (hold on to the event, it will only be released when the process ends) 467 | _ftprintf(stderr, TEXT("INSTANCE: No other instance found.\n")); 468 | return FALSE; 469 | } 470 | else if (ghStartEvent && lastError == ERROR_ALREADY_EXISTS) 471 | { 472 | // There is another instance running 473 | _ftprintf(stderr, TEXT("INSTANCE: Another instance found.\n")); 474 | //CloseHandle(ghStartEvent); 475 | //ghStartEvent = NULL; 476 | return TRUE; 477 | } 478 | else 479 | { 480 | // Otherwise, fail safe and assume not already running 481 | _ftprintf(stderr, TEXT("INSTANCE: Problem finding instance.\n")); 482 | return FALSE; 483 | } 484 | } 485 | 486 | HFONT hDlgFont = NULL; 487 | bool windowOpen = false; 488 | 489 | void PositionWindow(int x, int y) 490 | { 491 | RECT rect = {0}; 492 | GetWindowRect(ghWndMain, &rect); 493 | SIZE windowSize; 494 | windowSize.cx = rect.right - rect.left; 495 | windowSize.cy = rect.bottom - rect.top; 496 | 497 | // Fallback to (0,0) 498 | RECT rectSpace = {0}; 499 | 500 | // Prefer desktop window 501 | GetWindowRect(GetDesktopWindow(), &rectSpace); 502 | 503 | // Prefer the work area 504 | SystemParametersInfo(SPI_GETWORKAREA, 0, &rectSpace, 0); 505 | 506 | // Default to the lower-right corner 507 | rect.left = 0; 508 | rect.top = 0; 509 | if (rectSpace.right - rectSpace.left > 0) rect.left = rectSpace.right - windowSize.cx; 510 | if (rectSpace.bottom - rectSpace.top > 0) rect.top = rectSpace.bottom - windowSize.cy; 511 | rect.top = rectSpace.bottom - windowSize.cy; 512 | rect.right = rect.left + windowSize.cx; 513 | rect.bottom = rect.top + windowSize.cy; 514 | 515 | // Prefer a calculated position 516 | POINT anchorPoint; 517 | anchorPoint.x = x; 518 | anchorPoint.y = y; 519 | CalculatePopupWindowPosition(&anchorPoint, &windowSize, TPM_CENTERALIGN | TPM_BOTTOMALIGN | TPM_VERTICAL | TPM_WORKAREA, NULL, &rect); 520 | 521 | AdjustWindowRect(&rect, GetWindowLong(ghWndMain, GWL_STYLE), FALSE); 522 | SetWindowPos(ghWndMain, 0, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_NOZORDER | SWP_NOACTIVATE); 523 | } 524 | 525 | void OpenWindow(int x, int y) 526 | { 527 | // PositionWindow(x, y); 528 | // ShowWindow(ghWndMain, SW_SHOW); 529 | // SetForegroundWindow(ghWndMain); 530 | // windowOpen = true; 531 | } 532 | 533 | void HideWindow(void) 534 | { 535 | ShowWindow(ghWndMain, SW_HIDE); 536 | windowOpen = false; 537 | } 538 | 539 | void StartExit(void) 540 | { 541 | gbExiting = TRUE; 542 | if (ghStartEvent) 543 | { 544 | CloseHandle(ghStartEvent); 545 | ghStartEvent = NULL; 546 | } 547 | PostMessage(ghWndMain, WM_CLOSE, 0, 0); 548 | } 549 | 550 | void Startup(HWND hWnd) 551 | { 552 | _tprintf(TEXT("Startup...\n")); 553 | 554 | ghWndMain = hWnd; 555 | 556 | BOOL duplicateInstance = FALSE; 557 | int response = 0; 558 | if (!gbAllowDuplicate) 559 | { 560 | do 561 | { 562 | if (gbImmediatelyExit) break; // Allow duplicates if immediately exiting 563 | duplicateInstance = HasExistingInstance(); 564 | if (!duplicateInstance) break; 565 | response = MessageBox(NULL, TEXT("Toggle Dark/Light Mode is already running in the notification area.\r\nContinue with a new instance?"), TITLE, MB_ICONWARNING | MB_CANCELTRYCONTINUE | MB_DEFBUTTON1); 566 | if (response == IDCANCEL) 567 | { 568 | gbNotify = FALSE; 569 | gbImmediatelyExit = TRUE; 570 | } 571 | 572 | if (response == IDCONTINUE) 573 | { 574 | gbAllowDuplicate = TRUE; 575 | break; 576 | } 577 | } while (response == IDTRYAGAIN || response == IDCONTINUE); 578 | } 579 | 580 | if (response != IDCANCEL) 581 | { 582 | if (!gbImmediatelyExit) 583 | { 584 | AddNotificationIcon(ghWndMain); 585 | } 586 | } 587 | 588 | if (gbImmediatelyExit) StartExit(); 589 | } 590 | 591 | void Shutdown(void) 592 | { 593 | _tprintf(TEXT("Shutdown()...\n")); 594 | DeleteNotificationIcon(); 595 | _tprintf(TEXT("...END: Shutdown()\n")); 596 | } 597 | 598 | int IsLight(void) 599 | { 600 | HKEY hKeyMain = HKEY_CURRENT_USER; 601 | const TCHAR *subKey = TEXT("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"); 602 | const TCHAR *value = TEXT("SystemUsesLightTheme"); 603 | 604 | DWORD dwData = (DWORD)-1; 605 | 606 | HKEY hKey = NULL; 607 | LSTATUS lErrorCode = RegOpenKeyEx(hKeyMain, subKey, 0, KEY_NOTIFY, &hKey); 608 | if (lErrorCode != ERROR_SUCCESS) 609 | { 610 | _tprintf(TEXT("ERROR: RegOpenKeyEx() failed (%d): %ls\n"), lErrorCode, subKey); 611 | return -1; 612 | } 613 | 614 | // Query current value 615 | DWORD dwFlags = RRF_RT_REG_DWORD; 616 | DWORD cbData = sizeof(dwData); 617 | lErrorCode = RegGetValue(hKeyMain, subKey, value, dwFlags, NULL, &dwData, &cbData); 618 | RegCloseKey(hKey); 619 | if (lErrorCode != ERROR_SUCCESS) 620 | { 621 | _tprintf(TEXT("ERROR: RegGetValue() failed (%d): %ls\n"), lErrorCode, subKey); 622 | return (DWORD)-1; 623 | } 624 | 625 | if (dwData == 0 || dwData == 1) return dwData; // 1=light, 0=dark 626 | return -1; // error 627 | } 628 | 629 | // Data for sending messages to specific windows 630 | typedef struct { 631 | UINT msg; 632 | WPARAM wParam; 633 | LPARAM lParam; 634 | TCHAR *className; 635 | TCHAR *classNameAlt; 636 | UINT count; 637 | } send_message_t; 638 | 639 | BOOL CALLBACK SendEnumWindowsProc(HWND hWnd, LPARAM lParam) 640 | { 641 | send_message_t *msgData = (send_message_t *)lParam; 642 | if (!hWnd) return TRUE; 643 | 644 | // Filter by class name 645 | if (msgData->className != NULL || msgData->classNameAlt != NULL) 646 | { 647 | TCHAR szClassName[256]; 648 | GetClassName(hWnd, szClassName, sizeof(szClassName) / sizeof(szClassName[0])); 649 | 650 | BOOL match = false; 651 | if (msgData->className != NULL && _tcscmp(szClassName, msgData->className) == 0) match = true; 652 | if (msgData->classNameAlt != NULL && _tcscmp(szClassName, msgData->classNameAlt) == 0) match = true; 653 | 654 | if (!match) return TRUE; 655 | } 656 | 657 | SendMessage(hWnd, msgData->msg, msgData->wParam, msgData->lParam); 658 | //PostMessage(hWnd, msgData->msg, msgData->wParam, msgData->lParam); 659 | msgData->count++; 660 | return TRUE; 661 | } 662 | 663 | // Send a message just to non-primary display taskbars (which seem a bit troublesome...) 664 | UINT SendToTaskbar(UINT msg, WPARAM wParam, LPARAM lParam) 665 | { 666 | send_message_t msgData = {0}; 667 | msgData.msg = msg; 668 | msgData.wParam = wParam; 669 | msgData.lParam = lParam; 670 | msgData.className = (TCHAR *)&TEXT("Shell_TrayWnd"); 671 | msgData.classNameAlt = (TCHAR *)&TEXT("Shell_SecondaryTrayWnd"); 672 | msgData.count = 0; 673 | BOOL result = EnumWindows(SendEnumWindowsProc, (LPARAM)&msgData); 674 | if (!result) return 0; 675 | return (UINT)msgData.count; 676 | } 677 | 678 | BOOL SetLightDark(DWORD light) 679 | { 680 | HKEY hKeyMain = HKEY_CURRENT_USER; 681 | const TCHAR *subKey = TEXT("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"); 682 | const TCHAR *valueSystem = TEXT("SystemUsesLightTheme"); 683 | const TCHAR *valueApps = TEXT("AppsUseLightTheme"); 684 | const TCHAR *valueColorPrevalence = TEXT("ColorPrevalence"); 685 | 686 | HKEY hKey = NULL; 687 | LSTATUS lErrorCode = RegOpenKeyEx(hKeyMain, subKey, 0, KEY_SET_VALUE, &hKey); 688 | if (lErrorCode != ERROR_SUCCESS) 689 | { 690 | _tprintf(TEXT("ERROR: RegOpenKeyEx() failed (%d): %ls\n"), lErrorCode, subKey); 691 | return FALSE; 692 | } 693 | 694 | // Set the system key 695 | lErrorCode = RegSetValueEx(hKey, valueSystem, 0, REG_DWORD, (const BYTE *)&light, sizeof(light)); 696 | if (lErrorCode != ERROR_SUCCESS) 697 | { 698 | _tprintf(TEXT("ERROR: RegSetValueEx() failed (%d): %ls - %ls\n"), lErrorCode, subKey, valueSystem); 699 | RegCloseKey(hKey); 700 | return FALSE; 701 | } 702 | 703 | // Set the apps key 704 | lErrorCode = RegSetValueEx(hKey, valueApps, 0, REG_DWORD, (const BYTE *)&light, sizeof(light)); 705 | if (lErrorCode != ERROR_SUCCESS) 706 | { 707 | _tprintf(TEXT("ERROR: RegSetValueEx() failed (%d): %ls - %ls\n"), lErrorCode, subKey, valueApps); 708 | RegCloseKey(hKey); 709 | return FALSE; 710 | } 711 | 712 | // Set the ColorPrevalence key 713 | DWORD colorPrevalence = 0; // HACK: Force off for now as we don't know the previous state (TODO: Option to set this value in dark mode) 714 | if (light) colorPrevalence = 0; // Always off in light mode? 715 | lErrorCode = RegSetValueEx(hKey, valueColorPrevalence, 0, REG_DWORD, (const BYTE *)&colorPrevalence, sizeof(colorPrevalence)); 716 | if (lErrorCode != ERROR_SUCCESS) 717 | { 718 | _tprintf(TEXT("ERROR: RegSetValueEx() failed (%d): %ls - %ls\n"), lErrorCode, subKey, valueColorPrevalence); 719 | RegCloseKey(hKey); 720 | return FALSE; 721 | } 722 | 723 | RegCloseKey(hKey); 724 | 725 | // Delay -- this may to help update taskbars on other displays? 726 | //Sleep(500); 727 | 728 | // Send WM_SETTINGCHANGE message directly to primary and secondary taskbars 729 | UINT countSC = SendToTaskbar(WM_SETTINGCHANGE, 0, (LPARAM)TEXT("ImmersiveColorSet")); 730 | _tprintf(TEXT("SendToTaskbar(WM_SETTINGCHANGE): %d\n"), countSC); 731 | 732 | // Broadcast WM_SETTINGCHANGE using SendMessageTimeout with lParam set to "ImmersiveColorSet" -- required by some apps such as explorer.exe (File Explorer) 733 | { 734 | HWND hWnd = HWND_BROADCAST; 735 | UINT msg = WM_SETTINGCHANGE; 736 | WPARAM wParam = 0; 737 | LPARAM lParam = (LPARAM)TEXT("ImmersiveColorSet"); 738 | SendMessageTimeout(hWnd, msg, wParam, lParam, SMTO_ABORTIFHUNG, 5000, NULL); 739 | } 740 | 741 | // Broadcast a second WM_SETTINGCHANGE using SendMessageTimeout with lParam set to "ImmersiveColorSet" -- this second broadcast fixes Task Manager 742 | { 743 | HWND hWnd = HWND_BROADCAST; 744 | UINT msg = WM_SETTINGCHANGE; 745 | WPARAM wParam = 0; 746 | LPARAM lParam = (LPARAM)TEXT("ImmersiveColorSet"); 747 | SendMessageTimeout(hWnd, msg, wParam, lParam, SMTO_ABORTIFHUNG, 5000, NULL); 748 | } 749 | 750 | // // Broadcast WM_THEMECHANGED 751 | // { 752 | // HWND hWnd = HWND_BROADCAST; 753 | // UINT msg = WM_THEMECHANGED; 754 | // WPARAM wParam = 0; 755 | // LPARAM lParam = 0; 756 | // SendMessageTimeout(hWnd, msg, wParam, lParam, SMTO_ABORTIFHUNG, 5000, NULL); 757 | // } 758 | 759 | // // Broadcast WM_SYSCOLORCHANGE 760 | // { 761 | // HWND hWnd = HWND_BROADCAST; 762 | // UINT msg = WM_SYSCOLORCHANGE; 763 | // WPARAM wParam = 0; 764 | // LPARAM lParam = 0; 765 | // SendMessageTimeout(hWnd, msg, wParam, lParam, SMTO_ABORTIFHUNG, 5000, NULL); 766 | // } 767 | 768 | // WM_DWMCOMPOSITIONCHANGED 769 | 770 | 771 | // TODO: If occasional File Explorer glitches remain, consider these: 772 | 773 | // #include 774 | 775 | // SHCNE_ASSOCCHANGED (SHCNF_IDLIST=0x00000000) 776 | //SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL); 777 | 778 | // SHCNE_ALLEVENTS 779 | //SHChangeNotify(SHCNE_ALLEVENTS, 0, NULL, NULL); 780 | 781 | // // Broadcast WM_PAINT 782 | // { 783 | // long res = BroadcastSystemMessageEx(BSF_POSTMESSAGE, NULL, WM_PAINT, 0, 0, NULL); // BSF_POSTMESSAGE / BSF_SENDNOTIFYMESSAGE 784 | // if (res <= 0) { 785 | // _tprintf(TEXT("ERROR: BroadcastSystemMessageEx() WM_PAINT failed (0x%x / 0x%x)\n"), res, GetLastError()); 786 | // } 787 | // } 788 | 789 | return TRUE; 790 | } 791 | 792 | void ToggleDarkLight(BOOL notify) 793 | { 794 | _tprintf(TEXT("ToggleDarkLight()...\n")); 795 | int isLight = IsLight(); 796 | if (isLight == 0) // dark 797 | { 798 | _tprintf(TEXT("...isLight=FALSE, setting: light\n")); 799 | if (notify) Notify(TEXT("Toggle to light mode.")); 800 | SetLightDark(1); 801 | } 802 | else if (isLight == 1) // light 803 | { 804 | _tprintf(TEXT("...isLight=TRUE, setting: dark\n")); 805 | if (notify) Notify(TEXT("Toggle to dark mode.")); 806 | SetLightDark(0); 807 | } 808 | else 809 | { 810 | _tprintf(TEXT("...isLight=error\n")); 811 | } 812 | _tprintf(TEXT("...END: ToggleDarkLight()\n")); 813 | } 814 | 815 | void DumpDebug(FILE *file) 816 | { 817 | _ftprintf(file, TEXT("IsLight=%d\n"), IsLight()); 818 | } 819 | 820 | // Open hyperlink from TaskDialog 821 | HRESULT CALLBACK TaskDialogCallback(HWND hwnd, UINT uNotification, WPARAM wParam, LPARAM lParam, LONG_PTR dwRefData) 822 | { 823 | if (uNotification == TDN_HYPERLINK_CLICKED) 824 | { 825 | ShellExecute(hwnd, TEXT("open"), (TCHAR *)lParam, NULL, NULL, SW_SHOW); 826 | } 827 | return 0; 828 | } 829 | 830 | LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 831 | { 832 | switch (message) 833 | { 834 | case WM_CREATE: 835 | Startup(hwnd); 836 | break; 837 | 838 | case WM_ACTIVATE: 839 | { 840 | if (wParam == WA_INACTIVE) 841 | { 842 | HideWindow(); 843 | } 844 | } 845 | break; 846 | 847 | // Handled pre-translate message 848 | // case WM_KEYDOWN: 849 | // if (wParam == VK_ESCAPE) 850 | // { 851 | // HideWindow(); 852 | // } 853 | // break; 854 | 855 | case WM_CTLCOLORSTATIC: 856 | { 857 | //HDC hdcStatic = (HDC)wParam; 858 | //SetTextColor(hdcStatic, RGB(255,255,255)); 859 | //SetBkColor(hdcStatic, RGB(0,0,0)); 860 | return (INT_PTR)GetSysColorBrush(COLOR_WINDOW); // CreateSolidBrush(RGB(0,0,0)); 861 | } 862 | break; 863 | 864 | case WM_HOTKEY: 865 | { 866 | if (wParam == HOT_KEY_ID) 867 | { 868 | ToggleDarkLight(TRUE); 869 | 870 | // Remove any repeated WM_HOTKEY messages from the window queue 871 | MSG m; 872 | while (PeekMessage(&m, hwnd, WM_HOTKEY, WM_HOTKEY, PM_REMOVE)) { ; } 873 | 874 | return 0; 875 | } 876 | } 877 | 878 | 879 | case WM_COMMAND: 880 | { 881 | int const wmId = LOWORD(wParam); 882 | switch (wmId) 883 | { 884 | case IDM_TOGGLE: 885 | { 886 | // POINT mouse; 887 | // GetCursorPos(&mouse); 888 | // OpenWindow(mouse.x, mouse.y); 889 | ToggleDarkLight(TRUE); 890 | } 891 | break; 892 | case IDM_AUTOSTART: 893 | if (AutoStart(false, false)) 894 | { 895 | // Is auto-starting, remove 896 | AutoStart(true, false); 897 | } 898 | else 899 | { 900 | // Is not auto-starting, add 901 | AutoStart(true, true); 902 | } 903 | break; 904 | case IDM_DEBUG: 905 | { 906 | // Initial filename from local date/time 907 | TCHAR szFileName[MAX_PATH] = TEXT(""); 908 | time_t now; 909 | time(&now); 910 | struct tm *local = localtime(&now); 911 | _sntprintf(szFileName, sizeof(szFileName) / sizeof(szFileName[0]), TEXT("" TITLE_SAFE "_%04d-%02d-%02d_%02d-%02d-%02d.txt"), local->tm_year + 1900, local->tm_mon + 1, local->tm_mday, local->tm_hour, local->tm_min, local->tm_sec); 912 | 913 | OPENFILENAME openFilename = {0}; 914 | openFilename.lStructSize = sizeof(openFilename); 915 | openFilename.hwndOwner = hwnd; 916 | openFilename.lpstrFilter = TEXT("Text Files (*.txt)\0*.txt\0All Files (*.*)\0*.*\0"); 917 | openFilename.lpstrFile = szFileName; 918 | openFilename.nMaxFile = sizeof(szFileName) / sizeof(*szFileName); 919 | openFilename.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT; 920 | openFilename.lpstrDefExt = (LPCWSTR)L"txt"; 921 | openFilename.lpstrTitle = TITLE; 922 | 923 | // Write file to temp folder 924 | TCHAR szInitialDir[MAX_PATH] = TEXT(""); 925 | GetTempPath(sizeof(szInitialDir), szInitialDir); 926 | openFilename.lpstrInitialDir = szInitialDir[0] ? szInitialDir : NULL; 927 | 928 | if (GetSaveFileName(&openFilename)) 929 | { 930 | FILE *file = _tfopen(openFilename.lpstrFile, TEXT("w")); 931 | if (file) 932 | { 933 | DumpDebug(file); 934 | fclose(file); 935 | ShellExecute(NULL, TEXT("open"), openFilename.lpstrFile, NULL, NULL, SW_SHOW); 936 | } 937 | } 938 | } 939 | break; 940 | case IDM_ABOUT: 941 | { 942 | TCHAR *szTitle = TEXT("About " TITLE); 943 | TCHAR szHeader[128] = TEXT(""); 944 | _sntprintf(szHeader, sizeof(szHeader) / sizeof(szHeader[0]), TEXT("%s V%d.%d.%d"), TITLE, gVersion[0], gVersion[1], gVersion[2]); 945 | TCHAR *szContent = TEXT("Toggle dark/light mode.\nKeyboard shortcut: Win+Shift+D"); 946 | //TCHAR *szExtraInfo = TEXT("..."); 947 | TCHAR *szFooter = TEXT("Open source under MIT License, \u00A9" COPYRIGHT_YEAR " " COPYRIGHT_NAME "."); 948 | TASKDIALOG_BUTTON aCustomButtons[] = { 949 | { 1001, L"Project page\ngithub.com/danielgjackson/toggle-dark-light" }, 950 | { 1002, L"Check for updates" }, 951 | }; 952 | TASKDIALOGCONFIG tdc = {0}; 953 | tdc.cbSize = sizeof(tdc); 954 | tdc.hwndParent = ghWndMain; 955 | tdc.dwFlags = TDF_USE_HICON_MAIN | TDF_USE_COMMAND_LINKS | TDF_ENABLE_HYPERLINKS | TDF_EXPANDED_BY_DEFAULT | TDF_EXPAND_FOOTER_AREA | TDF_ALLOW_DIALOG_CANCELLATION; 956 | tdc.pButtons = aCustomButtons; 957 | tdc.cButtons = sizeof(aCustomButtons) / sizeof(aCustomButtons[0]); 958 | tdc.pszWindowTitle = szTitle; 959 | tdc.nDefaultButton = IDOK; 960 | tdc.hMainIcon = LoadIcon(ghInstance, TEXT("MAINICON")); 961 | tdc.pszMainInstruction = szHeader; 962 | tdc.pszContent = szContent; 963 | //tdc.pszExpandedInformation = szExtraInfo; 964 | tdc.pszFooter = szFooter; 965 | tdc.pszFooterIcon = TD_INFORMATION_ICON; 966 | tdc.dwCommonButtons = TDCBF_OK_BUTTON; 967 | tdc.pfCallback = TaskDialogCallback; 968 | tdc.lpCallbackData = (LONG_PTR)NULL; // no context needed 969 | int nClickedBtn; 970 | HRESULT hr = TaskDialogIndirect(&tdc, &nClickedBtn, NULL, NULL); // nClickedBtn == IDOK 971 | if (SUCCEEDED(hr)) 972 | { 973 | if (nClickedBtn == 1001) 974 | { 975 | ShellExecute(hwnd, TEXT("open"), TEXT("https://github.com/danielgjackson/toggle-dark-light/#readme"), NULL, NULL, SW_SHOW); 976 | } 977 | else if (nClickedBtn == 1002) 978 | { 979 | QueryUpdate(); 980 | } 981 | } 982 | } 983 | break; 984 | case IDM_EXIT: 985 | StartExit(); 986 | break; 987 | default: 988 | return DefWindowProc(hwnd, message, wParam, lParam); 989 | } 990 | } 991 | break; 992 | 993 | case WMAPP_NOTIFYCALLBACK: 994 | switch (LOWORD(lParam)) 995 | { 996 | case NIN_SELECT: 997 | { 998 | // POINT mouse; 999 | // mouse.x = LOWORD(wParam); 1000 | // mouse.y = HIWORD(wParam); 1001 | // OpenWindow(mouse.x, mouse.y); 1002 | ToggleDarkLight(TRUE); 1003 | } 1004 | break; 1005 | 1006 | case WM_RBUTTONUP: // If not using NOTIFYICON_VERSION_4 ? 1007 | case WM_CONTEXTMENU: 1008 | { 1009 | POINT const pt = { LOWORD(wParam), HIWORD(wParam) }; 1010 | ShowContextMenu(hwnd, pt); 1011 | } 1012 | break; 1013 | } 1014 | break; 1015 | 1016 | case WM_ENDSESSION: 1017 | StartExit(); 1018 | break; 1019 | 1020 | case WM_CLOSE: 1021 | if (gbExiting && hwnd == ghWndMain) 1022 | { 1023 | DestroyWindow(hwnd); 1024 | } 1025 | else 1026 | { 1027 | ShowWindow(hwnd, SW_HIDE); 1028 | } 1029 | break; 1030 | 1031 | case WM_DESTROY: 1032 | Shutdown(); 1033 | PostQuitMessage(0); 1034 | break; 1035 | 1036 | default: 1037 | return DefWindowProc(hwnd, message, wParam, lParam); 1038 | } 1039 | return 0; 1040 | } 1041 | 1042 | void done(EXCEPTION_POINTERS *exceptionInfo) 1043 | { 1044 | if (exceptionInfo) 1045 | { 1046 | TCHAR msg[512] = TEXT(""); 1047 | _sntprintf(msg, sizeof(msg) / sizeof(msg[0]), TEXT("ERROR: An unhandled error has occurred.")); 1048 | // [/CONSOLE:]* (* only as first parameter) 1049 | if (gbHasConsole) 1050 | { 1051 | _tprintf(msg); 1052 | } 1053 | MessageBox(NULL, msg, TITLE, MB_OK | MB_ICONERROR); 1054 | } 1055 | _tprintf(TEXT("DONE.\n")); 1056 | } 1057 | 1058 | void finish(void) 1059 | { 1060 | _tprintf(TEXT("END: Application ended.\n")); 1061 | done(NULL); 1062 | } 1063 | 1064 | LONG WINAPI UnhandledException(EXCEPTION_POINTERS *exceptionInfo) 1065 | { 1066 | _tprintf(TEXT("END: Unhandled exception.\n")); 1067 | done(exceptionInfo); 1068 | return EXCEPTION_EXECUTE_HANDLER; // EXCEPTION_EXECUTE_HANDLER; // EXCEPTION_CONTINUE_SEARCH; 1069 | } 1070 | 1071 | void SignalHandler(int signal) 1072 | { 1073 | _tprintf(TEXT("END: Received signal: %d\n"), signal); 1074 | //if (signal == SIGABRT) 1075 | done(NULL); 1076 | } 1077 | 1078 | BOOL WINAPI consoleHandler(DWORD signal) 1079 | { 1080 | _tprintf(TEXT("END: Received console control event: %u\n"), (unsigned int)signal); 1081 | //if (signal == CTRL_C_EVENT) 1082 | done(NULL); 1083 | return FALSE; 1084 | } 1085 | 1086 | int run(int argc, TCHAR *argv[], HINSTANCE hInstance, BOOL hasConsole) 1087 | { 1088 | _ftprintf(stderr, TEXT("run()\n")); 1089 | 1090 | ghInstance = hInstance; 1091 | gbHasConsole = hasConsole; 1092 | 1093 | if (gbHasConsole) 1094 | { 1095 | SetConsoleCtrlHandler(consoleHandler, TRUE); 1096 | } 1097 | signal(SIGABRT, SignalHandler); 1098 | SetUnhandledExceptionFilter(UnhandledException); 1099 | atexit(finish); 1100 | 1101 | ghInstance = hInstance; 1102 | 1103 | // Get module filename 1104 | TCHAR szModuleFileName[MAX_PATH] = {0}; 1105 | GetModuleFileName(NULL, szModuleFileName, sizeof(szModuleFileName) / sizeof(szModuleFileName[0])); 1106 | 1107 | // Detect if the executable is named as a "PortableApps"-compatible ".paf.exe" -- ensure no registry changes, no check for updates, etc. 1108 | if (_tcslen(szModuleFileName) > 8) 1109 | { 1110 | if (_tcsicmp(szModuleFileName + _tcslen(szModuleFileName) - 8, TEXT(".paf.exe")) == 0) 1111 | { 1112 | gbPortable = TRUE; 1113 | } 1114 | } 1115 | 1116 | // Get module version 1117 | DWORD dwVerHandle = 0; 1118 | DWORD dwVerLen = GetFileVersionInfoSize(szModuleFileName, &dwVerHandle); 1119 | if (dwVerLen > 0) 1120 | { 1121 | LPVOID verData = malloc(dwVerLen); 1122 | if (GetFileVersionInfo(szModuleFileName, dwVerHandle, dwVerLen, verData)) 1123 | { 1124 | VS_FIXEDFILEINFO *pFileInfo = NULL; 1125 | UINT uLenFileInfo = 0; 1126 | if (VerQueryValue(verData, TEXT("\\"), (LPVOID*)&pFileInfo, &uLenFileInfo)) 1127 | { 1128 | gVersion[0] = HIWORD(pFileInfo->dwProductVersionMS); // Major 1129 | gVersion[1] = LOWORD(pFileInfo->dwProductVersionMS); // Minor 1130 | gVersion[2] = HIWORD(pFileInfo->dwProductVersionLS); // Patch ('build' in MS version order) 1131 | gVersion[3] = LOWORD(pFileInfo->dwProductVersionLS); // Build ('revision' in MS version order) 1132 | //_ftprintf(stderr, TEXT("V%u.%u.%u.%u\n"), gVersion[0], gVersion[1], gVersion[2], gVersion[3]); 1133 | } 1134 | } 1135 | free(verData); 1136 | } 1137 | _stprintf(gUserAgent, TEXT("%s/%u.%u.%u.%u\n"), USER_AGENT_BASE, (unsigned int)gVersion[0], (unsigned int)gVersion[1], (unsigned int)gVersion[2], (unsigned int)gVersion[3]); 1138 | 1139 | BOOL bShowHelp = FALSE; 1140 | int positional = 0; 1141 | int errors = 0; 1142 | 1143 | for (int i = 0; i < argc; i++) 1144 | { 1145 | if (_tcsicmp(argv[i], TEXT("/?")) == 0) { bShowHelp = TRUE; } 1146 | else if (_tcsicmp(argv[i], TEXT("/HELP")) == 0) { bShowHelp = TRUE; } 1147 | else if (_tcsicmp(argv[i], TEXT("--help")) == 0) { bShowHelp = TRUE; } 1148 | else if (_tcsicmp(argv[i], TEXT("/AUTOSTART")) == 0) { gbAutoStart = TRUE; gbNotify = FALSE; } // Don't notify when auto-starting 1149 | else if (_tcsicmp(argv[i], TEXT("/NOTIFY")) == 0) { gbNotify = TRUE; } 1150 | else if (_tcsicmp(argv[i], TEXT("/NONOTIFY")) == 0) { gbNotify = FALSE; } 1151 | else if (_tcsicmp(argv[i], TEXT("/PORTABLE")) == 0) { gbPortable = TRUE; } 1152 | else if (_tcsicmp(argv[i], TEXT("/NOPORTABLE")) == 0) { gbPortable = FALSE; } // Allow override 1153 | else if (_tcsicmp(argv[i], TEXT("/ALLOWDUPLICATE")) == 0) { gbAllowDuplicate = TRUE; } 1154 | else if (_tcsicmp(argv[i], TEXT("/EXIT")) == 0) { gbImmediatelyExit = TRUE; } 1155 | else if (_tcsicmp(argv[i], TEXT("/DARK")) == 0) { gbSetLight = FALSE; gbSetDark = TRUE; } 1156 | else if (_tcsicmp(argv[i], TEXT("/LIGHT")) == 0) { gbSetLight = TRUE; gbSetDark = FALSE; } 1157 | else if (_tcsicmp(argv[i], TEXT("/TOGGLE")) == 0) { gbSetLight = TRUE; gbSetDark = TRUE; } 1158 | else if (_tcsicmp(argv[i], TEXT("/QUERY")) == 0) { gbQuery = TRUE; gbImmediatelyExit = TRUE; } 1159 | 1160 | else if (argv[i][0] == '/') 1161 | { 1162 | _ftprintf(stderr, TEXT("ERROR: Unexpected parameter: %s\n"), argv[i]); 1163 | errors++; 1164 | } 1165 | else 1166 | { 1167 | /* 1168 | if (positional == 0) 1169 | { 1170 | argv[i]; 1171 | } 1172 | else 1173 | */ 1174 | { 1175 | _ftprintf(stderr, TEXT("ERROR: Unexpected positional parameter: %s\n"), argv[i]); 1176 | errors++; 1177 | } 1178 | positional++; 1179 | } 1180 | } 1181 | 1182 | if (gbPortable) 1183 | { 1184 | _ftprintf(stdout, TEXT("NOTE: Running as a portable app.\n")); 1185 | } 1186 | 1187 | if (errors) 1188 | { 1189 | _ftprintf(stderr, TEXT("ERROR: %d parameter error(s).\n"), errors); 1190 | bShowHelp = true; 1191 | } 1192 | 1193 | if (bShowHelp) 1194 | { 1195 | TCHAR msg[512] = TEXT(""); 1196 | _sntprintf(msg, sizeof(msg) / sizeof(msg[0]), TEXT("%s V%d.%d.%d " COPYRIGHT_NAME ", " COPYRIGHT_YEAR ".\n\nParameters: [/LIGHT|/DARK|/TOGGLE] [/EXIT]\n\n"), TITLE, gVersion[0], gVersion[1], gVersion[2]); 1197 | // [/CONSOLE:]* (* only as first parameter) 1198 | if (gbHasConsole) 1199 | { 1200 | _tprintf(msg); 1201 | } 1202 | else 1203 | { 1204 | MessageBox(NULL, msg, TITLE, MB_OK | MB_ICONERROR); 1205 | } 1206 | return -1; 1207 | } 1208 | 1209 | // Options to change mode at start (can combine with /EXIT) 1210 | if (gbSetLight || gbSetDark) 1211 | { 1212 | if (gbSetLight && gbSetDark) ToggleDarkLight(FALSE); 1213 | else if (gbSetLight) SetLightDark(1); 1214 | else if (gbSetDark) SetLightDark(0); 1215 | } 1216 | 1217 | // Initialize COM 1218 | HRESULT hr; 1219 | hr = CoInitializeEx(0, COINIT_MULTITHREADED); 1220 | if (FAILED(hr)) { fprintf(stderr, "ERROR: Failed CoInitializeEx().\n"); return 1; } 1221 | hr = CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, NULL); 1222 | if (FAILED(hr)) { fprintf(stderr, "ERROR: Failed CoInitializeSecurity().\n"); return 2; } 1223 | 1224 | // Opportunity to immediately exit before window-specific code 1225 | if (gbImmediatelyExit) { 1226 | return gbQuery ? IsLight() : 0; 1227 | } 1228 | 1229 | // Initialize common controls 1230 | INITCOMMONCONTROLSEX icce = {0}; 1231 | icce.dwSize = sizeof(icce); 1232 | icce.dwICC = ICC_STANDARD_CLASSES | ICC_BAR_CLASSES; 1233 | if (!InitCommonControlsEx(&icce)) 1234 | { 1235 | fprintf(stderr, "WARNING: Failed InitCommonControlsEx().\n"); 1236 | } 1237 | 1238 | const TCHAR szWindowClass[] = TITLE; 1239 | WNDCLASSEX wcex = {sizeof(wcex)}; 1240 | wcex.style = CS_HREDRAW | CS_VREDRAW; 1241 | wcex.lpfnWndProc = WndProc; 1242 | wcex.hInstance = ghInstance; 1243 | wcex.hIcon = LoadIcon(ghInstance, TEXT("MAINICON")); 1244 | wcex.hCursor = LoadCursor(NULL, IDC_ARROW); 1245 | wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); 1246 | wcex.lpszMenuName = NULL; 1247 | wcex.lpszClassName = szWindowClass; 1248 | RegisterClassEx(&wcex); 1249 | 1250 | TCHAR *szTitle = TITLE; 1251 | DWORD dwStyle = 0; // WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_POPUP | WS_VISIBLE | WS_THICKFRAME 1252 | DWORD dwStyleEx = WS_EX_CONTROLPARENT | WS_EX_TOOLWINDOW | WS_EX_TOPMOST; // (WS_EX_TOOLWINDOW) & ~(WS_EX_APPWINDOW); 1253 | 1254 | HWND hWnd = CreateWindowEx(dwStyleEx, szWindowClass, szTitle, dwStyle, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, ghInstance, NULL); 1255 | 1256 | // Remove caption 1257 | SetWindowLong(hWnd, GWL_STYLE, GetWindowLong(hWnd, GWL_STYLE) & ~(WS_CAPTION)); 1258 | 1259 | // Fix font 1260 | NONCLIENTMETRICS ncm; 1261 | ncm.cbSize = sizeof(ncm); 1262 | SystemParametersInfo(SPI_GETNONCLIENTMETRICS, ncm.cbSize, &ncm, 0); 1263 | hDlgFont = CreateFontIndirect(&(ncm.lfMessageFont)); 1264 | SendMessage(hWnd, WM_SETFONT, (WPARAM)hDlgFont, MAKELPARAM(FALSE, 0)); 1265 | 1266 | ghWndMain = hWnd; 1267 | if (!hWnd) { return -1; } 1268 | 1269 | HideWindow(); // nCmdShow 1270 | 1271 | // Shift+Win+D - Toggle Dark Mode 1272 | if (!gbImmediatelyExit && !RegisterHotKey(ghWndMain, HOT_KEY_ID, MOD_WIN | MOD_SHIFT | MOD_NOREPEAT, (UINT)'D')) 1273 | { 1274 | _ftprintf(stderr, TEXT("ERROR: Problem with RegisterHotKey(): 0x%08x\n"), GetLastError()); 1275 | } 1276 | 1277 | MSG msg; 1278 | while (GetMessage(&msg, NULL, 0, 0) > 0) 1279 | { 1280 | TranslateMessage(&msg); 1281 | DispatchMessage(&msg); 1282 | } 1283 | 1284 | UnregisterHotKey(hWnd, HOT_KEY_ID); 1285 | 1286 | CoUninitialize(); 1287 | 1288 | return 0; 1289 | } 1290 | 1291 | // Entry point when compiled with console subsystem 1292 | int _tmain(int argc, TCHAR *argv[]) 1293 | { 1294 | HINSTANCE hInstance = (HINSTANCE)GetModuleHandle(NULL); 1295 | return run(argc - 1, argv + 1, hInstance, TRUE); 1296 | } 1297 | 1298 | // Entry point when compiled with Windows subsystem 1299 | int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) 1300 | { 1301 | gbSubsystemWindows = TRUE; 1302 | 1303 | int argc = 0; 1304 | TCHAR **argv = CommandLineToArgvW(GetCommandLine(), &argc); // Must be UNICODE for CommandLineToArgvW() 1305 | 1306 | BOOL bConsoleAttach = FALSE; 1307 | BOOL bConsoleCreate = FALSE; 1308 | BOOL hasConsole = FALSE; 1309 | int argOffset = 1; 1310 | for (; argc > argOffset; argOffset++) 1311 | { 1312 | if (_tcsicmp(argv[argOffset], TEXT("/CONSOLE:ATTACH")) == 0) 1313 | { 1314 | bConsoleAttach = TRUE; 1315 | } 1316 | else if (_tcsicmp(argv[argOffset], TEXT("/CONSOLE:CREATE")) == 0) 1317 | { 1318 | bConsoleCreate = TRUE; 1319 | } 1320 | else if (_tcsicmp(argv[argOffset], TEXT("/CONSOLE:ATTACH-CREATE")) == 0) 1321 | { 1322 | bConsoleAttach = TRUE; 1323 | bConsoleCreate = TRUE; 1324 | } 1325 | else if (_tcsicmp(argv[argOffset], TEXT("/CONSOLE:DEBUG")) == 0) // Use existing console 1326 | { 1327 | _dup2(_fileno(stderr), _fileno(stdout)); // stdout to stderr (stops too much interleaving from buffering while debugging) 1328 | fflush(stdout); 1329 | hasConsole = TRUE; 1330 | } 1331 | else // No more prefix arguments 1332 | { 1333 | //_ftprintf(stderr, TEXT("NOTE: Stopped finding prefix parameters at: %s\n"), argv[argOffset]); 1334 | break; 1335 | } 1336 | } 1337 | 1338 | if (!hasConsole) 1339 | { 1340 | hasConsole = RedirectIOToConsole(bConsoleAttach, bConsoleCreate); 1341 | } 1342 | return run(argc - argOffset, argv + argOffset, hInstance, hasConsole); 1343 | } 1344 | --------------------------------------------------------------------------------