Scripting a theme toggle with Powershell

I’m a big fan of dark mode. I like to use it everywhere. I like it because it’s easier on my eyes. Especially if it the room is dark. But my room is not dark all the time. The morning sun comes straight in my window, and the room is bright even with no lights on. So, in the morning I have the opposite problem. Dark themes strain my eyes.

So I started switching it up. I keep things light themed in the morning, and switch to dark themes in the afternoon. This is pretty simple these days. I just make sure all my apps’ themes default to the system theme. Then I change the system theme when it’s time. But of course, there are always apps that don’t let you do that.

So today, with Claude’s help, I wrote a powershell script to automate this. I was pretty amazed I got a 90% solution pretty quickly. You can change the app theme by setting the property AppsUseLightTheme. And when you set it, they change immediately. You can change the VS Code theme simply by changing the workbench.colorTheme in settings.json. That’s also instantaneous.

But the Windows taskbard was still not reflecting the theme change. Setting the property SystemUsesLightTheme did not seem to do anything. But, Claude seemed to think the property just wasn’t propogating to where it needed to be. We wound up with this code that imports user32.dll to, in Claude’s words, “broadcast a WM_SETTINGCHANGE message with the “ImmersiveColorSet” parameter” which apparently triggers a refresh. Then we restart Explorer, which I was very nervous about the first time.

Now I am about 95% the way there. MS Teams is still refusing to change.

Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;

public class WindowsTheme {
    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    public static extern IntPtr SendMessageTimeout(
        IntPtr hWnd, uint Msg, UIntPtr wParam, IntPtr lParam,
        uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);
}
"@

$path = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize"
$vsCodeSettingsPath = "$env:APPDATA\Code - Insiders\User\settings.json"

$themes = @{
  Light = @{
    WindowsValue = 1
    VSCodeTheme  = "Default Light+"
  }
  Dark  = @{
    WindowsValue = 0
    VSCodeTheme  = "Dracula Theme"
  }
}

# Check current theme
$currentTheme = Get-ItemProperty -Path $path -Name SystemUsesLightTheme
$vsCodeSettings = Get-Content $vsCodeSettingsPath -Raw | ConvertFrom-Json

# Determine new theme
$newThemeMode = if ($currentTheme.SystemUsesLightTheme -eq 0) { "Light" } else { "Dark" }

# Update Windows theme (both system and apps)
Set-ItemProperty -Path $path -Name SystemUsesLightTheme -Value $themes[$newThemeMode].WindowsValue
Set-ItemProperty -Path $path -Name AppsUseLightTheme -Value $themes[$newThemeMode].WindowsValue

# Update VS Code theme
$vsCodeSettings.'workbench.colorTheme' = $themes[$newThemeMode].VSCodeTheme
$vsCodeSettings | ConvertTo-Json -Depth 100 | Set-Content $vsCodeSettingsPath

# Broadcast theme change message
$HWND_BROADCAST = [IntPtr]0xffff
$WM_SETTINGCHANGE = 0x001A
$result = [UIntPtr]::Zero
[WindowsTheme]::SendMessageTimeout($HWND_BROADCAST, $WM_SETTINGCHANGE, [UIntPtr]::Zero, "ImmersiveColorSet", 2, 5000, [ref]$result) | Out-Null

Write-Host "Switched to $newThemeMode mode"

# Restart Explorer to ensure all elements update
Stop-Process -Name explorer -Force
Start-Process explorer
Write-Host "Explorer restarted. All changes should now be visible."