refactor(windows): причёсан стиль chezmoi-скриптов и pwsh-профиля
- Зачем: - код Codex работал, но был тяжёл для чтения и сопровождения: 4 naming-конвенции, скрытая мутация $script:changed, 298 строк WT-скрипта почти без комментариев. - Что: - profile.ps1: комментарии на русском перед каждой функцией, секционные разделители, wt-here → wt_here (единый snake_case), Get-PublicIp упрощён до Invoke-RestMethod, Chocolatey boilerplate обрезан. - WT-скрипт: Set/Remove-Prop возвращают bool вместо мутации $script:changed, naming выровнен в PascalCase, хелперы переименованы (Get-Prop, Set-Prop, Test-IsPS7, Test-IsStockPS7), комментарии к каждому блоку логики, вывод на русском. - Git-миграция: блок комментариев с описанием 4 сценариев, пояснение 3 вариантов ManagedStubs, naming выровнен, вывод на русском. - Проверка: - pwsh -NoProfile -Command "[System.Management.Automation.Language.Parser]::ParseFile(...)" — синтаксис всех трёх файлов OK. - chezmoi -S "$HOME\dotfiles\chezmoi" apply --dry-run --verbose. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c06ad1ecbb
commit
1963b6a117
3 files changed
+277
-383
No files matched your search
@@ -1,297 +1,239 @@
|
||||
# Идемпотентная настройка 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"
|
||||
$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
|
||||
}
|
||||
|
||||
$PowerShellGuid = '{574e775e-4f2a-5b96-ac1e-a2962a402336}'
|
||||
$PowerShellSource = 'Windows.Terminal.PowershellCore'
|
||||
$LegacyPowerShellGuid = '{61c54bbd-c2c6-5271-96e7-009a87ff44bf}'
|
||||
$changed = $false
|
||||
# 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
|
||||
|
||||
function Get-JsonPropertyValue {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Object,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Name
|
||||
)
|
||||
# ── JSON-хелперы ─────────────────────────────────────────────────────
|
||||
# ConvertFrom-Json даёт PSCustomObject; прямого .ContainsKey нет,
|
||||
# поэтому работаем через PSObject.Properties.
|
||||
|
||||
$Property = $Object.PSObject.Properties[$Name]
|
||||
if ($null -eq $Property) {
|
||||
return $null
|
||||
function Get-Prop ($Object, [string]$Name) {
|
||||
$P = $Object.PSObject.Properties[$Name]
|
||||
if ($null -eq $P) { return $null }
|
||||
return $P.Value
|
||||
}
|
||||
|
||||
return $Property.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
|
||||
}
|
||||
|
||||
function Test-JsonValueEqual {
|
||||
param(
|
||||
$Left,
|
||||
$Right
|
||||
)
|
||||
|
||||
if ($null -eq $Left -and $null -eq $Right) {
|
||||
if (-not (Test-PropEqual $P.Value $Value)) {
|
||||
$P.Value = $Value
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($null -eq $Left -or $null -eq $Right) {
|
||||
return $false
|
||||
}
|
||||
|
||||
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)
|
||||
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 Set-JsonPropertyValue {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Object,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Name,
|
||||
$Value
|
||||
)
|
||||
|
||||
$Property = $Object.PSObject.Properties[$Name]
|
||||
if ($null -eq $Property) {
|
||||
$Object | Add-Member -NotePropertyName $Name -NotePropertyValue $Value
|
||||
$script:changed = $true
|
||||
return
|
||||
function Test-PS7Available {
|
||||
try { return $null -ne (Get-Command pwsh -ErrorAction SilentlyContinue) }
|
||||
catch { return $false }
|
||||
}
|
||||
|
||||
if (-not (Test-JsonValueEqual -Left $Property.Value -Right $Value)) {
|
||||
$Property.Value = $Value
|
||||
$script:changed = $true
|
||||
}
|
||||
# ── Матчинг профилей ─────────────────────────────────────────────────
|
||||
|
||||
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 Remove-JsonPropertyValue {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Object,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
$Property = $Object.PSObject.Properties[$Name]
|
||||
if ($null -ne $Property) {
|
||||
[void]$Object.PSObject.Properties.Remove($Name)
|
||||
$script:changed = $true
|
||||
}
|
||||
}
|
||||
|
||||
function Test-PowerShell7Available {
|
||||
try {
|
||||
return $null -ne (Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object -First 1)
|
||||
}
|
||||
catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Test-IsPowerShell7Profile {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Profile
|
||||
)
|
||||
|
||||
$Source = [string](Get-JsonPropertyValue -Object $Profile -Name source)
|
||||
$Name = [string](Get-JsonPropertyValue -Object $Profile -Name name)
|
||||
$CommandLine = [string](Get-JsonPropertyValue -Object $Profile -Name commandline)
|
||||
|
||||
if ($Source -eq $PowerShellSource) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($CommandLine -match '(?i)(^|[\\/ ])pwsh(?:\.exe)?(?:\s|$)') {
|
||||
return $true
|
||||
}
|
||||
|
||||
return $Name -match '^(PowerShell|PowerShell 7)$'
|
||||
}
|
||||
|
||||
function Test-IsDisposablePowerShell7Profile {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Profile
|
||||
)
|
||||
|
||||
$Source = [string](Get-JsonPropertyValue -Object $Profile -Name source)
|
||||
$Name = [string](Get-JsonPropertyValue -Object $Profile -Name name)
|
||||
$CommandLine = [string](Get-JsonPropertyValue -Object $Profile -Name commandline)
|
||||
function Test-IsStockPS7 ($Profile) {
|
||||
# Узкий матч: только «стандартные» PS7-профили, которые безопасно удалять как дубли.
|
||||
# Кастомные профили с нестандартным именем НЕ удаляются.
|
||||
$Src = [string](Get-Prop $Profile source)
|
||||
$Cmd = [string](Get-Prop $Profile commandline)
|
||||
$Nm = [string](Get-Prop $Profile name)
|
||||
|
||||
return (
|
||||
$Name -match '^(PowerShell|PowerShell 7)$' -and
|
||||
(
|
||||
$Source -eq $PowerShellSource -or
|
||||
$CommandLine -match '(?i)(^|[\\/ ])pwsh(?:\.exe)?(?:\s|$)'
|
||||
)
|
||||
$Nm -match '^(PowerShell|PowerShell 7)$' -and
|
||||
($Src -eq $PS7Source -or $Cmd -match '(?i)(^|[\\/ ])pwsh(?:\.exe)?(?:\s|$)')
|
||||
)
|
||||
}
|
||||
|
||||
# ── Чтение settings.json ─────────────────────────────────────────────
|
||||
|
||||
try {
|
||||
$originalSettings = Get-Content -LiteralPath $settingsPath -Raw
|
||||
$settings = $originalSettings | ConvertFrom-Json
|
||||
$Settings = Get-Content -LiteralPath $SettingsPath -Raw | ConvertFrom-Json
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to read Windows Terminal settings: $settingsPath"
|
||||
Write-Warning "Failed to read Windows Terminal settings: $SettingsPath"
|
||||
Write-Warning $_
|
||||
return
|
||||
}
|
||||
|
||||
if ($null -eq (Get-JsonPropertyValue -Object $settings -Name profiles)) {
|
||||
$settings | Add-Member -NotePropertyName profiles -NotePropertyValue ([pscustomobject]@{
|
||||
defaults = [pscustomobject]@{}
|
||||
list = @()
|
||||
# Гарантируем наличие profiles.list (может отсутствовать на чистой установке).
|
||||
if ($null -eq (Get-Prop $Settings profiles)) {
|
||||
$Settings | Add-Member -NotePropertyName profiles -NotePropertyValue ([pscustomobject]@{
|
||||
defaults = [pscustomobject]@{}; list = @()
|
||||
})
|
||||
$changed = $true
|
||||
$Changed = $true
|
||||
}
|
||||
if ($null -eq (Get-Prop $Settings.profiles list)) {
|
||||
$Settings.profiles | Add-Member -NotePropertyName list -NotePropertyValue @()
|
||||
$Changed = $true
|
||||
}
|
||||
|
||||
if ($null -eq (Get-JsonPropertyValue -Object $settings.profiles -Name 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
|
||||
}
|
||||
|
||||
$profiles = @($settings.profiles.list)
|
||||
$powerShell7CandidateIndices = New-Object System.Collections.Generic.List[int]
|
||||
$legacyPowerShellIndex = $null
|
||||
|
||||
for ($i = 0; $i -lt $profiles.Count; $i++) {
|
||||
$profile = $profiles[$i]
|
||||
$guid = [string](Get-JsonPropertyValue -Object $profile -Name guid)
|
||||
$source = [string](Get-JsonPropertyValue -Object $profile -Name source)
|
||||
$commandLine = [string](Get-JsonPropertyValue -Object $profile -Name commandline)
|
||||
|
||||
if (Test-IsPowerShell7Profile -Profile $profile) {
|
||||
[void]$powerShell7CandidateIndices.Add($i)
|
||||
}
|
||||
|
||||
if (
|
||||
$guid -eq $LegacyPowerShellGuid -or
|
||||
$commandLine -match '(?i)WindowsPowerShell\\v1\.0\\powershell\.exe'
|
||||
) {
|
||||
$legacyPowerShellIndex = $i
|
||||
}
|
||||
|
||||
if ($source -eq 'Windows.Terminal.Azure') {
|
||||
Set-JsonPropertyValue -Object $profile -Name hidden -Value $true
|
||||
# Azure Cloud Shell — скрыть.
|
||||
if ([string](Get-Prop $P source) -eq 'Windows.Terminal.Azure') {
|
||||
if (Set-Prop $P hidden $true) { $Changed = $true }
|
||||
}
|
||||
}
|
||||
|
||||
$powerShell7Index = $null
|
||||
foreach ($candidateIndex in $powerShell7CandidateIndices) {
|
||||
$candidate = $profiles[$candidateIndex]
|
||||
if ([string](Get-JsonPropertyValue -Object $candidate -Name source) -eq $PowerShellSource) {
|
||||
$powerShell7Index = $candidateIndex
|
||||
break
|
||||
# Предпочитаем кандидата с 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]
|
||||
}
|
||||
|
||||
if ($null -eq $powerShell7Index -and $powerShell7CandidateIndices.Count -gt 0) {
|
||||
$powerShell7Index = $powerShell7CandidateIndices[0]
|
||||
}
|
||||
# ── Создание / обновление PS7-профиля ────────────────────────────────
|
||||
|
||||
if ($null -eq $powerShell7Index) {
|
||||
if (Test-PowerShell7Available) {
|
||||
# Create a dynamic-profile override instead of a manual pwsh.exe launcher.
|
||||
$profiles += [pscustomobject]@{
|
||||
guid = $PowerShellGuid
|
||||
hidden = $false
|
||||
name = 'PowerShell 7'
|
||||
source = $PowerShellSource
|
||||
startingDirectory = '%USERPROFILE%'
|
||||
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%'
|
||||
}
|
||||
$powerShell7Index = $profiles.Count - 1
|
||||
$changed = $true
|
||||
$PS7Index = $Profiles.Count - 1
|
||||
$Changed = $true
|
||||
}
|
||||
else {
|
||||
Write-Warning 'PowerShell 7 was not detected; leaving Windows Terminal default profile unchanged.'
|
||||
Write-Warning 'PowerShell 7 not found; default profile not changed.'
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -ne $powerShell7Index) {
|
||||
$powerShell7Profile = $profiles[$powerShell7Index]
|
||||
Set-JsonPropertyValue -Object $powerShell7Profile -Name guid -Value $PowerShellGuid
|
||||
Set-JsonPropertyValue -Object $powerShell7Profile -Name source -Value $PowerShellSource
|
||||
Remove-JsonPropertyValue -Object $powerShell7Profile -Name commandline
|
||||
Set-JsonPropertyValue -Object $powerShell7Profile -Name name -Value 'PowerShell 7'
|
||||
Set-JsonPropertyValue -Object $powerShell7Profile -Name hidden -Value $false
|
||||
Set-JsonPropertyValue -Object $powerShell7Profile -Name startingDirectory -Value '%USERPROFILE%'
|
||||
Set-JsonPropertyValue -Object $settings -Name defaultProfile -Value ([string](Get-JsonPropertyValue -Object $powerShell7Profile -Name guid))
|
||||
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 }
|
||||
}
|
||||
|
||||
if ($null -ne $legacyPowerShellIndex) {
|
||||
$legacyPowerShellProfile = $profiles[$legacyPowerShellIndex]
|
||||
Set-JsonPropertyValue -Object $legacyPowerShellProfile -Name name -Value 'Windows PowerShell 5.1 (Legacy)'
|
||||
Set-JsonPropertyValue -Object $legacyPowerShellProfile -Name hidden -Value $false
|
||||
# ── 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 }
|
||||
}
|
||||
|
||||
$removedIndices = New-Object System.Collections.Generic.HashSet[int]
|
||||
foreach ($candidateIndex in $powerShell7CandidateIndices) {
|
||||
if ($candidateIndex -eq $powerShell7Index) {
|
||||
continue
|
||||
}
|
||||
# ── Удаление дублей PS7 ──────────────────────────────────────────────
|
||||
# Оставляем выбранный PS7Index, остальные stock-профили удаляем.
|
||||
|
||||
if (Test-IsDisposablePowerShell7Profile -Profile $profiles[$candidateIndex]) {
|
||||
if ($removedIndices.Add($candidateIndex)) {
|
||||
$changed = $true
|
||||
}
|
||||
$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 }
|
||||
}
|
||||
}
|
||||
|
||||
$orderedProfiles = New-Object System.Collections.Generic.List[object]
|
||||
$addedIndices = New-Object System.Collections.Generic.HashSet[int]
|
||||
# ── Сборка нового порядка профилей ────────────────────────────────────
|
||||
# PS7 первый, PS 5.1 второй, остальные в исходном порядке.
|
||||
|
||||
function Add-ProfileByIndex {
|
||||
param(
|
||||
[int]$Index
|
||||
)
|
||||
$Ordered = New-Object System.Collections.Generic.List[object]
|
||||
$Added = New-Object System.Collections.Generic.HashSet[int]
|
||||
|
||||
if ($Index -lt 0) {
|
||||
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: настройки уже соответствуют baseline.'
|
||||
return
|
||||
}
|
||||
|
||||
if ($removedIndices.Contains($Index)) {
|
||||
return
|
||||
}
|
||||
|
||||
if ($addedIndices.Add($Index)) {
|
||||
$orderedProfiles.Add($profiles[$Index])
|
||||
}
|
||||
}
|
||||
|
||||
Add-ProfileByIndex -Index $powerShell7Index
|
||||
if ($null -ne $legacyPowerShellIndex) {
|
||||
Add-ProfileByIndex -Index $legacyPowerShellIndex
|
||||
}
|
||||
|
||||
for ($i = 0; $i -lt $profiles.Count; $i++) {
|
||||
Add-ProfileByIndex -Index $i
|
||||
}
|
||||
|
||||
$orderedProfilesArray = @($orderedProfiles.ToArray())
|
||||
if ((ConvertTo-Json $profiles -Depth 100 -Compress) -ne (ConvertTo-Json $orderedProfilesArray -Depth 100 -Compress)) {
|
||||
$changed = $true
|
||||
}
|
||||
$settings.profiles.list = $orderedProfilesArray
|
||||
|
||||
if (-not $changed) {
|
||||
Write-Host 'Windows Terminal settings already match the managed 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 utf8BOM
|
||||
Write-Host "Updated Windows Terminal settings: $settingsPath"
|
||||
Write-Host "Backup saved to: $backupPath"
|
||||
$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 utf8BOM
|
||||
Write-Host "Windows Terminal: обновлено $SettingsPath"
|
||||
Write-Host "Бэкап: $BackupPath"
|
||||
@@ -1,10 +1,25 @@
|
||||
# Миграция существующего ~/.gitconfig перед тем, как chezmoi подставит managed stub.
|
||||
#
|
||||
# Логика:
|
||||
# 1. Если ~/.gitconfig нет — создаём пустой ~/.gitconfig.local и выходим.
|
||||
# 2. Если ~/.gitconfig уже содержит managed stub — ничего не делаем.
|
||||
# 3. Если ~/.gitconfig.local нет — переносим текущий .gitconfig в .local,
|
||||
# удаляем из .local ключи, которые управляются baseline, и сохраняем бэкап.
|
||||
# 4. Если ~/.gitconfig.local уже есть — просто бэкапим текущий .gitconfig
|
||||
# (chezmoi перезапишет его managed stub-ом).
|
||||
#
|
||||
# Три варианта ManagedStubs — чтобы распознать и текущий формат,
|
||||
# и предыдущие итерации (когда baseline жил в ~/.config/git/config).
|
||||
|
||||
if (-not ($IsWindows -or $PSVersionTable.PSEdition -eq 'Desktop')) {
|
||||
return
|
||||
}
|
||||
|
||||
$GlobalGitConfig = Join-Path $HOME '.gitconfig'
|
||||
$LocalGitConfig = Join-Path $HOME '.gitconfig.local'
|
||||
$BackupPath = '{0}.pre-chezmoi.bak-{1}' -f $GlobalGitConfig, (Get-Date -Format 'yyyyMMdd-HHmmss')
|
||||
$GlobalConfig = Join-Path $HOME '.gitconfig'
|
||||
$LocalConfig = Join-Path $HOME '.gitconfig.local'
|
||||
$BackupPath = '{0}.pre-chezmoi.bak-{1}' -f $GlobalConfig, (Get-Date -Format 'yyyyMMdd-HHmmss')
|
||||
|
||||
# Все исторические варианты managed stub (нормализуются без \r и trim).
|
||||
$ManagedStubs = @(
|
||||
@'
|
||||
[core]
|
||||
@@ -38,61 +53,52 @@ $ManagedStubs = @(
|
||||
'@
|
||||
)
|
||||
|
||||
# Ключи baseline, которые нужно удалить из .local при миграции.
|
||||
$ManagedKeys = @(
|
||||
'core.autocrlf',
|
||||
'core.eol',
|
||||
'core.quotepath',
|
||||
'core.longpaths',
|
||||
'fetch.prune',
|
||||
'pull.ff',
|
||||
'init.defaultBranch'
|
||||
'core.autocrlf', 'core.eol', 'core.quotepath', 'core.longpaths',
|
||||
'fetch.prune', 'pull.ff', 'init.defaultBranch'
|
||||
)
|
||||
|
||||
function Test-ManagedGitStub {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Content
|
||||
)
|
||||
|
||||
$NormalizedContent = ($Content -replace "`r", '').Trim()
|
||||
foreach ($ManagedStub in $ManagedStubs) {
|
||||
$NormalizedStub = ($ManagedStub -replace "`r", '').Trim()
|
||||
if ($NormalizedContent -eq $NormalizedStub) {
|
||||
return $true
|
||||
function Test-IsManagedStub ([string]$Content) {
|
||||
$Norm = ($Content -replace "`r", '').Trim()
|
||||
foreach ($Stub in $ManagedStubs) {
|
||||
if ($Norm -eq ($Stub -replace "`r", '').Trim()) { return $true }
|
||||
}
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $GlobalGitConfig)) {
|
||||
if (-not (Test-Path -LiteralPath $LocalGitConfig)) {
|
||||
New-Item -ItemType File -Path $LocalGitConfig -Force | Out-Null
|
||||
# ── Основная логика ──────────────────────────────────────────────────
|
||||
|
||||
# Случай 1: .gitconfig отсутствует — chezmoi создаст его сам.
|
||||
if (-not (Test-Path -LiteralPath $GlobalConfig)) {
|
||||
if (-not (Test-Path -LiteralPath $LocalConfig)) {
|
||||
New-Item -ItemType File -Path $LocalConfig -Force | Out-Null
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
$CurrentContent = Get-Content -LiteralPath $GlobalGitConfig -Raw
|
||||
if (Test-ManagedGitStub -Content $CurrentContent) {
|
||||
if (-not (Test-Path -LiteralPath $LocalGitConfig)) {
|
||||
New-Item -ItemType File -Path $LocalGitConfig -Force | Out-Null
|
||||
# Случай 2: .gitconfig уже managed — ничего не делаем.
|
||||
$Content = Get-Content -LiteralPath $GlobalConfig -Raw
|
||||
if (Test-IsManagedStub $Content) {
|
||||
if (-not (Test-Path -LiteralPath $LocalConfig)) {
|
||||
New-Item -ItemType File -Path $LocalConfig -Force | Out-Null
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $LocalGitConfig)) {
|
||||
Copy-Item -LiteralPath $GlobalGitConfig -Destination $BackupPath -Force
|
||||
Copy-Item -LiteralPath $GlobalGitConfig -Destination $LocalGitConfig -Force
|
||||
|
||||
# Случай 3: .gitconfig.local нет — мигрируем.
|
||||
if (-not (Test-Path -LiteralPath $LocalConfig)) {
|
||||
Copy-Item -LiteralPath $GlobalConfig -Destination $BackupPath -Force
|
||||
Copy-Item -LiteralPath $GlobalConfig -Destination $LocalConfig -Force
|
||||
foreach ($Key in $ManagedKeys) {
|
||||
git config --file $LocalGitConfig --unset-all $Key 2>$null
|
||||
git config --file $LocalConfig --unset-all $Key 2>$null
|
||||
}
|
||||
|
||||
Write-Host "Migrated existing ~/.gitconfig to ~/.gitconfig.local"
|
||||
Write-Host "Backup saved to: $BackupPath"
|
||||
Write-Host "Мигрировано: ~/.gitconfig -> ~/.gitconfig.local"
|
||||
Write-Host "Бэкап: $BackupPath"
|
||||
return
|
||||
}
|
||||
|
||||
Copy-Item -LiteralPath $GlobalGitConfig -Destination $BackupPath -Force
|
||||
Write-Warning "~/.gitconfig.local already exists; current ~/.gitconfig was backed up before managed stub overwrite."
|
||||
Write-Host "Backup saved to: $BackupPath"
|
||||
# Случай 4: .gitconfig.local уже есть — просто бэкапим.
|
||||
Copy-Item -LiteralPath $GlobalConfig -Destination $BackupPath -Force
|
||||
Write-Warning "~/.gitconfig.local уже существует; текущий ~/.gitconfig сохранён в бэкап."
|
||||
Write-Host "Бэкап: $BackupPath"
|
||||
@@ -1,13 +1,17 @@
|
||||
# Keep PowerShell 7 console text I/O on UTF-8 for predictable native tool behavior.
|
||||
# ── UTF-8 ────────────────────────────────────────────────────────────
|
||||
# Принудительный UTF-8 для pwsh 7 в интерактивной консоли,
|
||||
# независимо от унаследованной code page.
|
||||
# НЕ трогает PS 5.1, VS Code terminal, remoting и т.д.
|
||||
if ($IsWindows -and $PSVersionTable.PSEdition -eq 'Core' -and $Host.Name -eq 'ConsoleHost') {
|
||||
chcp 65001 > $null
|
||||
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
[Console]::InputEncoding = $Utf8NoBom
|
||||
[Console]::OutputEncoding = $Utf8NoBom
|
||||
$OutputEncoding = $Utf8NoBom
|
||||
$Utf8 = New-Object System.Text.UTF8Encoding($false)
|
||||
[Console]::InputEncoding = $Utf8
|
||||
[Console]::OutputEncoding = $Utf8
|
||||
$OutputEncoding = $Utf8
|
||||
}
|
||||
|
||||
# Keep interactive editing comfortable without depending on extra prompt frameworks.
|
||||
# ── PSReadLine ───────────────────────────────────────────────────────
|
||||
# Предиктивный ввод по истории, только для интерактивной консоли.
|
||||
if (
|
||||
$Host.Name -eq 'ConsoleHost' -and
|
||||
-not [Console]::IsOutputRedirected -and
|
||||
@@ -20,165 +24,107 @@ if (
|
||||
Set-PSReadLineOption -HistoryNoDuplicates:$true -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
function Reload-Profile {
|
||||
. $PROFILE
|
||||
}
|
||||
# ── Unix-хелперы (lowercase — привычка из bash) ─────────────────────
|
||||
|
||||
function which {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Name
|
||||
)
|
||||
# Перечитать профиль без перезапуска сессии.
|
||||
function Reload-Profile { . $PROFILE }
|
||||
|
||||
# Показать все команды с данным именем (аналог which в bash).
|
||||
function which ($Name) {
|
||||
Get-Command -Name $Name -All | Select-Object CommandType, Name, Source
|
||||
}
|
||||
|
||||
# Создать файл или обновить mtime (аналог touch в bash).
|
||||
function touch {
|
||||
param(
|
||||
[Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)]
|
||||
[string[]]$Path
|
||||
)
|
||||
|
||||
param([Parameter(Mandatory, ValueFromRemainingArguments)][string[]]$Path)
|
||||
foreach ($Item in $Path) {
|
||||
if (Test-Path -LiteralPath $Item) {
|
||||
(Get-Item -LiteralPath $Item).LastWriteTime = Get-Date
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
New-Item -ItemType File -Path $Item -Force | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mkcd {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
# mkdir + cd в одно действие.
|
||||
function mkcd ($Path) {
|
||||
New-Item -ItemType Directory -Path $Path -Force | Out-Null
|
||||
Set-Location -Path $Path
|
||||
}
|
||||
|
||||
function open {
|
||||
param(
|
||||
[string]$Path = '.'
|
||||
)
|
||||
|
||||
Invoke-Item -Path $Path
|
||||
}
|
||||
# Открыть путь в Explorer (аналог xdg-open / macOS open).
|
||||
function open ($Path = '.') { Invoke-Item -Path $Path }
|
||||
|
||||
# Перейти в корень текущего git-репозитория.
|
||||
function croot {
|
||||
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
||||
Write-Error 'git is not available in PATH.'
|
||||
return
|
||||
Write-Error 'git is not available in PATH.'; return
|
||||
}
|
||||
|
||||
$Root = git rev-parse --show-toplevel 2>$null
|
||||
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($Root)) {
|
||||
Write-Error 'Not inside a Git repository.'
|
||||
return
|
||||
Write-Error 'Not inside a Git repository.'; return
|
||||
}
|
||||
|
||||
Set-Location -Path $Root.Trim()
|
||||
}
|
||||
|
||||
# ── Прокси (аналог bash proxy_on/proxy_off/myip) ────────────────────
|
||||
|
||||
# Получить текущий внешний IP (или $null при ошибке/таймауте).
|
||||
function Get-PublicIp {
|
||||
$CurlExe = Get-Command curl.exe -ErrorAction SilentlyContinue
|
||||
if ($null -ne $CurlExe) {
|
||||
try {
|
||||
$CurrentIp = & $CurlExe.Source --silent --show-error --max-time 3 https://ifconfig.me/ip
|
||||
return ([string]$CurrentIp).Trim()
|
||||
}
|
||||
catch {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return ([string](Invoke-RestMethod -Uri 'https://ifconfig.me/ip' -TimeoutSec 3)).Trim()
|
||||
}
|
||||
catch {
|
||||
return $null
|
||||
}
|
||||
try { return ([string](Invoke-RestMethod -Uri 'https://ifconfig.me/ip' -TimeoutSec 3)).Trim() }
|
||||
catch { return $null }
|
||||
}
|
||||
|
||||
# Включить прокси для текущей сессии (hproxy.local).
|
||||
function proxy_on {
|
||||
$ProxyHost = 'hproxy.local'
|
||||
$ProxyPort = 12334
|
||||
$Host_ = 'hproxy.local'; $Port = 12334
|
||||
|
||||
$env:HTTP_PROXY = 'http://{0}:{1}' -f $ProxyHost, $ProxyPort
|
||||
$env:HTTPS_PROXY = 'http://{0}:{1}' -f $ProxyHost, $ProxyPort
|
||||
$env:ALL_PROXY = 'socks5://{0}:{1}' -f $ProxyHost, $ProxyPort
|
||||
$env:NO_PROXY = '{0},localhost,127.0.0.1,.concept-software.ru' -f $ProxyHost
|
||||
$env:HTTP_PROXY = "http://${Host_}:${Port}"
|
||||
$env:HTTPS_PROXY = "http://${Host_}:${Port}"
|
||||
$env:ALL_PROXY = "socks5://${Host_}:${Port}"
|
||||
$env:NO_PROXY = "${Host_},localhost,127.0.0.1,.concept-software.ru"
|
||||
|
||||
'Proxy enabled. Checking route...'
|
||||
$CurrentIp = Get-PublicIp
|
||||
if ([string]::IsNullOrWhiteSpace($CurrentIp)) {
|
||||
'Current IP: unavailable'
|
||||
}
|
||||
else {
|
||||
'Current IP: {0}' -f $CurrentIp
|
||||
}
|
||||
$Ip = Get-PublicIp
|
||||
"Proxy ON. IP: $(if ($Ip) { $Ip } else { 'unavailable' })"
|
||||
}
|
||||
|
||||
# Выключить прокси для текущей сессии.
|
||||
function proxy_off {
|
||||
foreach ($VariableName in 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY') {
|
||||
Remove-Item -Path ('Env:{0}' -f $VariableName) -ErrorAction SilentlyContinue
|
||||
foreach ($V in 'HTTP_PROXY','HTTPS_PROXY','ALL_PROXY','NO_PROXY') {
|
||||
Remove-Item -Path "Env:$V" -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
'Proxy disabled. Checking route...'
|
||||
$CurrentIp = Get-PublicIp
|
||||
if ([string]::IsNullOrWhiteSpace($CurrentIp)) {
|
||||
'Current IP: unavailable'
|
||||
}
|
||||
else {
|
||||
'Current IP: {0}' -f $CurrentIp
|
||||
}
|
||||
$Ip = Get-PublicIp
|
||||
"Proxy OFF. IP: $(if ($Ip) { $Ip } else { 'unavailable' })"
|
||||
}
|
||||
|
||||
# Показать текущий внешний IP.
|
||||
function myip {
|
||||
$CurrentIp = Get-PublicIp
|
||||
if ([string]::IsNullOrWhiteSpace($CurrentIp)) {
|
||||
'IP: unavailable'
|
||||
}
|
||||
else {
|
||||
'IP: {0}' -f $CurrentIp
|
||||
}
|
||||
$Ip = Get-PublicIp
|
||||
"IP: $(if ($Ip) { $Ip } else { 'unavailable' })"
|
||||
}
|
||||
|
||||
# ── Шорткаты Windows Terminal ────────────────────────────────────────
|
||||
|
||||
if (Get-Command wt -ErrorAction SilentlyContinue) {
|
||||
function wt-here {
|
||||
wt -d (Get-Location).ProviderPath
|
||||
# Новое окно / вкладка / split-pane Windows Terminal в текущей директории.
|
||||
function wt_here { wt -d (Get-Location).ProviderPath }
|
||||
function wt_tab_here { wt -w 0 new-tab -d (Get-Location).ProviderPath }
|
||||
function wt_split_here { wt -w 0 split-pane -d (Get-Location).ProviderPath }
|
||||
}
|
||||
|
||||
function wt-tab-here {
|
||||
wt -w 0 new-tab -d (Get-Location).ProviderPath
|
||||
}
|
||||
# ── Локальные дополнения ─────────────────────────────────────────────
|
||||
# Машинно-специфичные добавки: profile.extra.ps1 или profile.d/*.ps1
|
||||
|
||||
function wt-split-here {
|
||||
wt -w 0 split-pane -d (Get-Location).ProviderPath
|
||||
}
|
||||
}
|
||||
|
||||
# Keep stable hooks for machine-local overrides and future managed fragments.
|
||||
$ExtraProfile = Join-Path $HOME '.config\powershell\profile.extra.ps1'
|
||||
if (Test-Path -LiteralPath $ExtraProfile) {
|
||||
. $ExtraProfile
|
||||
}
|
||||
if (Test-Path -LiteralPath $ExtraProfile) { . $ExtraProfile }
|
||||
|
||||
$ExtraProfileDir = Join-Path $HOME '.config\powershell\profile.d'
|
||||
if (Test-Path -LiteralPath $ExtraProfileDir) {
|
||||
foreach ($ProfileFragment in (Get-ChildItem -Path $ExtraProfileDir -Filter '*.ps1' -File | Sort-Object Name)) {
|
||||
. $ProfileFragment.FullName
|
||||
}
|
||||
Get-ChildItem -Path $ExtraProfileDir -Filter '*.ps1' -File |
|
||||
Sort-Object Name |
|
||||
ForEach-Object { . $_.FullName }
|
||||
}
|
||||
|
||||
# Import the Chocolatey Profile that contains the necessary code to enable
|
||||
# tab-completions to function for `choco`.
|
||||
# Be aware that if you are missing these lines from your profile, tab completion
|
||||
# for `choco` will not function.
|
||||
# See https://ch0.co/tab-completion for details.
|
||||
# ── Chocolatey tab-completion ────────────────────────────────────────
|
||||
$ChocolateyProfile = "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
|
||||
if (Test-Path -LiteralPath $ChocolateyProfile) {
|
||||
Import-Module $ChocolateyProfile
|
||||
}
|
||||
if (Test-Path -LiteralPath $ChocolateyProfile) { Import-Module $ChocolateyProfile }
|
||||
Reference in new issue
Block a user