Lumos.psm1
|
Function Invoke-Lumos { <# .SYNOPSIS Sets the Windows or Mac Theme to light or dark mode dependent on time of day. .DESCRIPTION Use this cmdlet to change the theme on Windows 10 or MacOS Mojave to the light of dark themes, either as specified by parameters or (for Windows only), automatically based on the local time of day and whether it is before or after sunrise/sunset. .PARAMETER Dark Switch to the Dark OS theme. .PARAMETER Light Switch to the Light OS theme. .PARAMETER ExcludeSystem Exclude changing the System theme when switching to Dark/Light (Windows only). .PARAMETER IncludeOfficeProPlus Include changing the theme of Microsoft Office to Dark/Light (Windows only). .PARAMETER ExcludeApps Exclude changing the Applications (where supported) theme when switching to Dark/Light (Windows only). .PARAMETER DarkWallpaper Specify a path to use to modify the Desktop Wallpaper to when switching to the Dark theme. .PARAMETER LightWallpaper Specify a path to use to modify the Desktop Wallpaper to when switching to the Light theme. .EXAMPLE Invoke-Lumos -Dark -DarkWallpaper ./dark-wallpaper.png Switches the OS theme to the Dark theme and specified Wallpaper. .EXAMPLE Invoke-Lumos -Light -LightWallpaper ./light-wallpaper.png Swithches the OS theme to the Light theme and specified Wallpaper. .EXAMPLE Invoke-Lumos -Dark -ExcludeApps Switches the OS theme to Dark, but (on Windows only) does not change the theme of apps that support Dark/Light theme. .Example Invoke-Lumos On Windows: Switches to either Dark or Light theme dependent on your current location/time of day. On MacOS: Switches current theme from either Light to Dark or Dark to Light. #> [cmdletbinding(DefaultParameterSetName = 'Dark')] Param( [Parameter(ParameterSetName = 'Dark')] [switch] $Dark, [Parameter(ParameterSetName = 'Light')] [switch] $Light, [switch] $ExcludeSystem, [switch] $IncludeOfficeProPlus, [switch] $ExcludeApps, [string] $DarkWallpaper, [string] $LightWallpaper ) if ($Dark) { $Lumos = 0 } elseif ($Light) { $Lumos = 1 } elseif ($IsMacOS) { ### MacOS ### # Leaving Lumos as undefined on MacOS will make it just alternate to whatever mode it currently is not $Lumos = 'Undefined' } else { ### Windows ### $CurrentTime = Get-Date $UserLocation = Get-UserLocation if ($UserLocation) { $DayLight = Get-LocalDaylight -Latitude $UserLocation.Latitude -Longitude $UserLocation.Longitude } else { Throw 'Could not get sunrise/sunset data for the current user.' } if ($CurrentTime -ge $DayLight.Sunrise -and $CurrentTime -lt $DayLight.Sunset) { $Lumos = 1 } else { $Lumos = 0 } } Switch ($Lumos) { 0 { $Status = 'Dark' if ($DarkWallpaper) { $Wallpaper = $DarkWallpaper } } 1 { $Status = 'Light' if ($LightWallpaper) { $Wallpaper = $LightWallpaper } } default { $Status = 'Undefined' } } if ($IsMacOS) { ### MacOS ### $MacCommand = if ($Lumos -eq 0) { 'tell application \"System Events\" to tell appearance preferences to set dark mode to true' } elseif ($Lumos -eq 1) { 'tell application \"System Events\" to tell appearance preferences to set dark mode to false' } else { 'tell application \"System Events\" to tell appearance preferences to set dark mode to not dark mode' } Invoke-AppleScript -Command $MacCommand if ($ExcludeSystem) { Write-Error '-ExcludeSystem is not currently supported on MacOS.' } if ($ExcludeApps) { Write-Error '-ExcludeApps is not currently supported on MacOS.' } if ($IncludeOfficeProPlus) { Write-Error '-OfficeProPlus is not currently supported on MacOS.' } if ($Wallpaper) { $MacCommand = "tell application \`"System Events\`" to tell current desktop to set picture to \`"$Wallpaper\`"" Invoke-AppleScript -Command $MacCommand } } elseif ($IsLinux) { ### Linux ### Throw 'Linux is not currently supported by this module.' } else { ### Windows ### $ThemeRegKey = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize' $OfficeThemeRegKey = 'HKCU:\Software\Microsoft\Office\16.0\Common' if (-not $ExcludeSystem) { $CurrentSystemTheme = (Get-ItemProperty -Path $ThemeRegKey -Name 'SystemUsesLightTheme' -ErrorAction SilentlyContinue).SystemUsesLightTheme if ($CurrentSystemTheme -ne $Lumos) { Write-Verbose "Setting System to $Status Theme.." Set-ItemProperty -Path $ThemeRegKey -Name 'SystemUsesLightTheme' -Value $Lumos # The taskbar (and Start, Action Center) follow SystemUsesLightTheme, not AppsUseLightTheme, and # Explorer's taskbar - including on secondary monitors - only repaints correctly after being # restarted. Only doing this when the theme actually changed avoids restarting Explorer on every # run of a frequently scheduled task (e.g. Register-LumosScheduledTask's 15 minute trigger). Write-Verbose 'Restarting Explorer to apply the theme change to the taskbar..' Stop-Process -ProcessName explorer } } if (-not $ExcludeApps) { $CurrentAppsTheme = (Get-ItemProperty -Path $ThemeRegKey -Name 'AppsUseLightTheme' -ErrorAction SilentlyContinue).AppsUseLightTheme if ($CurrentAppsTheme -ne $Lumos) { Write-Verbose "Setting Apps to $Status Theme.." Set-ItemProperty -Path $ThemeRegKey -Name 'AppsUseLightTheme' -Value $Lumos } } if ($IncludeOfficeProPlus) { $proPlusThemeValue = if ($Lumos -eq 0) { 4 } else { if (Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\O365ProPlusRetail*") { 5 } else { 0 } } Write-Verbose "Setting OfficeProPlus to $Status with value: $proPlusThemeValue .." Set-ItemProperty -Path $OfficeThemeRegKey -Name 'UI Theme' -Value $proPlusThemeValue -Type DWORD $IdentitiesRegKey = $OfficeThemeRegKey + "\Roaming\Identities\" if (Test-Path $IdentitiesRegKey) { Get-ChildItem -Path $IdentitiesRegKey | ForEach-Object { $identityPath = ($_.Name.Replace('HKEY_CURRENT_USER', 'HKCU:') + "\Settings\1186\{00000000-0000-0000-0000-000000000000}"); if (Get-ItemProperty -Path $identityPath -Name 'Data' -ErrorAction Ignore) { Write-Verbose "Active identity path for ProPlus installation: $identityPath" Set-ItemProperty -Path $identityPath -Name 'Data' -Value ([byte[]]($proPlusThemeValue, 0, 0, 0)) -Type Binary } Break } } } if ($Wallpaper) { Set-Wallpaper $Wallpaper } } } Function Register-LumosScheduledTask { <# .SYNOPSIS Registers a Scheduled Task to run Lumos automatically on Windows. .DESCRIPTION Use this cmdlet to register a scheduled task on Windows so that Invoke-Lumos is executed using your specified parameters repeatedly every 15 minutes. Invoke-Lumos looks up the current sunrise/sunset for your location on every run and only changes anything when the theme needs to change, so this keeps the Dark/Light switch closely aligned with sunrise and sunset without the scheduled task itself ever needing its trigger times updated. The task runs as the current user at standard (non-elevated) privilege - Lumos only ever changes current-user settings, so no administrator rights are required. Its trigger is fixed at registration time rather than being refreshed later, since repeatedly re-registering the task to update trigger times proved unreliable in practice. The task deliberately has no "at logon" trigger, since some endpoint security software blocks non-admin users from registering one (likely because it's a common persistence technique) - the repeating trigger fires immediately on registration and again within 15 minutes of any logon, so this has little practical effect. .PARAMETER ExcludeSystem Exclude changing the System theme when switching to Dark/Light (Windows only) when the task runs. .PARAMETER IncludeOfficeProPlus Include changing the theme of Microsoft Office to Dark/Light (Windows only) when the task runs. .PARAMETER ExcludeApps Exclude changing the Applications (where supported) theme when switching to Dark/Light (Windows only) when the task runs. .PARAMETER DarkWallpaper Specify a path to use to modify the Desktop Wallpaper to when the task runs and switches to the Dark theme. .PARAMETER LightWallpaper Specify a path to use to modify the Desktop Wallpaper to when the task runs and switches to the Light theme. .EXAMPLE Register-LumosScheduledTask -ExcludeApps -DarkWallpaper C:\Temp\dark.png -LightWallpaper C:\Temp\light.png Creates a scheduled task that runs every 15 minutes, switching just the OS theme to either dark or light based on the current local sunrise/sunset, along with the specified light or dark wallpaper. #> [cmdletbinding()] Param( [switch] $ExcludeSystem, [switch] $ExcludeApps, [switch] $IncludeOfficeProPlus, [string] $DarkWallpaper, [string] $LightWallpaper ) if (-not ($PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows)) { Write-Warning 'Register-LumosScheduledTask is only supported on Windows.' return } $ArgumentDefaults = '-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden' $LumosArgument = "$ArgumentDefaults -Command Invoke-Lumos" If ($ExcludeSystem) { $LumosArgument = $LumosArgument + " -ExcludeSystem" } If ($ExcludeApps) { $LumosArgument = $LumosArgument + " -ExcludeApps" } If ($IncludeOfficeProPlus) { $LumosArgument = $LumosArgument + " -IncludeOfficeProPlus" } If ($LightWallpaper) { $LumosArgument = $LumosArgument + " -LightWallpaper '$LightWallpaper'" } If ($DarkWallpaper) { $LumosArgument = $LumosArgument + " -DarkWallpaper '$DarkWallpaper'" } $LumosAction = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $LumosArgument $Principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive $TaskSettings = New-ScheduledTaskSettingsSet -StartWhenAvailable # Repeats indefinitely every 15 minutes so Invoke-Lumos re-checks sunrise/sunset regularly, without ever # needing the task's own triggers to be updated later. Deliberately not an "at logon" trigger - some # endpoint security software denies non-admin users permission to register one. $IntervalTrigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 15) New-ScheduledTask -Action $LumosAction -Principal $Principal -Settings $TaskSettings -Trigger $IntervalTrigger | Register-ScheduledTask -TaskName 'Lumos' -Force | Out-Null } if (-not (Test-Path alias:lumos)) { New-Alias -Name 'lumos' -Value 'Invoke-Lumos' Export-ModuleMember -Alias 'lumos' } Function Get-LocalDaylight { <# .SYNOPSIS Returns the current sunrise and sunset times for the local user in localtime. .EXAMPLE Get-LocalDaylight Result ----------- Sunrise : 06/08/2019 06:04:57 Sunset : 06/08/2019 20:22:17 #> [cmdletbinding()] Param( [Parameter(Mandatory)] [double] $Latitude, [Parameter(Mandatory)] [double] $Longitude ) # Return sunrise/sunset $Daylight = (Invoke-RestMethod "https://api.sunrise-sunset.org/json?lat=$Latitude&lng=$Longitude").results # Convert to local time datetime objects [pscustomobject]@{ Sunrise = ($Daylight.Sunrise | Get-Date).ToLocalTime() Sunset = ($Daylight.Sunset | Get-Date).ToLocalTime() } } Function Get-UserLocation { <# .SYNOPSIS Returns the approximate location of the local user, based on their public IP address. .DESCRIPTION Looks up the city-level location of the current public IP address via the ipinfo.io API. This is used instead of the Windows Location Service because that requires location permission to be granted interactively and is typically unavailable when this module is run from a Scheduled Task. .EXAMPLE Get-UserLocation Result ----------- Latitude : 51.5074 Longitude : -0.1278 #> [cmdletbinding()] Param() $IPInfo = Invoke-RestMethod -Uri 'https://ipinfo.io/json' if ($IPInfo.loc) { $Latitude, $Longitude = $IPInfo.loc -split ',' [pscustomobject]@{ Latitude = [double]$Latitude Longitude = [double]$Longitude } } } Function Invoke-AppleScript { <# .SYNOPSIS Executes a command string via Apple Script. #> [cmdletbinding(SupportsShouldProcess)] param( [Parameter(Mandatory, ValueFromPipeline, Position = 0)] [String] $Command ) Begin { } Process { If ($PSCmdlet.ShouldProcess('/usr/bin/osascript -e',$Command)){ /usr/bin/osascript -e $Command } } } Function Set-Wallpaper { <# .SYNOPSIS Applies a specified wallpaper to the current user's desktop .PARAMETER Image Provide the full path to the image .EXAMPLE Set-WallPaper -Image "C:\Wallpaper\Default.jpg" #> [cmdletbinding(SupportsShouldProcess)] Param( [string] $Image ) Add-Type -TypeDefinition @" using System; using System.Runtime.InteropServices; public class Params { [DllImport("User32.dll",CharSet=CharSet.Unicode)] public static extern int SystemParametersInfo (Int32 uAction, Int32 uParam, String lpvParam, Int32 fuWinIni); } "@ $SPI_SETDESKWALLPAPER = 0x0014 $UpdateIniFile = 0x01 $SendChangeEvent = 0x02 $fWinIni = $UpdateIniFile -bor $SendChangeEvent if ($PSCmdlet.ShouldProcess($Image)) { [void][Params]::SystemParametersInfo($SPI_SETDESKWALLPAPER, 0, $Image, $fWinIni) } } |