Files
dotfiles/chezmoi/.chezmoiscripts/run_20-configure-windows-terminal.ps1
T
ddadminandClaude Opus 4.6 6c183356aa fix(chezmoi): utf8BOM не поддерживается в PS 5.1
- Зачем:
  - chezmoi запускает скрипт через PS 5.1, где допустимы только
    UTF8/Unicode/ASCII и т.д., но не utf8BOM (появился в PS 7).
- Что:
  - заменено -Encoding utf8BOM на -Encoding UTF8 (в PS 5.1 пишет с BOM).
- Проверка:
  - chezmoi -S "$HOME\dotfiles\chezmoi" apply (на Windows с PS 5.1)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:13:52 +03:00

240 lines
10 KiB
PowerShell

# Идемпотентная настройка 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"