|
#----- POWERSHELL - CUSTOM PROMPT ----- |
|
# Powerline Prompt Configuration |
|
|
|
# 0. GLOBAL SETTINGS |
|
# Set UTF-8 encoding for proper PowerLine symbol display |
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 |
|
|
|
# 1. FORMATTING OPTIONS (Pallete: ~ One Dark Pro) |
|
# ANSI Escape Sequences for TrueColor (24-bit RGB) support: |
|
# Structure: $ESC[<Type>;2;<R>;<G>;<B>m |
|
# - $ESC : Escape character (ASCII 27) |
|
# - [ : Control Sequence Introducer |
|
# - Type : 38 for Foreground (Text), 48 for Background |
|
# - ;2; : Specifies RGB mode |
|
# - R;G;B : Red, Green, Blue values (0-255) |
|
# - m : End of sequence |
|
# Example: $ESC[38;2;244;71;71m sets text color to Red |
|
|
|
# RGB Values for ANSI Escape Codes |
|
$C_YEL = "255;215;95" # Yellow (Session BG / Timer FG) |
|
$C_ORG = "246;118;35" # Orange (Unused - Preservation) |
|
$C_PUR = "213;95;222" # Purple (Git BG) |
|
$C_BLU = "97;175;239" # Soft Blue (Path BG / Prompt Arrow FG) |
|
$C_RED = "244;71;71" # Pastel Red (Error BG) |
|
$C_BLK = "0;0;0" # Black (Local Session BG) |
|
$C_WHT = "255;255;255" # White (Error Text) |
|
$C_TXT = "28;44;52" # Dark Slate (Text Color for colored blocks) |
|
$C_GRY = "127;132;142" # Grey (Separators) |
|
|
|
$ESC = [char]27 |
|
$S_ITA = "$ESC[3m" # Italic Start |
|
$S_NIT = "$ESC[23m" # Italic Stop |
|
$S_REV = "$ESC[7m" # Reverse (Invert) Start |
|
$S_NRV = "$ESC[27m" # Reverse (Invert) Stop |
|
|
|
# 2. SYMBOL DEFINITIONS |
|
$S_CAP_L = [char]0xE0B6 # Start Cap (Left rounded) |
|
$S_CAP_R = [char]0xE0B4 # End Cap (Right rounded) |
|
$S_SEP_L = [char]0xE0B0 # Segment Separator (Filled arrow) |
|
$S_GAP_R = [char]0xE0D7 # Gap Separator (Thin right arrow) |
|
$S_ARR_R = [char]0xF054 # Prompt Arrow (Simple chevron) |
|
$S_ARR_U = [char]0xF062 # Up Arrow (Error indicator) |
|
$S_PIP_F = [char]0x2503 # ┃ Full Height Heavy Pipe (Separator) |
|
$S_PTH_S = [char]0xF0DA # Path Separator (Small arrow) |
|
$S_GIT_B = [char]0xE0A0 # Git Branch Symbol |
|
$S_GIT_A = [char]0x21E1 # ⇡ Git Ahead |
|
$S_GIT_D = [char]0x21E3 # ⇣ Git Behind |
|
$S_TIM_C = [char]0xF017 # Clock Symbol |
|
$S_ERR_S = [char]0xF00D # Error Symbol (Cross) |
|
$S_GIT_CLN = [char]::ConvertFromUtf32(0xF0E1E) # Clean State (Custom) |
|
if ($PSVersionTable.PSVersion.Major -lt 6) { $S_GIT_CLN = "?" } # Fallback for older PS |
|
$S_GIT_DRT = $S_ERR_S # Dirty State (Same as Error) |
|
|
|
# 3. HELPER FUNCTIONS |
|
|
|
function Set-AnsiColor { |
|
param ( |
|
[string]$FG, # "R;G;B" |
|
[string]$BG # "R;G;B" |
|
) |
|
$out = "" |
|
if ($FG) { $out += "$ESC[38;2;${FG}m" } |
|
if ($BG) { $out += "$ESC[48;2;${BG}m" } |
|
return $out |
|
} |
|
|
|
function Reset-Color { |
|
return "$ESC[0m" |
|
} |
|
|
|
# Helper: Prompt Transition (2-Char Solution) |
|
# Usage: prompt_trans [Previous BG] [Next BG] |
|
function Prompt-Trans { |
|
param ($PrevBG, $NextBG) |
|
# %k%F{$1}${S_SEP_L}%k%F{$2}${S_GAP_R} |
|
# Reset BG, FG=PrevBG, SEP_L, Reset BG, FG=NextBG, GAP_R |
|
$out = "$ESC[49m" + (Set-AnsiColor -FG $PrevBG) + $S_SEP_L |
|
$out += "$ESC[49m" + (Set-AnsiColor -FG $NextBG) + $S_GAP_R |
|
return $out |
|
} |
|
|
|
# 4. PROMPT FUNCTION |
|
function prompt { |
|
$lastExit = $LASTEXITCODE |
|
|
|
# --- Segment: ERROR (Conditional, Detached) --- |
|
# Logic: Appears at the end of previous output, pointing up |
|
$seg_error = "" |
|
if ($null -ne $lastExit -and $lastExit -ne 0) { |
|
# Structure: CapL + RedBG(Icon + Code + UpArrow) + CapR |
|
$seg_error = (Set-AnsiColor -FG $C_RED) + "$ESC[49m" + $S_CAP_L # NoBG |
|
$seg_error += (Set-AnsiColor -BG $C_RED -FG $C_TXT) + " $S_ERR_S $lastExit $S_ARR_U " |
|
$seg_error += "$ESC[49m" + (Set-AnsiColor -FG $C_RED) + $S_CAP_R # NoBG |
|
} |
|
|
|
# --- Segment: ENVIRONMENT (Session) --- |
|
# Logic: SSH check (SSH_CLIENT or SSH_TTY env vars) |
|
$isSSH = ($null -ne $env:SSH_CLIENT) -or ($null -ne $env:SSH_TTY) |
|
if ($isSSH) { $e_bg = $C_YEL; $e_fg = $C_TXT } else { $e_bg = $C_BLK; $e_fg = $C_YEL } |
|
|
|
$current_shell = "pwsh $($PSVersionTable.PSVersion.ToString())" |
|
|
|
$env_text = "" |
|
if ($env:ANDROID_DATA) { |
|
$env_text = "Termux" |
|
} else { |
|
$userName = $env:USERNAME |
|
if (-not $userName) { $userName = $env:USER } |
|
$hostName = $env:COMPUTERNAME |
|
if (-not $hostName) { $hostName = $env:HOSTNAME } |
|
if (-not $hostName) { $hostName = [System.Environment]::MachineName } |
|
|
|
$env_text = "$S_ITA $userName" + (Set-AnsiColor -FG $C_GRY -BG $e_bg) + "@" + (Set-AnsiColor -FG $e_fg -BG $e_bg) + "$hostName " |
|
} |
|
|
|
# %F{$e_bg}%k${S_CAP_L}%K{$e_bg}%F{$e_fg}${S_ITA} %n%F{$C_GRY}@%F{$e_fg}%m %F{$C_TXT}${S_PIP_F}%F{$e_fg} ${current_shell} ${S_NIT} |
|
|
|
$seg_env = (Set-AnsiColor -FG $e_bg) + "$ESC[49m" + $S_CAP_L |
|
$seg_env += (Set-AnsiColor -BG $e_bg -FG $e_fg) + $env_text |
|
# Perfect Cut (Inverse Method): FG=SessionColor, BG=Default, then Invert. |
|
# Result: BG=SessionColor, FG=TerminalDefaultBG (Transparent!) |
|
# FIX: Stop Italic before pipe, Restart after. |
|
$seg_env += (Set-AnsiColor -FG $e_bg) + "$ESC[49m" + $S_REV + "$S_NIT" + "$S_PIP_F" + "$S_NRV" |
|
$seg_env += (Set-AnsiColor -FG $e_fg -BG $e_bg) + $S_ITA + " $current_shell " + $S_NIT |
|
|
|
# --- Transition: Environment -> Path --- |
|
$trans_env_path = Prompt-Trans $e_bg $C_BLU |
|
|
|
# --- Segment: PATH --- |
|
# Logic: Soft Blue BG / Slate Text with Smart Truncation |
|
# Shorten path: Replace Home with ~ and separators with S_PTH_S |
|
$p = $PWD.Path |
|
if ($p.StartsWith($HOME)) { |
|
$p = "~" + $p.Substring($HOME.Length) |
|
} |
|
|
|
# Smart Truncation |
|
$max_path_len = 24 |
|
if ($p.Length -gt $max_path_len) { |
|
$p = ".." + $p.Substring($p.Length - $max_path_len) |
|
} |
|
|
|
# Replace separators (both \ and / just in case) |
|
$p = $p.Replace("\", " $S_PTH_S ").Replace("/", " $S_PTH_S ") |
|
|
|
$seg_path = (Set-AnsiColor -BG $C_BLU -FG $C_TXT) + " $p " |
|
|
|
# --- Segment: GIT (Conditional) --- |
|
$seg_git = "" |
|
# Better check: is-inside-work-tree |
|
$isGit = git rev-parse --is-inside-work-tree 2>$null |
|
if ($isGit -eq "true") { |
|
$ref = (git symbolic-ref --short HEAD 2>$null) |
|
if (-not $ref) { $ref = (git rev-parse --short HEAD 2>$null) } |
|
|
|
# Transition: Path (Blue) -> Git (Purple) |
|
$trans_path_git = Prompt-Trans $C_BLU $C_PUR |
|
|
|
# Body: Purple BG, Slate Text |
|
$body_git = (Set-AnsiColor -BG $C_PUR -FG $C_TXT) + " $S_GIT_B $ref " |
|
|
|
# Check Clean/Dirty |
|
$status = (git status --porcelain --ignore-submodules 2>$null) |
|
if ($status) { |
|
$body_git += "$S_GIT_DRT " |
|
|
|
# Calculate File Counts (Modified, Added, Deleted) |
|
$gMod = ($status | Where-Object { $_ -match '^\s*M' } | Measure-Object).Count |
|
$gAdd = ($status | Where-Object { $_ -match '^\?\?' } | Measure-Object).Count |
|
$gDel = ($status | Where-Object { $_ -match '^\s*D' } | Measure-Object).Count |
|
|
|
if ($gMod -gt 0) { $body_git += "~$gMod " } |
|
if ($gAdd -gt 0) { $body_git += "+$gAdd " } |
|
if ($gDel -gt 0) { $body_git += "-$gDel " } |
|
} else { |
|
$body_git += "$S_GIT_CLN " |
|
} |
|
|
|
# Check Upstream Status (Ahead/Behind) |
|
$git_counts = (git rev-list --left-right --count HEAD...@{u} 2>$null) |
|
if ($git_counts) { |
|
$counts = -split $git_counts |
|
$ahead = [int]$counts[0] |
|
$behind = [int]$counts[1] |
|
if ($ahead -gt 0) { $body_git += "$S_GIT_A$ahead " } |
|
if ($behind -gt 0) { $body_git += "$S_GIT_D$behind " } |
|
} |
|
|
|
# Exit: Git (Purple) -> Transparent |
|
# %k%F{$C_PUR}${S_SEP_L}%f |
|
$exit_git = "$ESC[49m" + (Set-AnsiColor -FG $C_PUR) + $S_SEP_L + (Reset-Color) |
|
|
|
$seg_git = "${trans_path_git}${body_git}${exit_git}" |
|
} else { |
|
# No Git? Just exit Path (Soft Blue) -> Transparent |
|
$seg_git = "$ESC[49m" + (Set-AnsiColor -FG $C_BLU) + $S_SEP_L + (Reset-Color) |
|
} |
|
|
|
# --- Segment: TIMER (Conditional, Detached) --- |
|
$seg_time = "" |
|
# Get duration of last command from history |
|
$history = Get-History -Count 1 -ErrorAction SilentlyContinue |
|
if ($history -and $null -ne $history.Duration) { |
|
$dur = $history.Duration |
|
if ($dur.TotalSeconds -ge 2) { |
|
# Format: Hours, Minutes, or Seconds depending on duration |
|
$time_str = if ($dur.TotalHours -ge 1) { |
|
$h = [math]::Floor($dur.TotalHours) |
|
"{0}h {1}m" -f $h, $dur.Minutes |
|
} elseif ($dur.TotalMinutes -ge 1) { |
|
"{0:0}m {1:0}s" -f $dur.TotalMinutes, $dur.Seconds |
|
} else { |
|
"{0:N1}s" -f $dur.TotalSeconds |
|
} |
|
$time_str = $time_str.Replace(".", ",") |
|
|
|
# Structure: CapL + YellowBG(Icon + Time) + CapR |
|
$seg_time = (Set-AnsiColor -FG $C_YEL) + "$ESC[49m" + $S_CAP_L # NoBG |
|
$seg_time += (Set-AnsiColor -BG $C_YEL -FG $C_TXT) + " $S_TIM_C $time_str " |
|
$seg_time += "$ESC[49m" + (Set-AnsiColor -FG $C_YEL) + $S_CAP_R # NoBG |
|
} |
|
} |
|
|
|
# --- Segment: PROMPT CHAR (Line 2) --- |
|
# Logic: Newline -> Indent -> User/Root char -> Soft Blue Arrow |
|
$isAdmin = $false |
|
if ($IsWindows) { |
|
$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) |
|
} else { |
|
# On Linux/macOS, check if effective user ID is 0 (root) |
|
$isAdmin = (id -u) -eq 0 |
|
} |
|
$sym_user = if ($isAdmin) { "#" } else { "$" } |
|
|
|
# Fix: Add 2 spaces indent for visual balance |
|
# Fix: Reset color before newline to prevent background bleed on wrap |
|
$seg_end = (Reset-Color) + "`n " + (Set-AnsiColor -FG $C_BLU) + "$sym_user $S_ARR_R " + (Reset-Color) |
|
|
|
# --- Final: PROMPT ASSEMBLY --- |
|
|
|
# 1. Handle Initial Newline: Check Cursor Position |
|
# If CursorTop > 0, it means there is content (Welcome message, previous output), so we need a newline. |
|
# If CursorTop == 0, we are at the top (Clear), no newline needed. |
|
$prefix = "" |
|
try { |
|
if ([Console]::CursorTop -gt 0) { $prefix = "`n" } |
|
} catch { |
|
# Fallback if host doesn't support CursorTop |
|
$prefix = "`n" |
|
} |
|
|
|
# 2. Restore LASTEXITCODE |
|
$global:LASTEXITCODE = $lastExit |
|
|
|
# Logic: |
|
# Top Line: [Error] [Time] (If any exists) |
|
# Main Line: [Env]... |
|
|
|
$top_line = "" |
|
if ($seg_error) { $top_line += $seg_error + " " } |
|
if ($seg_time) { $top_line += $seg_time } |
|
|
|
# If we have a top line (Error or Time), we print it, then double newline, then main prompt |
|
# Wait: The prefix (conditional newline) should go BEFORE the top line. |
|
|
|
if ($top_line) { |
|
# Trim trailing space if only error existed |
|
"$prefix$top_line`n`n$seg_env$trans_env_path$seg_path$seg_git$seg_end" |
|
} else { |
|
"$prefix$seg_env$trans_env_path$seg_path$seg_git$seg_end" |
|
} |
|
} |
|
|
|
# 5. GLOBAL ERROR CONFIGURATION |
|
# Reset error state from startup to prevent initial error segment |
|
$global:LASTEXITCODE = 0 |
|
|
|
# Set error text color to match prompt Error Background |
|
$Host.PrivateData.ErrorForegroundColor = 'Red' |
|
if ($PSStyle) { |
|
# Use the defined Red color for error messages |
|
$PSStyle.Formatting.Error = "$ESC[38;2;${C_RED}m" |
|
} |