- Зачем:
- 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>
240 lines
10 KiB
PowerShell
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"
|