|
#Requires -Version 5.1 |
|
|
|
|
|
|
|
|
|
|
|
|
$script:PS7 = $PSVersionTable.PSVersion.Major -ge 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$script:ConstrainedLang = $false |
|
try { $script:ConstrainedLang = $ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage' } catch { } |
|
|
|
|
|
|
|
|
$script:IsInteractive = $true |
|
try { |
|
if (-not [Environment]::UserInteractive) { $script:IsInteractive = $false } |
|
elseif (([Environment]::GetCommandLineArgs()) -match '^-NonInteractive') { $script:IsInteractive = $false } |
|
} catch { } |
|
|
|
|
|
|
$script:SupportsAnsi = $false |
|
try { $script:SupportsAnsi = $script:PS7 -and [bool]$Host.UI.SupportsVirtualTerminal } catch { } |
|
|
|
|
$script:SupportsTitle = $false |
|
try { $null = $Host.UI.RawUI.WindowTitle; $script:SupportsTitle = $true } catch { } |
|
|
|
|
|
|
|
|
|
|
|
if ($script:SupportsAnsi) { |
|
|
$E = [char]27 # ESC character |
|
|
|
|
function script:Fg { param([int]$R,[int]$G,[int]$B) "$E[38;2;$R;$G;${B}m" } |
|
function script:Bg { param([int]$R,[int]$G,[int]$B) "$E[48;2;$R;$G;${B}m" } |
|
$script:Reset = "$E[0m" |
|
$script:Bold = "$E[1m" |
|
$script:Dim = "$E[2m" |
|
|
|
|
$script:CBlack = Fg 12 12 12 # Black |
|
$script:CRed = Fg 197 15 31 # Red |
|
$script:CGreen = Fg 19 161 14 # Green |
|
$script:CYellow = Fg 193 156 0 # Yellow |
|
$script:CBlue = Fg 0 55 218 # Blue |
|
$script:CMagenta = Fg 136 0 128 # Magenta |
|
$script:CCyan = Fg 58 150 221 # Cyan |
|
$script:CWhite = Fg 204 204 204 # White (light gray) |
|
$script:CBrBlack = Fg 118 118 118 # Bright Black (dark gray) |
|
$script:CBrRed = Fg 231 72 86 # Bright Red |
|
$script:CBrGreen = Fg 22 198 12 # Bright Green |
|
$script:CBrYellow= Fg 249 241 165 # Bright Yellow |
|
$script:CBrBlue = Fg 59 120 255 # Bright Blue |
|
$script:CBrMag = Fg 180 0 158 # Bright Magenta |
|
$script:CBrCyan = Fg 97 214 214 # Bright Cyan |
|
$script:CBrWhite = Fg 242 242 242 # Bright White |
|
|
|
|
$script:ColPath = $script:CBrCyan |
|
$script:ColGitBranch= $script:CBrMag |
|
$script:ColGitDirty = $script:CBrRed |
|
$script:ColGitClean = $script:CBrGreen |
|
$script:ColGitInfo = $script:CYellow |
|
$script:ColPython = $script:CBrYellow |
|
$script:ColConda = $script:CYellow |
|
$script:ColNode = $script:CGreen |
|
$script:ColDotNet = $script:CBrBlue |
|
$script:ColErrCode = $script:CBrRed |
|
$script:ColOK = $script:CBrGreen |
|
$script:ColAdmin = $script:CBrRed |
|
$script:ColJobs = $script:CCyan |
|
$script:ColK8s = $script:CBrBlue |
|
$script:ColCloud = $script:CBrCyan |
|
$script:ColDepth = $script:CBrBlack |
|
$script:ColTime = $script:CBrBlack |
|
$script:ColSep = $script:CBrBlack |
|
$script:ColPromptOK = $script:CBrGreen |
|
$script:ColPromptErr= $script:CBrRed |
|
|
|
|
function script:Styled { |
|
param([string]$Color, [string]$Text, [switch]$Bold) |
|
if ($Bold) { return "$script:Bold$Color$Text$script:Reset" } |
|
return "$Color$Text$script:Reset" |
|
} |
|
|
|
} else { |
|
|
|
|
|
|
$script:ColPath = 'Cyan' |
|
$script:ColGitBranch = 'Magenta' |
|
$script:ColGitDirty = 'Red' |
|
$script:ColGitClean = 'Green' |
|
$script:ColGitInfo = 'Yellow' |
|
$script:ColPython = 'Yellow' |
|
$script:ColConda = 'Yellow' |
|
$script:ColNode = 'Green' |
|
$script:ColDotNet = 'Blue' |
|
$script:ColErrCode = 'Red' |
|
$script:ColOK = 'Green' |
|
$script:ColAdmin = 'Red' |
|
$script:ColJobs = 'Cyan' |
|
$script:ColK8s = 'Blue' |
|
$script:ColCloud = 'Cyan' |
|
$script:ColDepth = 'DarkGray' |
|
$script:ColTime = 'DarkGray' |
|
$script:ColSep = 'DarkGray' |
|
$script:ColPromptOK = 'Green' |
|
$script:ColPromptErr = 'Red' |
|
|
|
|
$script:PromptPieces = [System.Collections.Generic.List[hashtable]]::new() |
|
|
|
function script:Styled { |
|
param([string]$Color, [string]$Text, [switch]$Bold) |
|
|
$null = $script:PromptPieces.Add(@{ Color = $Color; Text = $Text }) |
|
return '' |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
$script:Sep = '|' |
|
|
|
|
|
|
|
|
$script:PathComparison = [System.StringComparison]::OrdinalIgnoreCase |
|
if ($script:PS7 -and -not $IsWindows) { |
|
$script:PathComparison = [System.StringComparison]::Ordinal |
|
} |
|
|
|
|
function script:Seg { |
|
param([string]$Color, [string]$Text) |
|
if ($script:SupportsAnsi) { |
|
return "$(Styled $Color "$Text")$( Styled $script:ColSep $script:Sep )" |
|
} else { |
|
$null = $script:PromptPieces.Add(@{ Color = $Color; Text = "$Text" }) |
|
$null = $script:PromptPieces.Add(@{ Color = $script:ColSep; Text = $script:Sep }) |
|
return '' |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
$script:CmdCache = @{} |
|
|
|
|
$script:NodeCache = @{} |
|
$script:DotNetCache = @{} |
|
function script:CommandExists { |
|
param([string]$cmd) |
|
if (-not $script:CmdCache.ContainsKey($cmd)) { |
|
$script:CmdCache[$cmd] = [bool](Get-Command $cmd -ErrorAction Ignore) |
|
} |
|
return $script:CmdCache[$cmd] |
|
} |
|
|
|
|
function script:TruncatePath { |
|
param([string]$FullPath, [int]$MaxDepth = 4) |
|
$home_path = $HOME -replace '\','/' |
|
$display = $FullPath -replace '\','/' |
|
|
|
if ($home_path -and $display.StartsWith($home_path, $script:PathComparison)) { |
|
$display = '~' + $display.Substring($home_path.Length) |
|
} |
|
$parts = $display.TrimStart('/').Split('/') |
|
if ($display.StartsWith('~')) { |
|
|
|
$rest = if ($display.Length -gt 2) { $display.Substring(2) } else { '' } |
|
$parts = @('~') + ($rest.Split('/') | Where-Object { $_ }) |
|
} |
|
$depth = $parts.Count |
|
if ($depth -le $MaxDepth) { return $display, $depth } |
|
|
|
|
$tail = $parts | Select-Object -Last ($MaxDepth - 1) |
|
$truncated = '…/' + ($tail -join '/') |
|
return $truncated, $depth |
|
} |
|
|
|
|
|
|
|
|
|
function script:Get-AdminSegment { |
|
$isAdmin = $false |
|
if ($script:PS7 -and $IsLinux) { |
|
$isAdmin = (id -u) -eq '0' |
|
} else { |
|
$principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() |
|
$isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) |
|
} |
|
if ($isAdmin) { return Seg $script:ColAdmin '⚡ADMIN' } |
|
return '' |
|
} |
|
|
|
|
function script:Get-ExitCodeSegment { |
|
param([int]$LastExit) |
|
if ($LastExit -ne 0) { |
|
return Seg $script:ColErrCode "✘ $LastExit" |
|
} |
|
return '' |
|
} |
|
|
|
|
function script:Get-JobsSegment { |
|
$jobs = @(Get-Job -ErrorAction SilentlyContinue | Where-Object { $.State -ne 'Completed' -and $.State -ne 'Failed' }) |
|
if ($jobs.Count -gt 0) { |
|
return Seg $script:ColJobs "⚙ $($jobs.Count)" |
|
} |
|
return '' |
|
} |
|
|
|
|
function script:Get-PathSegment { |
|
$raw = $PWD.ProviderPath |
|
$display, $depth = TruncatePath $raw 4 |
|
|
|
|
$depthStr = '' |
|
if ($depth -gt 3) { |
|
if ($script:SupportsAnsi) { $depthStr = "$(Styled $script:ColDepth "[$depth]")" } |
|
else { |
|
$null = $script:PromptPieces.Add(@{ Color = $script:ColDepth; Text = "[$depth]" }) |
|
} |
|
} |
|
|
|
$pathStr = Seg $script:ColPath $display |
|
if ($script:SupportsAnsi) { return "$depthStr$pathStr" } |
|
return '' |
|
} |
|
|
|
|
|
|
|
|
$script:GitShowExtras = $false |
|
|
|
function script:Get-GitSegment { |
|
if (-not (CommandExists 'git')) { return '' } |
|
|
|
|
|
|
|
$statusOut = git status --porcelain=v2 --branch 2>$null |
|
if ($LASTEXITCODE -ne 0) { return '' } |
|
|
|
$branch = ''; $oid = ''; $ahead = 0; $behind = 0 |
|
$xy = [System.Collections.Generic.List[string]]::new() |
|
foreach ($line in $statusOut) { |
|
if (-not $line) { continue } |
|
$c = $line[0] |
|
if ($c -eq '#') { |
|
if ($line.StartsWith('# branch.head ')) { $branch = $line.Substring(14) } |
|
elseif ($line.StartsWith('# branch.oid ')) { $oid = $line.Substring(13) } |
|
elseif ($line.StartsWith('# branch.ab ') -and $line -match '+(\d+)\s+-(\d+)') { |
|
$ahead = [int]$Matches[1]; $behind = [int]$Matches[2] |
|
} |
|
} |
|
|
|
elseif (($c -eq '1' -or $c -eq '2') -and $line.Length -ge 4) { $xy.Add(($line.Substring(2,2) -replace '.',' ')) } |
|
elseif ($c -eq 'u' -and $line.Length -ge 4) { $xy.Add($line.Substring(2,2)) } # unmerged |
|
elseif ($c -eq '?') { $xy.Add('??') } # untracked |
|
} |
|
|
|
|
if ($branch -eq '(detached)') { |
|
$branch = if ($oid.Length -ge 7) { "➦ $($oid.Substring(0,7))" } else { '➦ ?' } |
|
} |
|
|
|
|
|
$dirty = @($xy | Where-Object { $_ -match '[1][MADRCU?! ]' }).Count -gt 0 |
|
$staged = @($xy | Where-Object { $_ -match '[2]' }).Count -gt 0 |
|
|
|
|
|
|
$stashCount = 0 |
|
$worktreeStr = '' |
|
if ($script:GitShowExtras) { |
|
$stashCount = (git stash list 2>$null | Measure-Object).Count |
|
$worktreeCount = (git worktree list --porcelain 2>$null | Where-Object { $_ -match '^worktree ' } | Measure-Object).Count |
|
if ($worktreeCount -gt 1) { $worktreeStr = " ⎇$worktreeCount" } |
|
} |
|
|
|
$branchColor = if ($dirty -or $staged) { $script:ColGitDirty } else { $script:ColGitClean } |
|
$gitStr = "$branch$worktreeStr" |
|
|
|
$suffix = '' |
|
if ($staged) { $suffix += ' ●' } # staged changes |
|
if ($dirty) { $suffix += ' ✚' } # unstaged changes |
|
if ($ahead -gt 0) { $suffix += " ↑$ahead" } # ahead of remote |
|
if ($behind -gt 0) { $suffix += " ↓$behind" } # behind remote |
|
if ($stashCount -gt 0) { $suffix += " ≡$stashCount" } # stashes |
|
|
|
return Seg $branchColor "$gitStr$suffix" |
|
} |
|
|
|
|
function script:Get-PythonVenvSegment { |
|
$venv = $env:VIRTUAL_ENV |
|
if (-not $venv) { return '' } |
|
$venvName = Split-Path $venv -Leaf |
|
return Seg $script:ColPython "🐍 $venvName" |
|
} |
|
|
|
|
function script:Get-CondaSegment { |
|
$condaEnv = $env:CONDA_DEFAULT_ENV |
|
if (-not $condaEnv -or $condaEnv -eq 'base') { return '' } |
|
return Seg $script:ColConda "🅒 $condaEnv" |
|
} |
|
|
|
|
function script:Get-NodeSegment { |
|
if (-not (CommandExists 'node')) { return '' } |
|
|
|
$cwd = $PWD.ProviderPath |
|
$now = [datetime]::UtcNow |
|
$entry = $script:NodeCache[$cwd] |
|
if (-not $entry -or $now -ge $entry.Exp) { |
|
$entry = @{ Ver = (Find-NodeVersion $cwd); Exp = $now.AddSeconds(30) } |
|
$script:NodeCache[$cwd] = $entry |
|
} |
|
if ($entry.Ver) { return Seg $script:ColNode "⬡ $($entry.Ver)" } |
|
return '' |
|
} |
|
|
|
|
function script:Find-NodeVersion { |
|
param([string]$startDir) |
|
$markers = @('package.json','.nvmrc','.node-version') |
|
$check = $startDir |
|
while ($check -and $check -ne (Split-Path $check -Parent)) { |
|
foreach ($m in $markers) { |
|
if (Test-Path (Join-Path $check $m)) { |
|
$v = node --version 2>$null |
|
return $(if ($v) { "$v" } else { $null }) |
|
} |
|
} |
|
$check = Split-Path $check -Parent |
|
} |
|
return $null |
|
} |
|
|
|
|
function script:Get-DotNetSegment { |
|
if (-not (CommandExists 'dotnet')) { return '' } |
|
|
|
|
$cwd = $PWD.ProviderPath |
|
$now = [datetime]::UtcNow |
|
$entry = $script:DotNetCache[$cwd] |
|
if (-not $entry -or $now -ge $entry.Exp) { |
|
$entry = @{ Ver = (Find-DotNetVersion $cwd); Exp = $now.AddSeconds(30) } |
|
$script:DotNetCache[$cwd] = $entry |
|
} |
|
if ($entry.Ver) { return Seg $script:ColDotNet ".NET $($entry.Ver)" } |
|
return '' |
|
} |
|
|
|
|
|
|
function script:Find-DotNetVersion { |
|
param([string]$startDir) |
|
$check = $startDir |
|
while ($check -and $check -ne (Split-Path $check -Parent)) { |
|
if ((Test-Path (Join-Path $check 'global.json')) -or |
|
(Get-ChildItem -Path (Join-Path $check '') -Include '.csproj','.fsproj','.vbproj','*.sln' -File -ErrorAction Ignore | Select-Object -First 1)) { |
|
$v = dotnet --version 2>$null |
|
return $(if ($v) { "$v" } else { $null }) |
|
} |
|
$check = Split-Path $check -Parent |
|
} |
|
return $null |
|
} |
|
|
|
|
function script:Get-K8sSegment { |
|
if (-not (CommandExists 'kubectl')) { return '' } |
|
|
|
|
|
|
|
|
|
$now = [datetime]::UtcNow |
|
if (-not $script:K8sCacheExpiry -or $now -ge $script:K8sCacheExpiry) { |
|
$script:K8sCtx = kubectl config current-context 2>$null |
|
$script:K8sNs = kubectl config view --minify --output 'jsonpath={..namespace}' 2>$null |
|
$script:K8sCacheExpiry = $now.AddSeconds(30) |
|
} |
|
|
|
if ($script:K8sCtx) { |
|
$nsStr = if ($script:K8sNs -and $script:K8sNs -ne 'default') { ":$($script:K8sNs)" } else { '' } |
|
return Seg $script:ColK8s "☸ $($script:K8sCtx)$nsStr" |
|
} |
|
return '' |
|
} |
|
|
|
|
function script:Get-CloudSegment { |
|
$parts = [System.Collections.Generic.List[string]]::new() |
|
|
|
|
if (CommandExists 'az') { |
|
|
|
$azProfile = [IO.Path]::Combine($HOME, '.azure', 'azureProfile.json') |
|
if (Test-Path $azProfile) { |
|
try { |
|
$azData = Get-Content $azProfile -Raw | ConvertFrom-Json |
|
$sub = $azData.subscriptions | Where-Object { $_.isDefault } | Select-Object -First 1 |
|
if ($sub) { $parts.Add("☁ az:$($sub.name)") } |
|
} catch { } |
|
} |
|
} |
|
|
|
|
if (CommandExists 'aws') { |
|
$awsProfile = $env:AWS_PROFILE |
|
if (-not $awsProfile) { $awsProfile = $env:AWS_DEFAULT_PROFILE } |
|
if (-not $awsProfile) { $awsProfile = 'default' } |
|
if ($awsProfile -ne 'default') { $parts.Add("aws:$awsProfile") } |
|
} |
|
|
|
|
if (CommandExists 'gcloud') { |
|
|
$gcpConfig = [IO.Path]::Combine($HOME, '.config', 'gcloud', 'active_config') |
|
if (Test-Path $gcpConfig) { |
|
$cfg = (Get-Content $gcpConfig -ErrorAction SilentlyContinue).Trim() |
|
if ($cfg) { |
|
|
|
|
|
$now = [datetime]::UtcNow |
|
if ($script:GcpProjConfig -ne $cfg -or -not $script:GcpProjExpiry -or $now -ge $script:GcpProjExpiry) { |
|
$script:GcpProj = gcloud config get-value project 2>$null |
|
$script:GcpProjConfig = $cfg |
|
$script:GcpProjExpiry = $now.AddSeconds(30) |
|
} |
|
if ($script:GcpProj) { $parts.Add("gcp:$($script:GcpProj)") } else { $parts.Add("gcp:$cfg") } |
|
} |
|
} |
|
} |
|
|
|
if ($parts.Count -eq 0) { return '' } |
|
return Seg $script:ColCloud ($parts -join ' ') |
|
} |
|
|
|
|
function script:Get-TimeSegment { |
|
return Seg $script:ColTime (Get-Date -Format 'HH:mm:ss') |
|
} |
|
|
|
|
|
|
|
|
|
|
function script:Get-RichPrompt { |
|
param($RealLastExit) |
|
|
|
|
$lastExit = if ($null -ne $RealLastExit) { $RealLastExit } else { 0 } |
|
|
|
if (-not $script:SupportsAnsi) { |
|
|
$script:PromptPieces = [System.Collections.Generic.List[hashtable]]::new() |
|
} |
|
|
|
|
$line1 = '' |
|
|
|
|
$line1 += Get-AdminSegment |
|
$line1 += Get-ExitCodeSegment -LastExit $lastExit |
|
$line1 += Get-JobsSegment |
|
$line1 += Get-PathSegment |
|
$line1 += Get-GitSegment |
|
$line1 += Get-PythonVenvSegment |
|
$line1 += Get-CondaSegment |
|
$line1 += Get-NodeSegment |
|
$line1 += Get-DotNetSegment |
|
$line1 += Get-CloudSegment |
|
$line1 += Get-K8sSegment |
|
$line1 += Get-TimeSegment |
|
|
|
|
$glyphColor = if ($lastExit -eq 0) { $script:ColPromptOK } else { $script:ColPromptErr } |
|
$nestedDepth = $NestedPromptLevel |
|
$glyph = ('❯' * [Math]::Max(1, $nestedDepth + 1)) |
|
|
|
|
|
|
|
$global:LASTEXITCODE = $RealLastExit |
|
|
|
if ($script:SupportsAnsi) { |
|
|
return "n$line1 n$(Styled $glyphColor $glyph) " |
|
} else { |
|
|
Write-Host "n" -NoNewline | | foreach ($piece in $script:PromptPieces) { | | Write-Host $piece.Text -ForegroundColor $piece.Color -NoNewline | | } | | Write-Host "n" -NoNewline |
|
Write-Host $glyph -ForegroundColor $glyphColor -NoNewline |
|
return ' ' |
|
} |
|
} |
|
|
|
|
|
|
|
function script:Get-MinimalPrompt { |
|
param($RealLastExit) |
|
$loc = (Get-Location).Path |
|
$code = if ($RealLastExit -and $RealLastExit -ne 0) { " [$RealLastExit]" } else { '' } |
|
$depth = '>' * (1 + $NestedPromptLevel) |
|
return "PS $loc$code$depth " |
|
} |
|
|
|
function global:prompt { |
|
|
|
|
|
|
$realLastExit = $global:LASTEXITCODE |
|
$errMark = $global:Error.Count |
|
try { |
|
if ($script:SupportsTitle) { Set-PromptTitle } |
|
if ($script:ConstrainedLang) { return Get-MinimalPrompt $realLastExit } |
|
return Get-RichPrompt $realLastExit |
|
} catch { |
|
|
return Get-MinimalPrompt $realLastExit |
|
} finally { |
|
$global:LASTEXITCODE = $realLastExit |
|
|
|
while ($global:Error.Count -gt $errMark) { $global:Error.RemoveAt(0) } |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function script:Set-PromptTitle { |
|
$location = $PWD.ProviderPath |
|
$home_path = $HOME |
|
if ($home_path -and $location.StartsWith($home_path, $script:PathComparison)) { |
|
$location = '~' + $location.Substring($home_path.Length) |
|
} |
|
$proc = if ($script:PS7) { 'pwsh' } else { 'powershell' } |
|
|
|
|
try { $host.UI.RawUI.WindowTitle = "$proc $location" } catch { } |
|
} |
|
|
|
|
|
|
|
|
|
|
if ($script:IsInteractive -and (Get-Module -Name PSReadLine -ListAvailable -ErrorAction SilentlyContinue)) { |
|
Import-Module PSReadLine -ErrorAction SilentlyContinue |
|
|
|
Set-PSReadLineOption -EditMode Windows |
|
Set-PSReadLineOption -HistorySearchCursorMovesToEnd |
|
Set-PSReadLineOption -MaximumHistoryCount 10000 |
|
Set-PSReadLineOption -HistoryNoDuplicates |
|
|
|
|
Set-PSReadLineKeyHandler -Key UpArrow -Function HistorySearchBackward |
|
Set-PSReadLineKeyHandler -Key DownArrow -Function HistorySearchForward |
|
|
|
|
Set-PSReadLineKeyHandler -Key 'Ctrl+d' -Function DeleteCharOrExit |
|
|
|
if ($script:PS7) { |
|
|
try { |
|
Set-PSReadLineOption -PredictionSource History |
|
} catch { } |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
#function Reload-Profile { . $PROFILE } |
|
#Set-Alias -Name rp -Value Reload-Profile -Force -Scope Global |
|
|
|
|
function Edit-Profile { |
|
if (CommandExists 'code') { code $PROFILE } |
|
elseif (CommandExists 'notepad++') { & 'notepad++' $PROFILE } |
|
else { notepad $PROFILE } |
|
} |
|
#Set-Alias -Name ep -Value Edit-Profile -Force -Scope Global |
|
|
|
|
|
function Enable-GitExtras { $script:GitShowExtras = $true; Write-Host 'Git stash/worktree indicators: ON' -ForegroundColor Green } |
|
function Disable-GitExtras { $script:GitShowExtras = $false; Write-Host 'Git stash/worktree indicators: OFF' -ForegroundColor DarkGray } |
|
|
|
|
|
I built a static AI tools directory with 1,638 auto-generated pages — here's the full technical breakdown