Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,12 @@ matching what Windows itself would have done. All writes stay in
`HKEY_CURRENT_USER` and the script is idempotent — a second run reports zero
changes.

On a machine where Night Light has never been used the two values do not exist
yet. Rather than failing the apply, the script seeds them. Sunset and sunrise
are deliberately left unset in that case: Windows derives them from the machine
location, and seeding them would only risk storing wrong times. Until Windows
computes them, the solar window is unknown and Night Light is left off.

Reversal: open Settings > System > Display > Night light and turn it off, or
delete the two registry values above and sign out.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,56 @@ function Test-NightLightWithinNightWindow {

#endregion

function Test-WindowsHost {
<#
.SYNOPSIS
True when running on Windows, under either PowerShell edition.

.DESCRIPTION
$IsWindows only exists in PowerShell Core. Chezmoi runs .ps1 scripts
with `powershell` (Windows PowerShell 5.1, PSEdition "Desktop"),
where the variable is undefined, so a guard that negates it directly
is always true and skips the script on the very platform it targets.
Desktop edition only ships on Windows, so treat it as a match.
#>
[OutputType([bool])]
param()

if ($PSVersionTable.PSEdition -eq "Desktop") {
return $true
}

return [bool](Get-Variable -Name IsWindows -ValueOnly -ErrorAction SilentlyContinue)
}

function Get-NightLightDefaultSetting {
<#
.SYNOPSIS
Baseline settings for a machine where Night Light was never used.

.DESCRIPTION
Sunset and sunrise are left at their defaults (00:00). Windows
recomputes them from the machine location and writes them back, so
seeding them here would only risk storing wrong times.
#>
[OutputType([psobject])]
param()

return [pscustomobject]@{
ScheduleEnabled = $true
SetHoursMode = $false
StartHour = 21
StartMinute = 0
EndHour = 7
EndMinute = 0
ColorTemperature = $script:NightLightMaxKelvin
SunsetHour = 0
SunsetMinute = 0
SunriseHour = 0
SunriseMinute = 0
}
}

function Set-NightLightConfiguration {
<#
.SYNOPSIS
Expand Down Expand Up @@ -561,12 +611,18 @@ function Set-NightLightConfiguration {
$desiredKelvin = ConvertTo-NightLightColorTemperature -Strength $Strength

$settingsBlob = & $GetRegistryValue -Path $script:NightLightSettingsPath
if (-not $settingsBlob) {
throw "Night Light settings are not initialised. Open Settings > System > Display > Night light once, then re-run."
if ($settingsBlob) {
$settings = ConvertFrom-NightLightSettingsPayload -Payload (ConvertFrom-CloudStoreBlob -Blob ([byte[]]$settingsBlob)).Payload
}
else {
# Night Light has never been used on this machine (fresh install, or a
# host without the feature). Seed a baseline rather than failing the
# whole chezmoi apply.
Write-Verbose "Night Light settings not present; creating them from scratch."
$settings = Get-NightLightDefaultSetting
}

$settings = ConvertFrom-NightLightSettingsPayload -Payload (ConvertFrom-CloudStoreBlob -Blob ([byte[]]$settingsBlob)).Payload
$settingsCorrect = $settings.ScheduleEnabled -and -not $settings.SetHoursMode -and $settings.ColorTemperature -eq $desiredKelvin
$settingsCorrect = $settingsBlob -and $settings.ScheduleEnabled -and -not $settings.SetHoursMode -and $settings.ColorTemperature -eq $desiredKelvin

if ($settingsCorrect) {
$results += [pscustomobject]@{ Setting = "Schedule and strength"; Status = "AlreadySet"; Changed = $false }
Expand Down Expand Up @@ -605,7 +661,7 @@ function Set-NightLightConfiguration {
}

if (-not $SkipApply) {
if (-not $IsWindows) {
if (-not (Test-WindowsHost)) {
Write-Host "[SKIP] Night Light is a Windows-only setting." -ForegroundColor Yellow
return
}
Expand Down
85 changes: 82 additions & 3 deletions tests/powershell/NightLight.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,34 @@ Describe "Night Light script" -Tag "Unit" {
}
}

Describe "Windows host detection" -Tag "Unit" {
It "reports true on the current Windows host" {
Test-WindowsHost | Should -BeTrue
}

It "does not guard on the bare `$IsWindows variable" {
# $IsWindows is undefined in Windows PowerShell 5.1, which is the
# interpreter chezmoi uses for .ps1 scripts. A bare guard therefore
# skips the script on the exact platform it targets.
$content = Get-Content -Path $script:ScriptPath -Raw
$content | Should -Not -Match '-not\s+\$IsWindows'
}

It "returns true under Windows PowerShell 5.1" -Skip:(-not (Get-Command powershell.exe -ErrorAction SilentlyContinue)) {
$output = & powershell.exe -NoLogo -NoProfile -Command @"
. '$script:ScriptPath' -SkipApply
if (Test-WindowsHost) { 'True' } else { 'False' }
"@
$LASTEXITCODE | Should -Be 0
($output | Select-Object -Last 1) | Should -Be "True"
}

It "runs without skipping under Windows PowerShell 5.1" -Skip:(-not (Get-Command powershell.exe -ErrorAction SilentlyContinue)) {
$output = & powershell.exe -NoLogo -NoProfile -File $script:ScriptPath -WhatIf 2>&1
($output -join "`n") | Should -Not -Match 'Windows-only setting'
}
}

Describe "Bond CompactBinary primitives" -Tag "Unit" {
It "round-trips varints" -ForEach @(
@{ Value = [uint64]0 }
Expand Down Expand Up @@ -267,6 +295,25 @@ Describe "Strength conversion" -Tag "Unit" {
}
}

Describe "Default settings" -Tag "Unit" {
It "describes a sunset-to-sunrise schedule with no cached solar times" {
$defaults = Get-NightLightDefaultSetting
$defaults.ScheduleEnabled | Should -BeTrue
$defaults.SetHoursMode | Should -BeFalse
$defaults.SunsetHour | Should -Be 0
$defaults.SunriseHour | Should -Be 0
}

It "round-trips through the payload codec" {
$defaults = Get-NightLightDefaultSetting
$payload = ConvertTo-NightLightSettingsPayload -Settings $defaults
$decoded = ConvertFrom-NightLightSettingsPayload -Payload $payload
$decoded.StartHour | Should -Be $defaults.StartHour
$decoded.EndHour | Should -Be $defaults.EndHour
$decoded.ScheduleEnabled | Should -BeTrue
}
}

Describe "Night window detection" -Tag "Unit" {
BeforeAll {
$script:WindowSettings = [pscustomobject]@{
Expand Down Expand Up @@ -368,12 +415,44 @@ Describe "Set-NightLightConfiguration" -Tag "Unit" {
@($results | Where-Object { $_.Status -eq "WhatIf" }).Count | Should -BeGreaterThan 0
}

It "fails with actionable guidance when Night Light was never initialised" {
$fake = script:New-FakeNightLightRegistry -SettingsBlob $null
It "creates the settings from scratch when Night Light was never initialised" {
$fake = script:New-FakeNightLightRegistry -SettingsBlob $null -StateBlob $null
$results = Set-NightLightConfiguration -Strength 50 -Now ([DateTime]::Parse("22:00")) `
-GetRegistryValue { param([string]$Path) $fake.Store[$Path] } `
-SetRegistryValue { param([string]$Path, [byte[]]$Value) $fake.Store[$Path] = $Value; $fake.Writes.Add($Path) }

@($results | Where-Object { $_.Changed }).Count | Should -BeGreaterThan 0
$fake.Store.ContainsKey($script:NightLightSettingsPath) | Should -BeTrue

$written = ConvertFrom-NightLightSettingsPayload -Payload (ConvertFrom-CloudStoreBlob -Blob $fake.Store[$script:NightLightSettingsPath]).Payload
$written.ScheduleEnabled | Should -BeTrue
$written.SetHoursMode | Should -BeFalse
$written.ColorTemperature | Should -Be 3850
}

It "leaves sunset and sunrise unset when creating from scratch" {
# Windows computes these from the machine location; seeding them would
# only risk storing wrong times.
$fake = script:New-FakeNightLightRegistry -SettingsBlob $null -StateBlob $null
Set-NightLightConfiguration -Strength 50 -Now ([DateTime]::Parse("22:00")) `
-GetRegistryValue { param([string]$Path) $fake.Store[$Path] } `
-SetRegistryValue { param([string]$Path, [byte[]]$Value) $fake.Store[$Path] = $Value; $fake.Writes.Add($Path) } | Out-Null

$written = ConvertFrom-NightLightSettingsPayload -Payload (ConvertFrom-CloudStoreBlob -Blob $fake.Store[$script:NightLightSettingsPath]).Payload
$written.SunsetHour | Should -Be 0
$written.SunriseHour | Should -Be 0

# With no solar window known, Night Light must not be forced on.
$state = ConvertFrom-NightLightStatePayload -Payload (ConvertFrom-CloudStoreBlob -Blob $fake.Store[$script:NightLightStatePath]).Payload
$state.Enabled | Should -BeFalse
}

It "does not throw on a host where Night Light was never initialised" {
$fake = script:New-FakeNightLightRegistry -SettingsBlob $null -StateBlob $null
{
Set-NightLightConfiguration -Strength 50 `
-GetRegistryValue { param([string]$Path) $fake.Store[$Path] } `
-SetRegistryValue { param([string]$Path, [byte[]]$Value) $fake.Store[$Path] = $Value }
} | Should -Throw "*not initialised*"
} | Should -Not -Throw
}
}