feat(windows): добавлен chezmoi-слой для pwsh, terminal и git
- Зачем: - нужно перенести стабильную Windows-конфигурацию в dotfiles без поломки существующего stow/install.sh workflow для Linux и WSL. - нужно убрать типичные проблемы Git на Windows, включая line endings, из ручной настройки машины. - Что: - добавлен отдельный source dir `chezmoi/` с tiny stub для PowerShell 7 и managed profile в `~/.config/powershell`. - добавлены идемпотентная настройка Windows Terminal и managed Git baseline с миграцией локальных настроек в `~/.gitconfig.local`. - обновлён README: описан Windows workflow, helper-функции pwsh и состав Git baseline без персональных данных. - Проверка: - `chezmoi -S "$HOME\dotfiles\chezmoi" apply` - `pwsh -NoLogo -Command ''$PSVersionTable.PSVersion.ToString(); chcp; [Console]::InputEncoding.WebName; [Console]::OutputEncoding.WebName''` - `git config --global --includes --list --show-origin`
This commit is contained in:
1 parent
22d38f80e5
commit
8fd5b9c630
8 files changed
+750
-238
No files matched your search
@@ -0,0 +1,6 @@
|
||||
{{ if ne .chezmoi.os "windows" }}
|
||||
AppData/**
|
||||
Documents/**
|
||||
dot_config/**
|
||||
dot_gitconfig
|
||||
{{ end }}
|
||||
@@ -0,0 +1,267 @@
|
||||
if (-not $IsWindows) {
|
||||
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
|
||||
}
|
||||
|
||||
$PowerShellGuid = '{574e775e-4f2a-5b96-ac1e-a2962a402336}'
|
||||
$LegacyPowerShellGuid = '{61c54bbd-c2c6-5271-96e7-009a87ff44bf}'
|
||||
$changed = $false
|
||||
|
||||
function Get-JsonPropertyValue {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Object,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
$Property = $Object.PSObject.Properties[$Name]
|
||||
if ($null -eq $Property) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return $Property.Value
|
||||
}
|
||||
|
||||
function Test-JsonValueEqual {
|
||||
param(
|
||||
$Left,
|
||||
$Right
|
||||
)
|
||||
|
||||
if ($null -eq $Left -and $null -eq $Right) {
|
||||
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)
|
||||
}
|
||||
|
||||
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-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 'Windows.Terminal.PowershellCore') {
|
||||
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)
|
||||
|
||||
return (
|
||||
$Name -match '^(PowerShell|PowerShell 7)$' -and
|
||||
(
|
||||
$Source -eq 'Windows.Terminal.PowershellCore' -or
|
||||
$CommandLine -match '(?i)(^|[\\/ ])pwsh(?:\.exe)?(?:\s|$)'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
$originalSettings = Get-Content -LiteralPath $settingsPath -Raw
|
||||
$settings = $originalSettings | ConvertFrom-Json
|
||||
}
|
||||
catch {
|
||||
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 = @()
|
||||
})
|
||||
$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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
$powerShell7Index = $null
|
||||
foreach ($candidateIndex in $powerShell7CandidateIndices) {
|
||||
$candidate = $profiles[$candidateIndex]
|
||||
if ([string](Get-JsonPropertyValue -Object $candidate -Name source) -eq 'Windows.Terminal.PowershellCore') {
|
||||
$powerShell7Index = $candidateIndex
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -eq $powerShell7Index -and $powerShell7CandidateIndices.Count -gt 0) {
|
||||
$powerShell7Index = $powerShell7CandidateIndices[0]
|
||||
}
|
||||
|
||||
if ($null -eq $powerShell7Index) {
|
||||
$profiles += [pscustomobject]@{
|
||||
commandline = 'pwsh.exe'
|
||||
guid = $PowerShellGuid
|
||||
hidden = $false
|
||||
name = 'PowerShell 7'
|
||||
startingDirectory = '%USERPROFILE%'
|
||||
}
|
||||
$powerShell7Index = $profiles.Count - 1
|
||||
$changed = $true
|
||||
}
|
||||
|
||||
$powerShell7Profile = $profiles[$powerShell7Index]
|
||||
if ([string]::IsNullOrWhiteSpace([string](Get-JsonPropertyValue -Object $powerShell7Profile -Name guid))) {
|
||||
Set-JsonPropertyValue -Object $powerShell7Profile -Name guid -Value $PowerShellGuid
|
||||
}
|
||||
if ($null -eq (Get-JsonPropertyValue -Object $powerShell7Profile -Name commandline) -and [string](Get-JsonPropertyValue -Object $powerShell7Profile -Name source) -ne 'Windows.Terminal.PowershellCore') {
|
||||
Set-JsonPropertyValue -Object $powerShell7Profile -Name commandline -Value 'pwsh.exe'
|
||||
}
|
||||
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 $legacyPowerShellIndex) {
|
||||
$legacyPowerShellProfile = $profiles[$legacyPowerShellIndex]
|
||||
Set-JsonPropertyValue -Object $legacyPowerShellProfile -Name name -Value 'Windows PowerShell 5.1 (Legacy)'
|
||||
Set-JsonPropertyValue -Object $legacyPowerShellProfile -Name hidden -Value $false
|
||||
}
|
||||
|
||||
$removedIndices = New-Object System.Collections.Generic.HashSet[int]
|
||||
foreach ($candidateIndex in $powerShell7CandidateIndices) {
|
||||
if ($candidateIndex -eq $powerShell7Index) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (Test-IsDisposablePowerShell7Profile -Profile $profiles[$candidateIndex]) {
|
||||
if ($removedIndices.Add($candidateIndex)) {
|
||||
$changed = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$orderedProfiles = New-Object System.Collections.Generic.List[object]
|
||||
$addedIndices = New-Object System.Collections.Generic.HashSet[int]
|
||||
|
||||
function Add-ProfileByIndex {
|
||||
param(
|
||||
[int]$Index
|
||||
)
|
||||
|
||||
if ($Index -lt 0) {
|
||||
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 utf8
|
||||
Write-Host "Updated Windows Terminal settings: $settingsPath"
|
||||
Write-Host "Backup saved to: $backupPath"
|
||||
@@ -0,0 +1,67 @@
|
||||
if (-not $IsWindows) {
|
||||
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')
|
||||
$ManagedStub = @'
|
||||
[include]
|
||||
path = ~/.config/git/config
|
||||
|
||||
[include]
|
||||
path = ~/.gitconfig.local
|
||||
'@
|
||||
|
||||
$ManagedKeys = @(
|
||||
'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()
|
||||
$NormalizedStub = ($ManagedStub -replace "`r", '').Trim()
|
||||
return $NormalizedContent -eq $NormalizedStub
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $GlobalGitConfig)) {
|
||||
if (-not (Test-Path -LiteralPath $LocalGitConfig)) {
|
||||
New-Item -ItemType File -Path $LocalGitConfig -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
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $LocalGitConfig)) {
|
||||
Copy-Item -LiteralPath $GlobalGitConfig -Destination $BackupPath -Force
|
||||
Copy-Item -LiteralPath $GlobalGitConfig -Destination $LocalGitConfig -Force
|
||||
|
||||
foreach ($Key in $ManagedKeys) {
|
||||
git config --file $LocalGitConfig --unset-all $Key 2>$null
|
||||
}
|
||||
|
||||
Write-Host "Migrated existing ~/.gitconfig to ~/.gitconfig.local"
|
||||
Write-Host "Backup saved to: $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"
|
||||
@@ -0,0 +1,6 @@
|
||||
# Keep the live PowerShell 7 profile tiny and stable.
|
||||
$ManagedProfile = Join-Path $HOME '.config\powershell\profile.ps1'
|
||||
|
||||
if (Test-Path -LiteralPath $ManagedProfile) {
|
||||
. $ManagedProfile
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[core]
|
||||
autocrlf = false
|
||||
eol = lf
|
||||
quotepath = false
|
||||
longpaths = true
|
||||
|
||||
[fetch]
|
||||
prune = true
|
||||
|
||||
[pull]
|
||||
ff = only
|
||||
|
||||
[init]
|
||||
defaultBranch = main
|
||||
@@ -0,0 +1,135 @@
|
||||
# Keep PowerShell 7 console text I/O on UTF-8 for predictable native tool behavior.
|
||||
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
|
||||
}
|
||||
|
||||
# Keep interactive editing comfortable without depending on extra prompt frameworks.
|
||||
if (
|
||||
$Host.Name -eq 'ConsoleHost' -and
|
||||
-not [Console]::IsOutputRedirected -and
|
||||
$Host.UI.SupportsVirtualTerminal -and
|
||||
(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
|
||||
}
|
||||
|
||||
function Reload-Profile {
|
||||
. $PROFILE
|
||||
}
|
||||
|
||||
function which {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
Get-Command -Name $Name -All | Select-Object CommandType, Name, Source
|
||||
}
|
||||
|
||||
function touch {
|
||||
param(
|
||||
[Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)]
|
||||
[string[]]$Path
|
||||
)
|
||||
|
||||
foreach ($Item in $Path) {
|
||||
if (Test-Path -LiteralPath $Item) {
|
||||
(Get-Item -LiteralPath $Item).LastWriteTime = Get-Date
|
||||
}
|
||||
else {
|
||||
New-Item -ItemType File -Path $Item -Force | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mkcd {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
New-Item -ItemType Directory -Path $Path -Force | Out-Null
|
||||
Set-Location -Path $Path
|
||||
}
|
||||
|
||||
function open {
|
||||
param(
|
||||
[string]$Path = '.'
|
||||
)
|
||||
|
||||
Invoke-Item -Path $Path
|
||||
}
|
||||
|
||||
function croot {
|
||||
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
||||
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
|
||||
}
|
||||
|
||||
Set-Location -Path $Root.Trim()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
function codevpn {
|
||||
$PreviousHttpProxy = $env:HTTP_PROXY
|
||||
$PreviousHttpsProxy = $env:HTTPS_PROXY
|
||||
|
||||
try {
|
||||
$env:HTTP_PROXY = 'http://127.0.0.1:12334'
|
||||
$env:HTTPS_PROXY = 'http://127.0.0.1:12334'
|
||||
code .
|
||||
}
|
||||
finally {
|
||||
$env:HTTP_PROXY = $PreviousHttpProxy
|
||||
$env:HTTPS_PROXY = $PreviousHttpsProxy
|
||||
}
|
||||
}
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
$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
|
||||
}
|
||||
}
|
||||
|
||||
# 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.
|
||||
$ChocolateyProfile = "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
|
||||
if (Test-Path -LiteralPath $ChocolateyProfile) {
|
||||
Import-Module $ChocolateyProfile
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
[include]
|
||||
path = ~/.config/git/config
|
||||
|
||||
[include]
|
||||
path = ~/.gitconfig.local
|
||||
Reference in new issue
Block a user