diff --git a/chezmoi/.chezmoiscripts/run_20-configure-windows-terminal.ps1 b/chezmoi/.chezmoiscripts/run_20-configure-windows-terminal.ps1 index 4de0cdb..51ac11a 100644 --- a/chezmoi/.chezmoiscripts/run_20-configure-windows-terminal.ps1 +++ b/chezmoi/.chezmoiscripts/run_20-configure-windows-terminal.ps1 @@ -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 - } - - return $Property.Value +function Get-Prop ($Object, [string]$Name) { + $P = $Object.PSObject.Properties[$Name] + if ($null -eq $P) { return $null } + return $P.Value } -function Test-JsonValueEqual { - param( - $Left, - $Right - ) - - if ($null -eq $Left -and $null -eq $Right) { +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 ($null -eq $Left -or $null -eq $Right) { - return $false + if (-not (Test-PropEqual $P.Value $Value)) { + $P.Value = $Value + return $true } + return $false +} - if ( - $Left -is [System.Collections.IEnumerable] -and - -not ($Left -is [string]) - ) { +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 - } - - if (-not (Test-JsonValueEqual -Left $Property.Value -Right $Value)) { - $Property.Value = $Value - $script:changed = $true - } +function Test-PS7Available { + try { return $null -ne (Get-Command pwsh -ErrorAction SilentlyContinue) } + catch { return $false } } -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-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-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) -$powerShell7CandidateIndices = New-Object System.Collections.Generic.List[int] -$legacyPowerShellIndex = $null +$Profiles = @($Settings.profiles.list) +$PS7Candidates = New-Object System.Collections.Generic.List[int] +$PS51Index = $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) +for ($I = 0; $I -lt $Profiles.Count; $I++) { + $P = $Profiles[$I] - if (Test-IsPowerShell7Profile -Profile $profile) { - [void]$powerShell7CandidateIndices.Add($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 } - 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 $powerShell7Index -and $powerShell7CandidateIndices.Count -gt 0) { - $powerShell7Index = $powerShell7CandidateIndices[0] +if ($null -eq $PS7Index -and $PS7Candidates.Count -gt 0) { + $PS7Index = $PS7Candidates[0] } -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%' +# ── Создание / обновление 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%' } - $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) { - return - } - - if ($removedIndices.Contains($Index)) { - return - } - - if ($addedIndices.Add($Index)) { - $orderedProfiles.Add($profiles[$Index]) - } +function Add-ByIndex ([int]$Idx) { + if ($Idx -lt 0 -or $RemovedIndices.Contains($Idx)) { return } + if ($Added.Add($Idx)) { $Ordered.Add($Profiles[$Idx]) } } -Add-ProfileByIndex -Index $powerShell7Index -if ($null -ne $legacyPowerShellIndex) { - Add-ProfileByIndex -Index $legacyPowerShellIndex -} +Add-ByIndex $PS7Index +if ($null -ne $PS51Index) { Add-ByIndex $PS51Index } +for ($I = 0; $I -lt $Profiles.Count; $I++) { Add-ByIndex $I } -for ($i = 0; $i -lt $profiles.Count; $i++) { - Add-ProfileByIndex -Index $i +$OrderedArray = @($Ordered.ToArray()) +if ((ConvertTo-Json $Profiles -Depth 100 -Compress) -ne (ConvertTo-Json $OrderedArray -Depth 100 -Compress)) { + $Changed = $true } +$Settings.profiles.list = $OrderedArray -$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.' +if (-not $Changed) { + Write-Host 'Windows Terminal: настройки уже соответствуют 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" diff --git a/chezmoi/.chezmoiscripts/run_before_10-migrate-gitconfig.ps1 b/chezmoi/.chezmoiscripts/run_before_10-migrate-gitconfig.ps1 index c89a2f8..2d5754d 100644 --- a/chezmoi/.chezmoiscripts/run_before_10-migrate-gitconfig.ps1 +++ b/chezmoi/.chezmoiscripts/run_before_10-migrate-gitconfig.ps1 @@ -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" diff --git a/chezmoi/dot_config/powershell/profile.ps1 b/chezmoi/dot_config/powershell/profile.ps1 index 828763c..04db936 100644 --- a/chezmoi/dot_config/powershell/profile.ps1 +++ b/chezmoi/dot_config/powershell/profile.ps1 @@ -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 @@ -15,170 +19,112 @@ if ( (Get-Module -ListAvailable -Name PSReadLine) ) { Import-Module PSReadLine -ErrorAction SilentlyContinue - Set-PSReadLineOption -PredictionSource History -ErrorAction SilentlyContinue - Set-PSReadLineOption -PredictionViewStyle ListView -ErrorAction SilentlyContinue - Set-PSReadLineOption -HistoryNoDuplicates:$true -ErrorAction SilentlyContinue + Set-PSReadLineOption -PredictionSource History -ErrorAction SilentlyContinue + Set-PSReadLineOption -PredictionViewStyle ListView -ErrorAction SilentlyContinue + 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 - } - - 'Proxy disabled. Checking route...' - $CurrentIp = Get-PublicIp - if ([string]::IsNullOrWhiteSpace($CurrentIp)) { - 'Current IP: unavailable' - } - else { - 'Current IP: {0}' -f $CurrentIp + foreach ($V in 'HTTP_PROXY','HTTPS_PROXY','ALL_PROXY','NO_PROXY') { + Remove-Item -Path "Env:$V" -ErrorAction SilentlyContinue } + $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 - } - - 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 - } + # Новое окно / вкладка / 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 } } -# Keep stable hooks for machine-local overrides and future managed fragments. +# ── Локальные дополнения ───────────────────────────────────────────── +# Машинно-специфичные добавки: profile.extra.ps1 или profile.d/*.ps1 + $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 }