-
-
Save charlie89/5670d46c90abf3351ed012b2afd926ca to your computer and use it in GitHub Desktop.
| #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: Modern Excel versions already have a dark mode for the cells, just select the View tab, then Switch Modes button. Should work much better than this script, especially when minimizing the menu or making the formula line multi-line the script fails as it would need other coordinates.
But i really want this feature for the old Control Panel that @cfercl just mentioned (that has the top part dark and the rest white), so can you please make it available? You should be able to attach it here as a file (top right corner of the comment box) or just send it to me via email, or create a new gist, whatever you like. Thanks!
@cfercl: Open window spy (a tool from Autohotkey v2) and hover over the control panel, then it shows ahk_class CabinetWClass, so just add GroupAdd "auto_invert_gp", "ahk_class CabinetWClass" to the script. And you can always press Win+I to invert it manually.
Or just wait a few days until i added it to that script.
@cfercl: Open window spy (a tool from Autohotkey v2) and hover over the control panel, then it shows
ahk_class CabinetWClass, so just addGroupAdd "auto_invert_gp", "ahk_class CabinetWClass"to the script. And you can always pressWin+Ito invert it manually. Or just wait a few days until i added it to that script.
Hi @charlie89 , I did that, but, for some reason, mmc.exe and regedit.exe works OK but control panel no. Maybe some kind of issue in the script? Maybe you can try?
note: Win+I works OK with control panel.
windows 11 25h2
thank you
@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
@charlie89 Thank you for this script - far more suited to my needs than any of the commercial products I've seen. No names mentioned, but bloated suites, slow responses and regional inversion rather than true per window were issues :)
While I was using it I got the notion that I'd give it some per window configuration. Allowing me to (for instance) just invert the cells in Excel since the rest is in dark mode already.
all I need to do is add the program to this list:
New to Github - if anyone wants to see the modifications let me know the best way to post them - not sure the list of instructions I did for my friend would be welcome - it's a bit lengthy :)