Skip to content

Instantly share code, notes, and snippets.

@charlie89
Last active June 23, 2026 13:21
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
}
@therealorang-dot

Copy link
Copy Markdown

@charlie89 - okay, I so didn't know that. There are other places I like doing this, so worth the little bit of time :)

Hmm, the paperclip ony allows images? Okay, I got to get to actual work :) - so I'm just going to put the instructions I gave my friend Nick here.

1. Add this near the matrix definitions, after 

global Matrix := MatrixInv

; ---------------------------------------------------------------------------
; Per-window inversion rules
;
; Match:
;   Any normal AHK window selector string.
;   Examples:
;       "ahk_exe EXCEL.EXE"
;       "^Get External Data.* ahk_exe MSACCESS.EXE"
;       "ahk_class wndclass_desked_gsk"
;
; Matrix:
;   Which color matrix to use for this window.
;   Options already defined above:
;       MatrixInv
;       MatrixSmart
;       MatrixHigh
;
; Top / Bottom / Left / Right:
;   Pixel offsets to exclude parts of the window from inversion.
;   Example: for Excel, you might skip the ribbon and status bar:
;       Top: 145, Bottom: 28, Left: 0, Right: 0
;
; First matching rule wins.
; If no rule matches, DefaultInvertRule is used.
; ---------------------------------------------------------------------------

global DefaultInvertRule := {
    Matrix: MatrixInv,
    Top: 0,
    Bottom: 0,
    Left: 0,
    Right: 0
}

global InvertRules := [
    {
        Match: "ahk_exe EXCEL.EXE",
        Matrix: MatrixSmart,
        Top: 258,
        Bottom: 59,
        Left: 28,
        Right: 24
    }

]


2. Add this function somewhere above class Inverter

GetInvertRule(hTarget) {
    global InvertRules
    global DefaultInvertRule

    for rule in InvertRules {
        if WinExist(rule.Match " ahk_id " hTarget)
            return rule
    }

    return DefaultInvertRule
}

3. Change the class constructor

Replace:
__New(hTarget)
with
__New(hTarget, rule)


Then inside __New, right after:

this.hTarget := hTarget

add

this.Matrix := rule.Matrix
this.TopOffset := rule.Top
this.BottomOffset := rule.Bottom
this.LeftOffset := rule.Left
this.RightOffset := rule.Right

Then replace this:

Loop Parse Matrix, "|"
with
Loop Parse this.Matrix, "|"

Near the top of class inverter add

Matrix := ""
TopOffset := 0
BottomOffset := 0
LeftOffset := 0
RightOffset := 0


Right after the block that gets the window coordinates
x := NumGet(WINDOWINFO, 20, "int")
y := NumGet(WINDOWINFO, 8, "int")
w := NumGet(WINDOWINFO, 28, "int") - x
h := NumGet(WINDOWINFO, 32, "int") - y

add

x += this.LeftOffset
y += this.TopOffset
w -= this.LeftOffset + this.RightOffset
h -= this.TopOffset + this.BottomOffset


replace
tmp := Inverter(hTarget)
with
tmp := Inverter(hTarget, GetInvertRule(hTarget))



Add entries to the table for other programs.  The Excel one fits my layout perfectly so I don't invert the UI, only the cells



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