Private/Rendering/Edit-WindowsTerminalSettings.ps1
|
function Edit-WindowsTerminalSettings { <# .SYNOPSIS Adds or removes a color scheme, or sets the default profile font, in a Windows Terminal settings.json file. .DESCRIPTION The shared read-modify-write engine behind Install-WindowsTerminalScheme, Uninstall-WindowsTerminalScheme, and Set-WindowsTerminalFont, factored out so the backup + (de)serialize logic lives in one place and stays unit-testable. It reads the file, parses it (ConvertFrom-Json), then either edits the `schemes` array by scheme `name` (idempotent — an add replaces any same-named scheme rather than duplicating it; a remove drops the match) or sets profiles.defaults.font.face. It backs the original up to "<path>.bak", then writes the result back as UTF-8 (no BOM) via ConvertTo-Json. The caller is expected to have gated the call behind its own ShouldProcess, so this engine performs the write unconditionally. JSONC caveat: settings.json may contain // comments and trailing commas. ConvertFrom-Json tolerates them on read, but the parse -> reserialize round-trip does not reproduce comments or the original hand-formatting. The .bak backup written before the rewrite is the safety net. .PARAMETER Path Path to the settings.json file to edit. Must exist. .PARAMETER Scheme (Add set) The color scheme to add or replace, as a hashtable in Windows Terminal's scheme shape (its `name` key identifies it for idempotent replace). .PARAMETER SetDefault (Add set) When set, also point profiles.defaults.colorScheme at the scheme's name so it applies immediately. Skipped with a warning if the file's `profiles` isn't an editable object. .PARAMETER RemoveName (Remove set) The `name` of the scheme to remove. .PARAMETER FontFace (Font set) The font family name to set as profiles.defaults.font.face (e.g. 'MesloLGM Nerd Font'). Skipped with a warning if the file's `profiles` isn't an editable object. .EXAMPLE Edit-WindowsTerminalSettings -Path $p -Scheme $scheme -SetDefault .EXAMPLE Edit-WindowsTerminalSettings -Path $p -RemoveName 'Screw City' .EXAMPLE Edit-WindowsTerminalSettings -Path $p -FontFace 'MesloLGM Nerd Font' .NOTES Returns a result object describing what happened: Add -> @{ Action = 'Added' | 'Replaced' } Remove -> @{ Action = 'Removed' | 'NotFound'; StillReferenced = <bool> } Font -> @{ Action = 'SetFont'; FontFace = <string> } #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = '"Settings" names Windows Terminal''s settings.json file, a proper-noun plural; a singular "Setting" would misname the whole-file target this helper edits.')] [CmdletBinding(DefaultParameterSetName = 'Add')] param( [Parameter(Mandatory, Position = 0)] [string]$Path, [Parameter(Mandatory, ParameterSetName = 'Add')] [hashtable]$Scheme, [Parameter(ParameterSetName = 'Add')] [switch]$SetDefault, [Parameter(Mandatory, ParameterSetName = 'Remove')] [string]$RemoveName, [Parameter(Mandatory, ParameterSetName = 'Font')] [string]$FontFace ) $raw = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop if ([string]::IsNullOrWhiteSpace($raw)) { $settings = [pscustomobject]@{} } else { try { $settings = $raw | ConvertFrom-Json -ErrorAction Stop } catch { throw "Edit-WindowsTerminalSettings: could not parse '$Path' as JSON: $($_.Exception.Message)" } } if ($PSCmdlet.ParameterSetName -eq 'Add') { $existing = if ($settings.PSObject.Properties['schemes']) { @($settings.schemes) } else { @() } $name = $Scheme['name'] $replaced = [bool]($existing | Where-Object { $_.name -eq $name }) $kept = @($existing | Where-Object { $_.name -ne $name }) $newSchemes = @($kept) + ([pscustomobject]$Scheme) $action = if ($replaced) { 'Replaced' } else { 'Added' } $result = @{ Action = $action } } elseif ($PSCmdlet.ParameterSetName -eq 'Remove') { $existing = if ($settings.PSObject.Properties['schemes']) { @($settings.schemes) } else { @() } $name = $RemoveName $found = [bool]($existing | Where-Object { $_.name -eq $name }) $newSchemes = @($existing | Where-Object { $_.name -ne $name }) # Detect whether the removed scheme is still wired up as an active colorScheme. $stillReferenced = $false if ($settings.PSObject.Properties['profiles']) { $profiles = $settings.profiles if ($profiles.PSObject.Properties['defaults'] -and $profiles.defaults.PSObject.Properties['colorScheme'] -and $profiles.defaults.colorScheme -eq $name) { $stillReferenced = $true } if ($profiles.PSObject.Properties['list']) { foreach ($p in @($profiles.list)) { if ($p.PSObject.Properties['colorScheme'] -and $p.colorScheme -eq $name) { $stillReferenced = $true } } } } $action = if ($found) { 'Removed' } else { 'NotFound' } $result = @{ Action = $action; StillReferenced = $stillReferenced } } else { # Font set — sets profiles.defaults.font.face only; the schemes array is left untouched. $result = @{ Action = 'SetFont'; FontFace = $FontFace } } # Write back the (possibly empty) schemes array — Add/Remove only; the Font set touches no schemes. if ($PSCmdlet.ParameterSetName -ne 'Font') { if ($settings.PSObject.Properties['schemes']) { $settings.schemes = $newSchemes } else { $settings | Add-Member -NotePropertyName 'schemes' -NotePropertyValue $newSchemes } } if (($PSCmdlet.ParameterSetName -eq 'Add' -and $SetDefault) -or $PSCmdlet.ParameterSetName -eq 'Font') { if (-not $settings.PSObject.Properties['profiles']) { $settings | Add-Member -NotePropertyName 'profiles' -NotePropertyValue ([pscustomobject]@{}) } if ($settings.profiles -is [System.Management.Automation.PSCustomObject]) { $profiles = $settings.profiles if (-not $profiles.PSObject.Properties['defaults']) { $profiles | Add-Member -NotePropertyName 'defaults' -NotePropertyValue ([pscustomobject]@{}) } $defaults = $profiles.defaults if ($PSCmdlet.ParameterSetName -eq 'Font') { # Ensure profiles.defaults.font is an object, then set/add its `face`. if (-not $defaults.PSObject.Properties['font']) { $defaults | Add-Member -NotePropertyName 'font' -NotePropertyValue ([pscustomobject]@{}) } if ($defaults.font -is [System.Management.Automation.PSCustomObject]) { $font = $defaults.font if ($font.PSObject.Properties['face']) { $font.face = $FontFace } else { $font | Add-Member -NotePropertyName 'face' -NotePropertyValue $FontFace } } else { Write-Warning "Edit-WindowsTerminalSettings: 'profiles.defaults.font' in '$Path' isn't an object; left profiles.defaults.font.face unchanged." } } elseif ($defaults.PSObject.Properties['colorScheme']) { $defaults.colorScheme = $name } else { $defaults | Add-Member -NotePropertyName 'colorScheme' -NotePropertyValue $name } } else { $field = if ($PSCmdlet.ParameterSetName -eq 'Font') { 'font.face' } else { 'colorScheme' } Write-Warning "Edit-WindowsTerminalSettings: 'profiles' in '$Path' isn't an object; left profiles.defaults.$field unchanged." } } # Back up the original before overwriting, then write. Copy-Item -LiteralPath $Path -Destination "$Path.bak" -Force $json = $settings | ConvertTo-Json -Depth 32 Set-Content -LiteralPath $Path -Value $json -Encoding utf8 [pscustomobject]$result } |