# Идемпотентная настройка Windows Terminal: # - PS 7 = default profile (через source, без hardcoded commandline) # - PS 5.1 переименован в "Legacy" # - Azure Cloud Shell скрыт # - дубли PS7-профилей удаляются # - порядок: PS7 первый, PS5.1 второй, остальные как есть # # Скрипт НЕ перезаписывает settings.json, если ничего не изменилось. if (-not ($IsWindows -or $PSVersionTable.PSEdition -eq 'Desktop')) { return } $SettingsPath = Join-Path $env:LOCALAPPDATA 'Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json' if (-not (Test-Path -LiteralPath $SettingsPath)) { Write-Warning "Windows Terminal settings not found: $SettingsPath" return } # Well-known GUID и source для PS7 dynamic profile в WT. $PS7Guid = '{574e775e-4f2a-5b96-ac1e-a2962a402336}' $PS7Source = 'Windows.Terminal.PowershellCore' $PS51Guid = '{61c54bbd-c2c6-5271-96e7-009a87ff44bf}' $Changed = $false # ── JSON-хелперы ───────────────────────────────────────────────────── # ConvertFrom-Json даёт PSCustomObject; прямого .ContainsKey нет, # поэтому работаем через PSObject.Properties. function Get-Prop ($Object, [string]$Name) { $P = $Object.PSObject.Properties[$Name] if ($null -eq $P) { return $null } return $P.Value } function Set-Prop ($Object, [string]$Name, $Value) { # Возвращает $true, если значение действительно изменилось. $P = $Object.PSObject.Properties[$Name] if ($null -eq $P) { $Object | Add-Member -NotePropertyName $Name -NotePropertyValue $Value return $true } if (-not (Test-PropEqual $P.Value $Value)) { $P.Value = $Value return $true } return $false } function Remove-Prop ($Object, [string]$Name) { # Возвращает $true, если свойство существовало и было удалено. $P = $Object.PSObject.Properties[$Name] if ($null -ne $P) { [void]$Object.PSObject.Properties.Remove($Name) return $true } return $false } function Test-PropEqual ($Left, $Right) { if ($null -eq $Left -and $null -eq $Right) { return $true } if ($null -eq $Left -or $null -eq $Right) { return $false } # Массивы и объекты сравниваем через JSON-сериализацию. if ($Left -is [System.Collections.IEnumerable] -and -not ($Left -is [string])) { return (ConvertTo-Json $Left -Depth 100 -Compress) -eq (ConvertTo-Json $Right -Depth 100 -Compress) } return $Left -eq $Right } function Test-PS7Available { try { return $null -ne (Get-Command pwsh -ErrorAction SilentlyContinue) } catch { return $false } } # ── Матчинг профилей ───────────────────────────────────────────────── function Test-IsPS7 ($Profile) { # Широкий матч: source ИЛИ commandline ИЛИ имя. $Src = [string](Get-Prop $Profile source) $Cmd = [string](Get-Prop $Profile commandline) $Nm = [string](Get-Prop $Profile name) if ($Src -eq $PS7Source) { return $true } if ($Cmd -match '(?i)(^|[\\/ ])pwsh(?:\.exe)?(?:\s|$)') { return $true } return $Nm -match '^(PowerShell|PowerShell 7)$' } function Test-IsStockPS7 ($Profile) { # Узкий матч: только «стандартные» PS7-профили, которые безопасно удалять как дубли. # Кастомные профили с нестандартным именем НЕ удаляются. $Src = [string](Get-Prop $Profile source) $Cmd = [string](Get-Prop $Profile commandline) $Nm = [string](Get-Prop $Profile name) return ( $Nm -match '^(PowerShell|PowerShell 7)$' -and ($Src -eq $PS7Source -or $Cmd -match '(?i)(^|[\\/ ])pwsh(?:\.exe)?(?:\s|$)') ) } # ── Чтение settings.json ───────────────────────────────────────────── try { $Settings = Get-Content -LiteralPath $SettingsPath -Raw | ConvertFrom-Json } catch { Write-Warning "Failed to read Windows Terminal settings: $SettingsPath" Write-Warning $_ return } # Гарантируем наличие profiles.list (может отсутствовать на чистой установке). if ($null -eq (Get-Prop $Settings profiles)) { $Settings | Add-Member -NotePropertyName profiles -NotePropertyValue ([pscustomobject]@{ defaults = [pscustomobject]@{}; list = @() }) $Changed = $true } if ($null -eq (Get-Prop $Settings.profiles list)) { $Settings.profiles | Add-Member -NotePropertyName list -NotePropertyValue @() $Changed = $true } # ── Поиск профилей ─────────────────────────────────────────────────── $Profiles = @($Settings.profiles.list) $PS7Candidates = New-Object System.Collections.Generic.List[int] $PS51Index = $null for ($I = 0; $I -lt $Profiles.Count; $I++) { $P = $Profiles[$I] if (Test-IsPS7 $P) { [void]$PS7Candidates.Add($I) } # PS 5.1: по well-known GUID или по commandline. $Guid = [string](Get-Prop $P guid) $Cmd = [string](Get-Prop $P commandline) if ($Guid -eq $PS51Guid -or $Cmd -match '(?i)WindowsPowerShell\\v1\.0\\powershell\.exe') { $PS51Index = $I } # Azure Cloud Shell — скрыть. if ([string](Get-Prop $P source) -eq 'Windows.Terminal.Azure') { if (Set-Prop $P hidden $true) { $Changed = $true } } } # Предпочитаем кандидата с source (dynamic profile), иначе первый найденный. $PS7Index = $null foreach ($Idx in $PS7Candidates) { if ([string](Get-Prop $Profiles[$Idx] source) -eq $PS7Source) { $PS7Index = $Idx; break } } if ($null -eq $PS7Index -and $PS7Candidates.Count -gt 0) { $PS7Index = $PS7Candidates[0] } # ── Создание / обновление PS7-профиля ──────────────────────────────── if ($null -eq $PS7Index) { if (Test-PS7Available) { # Создаём override для dynamic profile (source, НЕ commandline). $Profiles += [pscustomobject]@{ guid = $PS7Guid; hidden = $false; name = 'PowerShell 7' source = $PS7Source; startingDirectory = '%USERPROFILE%' } $PS7Index = $Profiles.Count - 1 $Changed = $true } else { Write-Warning 'PowerShell 7 not found; default profile not changed.' } } if ($null -ne $PS7Index) { $PS7Profile = $Profiles[$PS7Index] # source вместо commandline — WT сам найдёт pwsh через dynamic profile. if (Set-Prop $PS7Profile guid $PS7Guid) { $Changed = $true } if (Set-Prop $PS7Profile source $PS7Source) { $Changed = $true } if (Remove-Prop $PS7Profile commandline) { $Changed = $true } if (Set-Prop $PS7Profile name 'PowerShell 7') { $Changed = $true } if (Set-Prop $PS7Profile hidden $false) { $Changed = $true } if (Set-Prop $PS7Profile startingDirectory '%USERPROFILE%'){ $Changed = $true } if (Set-Prop $Settings defaultProfile (Get-Prop $PS7Profile guid)) { $Changed = $true } } # ── PS 5.1 — переименовать в Legacy ────────────────────────────────── if ($null -ne $PS51Index) { $PS51 = $Profiles[$PS51Index] if (Set-Prop $PS51 name 'Windows PowerShell 5.1 (Legacy)') { $Changed = $true } if (Set-Prop $PS51 hidden $false) { $Changed = $true } } # ── Удаление дублей PS7 ────────────────────────────────────────────── # Оставляем выбранный PS7Index, остальные stock-профили удаляем. $RemovedIndices = New-Object System.Collections.Generic.HashSet[int] foreach ($Idx in $PS7Candidates) { if ($Idx -eq $PS7Index) { continue } if (Test-IsStockPS7 $Profiles[$Idx]) { if ($RemovedIndices.Add($Idx)) { $Changed = $true } } } # ── Сборка нового порядка профилей ──────────────────────────────────── # PS7 первый, PS 5.1 второй, остальные в исходном порядке. $Ordered = New-Object System.Collections.Generic.List[object] $Added = New-Object System.Collections.Generic.HashSet[int] function Add-ByIndex ([int]$Idx) { if ($Idx -lt 0 -or $RemovedIndices.Contains($Idx)) { return } if ($Added.Add($Idx)) { $Ordered.Add($Profiles[$Idx]) } } Add-ByIndex $PS7Index if ($null -ne $PS51Index) { Add-ByIndex $PS51Index } for ($I = 0; $I -lt $Profiles.Count; $I++) { Add-ByIndex $I } $OrderedArray = @($Ordered.ToArray()) if ((ConvertTo-Json $Profiles -Depth 100 -Compress) -ne (ConvertTo-Json $OrderedArray -Depth 100 -Compress)) { $Changed = $true } $Settings.profiles.list = $OrderedArray # ── Запись, только если что-то реально изменилось ───────────────────── if (-not $Changed) { Write-Host 'Windows Terminal: settings already match baseline.' return } $BackupPath = '{0}.bak-{1}' -f $SettingsPath, (Get-Date -Format 'yyyyMMdd-HHmmss') Copy-Item -LiteralPath $SettingsPath -Destination $BackupPath -Force $Settings | ConvertTo-Json -Depth 100 | Set-Content -LiteralPath $SettingsPath -Encoding UTF8 Write-Host "Windows Terminal: updated $SettingsPath" Write-Host "Backup: $BackupPath"