diff --git a/powershell.ps1 b/powershell.ps1 index dcee717..fc69de5 100644 --- a/powershell.ps1 +++ b/powershell.ps1 @@ -1,116 +1,943 @@ -set-alias ll ls -set-alias mamba micromamba - -function su { powershell Start-Process powershell -Verb runAs } -function pwdd { $("$PWD".replace($HOME, '~')) } - -# ln -s -function ln-s ($target, $link) { - New-Item -Path $link -ItemType SymbolicLink -Value $target +$script:PwshRcRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +if (-not $script:PwshRcRoot) { + $script:PwshRcRoot = Join-Path $HOME 'zshrc' +} +if (-not (Test-Path -LiteralPath (Join-Path $script:PwshRcRoot 'scripts') -PathType Container)) { + $homeRepo = Join-Path $HOME 'zshrc' + if (Test-Path -LiteralPath (Join-Path $homeRepo 'scripts') -PathType Container) { + $script:PwshRcRoot = $homeRepo + } } -# Paths -$env:path = "$env:path" + - ";C:\users\me\appdata\roaming\python\python39\scripts" + - ";C:\Users\me\AppData\Roaming\npm" + - ";C:\Users\me\AppData\Local\Yarn\bin" + - ";C:\Programs\bin" +$env:ZSHRC_ROOT = $script:PwshRcRoot +if (-not $env:SCR) { $env:SCR = Join-Path $env:ZSHRC_ROOT 'scripts' } +if (-not $env:BASEDIR) { $env:BASEDIR = $env:ZSHRC_ROOT } +if (-not $env:LANG) { $env:LANG = 'en_US.UTF-8' } +if (-not $env:LC_ALL) { $env:LC_ALL = 'en_US.UTF-8' } +$global:__PwshRcProxySegment = '' +$global:__PwshRcGitIdSegment = '' +$global:__PwshRcPromptPrCacheKey = '' +$global:__PwshRcPromptPrCacheTime = 0 +$global:__PwshRcPromptPrCacheValue = @('__none') -# Python versions -set-alias python3.9 C:\"Program Files"\Python39\python.exe -set-alias pip3.9 C:\"Program Files"\Python39\Scripts\pip3.exe -set-alias python3.10 C:\Python310\python.exe -set-alias python3.10 C:\Python310\Scripts\pip3.exe - -# Video tools -function vcompy() { - ipython -i $HOME\zshrc\scripts\helpers\video.py +function has { + param([Parameter(Mandatory = $true)][string]$Command) + return $null -ne (Get-Command $Command -ErrorAction SilentlyContinue) } -# Minecraft coloring -function color($tmp) { - $033 = [char]27 - $tmp = "$tmp&r" - $tmp = $tmp.replace("&0", "$033[0;30m") - $tmp = $tmp.replace("&1", "$033[0;34m") - $tmp = $tmp.replace("&2", "$033[0;32m") - $tmp = $tmp.replace("&3", "$033[0;36m") - $tmp = $tmp.replace("&4", "$033[0;31m") - $tmp = $tmp.replace("&5", "$033[0;35m") - $tmp = $tmp.replace("&6", "$033[0;33m") - $tmp = $tmp.replace("&7", "$033[0;37m") - $tmp = $tmp.replace("&8", "$033[1;30m") - $tmp = $tmp.replace("&9", "$033[1;34m") - $tmp = $tmp.replace("&a", "$033[1;32m") - $tmp = $tmp.replace("&b", "$033[1;36m") - $tmp = $tmp.replace("&c", "$033[1;31m") - $tmp = $tmp.replace("&d", "$033[1;35m") - $tmp = $tmp.replace("&e", "$033[1;33m") - $tmp = $tmp.replace("&f", "$033[1;37m") - $tmp = $tmp.replace("&r", "$033[0m") - $tmp = $tmp.replace("&n", "`r`n") - $tmp +function Get-ExternalCommandPath { + param([Parameter(Mandatory = $true)][string]$Command) + $cmd = Get-Command $Command -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($cmd) { return $cmd.Source } + return $null } -function prompt -{ - color ("&n" + - "&5$(get-date -UFormat "%a %m-%d %H:%M") &1$($env:computername) &eAzalea &r$(pwdd)&n" + - "> ") +function Remove-AliasIfExists { + param([Parameter(Mandatory = $true)][string]$Name) + if (Get-Alias -Name $Name -ErrorAction SilentlyContinue) { + Remove-Item -Path "Alias:$Name" -Force -ErrorAction SilentlyContinue + } } -function cropv($file, $len) -{ - ffmpeg -i $file -filter:v "crop=$(len):1440:$([math]::floor((2560-$len)/2)):0" out.mp4 +function Invoke-ExternalCommand { + if ($args.Count -lt 1) { + Write-Error 'Invoke-ExternalCommand requires a command name.' + return 127 + } + + $Command = [string]$args[0] + $CommandArgs = if ($args.Count -gt 1) { @($args[1..($args.Count - 1)]) } else { @() } + $cmd = Get-ExternalCommandPath $Command + if (-not $cmd) { + Write-Error "$Command is not installed." + return 127 + } + + & $cmd @CommandArgs +} + +function Add-PathIfExists { + param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Paths) + $separator = [IO.Path]::PathSeparator + $current = @($env:Path -split [regex]::Escape($separator) | Where-Object { $_ }) + + foreach ($path in $Paths) { + if (-not $path) { continue } + $expanded = [Environment]::ExpandEnvironmentVariables($path) + if (-not (Test-Path -LiteralPath $expanded -PathType Container)) { continue } + if ($current -notcontains $expanded) { + $current = @($expanded) + $current + } + } + + $env:Path = ($current -join $separator) +} + +Add-PathIfExists ` + (Join-Path $env:SCR 'bin') ` + (Join-Path $HOME '.local/bin') ` + (Join-Path $HOME '.cargo/bin') ` + (Join-Path $HOME 'AppData/Roaming/Python/Python39/Scripts') ` + (Join-Path $HOME 'AppData/Roaming/npm') ` + (Join-Path $HOME 'AppData/Local/Yarn/bin') ` + 'C:\Programs\bin' + +if ($IsLinux -and [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::X64) { + Add-PathIfExists (Join-Path $env:SCR 'bin/linux-x64') +} + +function pwdd { + $path = $PWD.ProviderPath + if ($path.StartsWith($HOME, [StringComparison]::OrdinalIgnoreCase)) { + return ('~' + $path.Substring($HOME.Length)) + } + return $path +} + +function ln-s { + param( + [Parameter(Mandatory = $true)][string]$Target, + [Parameter(Mandatory = $true)][string]$Link + ) + New-Item -Path $Link -ItemType SymbolicLink -Value $Target +} + +function su { + if ($IsWindows -or -not (Get-Variable IsWindows -ErrorAction SilentlyContinue)) { + $shell = Get-ExternalCommandPath pwsh + if (-not $shell) { $shell = Get-ExternalCommandPath powershell } + if (-not $shell) { $shell = 'powershell' } + Start-Process $shell -Verb RunAs + } else { + Write-Error 'su is only configured for Windows PowerShell sessions.' + } +} + +function Set-AliasIfCommand { + param( + [Parameter(Mandatory = $true)][string]$Name, + [Parameter(Mandatory = $true)][string]$Command + ) + if (has $Command) { + Set-Alias -Scope Global -Name $Name -Value $Command -Force + } +} + +function Register-ForwardingFunction { + param( + [Parameter(Mandatory = $true)][string]$Name, + [Parameter(Mandatory = $true)][string]$Command, + [object[]]$PrefixArgs = @() + ) + + $scriptBlock = { + Invoke-ExternalCommand $Command @PrefixArgs @args + }.GetNewClosure() + Remove-AliasIfExists $Name + Set-Item -Path "function:global:$Name" -Value $scriptBlock +} + +function modern-replace { + param( + [Parameter(Mandatory = $true)][string]$OriginalCommand, + [Parameter(Mandatory = $true)][string]$NewCommand, + [object[]]$NewCommandArgs = @() + ) + + if (has $NewCommand) { + Register-ForwardingFunction -Name $OriginalCommand -Command $NewCommand -PrefixArgs $NewCommandArgs + } +} + +foreach ($name in @('ll', 'l', 'lla', 'llg', 'open', 'gradle', 'git', '7z', 'ssh', 'ffmpeg', 'ffprobe')) { + Remove-AliasIfExists $name +} + +function ll { + if (has eza) { Invoke-ExternalCommand eza -l @args } + else { Get-ChildItem @args } +} + +function l { ll @args } + +function lla { + if (has eza) { Invoke-ExternalCommand eza -la @args } + else { Get-ChildItem -Force @args } +} + +function llg { + if (has eza) { Invoke-ExternalCommand eza -l --git --git-repos @args } + else { ll @args } +} + +function mkdirs { + param([Parameter(Mandatory = $true)][string]$Path) + New-Item -ItemType Directory -Force -Path $Path +} + +function open { Invoke-Item @args } +Set-Alias -Scope Global -Name clr -Value Clear-Host -Force + +function colors { + color '&000&111&222&333&444&555&666&777&888&999&aaa&bbb&ccc&ddd&eee&fff' +} + +function ports { + if (Get-Command Get-NetTCPConnection -ErrorAction SilentlyContinue) { + Get-NetTCPConnection -State Listen | Sort-Object LocalPort | Select-Object LocalAddress, LocalPort, OwningProcess + } elseif (has netstat) { + Invoke-ExternalCommand netstat -ano | Select-String -Pattern 'LISTEN|Listen' + } else { + Write-Error 'Neither Get-NetTCPConnection nor netstat is available.' + } +} + +function clean-empty-dir { + Get-ChildItem -Directory -Recurse -Force | Sort-Object FullName -Descending | Where-Object { + -not (Get-ChildItem -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue | Select-Object -First 1) + } | ForEach-Object { + Remove-Item -LiteralPath $_.FullName -Force + $_.FullName + } +} + +function addline { + param( + [Parameter(Mandatory = $true)][string]$File, + [Parameter(Mandatory = $true)][string]$Line + ) + + if (-not (Test-Path -LiteralPath $File)) { + New-Item -ItemType File -Path $File -Force | Out-Null + } + + $exists = Select-String -LiteralPath $File -SimpleMatch -Pattern $Line -Quiet -ErrorAction SilentlyContinue + if (-not $exists) { + Add-Content -LiteralPath $File -Value $Line + } +} + +function mkcd { + param([Parameter(Mandatory = $true)][string]$Path) + New-Item -ItemType Directory -Force -Path $Path | Out-Null + Set-Location -LiteralPath $Path +} + +function spushd { Push-Location @args | Out-Null } +function spopd { Pop-Location @args | Out-Null } + +function set-java { + param([Parameter(Mandatory = $true)][string]$Version) + + $candidates = @() + if ($IsWindows -or -not (Get-Variable IsWindows -ErrorAction SilentlyContinue)) { + $candidates += Get-ChildItem 'C:\Program Files\Java', 'C:\Program Files\Eclipse Adoptium', 'C:\Program Files\Microsoft' -Directory -ErrorAction SilentlyContinue + } else { + $candidates += Get-ChildItem '/usr/lib/jvm' -Directory -ErrorAction SilentlyContinue + } + + $javaHome = $candidates | Where-Object { $_.Name -like "*$Version*" -and $_.Name -match 'jdk|java' } | Select-Object -First 1 + if (-not $javaHome) { + Write-Error "Java version $Version was not found." + return 1 + } + + $env:JAVA_HOME = $javaHome.FullName + Add-PathIfExists (Join-Path $env:JAVA_HOME 'bin') +} + +function upload-daisy { + param([Parameter(Mandatory = $true)][string]$File) + if (-not (has curl.exe)) { + Write-Error 'curl.exe is required for upload-daisy.' + return 1 + } + Invoke-ExternalCommand curl.exe -u azalea -F "path=@$File" 'https://daisy-ddns.hydev.org/upload?path=/' +} + +if (has micro) { $env:EDITOR = 'micro' } +elseif (has nano) { $env:EDITOR = 'nano' } + +if (-not $env:GRADLE -and (has gradle)) { + $env:GRADLE = (Get-ExternalCommandPath gradle) +} + +function gradle { + $wrapper = if ($IsWindows -or -not (Get-Variable IsWindows -ErrorAction SilentlyContinue)) { '.\gradlew.bat' } else { './gradlew' } + if (Test-Path -LiteralPath $wrapper) { + & $wrapper @args + } elseif ($env:GRADLE) { + & $env:GRADLE @args + } else { + Write-Error 'Neither gradle nor ./gradlew is found, please install it and restart PowerShell.' + return 1 + } +} + +function global:7z { + if ($args.Count -gt 0 -and $args[0] -eq 'd') { + Write-Host '7z d is blocked. It does not stand for decompress, it stands for delete.' + } else { + Invoke-ExternalCommand 7z @args + } +} + +function lisp { + param([Parameter(Mandatory = $true)][string]$File) + Invoke-ExternalCommand ros run --load $File --quit +} + +function adblan { + param([Parameter(Mandatory = $true)][string]$HostName) + Invoke-ExternalCommand adb connect "$HostName`:16523" +} + +function adblan-start { Invoke-ExternalCommand adb tcpip 16523 } + +function setproxy { + param( + [string]$Address = '127.0.0.1', + [int]$Port = 7890 + ) + + $full = "$Address`:$Port" + $proxy = "http://$full" + $env:https_proxy = $proxy + $env:http_proxy = $proxy + $env:all_proxy = $proxy + $env:HTTPS_PROXY = $proxy + $env:HTTP_PROXY = $proxy + $env:ALL_PROXY = $proxy + $global:__PwshRcProxySegment = "proxy $full " + Write-Host "Using proxy! $full" -ForegroundColor Green +} + +function global:ssh { + if ($env:TERM -eq 'xterm-kitty') { + $oldTerm = $env:TERM + $env:TERM = 'xterm-256color' + try { Invoke-ExternalCommand ssh @args } + finally { $env:TERM = $oldTerm } + } else { + Invoke-ExternalCommand ssh @args + } +} + +function subtitle { + param([Parameter(Mandatory = $true)][string]$File) + $oldCuda = $env:CUDA_VISIBLE_DEVICES + $env:CUDA_VISIBLE_DEVICES = '1' + try { Invoke-ExternalCommand auto_subtitle --srt_only True --model large $File } + finally { $env:CUDA_VISIBLE_DEVICES = $oldCuda } +} + +function upload { + param([Parameter(Mandatory = $true)][string]$File) + if (-not $env:UP_PASSWORD) { + Write-Error 'Password not set, please set $env:UP_PASSWORD = "xxx".' + return 1 + } + if (-not (Test-Path -LiteralPath $File -PathType Leaf)) { + Write-Error 'File not found.' + return 1 + } + if (-not (has curl.exe)) { + Write-Error 'curl.exe is required for upload.' + return 1 + } + + $credential = 'azalea:{0}' -f $env:UP_PASSWORD + Invoke-ExternalCommand curl.exe -u $credential -F "path=@$File" 'https://daisy.hydev.org/upload?path=/' +} + +function global:ffmpeg { Invoke-ExternalCommand ffmpeg -hide_banner @args } +function global:ffprobe { Invoke-ExternalCommand ffprobe -hide_banner @args } + +function vcompy { + $videoHelper = Join-Path $env:SCR 'helpers/video.py' + Invoke-ExternalCommand ipython -i $videoHelper +} + +function cropv { + param( + [Parameter(Mandatory = $true)][string]$File, + [Parameter(Mandatory = $true)][int]$Length + ) + $x = [math]::Floor((2560 - $Length) / 2) + ffmpeg -i $File -filter:v "crop=$Length`:1440:$x`:0" out.mp4 } function mp3v0 { - param ( - [Parameter(Mandatory = $true)] - [string]$inputFile - ) - - # Extract file name without extension - $baseName = [System.IO.Path]::GetFileNameWithoutExtension($inputFile) - - # Set output filename - $outputFile = "$baseName.mp3" - - # Run ffmpeg with specified arguments - ffmpeg -i "$inputFile" -c:a libmp3lame -q:a 0 "$outputFile" + param([Parameter(Mandatory = $true)][string]$InputFile) + $baseName = [System.IO.Path]::GetFileNameWithoutExtension($InputFile) + ffmpeg -i $InputFile -c:a libmp3lame -q:a 0 "$baseName.mp3" } +function dc { + if (has docker-compose) { Invoke-ExternalCommand docker-compose @args } + else { Invoke-ExternalCommand docker compose @args } +} + +if (-not (has docker) -and (has podman)) { + Set-Alias -Scope Global -Name docker -Value podman -Force + if (has podman-compose) { + Set-Alias -Scope Global -Name docker-compose -Value podman-compose -Force + } +} + +function docker-ip { + param([Parameter(Mandatory = $true)][string]$Container) + docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $Container +} + +function dockers { + docker ps --format 'table {{.Names}} {{.Image}} {{.Status}}' +} + +function docker-compose-path { + param([Parameter(Mandatory = $true)][string]$Container) + docker inspect $Container | Select-String -Pattern 'com.docker.compose.project.working_dir' +} + +modern-replace ls eza +modern-replace cat bat +modern-replace man tldr +modern-replace top btop +modern-replace nano micro +modern-replace curl curlie +modern-replace vi nvim +modern-replace vim nvim + +if (Test-Path 'C:\Program Files\Python39\python.exe') { + Set-Alias -Scope Global -Name python3.9 -Value 'C:\Program Files\Python39\python.exe' -Force +} +if (Test-Path 'C:\Program Files\Python39\Scripts\pip3.exe') { + Set-Alias -Scope Global -Name pip3.9 -Value 'C:\Program Files\Python39\Scripts\pip3.exe' -Force +} +if (Test-Path 'C:\Python310\python.exe') { + Set-Alias -Scope Global -Name python3.10 -Value 'C:\Python310\python.exe' -Force +} +if (Test-Path 'C:\Python310\Scripts\pip3.exe') { + Set-Alias -Scope Global -Name pip3.10 -Value 'C:\Python310\Scripts\pip3.exe' -Force +} + +if (has micromamba) { + Set-Alias -Scope Global -Name mamba -Value micromamba -Force + if (-not $env:MAMBA_ROOT_PREFIX) { $env:MAMBA_ROOT_PREFIX = Join-Path $HOME '.conda' } + $mambaExe = Get-ExternalCommandPath micromamba + $mambaHook = & $mambaExe shell hook --shell powershell --root-prefix (Join-Path $HOME 'micromamba') 2>$null + if ($LASTEXITCODE -eq 0 -and $mambaHook) { + Invoke-Expression ($mambaHook -join [Environment]::NewLine) + if (-not (has conda)) { Set-Alias -Scope Global -Name conda -Value mamba -Force } + } +} + +if (has pyenv) { + $pyenvHook = Invoke-ExternalCommand pyenv init - powershell 2>$null + if ($LASTEXITCODE -eq 0 -and $pyenvHook) { + Invoke-Expression ($pyenvHook -join [Environment]::NewLine) + } + try { + $pyenvRoot = Invoke-ExternalCommand pyenv root 2>$null + if ($LASTEXITCODE -eq 0 -and $pyenvRoot) { Add-PathIfExists (Join-Path ($pyenvRoot | Select-Object -First 1) 'shims') } + } catch {} +} + +function Invoke-RawGit { + $GitArgs = @($args) + $gitExe = Get-ExternalCommandPath git + if (-not $gitExe) { + Write-Error 'git is not installed.' + return 127 + } + & $gitExe @GitArgs +} + +function Invoke-GitCommand { + $GitArgs = @($args) + if ($env:GIT_USER) { + Invoke-RawGit -c "user.name=$env:GIT_USER" -c "user.email=$env:GIT_EMAIL" -c commit.gpgsign=false @GitArgs + } else { + Invoke-RawGit @GitArgs + } +} + +function global:git { Invoke-GitCommand @args } + +function commit { + if ($args.Count -eq 0) { git commit } + else { git commit -m ($args -join ' ') } +} + +function commitall { + git add . + commit @args +} + +Set-Alias -Scope Global -Name commita -Value commitall -Force + +function compush { + commitall @args + git push +} + +function git-id-prompt { + if (-not $env:GIT_USER -and -not $env:GIT_EMAIL) { + $global:__PwshRcGitIdSegment = '' + } else { + $global:__PwshRcGitIdSegment = "Git ID: $env:GIT_USER | $env:GIT_EMAIL " + } +} + +function git-id { + param( + [Parameter(Mandatory = $true)][string]$User, + [Parameter(Mandatory = $true)][string]$Email + ) + $env:GIT_USER = $User + $env:GIT_EMAIL = $Email + git-id-prompt +} + +function git-ida { + param([Parameter(Mandatory = $true)][string]$Identity) + $identityParts = Invoke-ExternalCommand git-id-list get $Identity + if ($LASTEXITCODE -ne 0 -or -not $identityParts) { return $LASTEXITCODE } + git-id $identityParts[0] $identityParts[1] +} + +function Test-GitCleanWorktree { + Invoke-RawGit rev-parse --is-inside-work-tree *> $null + if ($LASTEXITCODE -ne 0) { return $false } + + $statusLines = Invoke-RawGit status --porcelain 2>$null + if ($statusLines) { + Write-Host 'Workspace is not clean.' + Invoke-RawGit status --short + return $false + } + + return $true +} + +function git-require-clean { + if (-not (Test-GitCleanWorktree)) { return 1 } +} + +function git-main-branch { + $remoteHead = Invoke-RawGit symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>$null + if ($LASTEXITCODE -eq 0 -and $remoteHead) { + return (($remoteHead | Select-Object -First 1) -replace '^origin/', '') + } + + foreach ($branch in @('main', 'master', 'trunk', 'develop')) { + Invoke-RawGit show-ref --verify --quiet "refs/heads/$branch" *> $null + if ($LASTEXITCODE -eq 0) { return $branch } + Invoke-RawGit show-ref --verify --quiet "refs/remotes/origin/$branch" *> $null + if ($LASTEXITCODE -eq 0) { return $branch } + } + + Write-Error 'Could not determine main branch.' + return 1 +} + +function git-update-main { + param([string]$MainBranch) + if (-not $MainBranch) { $MainBranch = git-main-branch } + if (-not $MainBranch) { return 1 } + + Invoke-RawGit checkout $MainBranch + if ($LASTEXITCODE -ne 0) { return $LASTEXITCODE } + Invoke-RawGit pull --ff-only +} + +function br { + param([Parameter(Mandatory = $true)][string]$Branch) + if (-not (Test-GitCleanWorktree)) { return 1 } + + Invoke-RawGit show-ref --verify --quiet "refs/heads/$Branch" *> $null + $hasLocal = $LASTEXITCODE -eq 0 + Invoke-RawGit show-ref --verify --quiet "refs/remotes/origin/$Branch" *> $null + $hasRemote = $LASTEXITCODE -eq 0 + if ($hasLocal -or $hasRemote) { + Invoke-RawGit checkout $Branch + return $LASTEXITCODE + } + + $mainBranch = git-main-branch + if (-not $mainBranch) { return 1 } + git-update-main $mainBranch + if ($LASTEXITCODE -ne 0) { return $LASTEXITCODE } + Invoke-RawGit checkout -b $Branch +} + +function bru { + $currentBranch = Invoke-RawGit symbolic-ref --quiet --short HEAD 2>$null + if ($LASTEXITCODE -ne 0 -or -not $currentBranch) { + Write-Error 'Could not determine current branch.' + return 1 + } + $currentBranch = $currentBranch | Select-Object -First 1 + + if (-not (Test-GitCleanWorktree)) { return 1 } + + $mainBranch = git-main-branch + if (-not $mainBranch) { return 1 } + if ($currentBranch -eq $mainBranch) { + Write-Error "Already on $mainBranch." + return 1 + } + + git-update-main $mainBranch + if ($LASTEXITCODE -ne 0) { return $LASTEXITCODE } + Invoke-RawGit checkout $currentBranch + if ($LASTEXITCODE -ne 0) { return $LASTEXITCODE } + Invoke-RawGit rebase $mainBranch +} + +function git-env { + foreach ($cmd in @('add', 'bisect', 'branch', 'checkout', 'clone', 'commit', 'diff', 'fetch', 'grep', 'init', 'log', 'merge', 'pull', 'push', 'rebase', 'reset', 'restore', 'show', 'stash', 'tag')) { + $name = $cmd + Set-Item -Path "function:global:$name" -Value { git $name @args }.GetNewClosure() + } + + Set-Item -Path 'function:global:grm' -Value { git rm @args } + Set-Item -Path 'function:global:gmv' -Value { git mv @args } + Set-Item -Path 'function:global:st' -Value { git status @args } +} + +function git-unenv { + foreach ($cmd in @('add', 'bisect', 'branch', 'checkout', 'clone', 'commit', 'diff', 'fetch', 'grep', 'init', 'log', 'merge', 'pull', 'push', 'rebase', 'reset', 'restore', 'show', 'stash', 'tag', 'grm', 'gmv', 'st')) { + Remove-Item -Path "function:global:$cmd" -ErrorAction SilentlyContinue + } +} + +function prompt-reset { + $global:__PwshRcProxySegment = '' + git-id-prompt +} + +function Get-PromptPrState { + param([string]$Branch) + if (-not $Branch -or -not (has gh)) { return $null } + + $repoKey = Invoke-RawGit rev-parse --show-toplevel 2>$null + if ($LASTEXITCODE -ne 0 -or -not $repoKey) { + if (has jj) { $repoKey = Invoke-ExternalCommand jj root --ignore-working-copy 2>$null } + } + if (-not $repoKey) { return $null } + $repoKey = $repoKey | Select-Object -First 1 + + $cacheKey = "$repoKey`:$Branch" + $now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + if ($global:__PwshRcPromptPrCacheKey -eq $cacheKey -and ($now - [int64]$global:__PwshRcPromptPrCacheTime) -lt 300) { + if ($global:__PwshRcPromptPrCacheValue[0] -eq '__none') { return $null } + return [pscustomobject]@{ Number = $global:__PwshRcPromptPrCacheValue[0]; Color = $global:__PwshRcPromptPrCacheValue[1] } + } + + $jq = 'map(select(.state == "OPEN" or .state == "MERGED")) | sort_by(.updatedAt) | reverse | .[0] | select(.number != null) | .number, .state' + $prLine = Invoke-ExternalCommand gh pr list --head $Branch --state all --limit 20 --json number,state,updatedAt --jq $jq 2>$null + $prNumber = $prLine | Select-Object -First 1 + $prState = $prLine | Select-Object -Skip 1 -First 1 + $prColor = if ($prState -eq 'MERGED') { 'Magenta' } else { 'Green' } + + $global:__PwshRcPromptPrCacheKey = $cacheKey + $global:__PwshRcPromptPrCacheTime = $now + if (-not ($prNumber -match '^[0-9]+$')) { + $global:__PwshRcPromptPrCacheValue = @('__none') + return $null + } + + $global:__PwshRcPromptPrCacheValue = @($prNumber, $prColor) + return [pscustomobject]@{ Number = $prNumber; Color = $prColor } +} + +function Get-GitUnpushedCount { + $upstream = Invoke-RawGit rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>$null + if ($LASTEXITCODE -ne 0 -or -not $upstream) { return $null } + $upstream = $upstream | Select-Object -First 1 + Invoke-RawGit rev-list --count "$upstream..HEAD" 2>$null +} + +function Get-GitPromptState { + if (-not (Get-ExternalCommandPath git)) { return $null } + Invoke-RawGit rev-parse --is-inside-work-tree *> $null + if ($LASTEXITCODE -ne 0) { return $null } + + $branch = Invoke-RawGit symbolic-ref --quiet --short HEAD 2>$null + $prBranch = $branch | Select-Object -First 1 + if (-not $branch) { $branch = Invoke-RawGit rev-parse --short HEAD 2>$null } + if (-not $branch) { return $null } + $branch = $branch | Select-Object -First 1 + + $gitStatus = @(Invoke-RawGit status --porcelain=v1 --branch 2>$null) + $flags = New-Object System.Collections.Generic.List[string] + $changed = $false + + $header = $gitStatus | Select-Object -First 1 + if ($header -match 'behind ([0-9]+)') { $flags.Add("v$($Matches[1])") } + + $aheadCount = Get-GitUnpushedCount | Select-Object -First 1 + if ($aheadCount -match '^[0-9]+$' -and [int]$aheadCount -gt 0) { + $flags.Add("^$aheadCount") + $changed = $true + } + + foreach ($line in ($gitStatus | Select-Object -Skip 1)) { + if (-not $line) { continue } + $changed = $true + $index = if ($line.Length -ge 1) { $line.Substring(0, 1) } else { ' ' } + $worktree = if ($line.Length -ge 2) { $line.Substring(1, 1) } else { ' ' } + + if ($line -match '^(UU|AA|DD|AU|UA|DU|UD)') { + if (-not $flags.Contains('x')) { $flags.Add('x') } + continue + } + if ($line -like '??*') { + if (-not $flags.Contains('?')) { $flags.Add('?') } + continue + } + if ($index -ne ' ' -and -not $flags.Contains('+')) { $flags.Add('+') } + if ($worktree -ne ' ' -and -not $flags.Contains('!')) { $flags.Add('!') } + } + + $segment = "git:$branch" + if ($flags.Count -gt 0) { $segment = "$segment $($flags -join '')" } + $pr = Get-PromptPrState $prBranch + + return [pscustomobject]@{ + Segment = $segment + Color = if ($changed) { 'Yellow' } else { 'DarkGray' } + Pr = $pr + } +} + +function Get-JjPromptState { + if (-not (has jj)) { return $null } + Invoke-ExternalCommand jj root --ignore-working-copy *> $null + if ($LASTEXITCODE -ne 0) { return $null } + + $info = Invoke-ExternalCommand jj log --no-graph --ignore-working-copy --color=never -r '@' --template 'separate(" ", change_id.shortest(8), bookmarks.join("|"), if(conflict, "x")) ++ "\n"' 2>$null + if (-not $info) { return $null } + $info = $info | Select-Object -First 1 + + $diffSummary = Invoke-ExternalCommand jj diff --summary --ignore-working-copy 2>$null + $bookmark = Invoke-ExternalCommand jj log --no-graph --ignore-working-copy --color=never -r 'bookmarks() & @' --template 'bookmarks.join("\n") ++ "\n"' 2>$null | Select-Object -First 1 + if ($bookmark) { $bookmark = $bookmark -replace '\*$', '' } + + return [pscustomobject]@{ + Segment = "jj:$info" + Color = if ($diffSummary) { 'Yellow' } else { 'DarkGray' } + Pr = Get-PromptPrState $bookmark + } +} + +function Get-VcsPromptState { + $jj = Get-JjPromptState + if ($jj) { return $jj } + Get-GitPromptState +} + +function color { + param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Text) + $esc = [char]27 + $tmp = (($Text -join ' ') + '&r') + $replacements = [ordered]@{ + '&0' = "$esc[0;30m"; '&1' = "$esc[0;34m"; '&2' = "$esc[0;32m"; '&3' = "$esc[0;36m" + '&4' = "$esc[0;31m"; '&5' = "$esc[0;35m"; '&6' = "$esc[0;33m"; '&7' = "$esc[0;37m" + '&8' = "$esc[1;30m"; '&9' = "$esc[1;34m"; '&a' = "$esc[1;32m"; '&b' = "$esc[1;36m" + '&c' = "$esc[1;31m"; '&d' = "$esc[1;35m"; '&e' = "$esc[1;33m"; '&f' = "$esc[1;37m" + '&r' = "$esc[0m"; '&n' = "`r`n" + } + foreach ($key in $replacements.Keys) { $tmp = $tmp.Replace($key, $replacements[$key]) } + $tmp +} + +function pcolor { + $promptHelper = Join-Path $env:SCR 'helpers/prompt.py' + if (Test-Path -LiteralPath $promptHelper) { + & $promptHelper ($args -join ' ') color + } else { + color ($args -join ' ') + } +} + +function global:prompt { + $hostName = [System.Net.Dns]::GetHostName() -replace '^HyDEV-', '' + $date = Get-Date + + Write-Host '' + if ($hostName -eq 'HyDEV') { + Write-Host ($date.ToString('ddd MM-dd HH:mm')) -NoNewline -ForegroundColor Magenta + Write-Host ' ' -NoNewline + } else { + Write-Host ($date.ToString('ddd ')) -NoNewline -ForegroundColor Cyan + Write-Host ($date.ToString('MM-')) -NoNewline -ForegroundColor Magenta + Write-Host ($date.ToString('dd ')) -NoNewline -ForegroundColor White + Write-Host ($date.ToString('HH:')) -NoNewline -ForegroundColor Magenta + Write-Host ($date.ToString('mm ')) -NoNewline -ForegroundColor Cyan + } + + if ($hostName -eq 'HyDEV') { + Write-Host 'H' -NoNewline -ForegroundColor Cyan + Write-Host 'y' -NoNewline -ForegroundColor Magenta + Write-Host 'D' -NoNewline -ForegroundColor White + Write-Host 'E' -NoNewline -ForegroundColor Magenta + Write-Host 'V ' -NoNewline -ForegroundColor Cyan + } else { + Write-Host "$hostName " -NoNewline -ForegroundColor Blue + } + + if ($global:__PwshRcGitIdSegment) { + Write-Host $global:__PwshRcGitIdSegment -NoNewline -ForegroundColor Yellow + } else { + $userName = if ($env:USERNAME) { $env:USERNAME } else { $env:USER } + Write-Host "$userName " -NoNewline -ForegroundColor Yellow + } + + if ($global:__PwshRcProxySegment) { + Write-Host $global:__PwshRcProxySegment -NoNewline -ForegroundColor Green + } + + Write-Host (pwdd) -NoNewline + + $vcs = Get-VcsPromptState + if ($vcs) { + Write-Host ' [' -NoNewline + Write-Host $vcs.Segment -NoNewline -ForegroundColor $vcs.Color + if ($vcs.Pr) { + Write-Host ' ' -NoNewline -ForegroundColor $vcs.Color + Write-Host "#$($vcs.Pr.Number)" -NoNewline -ForegroundColor $vcs.Pr.Color + } + Write-Host ']' -NoNewline -ForegroundColor $vcs.Color + } + + return "`n> " +} + +function Start-ZshrcAutoUpdate { + if (-not (Get-ExternalCommandPath git)) { return } + if ($env:PWSHRC_UPDATE_DISABLED -or $env:ZSHRC_UPDATE_DISABLED) { return } + if (-not (Test-Path -LiteralPath (Join-Path $env:ZSHRC_ROOT '.git') -PathType Container)) { return } + + $root = $env:ZSHRC_ROOT + $remoteRef = if ($env:ZSHRC_UPDATE_REF) { $env:ZSHRC_UPDATE_REF } else { 'origin/master' } + $verboseUpdate = [bool]$env:ZSHRC_UPDATE_VERBOSE + + Start-Job -Name "zshrc-auto-update-$PID" -ArgumentList $root, $remoteRef, $verboseUpdate -ScriptBlock { + param([string]$Root, [string]$RemoteRef, [bool]$VerboseUpdate) + + $git = Get-Command git -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $git) { return } + $git = $git.Source + + $oldLocation = Get-Location + Set-Location -LiteralPath $Root + $lockDir = Join-Path $Root '.git/zshrc-update.lock' + + try { + New-Item -ItemType Directory -Path $lockDir -ErrorAction Stop | Out-Null + } catch { + Set-Location -LiteralPath ($oldLocation.Path) + return + } + + $stashCreated = $false + function Invoke-UpdateGit { & $git @args } + function Test-UpdateDirty { + Invoke-UpdateGit diff --quiet --ignore-submodules -- *> $null + if ($LASTEXITCODE -ne 0) { return $true } + Invoke-UpdateGit diff --cached --quiet --ignore-submodules -- *> $null + if ($LASTEXITCODE -ne 0) { return $true } + $others = Invoke-UpdateGit ls-files --others --exclude-standard + return [bool]$others + } + function Save-UpdateStash { + if (Test-UpdateDirty) { + Invoke-UpdateGit stash push -u -m "zshrc auto-update before applying $RemoteRef" *> $null + if ($LASTEXITCODE -eq 0) { $script:stashCreated = $true } + } + } + function Restore-UpdateStash { + if ($script:stashCreated) { Invoke-UpdateGit stash pop *> $null } + } + + try { + Invoke-UpdateGit fetch origin --quiet *> $null + $fetched = $LASTEXITCODE -eq 0 + Invoke-UpdateGit rev-parse --verify --quiet $RemoteRef *> $null + if ($fetched -and $LASTEXITCODE -eq 0) { + Invoke-UpdateGit merge-base --is-ancestor HEAD $RemoteRef *> $null + if ($LASTEXITCODE -ne 0) { + Save-UpdateStash + Invoke-UpdateGit reset --hard $RemoteRef *> $null + if ($LASTEXITCODE -eq 0) { + Invoke-UpdateGit submodule update --init --recursive --depth 1 *> $null + Restore-UpdateStash + } + } else { + $updates = Invoke-UpdateGit log "HEAD..$RemoteRef" --oneline + if ($updates) { + Save-UpdateStash + Invoke-UpdateGit merge --ff-only $RemoteRef *> $null + if ($LASTEXITCODE -eq 0) { + Invoke-UpdateGit submodule update --init --recursive --depth 1 *> $null + Restore-UpdateStash + } + } + } + } elseif ($VerboseUpdate) { + Write-Output 'Update check failed.' + } + } finally { + Remove-Item -LiteralPath $lockDir -Force -ErrorAction SilentlyContinue + Set-Location -LiteralPath ($oldLocation.Path) + } + } | Out-Null +} + +if (has thefuck) { + $thefuckAlias = Invoke-ExternalCommand thefuck --alias 2>$null + if ($LASTEXITCODE -eq 0 -and $thefuckAlias) { Invoke-Expression ($thefuckAlias -join [Environment]::NewLine) } +} -# ls coloring if (Get-Module -ListAvailable -Name PSColor) { Import-Module PSColor $global:PSColor = @{ File = @{ - Default = @{ Color = 'White' } - Directory = @{ Color = 'Blue'} - Hidden = @{ Color = 'DarkGray'; Pattern = '^\.' } - Code = @{ Color = 'Magenta'; Pattern = '\.(java|c|cpp|cs|js|css|html)$' } + Default = @{ Color = 'White' } + Directory = @{ Color = 'Blue' } + Hidden = @{ Color = 'DarkGray'; Pattern = '^\.' } + Code = @{ Color = 'Magenta'; Pattern = '\.(java|c|cpp|cs|js|css|html|ps1)$' } Executable = @{ Color = 'Red'; Pattern = '\.(exe|bat|cmd|py|pl|ps1|psm1|vbs|rb|reg)$' } - Text = @{ Color = 'Yellow'; Pattern = '\.(txt|cfg|conf|ini|csv|log|config|xml|yml|md|markdown)$' } - Compressed = @{ Color = 'Green'; Pattern = '\.(zip|tar|gz|rar|jar|war)$' } + Text = @{ Color = 'Yellow'; Pattern = '\.(txt|cfg|conf|ini|csv|log|config|xml|yml|yaml|md|markdown)$' } + Compressed = @{ Color = 'Green'; Pattern = '\.(zip|tar|gz|rar|jar|war|7z)$' } } Service = @{ Default = @{ Color = 'White' } Running = @{ Color = 'DarkGreen' } - Stopped = @{ Color = 'DarkRed' } + Stopped = @{ Color = 'DarkRed' } } Match = @{ - Default = @{ Color = 'White' } - Path = @{ Color = 'Cyan'} + Default = @{ Color = 'White' } + Path = @{ Color = 'Cyan' } LineNumber = @{ Color = 'Yellow' } - Line = @{ Color = 'White' } + Line = @{ Color = 'White' } } NoMatch = @{ - Default = @{ Color = 'White' } - Path = @{ Color = 'Cyan'} + Default = @{ Color = 'White' } + Path = @{ Color = 'Cyan' } LineNumber = @{ Color = 'Yellow' } - Line = @{ Color = 'White' } + Line = @{ Color = 'White' } } } } + +git-id-prompt +Start-ZshrcAutoUpdate + +$extraRc = Join-Path $HOME 'extra.rc.ps1' +if (Test-Path -LiteralPath $extraRc) { + . $extraRc +}