Last active
March 13, 2018 00:18
-
-
Save Jaykul/2388b845cca0ef219b434d8c5e2c26ea to your computer and use it in GitHub Desktop.
PowerLines - a better way to build a prompt function
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## TODO: Make this a ReadMe. | |
## In the meantime, this is a series of lines meant to be typed into a PowerShell host as a demo: | |
# dull prompt | |
ipmo Powerline; Set-PowerlinePrompt | |
$function:prompt | |
# That's a little prettier ... | |
pushd .\Documents\WindowsPowerShell\ | |
# Oh, I want to see the location stack size | |
Get-Location -Stack | |
if($pushd = (Get-Location -Stack).count) { "?" + $pushd } | |
# That'll work ... let's try putting that in our prompt | |
using module PowerLine | |
$PushLine = [PowerLineBlock]@{ | |
bg = "cyan"; | |
fg = "white" | |
text = { if($pushd = (Get-Location -Stack).count) { "»" + $pushd } } | |
} | |
$PushLine | |
"$PushLine" | |
# Perfect, now let's insert that ... | |
$PowerLinePrompt | |
$PowerLinePrompt.Lines | |
$PowerLinePrompt.Lines[0].Blocks | |
$PowerLinePrompt.Lines[0].Blocks.Insert(2,$PushLine) | |
# Tada!!! | |
popd | |
# Now let's see about adding the elapsed time on the right side | |
$PromptLine = [PowerLine]::New(@( | |
[PowerLineBlock]::Column # This is the column break, the rest is right-aligned | |
[PowerLineBlock]@{ bg = "DarkGray"; fg = "white"; text = { Get-Elapsed } } | |
) | |
) | |
"$PromptLine" | |
# Looks good, we'll stick it in as the first line | |
$PowerLinePrompt.Lines.Insert(0, $PromptLine) | |
# And to make it overlap the previous output, set the count of "PrefixLines" | |
$PowerLinePrompt.PrefixLines = 1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using namespace System.Collections.Generic | |
class PowerLineBlock { | |
[Nullable[ConsoleColor]]$BackgroundColor | |
[Nullable[ConsoleColor]]$ForegroundColor | |
[Object]$Content | |
[bool]$Clear = $false | |
PowerLineBlock() {} | |
PowerLineBlock([hashtable]$values) { | |
foreach($key in $values.Keys) { | |
if("bg" -eq $key -or "BackgroundColor" -match "^$key") { | |
$this.BackgroundColor = $values.$key | |
} | |
elseif("fg" -eq $key -or "ForegroundColor" -match "^$key") { | |
$this.ForegroundColor = $values.$key | |
} | |
elseif("fg" -eq $key -or "ForegroundColor" -match "^$key") { | |
$this.ForegroundColor = $values.$key | |
} | |
elseif("text" -match "^$key" -or "Content" -match "^$key") { | |
$this.Content = $values.$key | |
} | |
elseif("Clear" -match "^$key") { | |
$this.Clear = $values.$key | |
} | |
else { | |
throw "Unknown key '$key' in hashtable. Allowed values are BackgroundColor, ForegroundColor, Content, and Clear" | |
} | |
} | |
} | |
[string] GetText() { | |
if($this.Content -is [scriptblock]) { | |
return & $this.Content | |
} else { | |
return $this.Content | |
} | |
} | |
[string] ToString() { | |
return $( | |
if($this.BackgroundColor) { | |
[PowerLineBlock]::EscapeCodes.bg."$($this.BackgroundColor)" | |
} else { | |
[PowerLineBlock]::EscapeCodes.bg.Clear | |
} | |
) + $( | |
if($this.ForegroundColor) { | |
[PowerLineBlock]::EscapeCodes.fg."$($this.ForegroundColor)" | |
} else { | |
[PowerLineBlock]::EscapeCodes.fg.Clear | |
} | |
) + $this.GetText() + $( | |
if($this.Clear) { | |
[PowerLineBlock]::EscapeCodes.bg.Clear | |
[PowerLineBlock]::EscapeCodes.fg.Clear | |
} | |
) | |
} | |
static [PowerLineBlock] $Column = [PowerLineBlockCache][PowerLineBlock]@{Content="`t"} | |
static [hashtable] $EscapeCodes = @{ | |
ESC = ([char]27) + "[" | |
CSI = [char]155 | |
Clear = ([char]27) + "[0m" | |
fg = @{ | |
Clear = ([char]27) + "[39m" | |
Black = ([char]27) + "[30m"; DarkGray = ([char]27) + "[90m" | |
DarkRed = ([char]27) + "[31m"; Red = ([char]27) + "[91m" | |
DarkGreen = ([char]27) + "[32m"; Green = ([char]27) + "[92m" | |
DarkYellow = ([char]27) + "[33m"; Yellow = ([char]27) + "[93m" | |
DarkBlue = ([char]27) + "[34m"; Blue = ([char]27) + "[94m" | |
DarkMagenta = ([char]27) + "[35m"; Magenta = ([char]27) + "[95m" | |
DarkCyan = ([char]27) + "[36m"; Cyan = ([char]27) + "[96m" | |
Gray = ([char]27) + "[37m"; White = ([char]27) + "[97m" | |
} | |
bg = @{ | |
Clear = ([char]27) + "[49m" | |
Black = ([char]27) + "[40m"; DarkGray = ([char]27) + "[100m" | |
DarkRed = ([char]27) + "[41m"; Red = ([char]27) + "[101m" | |
DarkGreen = ([char]27) + "[42m"; Green = ([char]27) + "[102m" | |
DarkYellow = ([char]27) + "[43m"; Yellow = ([char]27) + "[103m" | |
DarkBlue = ([char]27) + "[44m"; Blue = ([char]27) + "[104m" | |
DarkMagenta = ([char]27) + "[45m"; Magenta = ([char]27) + "[105m" | |
DarkCyan = ([char]27) + "[46m"; Cyan = ([char]27) + "[106m" | |
Gray = ([char]27) + "[47m"; White = ([char]27) + "[107m" | |
} | |
} | |
} | |
class PowerLineBlockCache : PowerLineBlock { | |
[string]$Content | |
[int]$Length | |
PowerLineBlockCache([PowerLineBlock] $output) { | |
$this.BackgroundColor = $output.BackgroundColor | |
$this.ForegroundColor = $output.ForegroundColor | |
$this.Content = $output.GetText() | |
$this.Length = $this.Content.Length | |
} | |
} | |
class PowerLine { | |
static [char]$LeftCap = [char]0xe0b0 # right-pointing arrow | |
static [char]$RightCap = [char]0xe0b2 # left-pointing arrow | |
static [char]$LeftSep = [char]0xe0b1 # left open > | |
static [char]$RightSep = [char]0xe0b3 # right open < | |
static [char]$Branch = [char]0xe0a0 # Branch symbol | |
static [char]$LOCK = [char]0xe0a2 # Padlock | |
static [char]$GEAR = [char]0x26ef # The settings icon, I use it for debug | |
static [char]$POWER = [char]0x26a1 # The Power lightning-bolt icon | |
[bool]$IsPromptLine = $false | |
[System.Collections.Generic.List[PowerLineBlock]]$Blocks = [System.Collections.Generic.List[PowerLineBlock]]@() | |
PowerLine() {} | |
PowerLine([PowerLineBlock[]]$Blocks) { | |
$this.Blocks = $Blocks | |
} | |
PowerLine([PowerLineBlock[]]$Blocks, [bool]$PromptLine) { | |
$this.Blocks = $Blocks | |
$this.IsPromptLine = $PromptLine | |
} | |
[string] ToString() { | |
# Initialize variables ... | |
$width = [Console]::BufferWidth | |
$leftLength = 0 | |
$rightLength = 0 | |
# Precalculate all the text and remove empty blocks | |
$Output = ([PowerLineBlockCache[]]@($this.Blocks)) | Where Length | |
# Output each block with appropriate separators and caps | |
return $(for($l=0; $l -lt $Output.Length; $l++) { | |
$block = $Output[$l] | |
if([PowerLineBlock]::Column -eq $block) { | |
# the length of the second column | |
$rightLength = ($(for($r=$l+1; $r -lt $Output.Length; $r++) { | |
$Output[$r].length + 1 | |
}) | Measure-Object -Sum).Sum | |
$space = $width - $rightLength | |
if($leftLength) { | |
# Output a cap on the left if there's output there | |
# Use the Background of the previous block as the foreground | |
[PowerLineBlock]@{ | |
ForegroundColor = ($Output[($l-1)]).BackgroundColor | |
Content = [PowerLine]::LeftCap | |
Clear = $true | |
} | |
} | |
if($this.IsPromptLine) { | |
"$([PowerLineBlock]::EscapeCodes.ESC)s" | |
} | |
"$([PowerLineBlock]::EscapeCodes.ESC)${space}G" | |
# the right cap uses the background of the next block as it's foreground | |
[PowerLineBlock]@{ | |
ForegroundColor = ($Output[($l+1)]).BackgroundColor | |
Content = [PowerLine]::RightCap | |
} | |
} else { | |
if($leftLength -eq 0 -and $rightLength -eq 0) { | |
# On a new line, recalculate the length of the "left-aligned" line | |
$leftLength = ($(for($r=$l; $r -lt $Output.Length -and $Output[$r] -ne [PowerLineBlock]::NewLine -and $Output[$r] -ne [PowerLineBlock]::Column; $r++) { | |
$Output[$r].length + 1 | |
}) | Measure-Object -Sum).Sum | |
} | |
$block # the actual output | |
if($Output[($l+1)] -ne [PowerLineBlock]::NewLine -and $Output[($l+1)] -ne [PowerLineBlock]::Column) | |
{ | |
# if the next block is the sambe background color, use a > | |
if($block.BackgroundColor -eq $Output[($l+1)].BackgroundColor) { | |
if($rightLength) { | |
[PowerLine]::RightSep | |
} else { | |
[PowerLine]::LeftSep | |
} | |
} else { | |
# Otherwise output a cap | |
[PowerLineBlock]@{ | |
ForegroundColor = $block.BackgroundColor | |
BackgroundColor = $Output[($l+1)].BackgroundColor | |
Content = if($rightLength) { | |
[PowerLine]::RightCap | |
} else { | |
[PowerLine]::LeftCap | |
} | |
} | |
} | |
} | |
} | |
} | |
# Output a cap on the left if we didn't already | |
if(!$rightLength -and $leftLength) { | |
[PowerLineBlock]@{ | |
ForegroundColor = ($Output[($l-1)]).BackgroundColor | |
Content = [PowerLine]::LeftCap | |
Clear = $true | |
} | |
} | |
[PowerLineBlock]::EscapeCodes.fg.Clear | |
[PowerLineBlock]::EscapeCodes.bg.Clear | |
# Anchor here if we didn't already | |
if($this.IsPromptLine -and !$rightLength) { | |
"$([PowerLineBlock]::EscapeCodes.ESC)s" | |
}) -join "" | |
} | |
} | |
class PowerLinePrompt { | |
[bool]$SetTitle = $true | |
[bool]$SetCwd = $true | |
[int]$PrefixLines = 0 | |
[System.Collections.Generic.List[PowerLine]]$Lines = [System.Collections.Generic.List[PowerLine]]@() | |
PowerLinePrompt() { } | |
PowerLinePrompt([PowerLine[]]$PowerLines) { | |
$this.Lines.AddRange($PowerLines) | |
} | |
PowerLinePrompt([PowerLine[]]$PowerLines, [int]$PrefixLines) { | |
$this.Lines.AddRange($PowerLines) | |
$this.PrefixLines = $PrefixLines | |
} | |
} | |
if(!(Test-Path Variable:Global:PowerLinePrompt)) { | |
$PromptLine = [PowerLine]::New(@( | |
[PowerLineBlock]@{ bg = "blue"; fg = "white"; text = { $MyInvocation.HistoryId } } | |
[PowerLineBlock]@{ bg = "cyan"; fg = "white"; text = { "$([PowerLine]::Gear)" * $NestedPromptLevel } } | |
[PowerLineBlock]@{ bg = "darkblue"; fg = "white"; text = { $pwd.Drive.Name } } | |
[PowerLineBlock]@{ bg = "darkblue"; fg = "white"; text = { Split-Path $pwd -leaf } } | |
) | |
) | |
# Get-Location -Stack doesn't work when we define the scriptblock in the module -- not sure why | |
# [PowerLineBlock]@{ bg = "cyan"; fg = "white"; text = { if($pushd = (Get-Location -Stack).count) { "»" + $pushd } } } | |
$PromptLine.IsPromptLine = $true | |
$global:PowerLinePrompt = [PowerLinePrompt]::new(@($PromptLine)) | |
} | |
# Add calculated values for the "Default" colors | |
[PowerLineBlock]::EscapeCodes.fg.Default = [PowerLineBlock]::EscapeCodes.fg."$($Host.UI.RawUI.ForegroundColor)" | |
[PowerLineBlock]::EscapeCodes.fg.Background = [PowerLineBlock]::EscapeCodes.fg."$($Host.UI.RawUI.BackgroundColor)" | |
[PowerLineBlock]::EscapeCodes.bg.Default = [PowerLineBlock]::EscapeCodes.bg."$($Host.UI.RawUI.BackgroundColor)" | |
function Get-Elapsed { | |
[CmdletBinding()] | |
param( | |
[Parameter()] | |
[int]$Id, | |
[Parameter()] | |
[string]$Format = "{0:h\:mm\:ss\.ffff}" | |
) | |
$LastCommand = Get-History -Count 1 @PSBoundParameters | |
if(!$LastCommand) { return "" } | |
$Duration = $LastCommand.EndExecutionTime - $LastCommand.StartExecutionTime | |
$Format -f $Duration | |
} | |
function Set-PowerLinePrompt { | |
$function:global:prompt = { Write-PowerLine $global:PowerLinePrompt } | |
} | |
function Write-PowerLine { | |
[CmdletBinding()] | |
param([PowerLinePrompt]$PowerLinePrompt = $global:PowerLinePrompt) | |
# FIRST, make a note if there was an error in the previous command | |
$err = !$? | |
$e = ([char]27) + "[" | |
# PowerLine font characters | |
try { | |
if($PowerLinePrompt.SetTitle) { | |
# Put the path in the title ... (don't restrict this to the FileSystem) | |
$Host.UI.RawUI.WindowTitle = "{0} - {1} ({2})" -f $global:WindowTitlePrefix, (Convert-Path $pwd), $pwd.Provider.Name | |
} | |
if($PowerLinePrompt.SetCwd) { | |
# Make sure Windows & .Net know where we are | |
# They can only handle the FileSystem, and not in .Net Core | |
[Environment]::CurrentDirectory = (Get-Location -PSProvider FileSystem).ProviderPath | |
} | |
} catch {} | |
$(if($Host.UI.SupportsVirtualTerminal) { | |
# Like output on the previous line(s) | |
if($PowerlinePrompt.PrefixLines -ne 0) | |
{ | |
"${e}1A" * [Math]::Abs($PowerlinePrompt.PrefixLines) | |
} | |
$PowerLinePrompt.Lines -join "`n" | |
# RECALL LOCATION | |
"${e}u" + [PowerLineBlock]::EscapeCodes.fg.Default | |
} else { | |
"> " | |
}) -join "" | |
} | |
#> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#requires -module PowerLine | |
using module PowerLine | |
$global:PowerLinePrompt = [PowerLinePrompt]::new(@( | |
[PowerLine]::New(@( | |
[PowerLineBlock]::Column # Column break, the rest is right-aligned | |
@{ bg = "DarkGray"; fg = "white"; text = { Get-Elapsed } } | |
) | |
) | |
[PowerLine]::New(@( | |
@{ bg = "blue"; fg = "white"; text = { $MyInvocation.HistoryId } } | |
@{ bg = "cyan"; fg = "white"; text = { "$([PowerLine]::Gear)" * $NestedPromptLevel } } | |
@{ bg = "cyan"; fg = "white"; text = { if($pushd = (Get-Location -Stack).count) { "$([char]187)" + $pushd } } } | |
@{ bg = "darkblue"; fg = "white"; text = { $pwd.Drive.Name } } | |
@{ bg = "darkblue"; fg = "white"; text = { Split-Path $pwd -leaf } } | |
), | |
$true | |
) | |
), 1) | |
Set-PowerLinePrompt |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment