How a simple settings tweak ended up with a lesson on writing and automating PowerShell scripts.
Light and Dark Themes
Like the MacOS, Windows 11 has a system-wide light and dark theme option.
For the uninitiated, a light theme is what we’ve been using until last week:

Some time ago, somebody–possibly working way into the night or just vampiric in the sense that they are somehow aversive to brightness–decided to start a trend of dark themes. When enabled, it turns everything … dark:

Windows 11 has two ways for a program an app to switch themes. Either it is passive and accepts the current default app theme, or it has its own app theme:

Some programs have this in their settings. Some don’t and will just default to the current system theme.
What’s the default app theme? Surprise! It’s actually called default app mode. 🤔
Switching Theme Mode
This is in Settings > Personalization > Colors

Someday, the Personalization department will talk with the Systems department and agree on a name. For now, it’s “mode” in Personalization and “theme” elsewhere.
If you simply choose “Light” in the “Choose your mode,” then everything is Light. Same for “Dark.” If you choose “Custom,” then you can set different modes for Windows itself (like the task bar) and the apps’ default mode.
If you want to set it up like in MacOS when it changes the mode during day- and night-time, tough cookies. Windows 11 doesn’t have that feature (yet?)
Like every other idiot, I then asked Copilot. And its response was (paraphrasing), “Sure! You can write a PowerShell script and schedule it to run periodically to update the theme.”
And that is when Dorothy jumps into the rabbit hole.
After some prompting exercises, here’s what Copilot suggested:
$location = "YOUR_LATITUDE,YOUR_LONGITUDE"
$url = "https://api.sunrise-sunset.org/json?lat=$location&formatted=0"
$response = Invoke-RestMethod -Uri $url
$sunrise = $response.results.sunrise
$sunset = $response.results.sunset
$sunriseLocal = [System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId([DateTime]::Parse($sunrise), "Pacific Standard Time")
$sunsetLocal = [System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId([DateTime]::Parse($sunset), "Pacific Standard Time")
if ((Get-Date) -ge $sunsetLocal) {
New-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" -Name "AppsUseLightTheme" -Value 0 -PropertyType DWord -Force
} elseif ((Get-Date) -ge $sunriseLocal) {
New-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" -Name "AppsUseLightTheme" -Value 1 -PropertyType DWord -Force
}
Going to https://www.mapdevelopers.com/where-am-i.php, I found my latitude and longitude.
I plugged those into $location
and ran the script. Then I opened up a PowerShell command prompt and ran the script.
Security
The dreaded security. This came up:
<my script file> cannot be loaded because running scripts is disabled on this system. For more information, see
about_Execution_Policies at https:/go.microsoft.com/fwlink/?LinkID=135170.
That link goes to: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.5
As is anything security related, it’s complicated and layered.
Firstly, there is a hierarchy of layers (called scopes) (in descending precedence):
MachinePolicy
— all users of the computer.UserPolicy
— based on the group policy of the current user.Process
— the current PowerShell “session.”CurrentUser
— the configuration file for the current user.LocalMachine
— the configuration file for all users (note this is not the same as MachinePolicy).
Each scope can have one of these execution policies:
AllSigned
Bypass
Default
RemoteSigned
Restricted
Undefined
— this is the default for the layers in my computer.Unrestricted
Due to the precedence rules, if a policy is found while walking down the scopes, the process will stop, and the policy found will be the answer.
For example, if a policy other than Undefined
is set in the MachinePolicy
scope, then it doesn’t matter what the other scopes say.
Also as documented, when all scopes are at Undefined
, since my computer is a regular Windows client, it maps to Restricted
. And that’s why the script cannot run:
> Get-ExecutionPolicy -list
Scope ExecutionPolicy
----- ---------------
MachinePolicy Undefined
UserPolicy Undefined
Process Undefined
CurrentUser Undefined
LocalMachine Undefined
> Get-ExecutionPolicy
Restricted
Since I wrote the script and it’s not downloaded, I can just set my policy to RemoteSigned
. This way, I still get some protection from malicious scripts from the Internet. Which scope to use? Since I have a PowerShell console open to iterate on the script, and I won’t be running the script any other way, I’ll use Process
. This way, I won’t leave the settings in a way that opens up the possibility to run other scripts that I don’t know about.
> Set-ExecutionPolicy RemoteSigned -scope Process
> Get-ExecutionPolicy -list
Scope ExecutionPolicy
----- ---------------
MachinePolicy Undefined
UserPolicy Undefined
Process RemoteSigned
CurrentUser Undefined
LocalMachine Undefined
> Get-ExecutionPolicy
RemoteSigned
This gets the script running.
Van 1. Security 0. Hell yeah.
Oh. There is another policy that I went through: using the AllSigned
policy and actually generating a self-signed certificate to sign the script with. I’ll save that for when I do feel masochistic enough to document. I might post something about that to https://pn.therealvan.com.
Bugs
Request Parameters
Now that the script runs, this comes up:
Invoke-RestMethod : The remote server returned an error: (400) Bad Request.
At ...
+ $response = Invoke-RestMethod -Uri $url
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod], WebException
+ FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand
Looking at the URL used in the script, I put it into Edge to see just what’s going on:
https://api.sunrise-sunset.org/json?lat=xxx,yyy&formatted=0
where xxx is my latitude and yyy is my longitude.
This came back from api.sunrise-sunset.org:
{"results":"","status":"INVALID_REQUEST"}
I then just tried https://sunrise-sunset.org/api to see if anything useful comes back. Sure enough, there is a pretty HTML page describing the endpoint. As far as the parameters:
lat (float): Latitude in decimal degrees. Required.
lng (float): Longitude in decimal degrees. Required.
date (string): Date in YYYY-MM-DD format. Also accepts other date formats and even relative date formats. If not present, date defaults to current date. Optional.
callback (string): Callback function name for JSONP response. Optional.
formatted (integer): 0 or 1 (1 is default). Time values in response will be expressed following ISO 8601 and day_length will be expressed in seconds. Optional.
tzid (string): A timezone identifier, like for example: UTC, Africa/Lagos, Asia/Hong_Kong, or Europe/Lisbon. The list of valid identifiers is available in this List of Supported Timezones. If provided, the times in the response will be referenced to the given Time Zone. Optional.
Looks like Copilot may have screwed me with that logic:
$location = "YOUR_LATITUDE,YOUR_LONGITUDE"
$url = "https://api.sunrise-sunset.org/json?lat=$location&formatted=0"
To be sure, I tried this URL in Edge:
https://api.sunrise-sunset.org/json?lat=xxx&lng=yyy&formatted=0
This time, the pretty JSON came back:
{"results":{"sunrise":"2025-06-17T12:44:50+00:00","sunset":"2025-06-18T03:34:00+00:00","solar_noon":"2025-06-17T20:09:25+00:00","day_length":53350,"civil_twilight_begin":"2025-06-17T12:15:07+00:00","civil_twilight_end":"2025-06-18T04:03:43+00:00","nautical_twilight_begin":"2025-06-17T11:35:57+00:00","nautical_twilight_end":"2025-06-18T04:42:53+00:00","astronomical_twilight_begin":"2025-06-17T10:51:36+00:00","astronomical_twilight_end":"2025-06-18T05:27:14+00:00"},"status":"OK","tzid":"UTC"}
So I modified the script from Copilot:
$lat = xxx
$lng = yyy
$url = "https://api.sunrise-sunset.org/json?lat=$lat&lng=$lng&formatted=0"
Now the script runs.
BUT IT DOES NOTHING.
More Bugs
Let’s see… There were bugs with time zone considerations. Then there was the bug that happened when the time was past midnight but before sunrise. Then the code only changed the app mode but not the system mode. Then there was a taskbar bug that didn’t update when the system color mode changed. Then there was the fact that the API returns only future instances. Then there was ….
There was a lot of interaction between Copilot and me. Admittedly, I was new to and was learning PowerShell scripting while doing this. So, looks like both Copilot and I learned something, if the session did indeed send feedback to Microsoft somewhere.
The resulting script, if you care:
# TODO: Plug in your own coordinates.
# One way is https://www.mapdevelopers.com/where-am-i.php
$lat = xxx
$lng = yyy
$url = "https://api.sunrise-sunset.org/json?lat=$lat&lng=$lng&formatted=0&tzid=utc"
$response = Invoke-RestMethod -Uri $url
# NOTE: The following will have converted the UTC datetimes to the local tz
$sunrise = [DateTime]::Parse($response.results.sunrise)
$sunset = [DateTime]::Parse($response.results.sunset)
# Extract only the time component
$sunriseTime = $sunrise.TimeOfDay
$sunsetTime = $sunset.TimeOfDay
$currentTime = (Get-Date).TimeOfDay
#Write-Output "Current Time: $currentTime"
#Write-Output "Sunrise Time: $sunriseTime"
#Write-Output "Sunset Time: $sunsetTime"
# If true, this will update the taskbar but at the cost of restarting explorer which can be disruptive.
$restartExplorer = $false
# Read the current Windows theme setting
$themePath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"
# Check if the registry key exists
if (!(Test-Path $themePath)) {
Write-Output "Registry key does not exist. Creating it..."
New-Item -Path $themePath -Force
}
# Check if the properties exist and create them if missing
$properties = @("AppsUseLightTheme", "SystemUsesLightTheme")
foreach ($property in $properties) {
try {
$value = Get-ItemProperty -Path $themePath -Name $property -ErrorAction Stop | Select-Object -ExpandProperty $property
} catch {
Write-Output "Property '$property' does not exist. Creating it..."
New-ItemProperty -Path $themePath -Name $property -Value 1 -PropertyType DWord -Force
}
}
$currentMode = Get-ItemProperty -Path $themePath -Name "SystemUsesLightTheme" | Select-Object -ExpandProperty SystemUsesLightTheme
# Determine the target mode based on time
$desiredMode = if (($currentTime -ge $sunsetTime) -or ($currentTime -lt $sunriseTime)) { 0 } else { 1 }
if ($currentMode -ne $desiredMode) {
Set-ItemProperty -Path $themePath -Name "AppsUseLightTheme" -Value $desiredMode -Force
Set-ItemProperty -Path $themePath -Name "SystemUsesLightTheme" -Value $desiredMode -Force
if ($restartExplorer) {
# Restart Explorer to refresh taskbar UI
Stop-Process -Name explorer -Force
Start-Process explorer.exe
}
} else {
Write-Output "No theme change needed."
}
If you looked carefully, paid attention, and thought about it, you probably would realize that I’m using the sunrise and sunset times in the future (e.g. tomorrow) instead of today.
This is a limitation due to the API returning only future instances. However, the way the solar system works right now, the sunrise and sunset times of any given location on Earth do not vary too much between two consecutive days. At least that’s my understanding. I might be wrong. If I’m right, I think most of us can live with the small discrepancy. You’re welcome to find a better way to get more accurate sunrise and sunset times.
Furthermore, there is no need to actually go by sunrise and sunset times; one can easily work with a set time pair like 06:30 and 19:00. These need adjusting only twice a year, thanks to daylight savings.
Scheduling the Script
To schedule the PowerShell script,
- Open the program Task Scheduler.
- On the right pane, click “Create a Basic Task.” Give it a name. Click “Next.”
- Set the Trigger to “Daily.” Click “Next.”
- Set Recur every 1 day. Click “Next.”
- For Action, choose “Start a program.” Click “Next.”
- For Program/script, enter: “
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
” (or the full path to wherever yourpowershell.exe
is). - For Add arguments, enter: “
-ExecutionPolicy Bypass -File <full path to the script file>
”- Remember the discussion on execution policy above? Using
-ExecutionPolicy Bypass
here is the same as setting the policy toBypass
for the scopeProcess
. We could have used the policyRemoteSigned
as well. However, since the task specifically calls a script that we know well, there’s really no need to check. - There is a slight chance that some malicious code modifies the script. 🤔
- Remember the discussion on execution policy above? Using
- Save the task.
This will run the script ONCE daily.
- Ensure the “Task Scheduler Library” node is selected on the left pane.
- In the center top, locate the entry for the task added previously.
- Right-click on the task and bring up its properties.
- Click on the “Triggers” tab. Click on the “Daily” trigger and click “Edit…”
- In the pop-up, look under the section “Advanced settings” for the “Repeat task every:” setting and set that to “1 hour” for a duration of “1 day.” Of course, be sure it’s “Enabled.”
- Optional:
- Check “Stop all running tasks at end of repetition duration.”
- Set “Stop task if it runs longer than:” to “30 minutes.” (The task should run at most a few seconds typically. The wildcard is that REST call to api.sunrise-sunset.org.)
- Click OK.

This should now run the script hourly.
Epilog
I’ll say this, though: it was a pleasant experience pairing with Copilot. While I would’ve appreciated a bug-free working script to start with, going through this experience made me learn more about PowerShell.
I see why some people are going gaga over this whole AI thing.