Skip to content

Instantly share code, notes, and snippets.

@charlie89
Last active February 21, 2026 21:45
Show Gist options
  • Select an option

  • Save charlie89/5670d46c90abf3351ed012b2afd926ca to your computer and use it in GitHub Desktop.

Select an option

Save charlie89/5670d46c90abf3351ed012b2afd926ca to your computer and use it in GitHub Desktop.
AutoHotkey v2 Dark Mode
#Requires AutoHotkey v2.0
; This script creates a dark mode for specific windows by inverting their colors.
; It uses the Windows Magnifier in the background for the color inverting, but it only inverts just the rectangle in place
; of the specific windows, NOT the whole screen like the Magnifier applicaion does. This is possible through DLL calls.
; Big thanks to the original source at https://www.autohotkey.com/boards/viewtopic.php?p=563023#p563023.
; Requirements:
; - AutoHotkey needs to be installed in C:\Program Files (or use the workaround from https://www.reddit.com/r/AutoHotkey/comments/16ux144/ui_access_without_reinstalling_in_program_files_v2/).
; - In the 'Launch Settings' of 'AutoHotkey Dash' you need to enable 'UI Access'.
; Usage:
; Use Win+i to invert a window manually or add it to the auto_invert_gp group to invert it automatically.
; Run with UI Access
if (!A_IsCompiled && !InStr(A_AhkPath, "_UIA")) {
Run "*uiAccess " A_ScriptFullPath
ExitApp
}
; Check for existing instances and close them
; For whatever reason SingleInstance does not work with UI Access, so we need
; to run our own code to kill old instances of the script.
#SingleInstance Off
for process in ComObjGet("winmgmts:").ExecQuery("Select * from Win32_Process")
{
if (process.Name == "AutoHotkey64_UIA.exe")
&& InStr(process.CommandLine, A_ScriptName)
&& process.ProcessId != DllCall("GetCurrentProcessId")
{
ProcessClose(process.ProcessId)
Sleep 500
}
}
SetTitleMatchMode "RegEx"
; Add Programs for automatic dark mode here:
GroupAdd "auto_invert_gp", "ahk_class #32770", , "Find|Öffnen|Speichern unter" ; Windows Explorer properties window and many others (but not the file open/save dialog that is already dark)
GroupAdd "auto_invert_gp", "ahk_class bosa_sdm_XL9" ; Excel: Zellen formatieren
GroupAdd "auto_invert_gp", "ahk_class OperationStatusWindow" ; Windows Explorer dialogs
GroupAdd "auto_invert_gp", "ahk_class NativeHWNDHost" ; Windows Extract ZIP archive
GroupAdd "auto_invert_gp", "ahk_class TWizardForm" ; Div. Installer
GroupAdd "auto_invert_gp", "ahk_exe AutoHotkey64.exe"
GroupAdd "auto_invert_gp", "ahk_exe AutoHotkeyUX.exe"
GroupAdd "auto_invert_gp", "ahk_exe Core Temp.exe" ; Core Temp
GroupAdd "auto_invert_gp", "ahk_exe hh.exe" ; Windows Help
GroupAdd "auto_invert_gp", "ahk_exe mmc.exe"
GroupAdd "auto_invert_gp", "ahk_exe msiexec.exe" ; .msi installer
GroupAdd "auto_invert_gp", "ahk_exe perfmon.exe"
GroupAdd "auto_invert_gp", "ahk_exe regedit.exe"
GroupAdd "auto_invert_gp", "ahk_exe _unins.exe" ; various Uninstallers
GroupAdd "auto_invert_gp", "ahk_exe 7zFM.exe"
GroupAdd "auto_invert_gp", "ahk_exe WinRAR.exe"
; Add Programs where automatic dark mode should never be applied here:
; This is mostly because they have dialogs that match ahk_class #32770 but are already in dark mode
AutoInvIgnProcName:= ["Notepad++.exe", "OUTLOOK.EXE", "procexp.exe", "procmon.exe", "WINWORD.EXE"]
#MaxThreadsPerHotkey 2
DetectHiddenWindows true
; SetBatchLines -1
SetWinDelay -1
OnExit Uninitialize
global Inverters := []
global WINDOWINFO
global pTarget := 0
; Color Matrix used to transform the colors
; Special thanks to https://github.com/mlaily/NegativeScreen/blob/master/NegativeScreen/Configuration.cs, they have many more color matrixes.
; I'm not too keen on the maths, so maybe I'm missing some things,
; but here is my basic understanding of what does what in a color matrix, when applied to a color vector:
; r*=x g+=x*r b+=x*r a+=x*r 0
; r+=x*g g*=x b+=x*g a+=x*g 0
; r+=x*b g+=x*b b*=x a+=x*b 0
; r+=x*a g+=x*a b+=x*a a*=x 0
; r+=x g+=x b+=x a+=x 1
; Simple Inversion
MatrixInv := "-1|0|0|0|0|"
. "0|-1|0|0|0|"
. "0|0|-1|0|0|"
. "0|0|0|1|0|"
. "1|1|1|0|1"
; Smart Inversion
MatrixSmart := "0.333|-0.667|-0.667|0|0|"
. "-0.667| 0.333|-0.667|0|0|"
. "-0.667|-0.667|0.333|0|0|"
. "0|0|0|1|0|"
. "1|1|1|0|1"
; High saturation, good pure colors
MatrixHigh := "1|-1|-1|0|0|"
. "-1|1|-1|0|0|"
. "-1|-1|1|0|0|"
. "0|0|0|1|0|"
. "1|1|1|0|1"
; set the wanted color matix
global Matrix := MatrixInv
; ==== changes below here only for more experienced users, all casual settings are above ====
; enable per-monitor DPI awareness
try dac := DllCall("SetThreadDpiAwarenessContext", 'ptr', -3, 'ptr')
; globals for taskbar height handling
global TaskBarAtBottom := true
global VisibleScreenHeight := A_ScreenHeight
; A window must never invert over a location that is used by the windows taskbar (ugly).
; The taskbar can be dynamically shown or hidden, so this calculates how much screen height
; the inverted window can have from the top down to the taskbar.
refreshVisibleScreenHeight()
{
global TaskBarAtBottom
global VisibleScreenHeight
try {
WinGetPos ,&y,, &h, "ahk_class Shell_TrayWnd"
} catch {
; an error can occur when you change the windows setting "Automatically hide the taskbar"
; under Personalization -> Taskbar -> Taskbar behavioors
; because the taskbar seems to get destroyed and recreated
TaskBarAtBottom := false
VisibleScreenHeight:= A_ScreenHeight
return
}
if ( y + 2 = A_ScreenHeight) { ; Taskbar hidden (it still takes up 2 pixels)
TaskBarAtBottom := true
VisibleScreenHeight := A_ScreenHeight
}
else if (A_ScreenHeight - y <= h) { ; Taskhar shown (or in the move in/out animation)
TaskBarAtBottom := true
VisibleScreenHeight := y
}
else {
TaskBarAtBottom := false
VisibleScreenHeight:= A_ScreenHeight
}
}
inGroup(GroupName, WinTitle := "A", WinText := "", ExcludeTitle := "", ExcludeText := "")
{
GroupIDs := WinGetList("ahk_group " GroupName)
Window := WinExist(WinTitle, WinText, ExcludeTitle, ExcludeText)
Loop GroupIDs.Length
{
if (GroupIDs[A_Index] = Window)
{
;found match
return Window
}
}
return false
}
; Checks if window is in dark mode
IsWindowDarkMode(hwnd) {
; needs Windows 11 Build 22000 or newer
static DWMWA_USE_IMMERSIVE_DARK_MODE := 20
local attr := 0
if DllCall("dwmapi\DwmGetWindowAttribute"
, "ptr", hwnd
, "int", DWMWA_USE_IMMERSIVE_DARK_MODE
, "int*", &attr
, "int", 4
, "int") = 0
return attr != 0
return false
}
class Inverter
{
hTarget := ""
hGui := ""
hGui1 := ""
hGui2 := ""
hChildMagnifier := ""
hChildMagnifier1 := ""
hChildMagnifier2 := ""
xPrev := 0
yPrev := 0
wPrev := 0
hPrev := 0
VisibleScreenHeightPrev := 0
stopped := 0
__New(hTarget)
{
DetectHiddenWindows true
this.hTarget := hTarget
DllCall("LoadLibrary", "str", "magnification.dll")
DllCall("magnification.dll\MagInitialize")
MAGCOLOREFFECT := Buffer(100, 0)
Loop Parse Matrix, "|"
NumPut("Float", A_LoopField, MAGCOLOREFFECT, (A_Index - 1) * 4)
Loop 2
{
gid := hTarget "_" A_Index
MyGui := Gui(, gid,)
if (A_Index = 2)
MyGui.Opt("+AlwaysOnTop") ; needed for ZBID_UIACCESS
; +HWNDhGui%A_Index%
MyGui.Opt("+DPIScale +toolwindow -Caption +E0x02000000 +E0x00080000 +E0x20") ; WS_EX_COMPOSITED := E0x02000000 WS_EX_LAYERED := E0x00080000 WS_EX_CLICKTHROUGH := E0x20
MyGui.Show("NA")
this.hGui%A_Index%:= MyGui.Hwnd
this.hChildMagnifier%A_Index% := DllCall("CreateWindowEx", "uint", 0, "str", "Magnifier", "str", "MagnifierWindow", "uint", WS_CHILD := 0x40000000, "int", 0, "int", 0, "int", 0, "int", 0, "ptr", this.hGui%A_Index%, "uint", 0, "ptr", DllCall("GetWindowLong" (A_PtrSize=8 ? "Ptr" : ""), "ptr", this.hGui%A_Index%, "int", GWL_HINSTANCE := -6 , "ptr"), "uint", 0, "ptr")
DllCall("magnification.dll\MagSetColorEffect", "ptr", this.hChildMagnifier%A_Index%, "ptr", MAGCOLOREFFECT)
}
gid := hTarget "_" 2
this.hGui := this.hGui1
this.hChildMagnifier := this.hChildMagnifier1
return this
}
stop() {
DetectHiddenWindows true
if(!this.stopped) {
this.stopped := 1
hGui := this.hGui
WinHide "ahk_id " hGui
hGui := this.hGui1
WinHide "ahk_id " hGui
hGui := this.hGui2
WinHide "ahk_id " hGui
hChildMagnifier := this.hChildMagnifier
WinHide "ahk_id " hChildMagnifier
hChildMagnifier := this.hChildMagnifier1
WinHide "ahk_id " hChildMagnifier
hChildMagnifier := this.hChildMagnifier2
WinHide "ahk_id " hChildMagnifier
}
}
start() {
DetectHiddenWindows true
if(this.stopped) {
this.stopped := 0
hChildMagnifier := this.hChildMagnifier
WinShow "ahk_id " hChildMagnifier
hGui := this.hGui
WinShow "ahk_id " hGui
}
}
doit()
{
DetectHiddenWindows true
hTarget := this.hTarget
hGui := this.hGui
hGui1 := this.hGui1
hGui2 := this.hGui2
hChildMagnifier := this.hChildMagnifier
hChildMagnifier1 := this.hChildMagnifier1
hChildMagnifier2 := this.hChildMagnifier2
hideGui := ""
WINDOWINFO := Buffer(60, 0)
if (this.stopped or DllCall("GetWindowInfo", "ptr", hTarget, "ptr", WINDOWINFO) = 0) and (A_LastError = 1400)
{
; xx("destroyed")
return
}
if (NumGet(WINDOWINFO, 36, "uint") & 0x20000000) or !(NumGet(WINDOWINFO, 36, "uint") & 0x10000000)
{
; minimized or not visible
if (this.wPrev != 0)
{
WinHide "ahk_id " hGui
this.wPrev := 0
}
sleep 10
return 1
}
x := NumGet(WINDOWINFO, 20, "int")
y := NumGet(WINDOWINFO, 8, "int")
w := NumGet(WINDOWINFO, 28, "int") - x
h := NumGet(WINDOWINFO, 32, "int") - y
move := 0
if (hGui = hGui1) and ((NumGet(WINDOWINFO, 44, "uint") = 1) or (DllCall("GetAncestor", "ptr", WinExist("A"), "uint", GA_ROOTOWNER := 3, "ptr") = hTarget))
{
; xx("activated")
hGui := hGui2
hChildMagnifier := hChildMagnifier2
move := 1
hideGui := hGui1
}
else if (hGui = hGui2) and (NumGet(WINDOWINFO, 44, "uint") != 1) and ((hr := DllCall("GetAncestor", "ptr", WinExist("A"), "uint", GA_ROOTOWNER := 3, "ptr")) != hTarget) and hr
{
; deactivated
hGui := hGui1
hChildMagnifier := hChildMagnifier1
WinMove x, y, w, h, "ahk_id " hGui
WinMove 0, 0, w, h, "ahk_id " hChildMagnifier
WinShow "ahk_id " hChildMagnifier
DllCall("SetWindowPos", "ptr", hGui, "ptr", hTarget, "int", 0, "int", 0, "int", 0, "int", 0, "uint", 0x0040|0x0010|0x001|0x002)
DllCall("SetWindowPos", "ptr", hTarget, "ptr", 1, "int", 0, "int", 0, "int", 0, "int", 0, "uint", 0x0040|0x0010|0x001|0x002) ; some windows can not be z-positioned before setting them to bottom
DllCall("SetWindowPos", "ptr", hTarget, "ptr", hGui, "int", 0, "int", 0, "int", 0, "int", 0, "uint", 0x0040|0x0010|0x001|0x002)
hideGui := hGui2
}
else if (x != this.xPrev) or (y != this.yPrev) or (w != this.wPrev) or (h != this.hPrev)
or (TaskBarAtBottom and (VisibleScreenHeight != this.VisibleScreenHeightPrev) and (y + h > VisibleScreenHeight))
{
; location changed or taskbar changed and is over window
move := 1
}
if(move) {
; never invert over a location that is used by the windows taskbar
if (TaskBarAtBottom and (y + h > VisibleScreenHeight)) {
h := VisibleScreenHeight - y ; escape taskbar
}
WinMove x, y, w, h, "ahk_id " hGui
WinMove 0, 0, w, h, "ahk_id " hChildMagnifier
WinShow "ahk_id " hChildMagnifier
WinShow "ahk_id " hGui
}
if (A_PtrSize = 8)
{
RECT := Buffer(16, 0)
NumPut("int", x, RECT, 0)
NumPut("int", y, RECT, 4)
NumPut("int", w, RECT, 8)
NumPut("int", h, RECT, 12)
DllCall("magnification.dll\MagSetWindowSource", "ptr", hChildMagnifier, "ptr", RECT)
}
else
DllCall("magnification.dll\MagSetWindowSource", "ptr", hChildMagnifier, "int", x, "int", y, "int", w, "int", h)
this.xPrev := x, this.yPrev := y, this.wPrev := w, this.hPrev := h, this.VisibleScreenHeightPrev := VisibleScreenHeight
this.hChildMagnifier := hChildMagnifier
this.hGui := hGui
if hideGui
{
WinHide "ahk_id " hideGui
hideGui := ""
}
return 1
}
}
; Automatically turn on inversion filter
loop
{
DetectHiddenWindows false ;would otherwise find many other windows
aTarget := WinExist("A")
if (aTarget != pTarget) { ; other window focused, check if it should be automatically inverted
pTarget:= aTarget
GroupIDs := WinGetList("ahk_group auto_invert_gp")
;Concat := ""
;For Each, Element In GroupIDs {
; If (Concat != "") {
; Concat .= "`n"
; }
; Concat .= Element ": " WinGetProcessName(Element) ;WinGetTitle(Element)
;}
;MsgBox Concat
Loop GroupIDs.Length
{
hTarget := GroupIDs[A_Index]
if (hTarget = aTarget) { ;currently focused window in auto_invert_gp group
found:= 0
For index, tmp in Inverters {
if(tmp.hTarget = hTarget) {
found:= 1
Break
}
}
if (found = 0) {
;MsgBox "Invert " hTarget " " WinGetProcessName(hTarget)
if IsWindowDarkMode(hTarget)
Break
hTargetName:= WinGetProcessName(hTarget)
ignore:= 0
Loop AutoInvIgnProcName.Length
{
if AutoInvIgnProcName[A_Index] = hTargetName {
ignore:= 1
Break
}
}
if (ignore = 0)
ToggleInversion(hTarget)
}
Break
}
}
}
; Refresh all inverted windows
first := true
For index, tmp in Inverters {
if(!tmp.stopped) {
; on first window refresh visible screen height
if (first) {
first := false
refreshVisibleScreenHeight()
}
ret := tmp.doit()
if(!ret) {
tmp.stop()
Inverters.removeAt(index)
Break
}
}
}
Sleep 100 ; sleep 100ms
}
ToggleInversion(hTarget) {
found:= 0
For index, tmp in Inverters {
if(tmp.hTarget=hTarget) {
found:= 1
if(tmp.stopped) {
tmp.start()
}
else {
tmp.stop()
}
Break
}
}
if (found = 0) {
tmp := Inverter(hTarget)
Inverters.push(tmp)
}
}
#i:: ;Win+i
{
ToggleInversion(WinExist("A"))
}
Uninitialize(ExitReason, ExitCode)
{
; When logging in on the PC after having a remotedesktop session to it somehow
; the app wants to exit with ExitReason=Close. This code then restarts the script.
if (ExitReason != "Menu") && (ExitReason != "Shutdown") {
DllCall("magnification.dll\MagUninitialize")
sleep 1000
Run "*uiAccess " A_ScriptFullPath ;restart script
}
ExitApp
}
@xypha
Copy link

xypha commented May 8, 2025

@charlie89 Thank you. This helps a lot :)

Looks like the inGroup() function is redundant and can be removed.

The text on my pc (acer EX215-31 win11 24h2) appears to be blurred in all apps I've tried (hh.exe, everything.exe, and AHK msgbox).
See example below with AHK help file showing comparison between this script with MatrixHigh dark mode (Feb 18 2025 version) and in-built dark mode.
Is there a way to correct this text blurring?

2025-05-08 @ 19:16:27

@charlie89
Copy link
Author

Updated the script to not automatically invert windows that are already dark (those who have attribute DWMWA_USE_IMMERSIVE_DARK_MODE set), i.e. the copy progress or delete confirmation dialogues in windows explorer in recent windows versions.

@xypha Sorry for the late reply: Windows font smoothing uses some colors, even for white and black fonts. The MatrixHigh filter inverts only the white and black part of the font but not the colored pixels for smoothing and that creates a blurry effect. MatrixInv inverts everything and thus doesn't look as blurry.

@xypha
Copy link

xypha commented Feb 21, 2026

@charlie89 Thank you. I didn't know this about font smoothing. I will try the updated script later and comment further if needed. Again, thank you for all of this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment