Public/Install-TerminalColorsBackdrop.ps1
|
# Opaque backdrop: what makes it possible to have the tab, the title bar and the # border in PURE colour while leaving the pane background unchanged. # # Why it is needed. A Windows Terminal theme only accepts four values for # tab.background and tabRow.background: terminalBackground, accent, a fixed # colour, or nothing. The only one steerable at runtime is terminalBackground. # In other words [tab colour] and [background colour] are one and the same # channel - hence the historical 30 % dilution. # # The backdrop decouples the two: OSC 11 sends the PURE project colour (which # Windows Terminal copies onto the tab and the title bar), and an opaque # background image of your usual background colour is laid over the pane. Windows # Terminal paints the tab from the background colour, never from the image: the # tab becomes vivid, the pane stays as it was. # # Verified: background colour #FF0000 plus a #0C0C0C image does give a tab and a # title bar at #FF0000 for a pane measured at #0C0C0C. $script:TcBackdropPrefix = 'backdrop-' function Get-TcBackdropDirectory { return (Get-TcDataDirectory) } function Get-TcBackdropPath { param([hashtable] $Rgb) $name = '{0}{1:X2}{2:X2}{3:X2}.png' -f $script:TcBackdropPrefix, [int]$Rgb.R, [int]$Rgb.G, [int]$Rgb.B return (Join-Path (Get-TcBackdropDirectory) $name) } function Test-TcBackdropPath { <# .SYNOPSIS Indicates whether a background image path is an image generated by TerminalColors (rather than one of the user's own). #> param([string] $Path) if ([string]::IsNullOrWhiteSpace($Path)) { return $false } $leaf = '' try { $leaf = Split-Path $Path -Leaf } catch { return $false } return ($leaf -like ($script:TcBackdropPrefix + '*.png')) } function ConvertTo-TcJsonString { <# .SYNOPSIS JSON string literal, quotes included, escaping applied. #> param([string] $Value) $escaped = $Value.Replace('\', '\\').Replace('"', '\"') return '"' + $escaped + '"' } function Initialize-TcWtProfileDefaults { <# .SYNOPSIS Guarantees that profiles.defaults exists in the settings.json text and returns @{ Text; Anchor } where Anchor points at that object's opening brace. #> param([string] $Text, [int] $Depth = 0) if ($Depth -gt 3) { throw 'TerminalColors: unable to create profiles.defaults in settings.json.' } $masked = Get-TcMaskedJson -Text $Text $m = [regex]::Match($masked, '"profiles"\s*:\s*') if (-not $m.Success) { $root = Find-TcJsonRootIndex -Masked $masked $fragment = [Environment]::NewLine + ' "profiles":' + [Environment]::NewLine + ' {' + [Environment]::NewLine + ' "defaults": {}' + [Environment]::NewLine + ' },' return (Initialize-TcWtProfileDefaults -Text $Text.Insert($root + 1, $fragment) -Depth ($Depth + 1)) } $valueStart = $m.Index + $m.Length if ($valueStart -ge $masked.Length) { throw 'TerminalColors: "profiles" is truncated in settings.json.' } if ($masked[$valueStart] -eq '[') { throw 'TerminalColors: this settings.json uses the legacy format where "profiles" is an array. Open the Windows Terminal settings and save them once to convert it, then try again.' } if ($masked[$valueStart] -ne '{') { throw 'TerminalColors: "profiles" is not an object in settings.json.' } $profilesSpan = Find-TcJsonBlockSpan -Text $masked -Index $valueStart if ($null -eq $profilesSpan) { throw 'TerminalColors: "profiles" is malformed in settings.json.' } $defaults = Find-TcJsonMember -Masked $masked -Span $profilesSpan -Name 'defaults' if ($null -eq $defaults) { $fragment = [Environment]::NewLine + ' "defaults": {},' return (Initialize-TcWtProfileDefaults -Text $Text.Insert($profilesSpan.Start + 1, $fragment) -Depth ($Depth + 1)) } if ($masked[$defaults.ValueStart] -ne '{') { throw 'TerminalColors: profiles.defaults is not an object in settings.json.' } return @{ Text = $Text; Anchor = $defaults.ValueStart } } function Get-TcBackdropSettings { <# .SYNOPSIS Reads the backdrop state from a parsed settings.json. #> param($Settings) $result = @{ Image = $null; Opacity = $null; StretchMode = $null; ProfileOverrides = @() } $profiles = Get-TcJsonProperty -InputObject $Settings -Name 'profiles' if ($null -eq $profiles) { return $result } $defaults = Get-TcJsonProperty -InputObject $profiles -Name 'defaults' if ($defaults) { $result.Image = Get-TcJsonProperty -InputObject $defaults -Name 'backgroundImage' $result.Opacity = Get-TcJsonProperty -InputObject $defaults -Name 'backgroundImageOpacity' $result.StretchMode = Get-TcJsonProperty -InputObject $defaults -Name 'backgroundImageStretchMode' } # An image set on a specific profile overrides profiles.defaults: the backdrop # would then have no effect in that profile. $list = Get-TcJsonProperty -InputObject $profiles -Name 'list' if ($list) { $overrides = @() foreach ($p in $list) { $image = Get-TcJsonProperty -InputObject $p -Name 'backgroundImage' if ($image -and -not (Test-TcBackdropPath -Path ([string]$image))) { $label = [string](Get-TcJsonProperty -InputObject $p -Name 'name') if (-not $label) { $label = [string](Get-TcJsonProperty -InputObject $p -Name 'guid') } $overrides += $label } } $result.ProfileOverrides = $overrides } return $result } function Install-TerminalColorsBackdrop { <# .SYNOPSIS Installs the opaque backdrop: the tab, the title bar and the border switch to pure colour, and the pane background stops changing. .DESCRIPTION Generates a solid PNG image of your current background colour and declares it as the background image in Windows Terminal's profiles.defaults (backgroundImage, backgroundImageOpacity, backgroundImageStretchMode). Windows Terminal paints the tab and the title bar from the background colour, never from the background image. The background colour can therefore carry the pure project colour without the pane changing at all. Combine this with [Enable-TerminalColors -PureColor], which sends the project colour undiluted. The TerminalColors theme is still required: it is what ties the tab to the background colour. settings.json is modified by targeted insertion, with a backup taken first and the result validated before writing. .PARAMETER SettingsPath Path of the settings.json to modify. Detected automatically by default. .PARAMETER Color Backdrop colour, that is the colour the pane will keep. Defaults to the background colour already declared by your profile or your Windows Terminal colour scheme, so the pane stays exactly as it is. .PARAMETER Force Replaces a background image already declared that does not come from TerminalColors. Without this switch the command stops rather than overwriting your setting. .PARAMETER NoBackup Does not write a backup copy. .EXAMPLE Install-TerminalColorsBackdrop .EXAMPLE Install-TerminalColorsBackdrop -Color '#000000' Pins a perfectly black pane, whatever the profile's colour scheme. .EXAMPLE Install-TerminalColorsBackdrop -WhatIf Shows what would change without writing anything. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] [OutputType([pscustomobject])] param( [string] $SettingsPath, [string] $Color, [switch] $Force, [switch] $NoBackup ) $SettingsPath = Resolve-TcWtSettingsPath -Path $SettingsPath $text = [System.IO.File]::ReadAllText($SettingsPath) $settings = ConvertFrom-TcJsonText -Text $text if ($null -eq $settings) { throw "TerminalColors: [$SettingsPath] is not valid JSON. Fix it before installing the backdrop." } # --- 1. Backdrop colour -------------------------------------------------- if ($Color) { $rgb = ConvertFrom-TcColor -Value $Color if ($null -eq $rgb) { throw "TerminalColors: invalid backdrop colour [$Color]." } } else { $rgb = Get-TcSettingsBackground -Settings $settings -ProfileId $env:WT_PROFILE_ID if ($null -eq $rgb) { $rgb = ConvertFrom-TcColor -Value $script:TcDefaultBaseBackground } } $hex = ConvertTo-TcHex -Rgb $rgb $imagePath = Get-TcBackdropPath -Rgb $rgb # --- 2. Refuse to overwrite an image of the user's own ------------------- $current = Get-TcBackdropSettings -Settings $settings $previousImage = [string]$current.Image $foreign = ($previousImage -and -not (Test-TcBackdropPath -Path $previousImage)) if ($foreign -and -not $Force) { throw "TerminalColors: a background image is already declared in profiles.defaults [$previousImage]. Use -Force to replace it (it will be restored by Uninstall-TerminalColorsBackdrop)." } # --- 3. Edit settings.json ------------------------------------------------ $prepared = Initialize-TcWtProfileDefaults -Text $text $newText = $prepared.Text $anchor = $prepared.Anchor $changes = @() $members = @( @{ Name = 'backgroundImage'; Literal = (ConvertTo-TcJsonString -Value $imagePath) } @{ Name = 'backgroundImageOpacity'; Literal = '1.0' } @{ Name = 'backgroundImageStretchMode'; Literal = '"fill"' } ) foreach ($member in $members) { # Every edit invalidates the indexes: relocate the anchor each time. $prepared = Initialize-TcWtProfileDefaults -Text $newText $newText = $prepared.Text $anchor = $prepared.Anchor $applied = Set-TcJsonMember -Text $newText -AnchorIndex $anchor -Name $member.Name -Literal $member.Literal $newText = $applied.Text if ($applied.Action -ne 'unchanged') { $changes += "$($member.Name) $($applied.Action)" } } # --- 4. Validate before writing ------------------------------------------- $parsed = ConvertFrom-TcJsonText -Text $newText if ($null -eq $parsed) { throw 'TerminalColors: the change would have produced invalid JSON. Nothing was written. Please report this case along with your settings.json.' } $check = Get-TcBackdropSettings -Settings $parsed if ([string]$check.Image -ne $imagePath) { throw 'TerminalColors: verification failed (background image not declared). Nothing was written.' } if ([double]$check.Opacity -ne 1.0) { throw 'TerminalColors: verification failed (opacity other than 1). Nothing was written.' } if ([string]$check.StretchMode -ne 'fill') { throw 'TerminalColors: verification failed (unexpected stretch mode). Nothing was written.' } # --- 5. Write ------------------------------------------------------------- $backupPath = $null $written = $false $target = "Install the opaque backdrop $hex" if ($changes.Count -gt 0) { $target = "$target ($($changes -join ', '))" } if ($PSCmdlet.ShouldProcess($SettingsPath, $target)) { Write-TcSolidPng -Path $imagePath -Rgb $rgb | Out-Null # Backdrops for older colours are of no further use. Get-ChildItem -Path (Get-TcBackdropDirectory) -Filter ($script:TcBackdropPrefix + '*.png') -ErrorAction SilentlyContinue | Where-Object { $_.FullName -ne $imagePath } | Remove-Item -Force -ErrorAction SilentlyContinue if ($changes.Count -gt 0) { if ($foreign) { Set-TcInstallState -Name 'PreviousBackgroundImage' -Value $previousImage Set-TcInstallState -Name 'PreviousBackgroundImageOpacity' -Value $current.Opacity Set-TcInstallState -Name 'PreviousBackgroundImageStretchMode' -Value $current.StretchMode } $backupPath = Save-TcWtSettings -Path $SettingsPath -Text $newText -NoBackup:$NoBackup } $written = $true } return [pscustomobject]@{ SettingsPath = $SettingsPath Changed = ($changes.Count -gt 0) Changes = $changes Color = $hex ImagePath = $imagePath ImageWritten = $written Replaced = $foreign Backup = $backupPath } } function Uninstall-TerminalColorsBackdrop { <# .SYNOPSIS Removes the opaque backdrop and restores the previous background image if there was one. .DESCRIPTION After this command the pane background follows the colour sent by the module again: remember to drop [-PureColor] from your profile, otherwise the pane will take the pure project colour. .PARAMETER SettingsPath Path of the settings.json to modify. Detected automatically by default. .PARAMETER NoBackup Does not write a backup copy. .EXAMPLE Uninstall-TerminalColorsBackdrop #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] [OutputType([pscustomobject])] param( [string] $SettingsPath, [switch] $NoBackup ) $SettingsPath = Resolve-TcWtSettingsPath -Path $SettingsPath $text = [System.IO.File]::ReadAllText($SettingsPath) $settings = ConvertFrom-TcJsonText -Text $text if ($null -eq $settings) { throw "TerminalColors: [$SettingsPath] is not valid JSON." } $current = Get-TcBackdropSettings -Settings $settings if (-not $current.Image) { Write-Warning 'TerminalColors: no opaque backdrop declared in profiles.defaults.' return } if (-not (Test-TcBackdropPath -Path ([string]$current.Image))) { Write-Warning "TerminalColors: the declared background image [$($current.Image)] does not come from TerminalColors; it is left in place." return } $restoreImage = Get-TcInstallState -Name 'PreviousBackgroundImage' $newText = $text $changes = @() if ($restoreImage) { $values = @( @{ Name = 'backgroundImage'; Literal = (ConvertTo-TcJsonString -Value ([string]$restoreImage)) } @{ Name = 'backgroundImageOpacity'; State = 'PreviousBackgroundImageOpacity' } @{ Name = 'backgroundImageStretchMode'; State = 'PreviousBackgroundImageStretchMode' } ) foreach ($value in $values) { $literal = $value.Literal if (-not $literal) { $saved = Get-TcInstallState -Name $value.State if ($null -eq $saved) { continue } if ($saved -is [string]) { $literal = ConvertTo-TcJsonString -Value $saved } else { $literal = [string]$saved } } $prepared = Initialize-TcWtProfileDefaults -Text $newText $applied = Set-TcJsonMember -Text $prepared.Text -AnchorIndex $prepared.Anchor -Name $value.Name -Literal $literal $newText = $applied.Text if ($applied.Action -ne 'unchanged') { $changes += "$($value.Name) restored" } } } else { foreach ($name in @('backgroundImage', 'backgroundImageOpacity', 'backgroundImageStretchMode')) { $prepared = Initialize-TcWtProfileDefaults -Text $newText $applied = Remove-TcJsonMember -Text $prepared.Text -AnchorIndex $prepared.Anchor -Name $name $newText = $applied.Text if ($applied.Action -eq 'removed') { $changes += "$name removed" } } } if ($changes.Count -eq 0) { Write-Warning 'TerminalColors: nothing to remove.' return } if ($null -eq (ConvertFrom-TcJsonText -Text $newText)) { throw 'TerminalColors: the change would have produced invalid JSON. Nothing was written.' } $backupPath = $null if ($PSCmdlet.ShouldProcess($SettingsPath, "Remove the opaque backdrop ($($changes -join ', '))")) { $backupPath = Save-TcWtSettings -Path $SettingsPath -Text $newText -NoBackup:$NoBackup Get-ChildItem -Path (Get-TcBackdropDirectory) -Filter ($script:TcBackdropPrefix + '*.png') -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue foreach ($name in @('PreviousBackgroundImage', 'PreviousBackgroundImageOpacity', 'PreviousBackgroundImageStretchMode')) { Remove-TcInstallState -Name $name } } return [pscustomobject]@{ SettingsPath = $SettingsPath Changed = $true Changes = $changes Backup = $backupPath } } function Test-TerminalColorsBackdrop { <# .SYNOPSIS Opaque backdrop state: is it declared, does the image exist, and which colour is the pane pinned to. .PARAMETER SettingsPath Path of the settings.json to inspect. Detected automatically by default. .EXAMPLE Test-TerminalColorsBackdrop #> [CmdletBinding()] [OutputType([pscustomobject])] param([string] $SettingsPath) if (-not $SettingsPath) { $SettingsPath = Get-TcWtSettingsPath } $result = [pscustomobject]@{ SettingsPath = $SettingsPath Installed = $false ImagePath = $null ImageExists = $false Color = $null Opacity = $null StretchMode = $null ForeignImage = $false ProfileOverrides = @() } if (-not $SettingsPath -or -not [System.IO.File]::Exists($SettingsPath)) { return $result } $settings = ConvertFrom-TcJsonFile -Path $SettingsPath if ($null -eq $settings) { return $result } $current = Get-TcBackdropSettings -Settings $settings $image = [string]$current.Image $result.ImagePath = $current.Image $result.Opacity = $current.Opacity $result.StretchMode = $current.StretchMode $result.ProfileOverrides = $current.ProfileOverrides if (-not $image) { return $result } if (-not (Test-TcBackdropPath -Path $image)) { $result.ForeignImage = $true return $result } $result.Installed = $true $result.ImageExists = [System.IO.File]::Exists($image) $leaf = Split-Path $image -Leaf $m = [regex]::Match($leaf, '^' + [regex]::Escape($script:TcBackdropPrefix) + '([0-9A-Fa-f]{6})\.png$') if ($m.Success) { $result.Color = '#' + $m.Groups[1].Value.ToUpperInvariant() } return $result } |