scripts/ConsoleLib.ps1
function Get-ConsoleList( [Parameter(Position=0)] $Content , [ref] $Repeat ) { # auto return single item without menu if ($Content.Count -eq 1 -and $GuiCompletionConfig.AutoReturnSingle) { $Content | .{process{$_.CompletionText}} return } # create console list $Prefix = Get-CommonPrefix $Content $Filter = '' $Colors = $GuiCompletionConfig.Colors $ListHandle = New-ConsoleList $Content $Colors.BorderColor $Colors.BorderBackColor $Colors.TextColor $Colors.BackColor function Set-Status { # filter buffer (header) shows the current filter after the last word $FilterBuffer = New-BufferCellArray " $Prefix[$Filter] " $Colors.FilterColor $Colors.BorderBackColor $FilterPosition = $ListHandle.Position $FilterPosition.X += 2 $FilterHandle = New-Buffer $FilterPosition $FilterBuffer # status buffer (footer) shows selected item number, visible items number range, total item count $StatusBuffer = New-StatusBufferCellArray $ListHandle $StatusPosition = $ListHandle.Position $StatusPosition.X += 2 $StatusPosition.Y += $listHandle.ListConfig.ListHeight - 1 $StatusHandle = New-Buffer $StatusPosition $StatusBuffer } . Set-Status # select the first item $SelectedItem = 0 Set-Selection 1 ($SelectedItem + 1) ($ListHandle.ListConfig.ListWidth - 3) $Colors.SelectedTextColor $Colors.SelectedBackColor ### Keys :loop while(($Key = $UI.ReadKey('NoEcho,IncludeKeyDown,AllowCtrlC')).VirtualKeyCode -ne 27) { $ShiftPressed = 0x10 -band [int]$Key.ControlKeyState switch($Key.VirtualKeyCode) { ### Tab 9 { # in Visual Studio, Tab acts like Enter if ($GuiCompletionConfig.VisualStudioTabBehavior) { # out selected $ListHandle.Items[$ListHandle.SelectedItem].CompletionText break loop } elseif ($ShiftPressed) { # Up Move-Selection -1 } else { # Down Move-Selection 1 } break } ### Up Arrow 38 { if ($ShiftPressed) { # fast scroll selected if ($GuiCompletionConfig.FastScrollItemCount -gt ($ListHandle.Items.Count - 1)) { $Count = $ListHandle.Items.Count - 1 } else { $Count = $GuiCompletionConfig.FastScrollItemCount } Move-Selection (-$Count) } else { Move-Selection -1 } break } ### Down Arrow 40 { if ($ShiftPressed) { # fast scroll selected if ($GuiCompletionConfig.FastScrollItemCount -gt ($ListHandle.Items.Count - 1)) { $Count = $ListHandle.Items.Count - 1 } else { $Count = $GuiCompletionConfig.FastScrollItemCount } Move-Selection $Count } else { Move-Selection 1 } break } ### Page Up 33 { $Count = $ListHandle.Items.Count if ($Count -gt $ListHandle.MaxItems) { $Count = $ListHandle.MaxItems } Move-Selection (1 - $Count) break } ### Page Down 34 { $Count = $ListHandle.Items.Count if ($Count -gt $ListHandle.MaxItems) { $Count = $ListHandle.MaxItems } Move-Selection ($Count - 1) break } ### Period 190 { if ($GuiCompletionConfig.DotComplete) { if ($GuiCompletionConfig.AutoExpandOnDot) { $Repeat.Value = $true } # out selected + period $ListHandle.Items[$ListHandle.SelectedItem].CompletionText + '.' break loop } } ### Enter 13 { # out selected $ListHandle.Items[$ListHandle.SelectedItem].CompletionText break loop } ### Slash {$GuiCompletionConfig.BackSlashComplete -and '\/'.Contains($Key.Character)} { if ($GuiCompletionConfig.AutoExpandOnBackSlash) { $Repeat.Value = $true } # out selected + char $ListHandle.Items[$ListHandle.SelectedItem].CompletionText + $Key.Character break loop } ### Custom {$GuiCompletionConfig.CustomComplete -and $GuiCompletionConfig.CustomCompletionChars.Contains($Key.Character)} { # out selected + char $Item = $ListHandle.Items[$ListHandle.SelectedItem].CompletionText if ($Item.EndsWith($Key.Character)) {$Item} else {$Item + $Key.Character} break loop } ### Character/Backspace {$Key.Character} { # update filter $oldFilter = $Filter if ($Key.Character -eq 8) { if ($Filter) { $Filter = $Filter.Substring(0, $Filter.Length - 1) } else { break } } else { # add char $Filter += $Key.Character } # get new items $Items = @(Select-Item $Content $Prefix $Filter) if (!$Items) { # new filter gives no items, undo $Filter = $oldFilter } elseif ($Items.Count -ne $ListHandle.Items.Count) { # items changed, update $ListHandle.Clear() $ListHandle = New-ConsoleList $Items $Colors.BorderColor $Colors.BorderBackColor $Colors.TextColor $Colors.BackColor # update status buffer . Set-Status # select first item $SelectedItem = 0 Set-Selection 1 ($SelectedItem + 1) ($ListHandle.ListConfig.ListWidth - 3) $Colors.SelectedTextColor $Colors.SelectedBackColor } else { # update status buffer . Set-Status } break } } } $ListHandle.Clear() } function New-Box( [System.Drawing.Size]$Size , [System.ConsoleColor]$ForegroundColor = $UI.ForegroundColor , [System.ConsoleColor]$BackgroundColor = $UI.BackgroundColor ) { $HorizontalDouble = [string][char]9552 $VerticalDouble = [string][char]9553 $TopLeftDouble = [string][char]9556 $TopRightDouble = [string][char]9559 $BottomLeftDouble = [string][char]9562 $BottomRightDouble = [string][char]9565 $Horizontal = [string][char]9472 $Vertical = [string][char]9474 $TopLeft = [string][char]9484 $TopRight = [string][char]9488 $BottomLeft = [string][char]9492 $BottomRight = [string][char]9496 $TopLeftDoubleSingle = [string][char]9554 $TopRightDoubleSingle = [string][char]9557 $BottomLeftDoubleSingle = [string][char]9560 $BottomRightDoubleSingle = [string][char]9563 if ($GuiCompletionConfig.DoubleBorder) { # double box $LineTop = $TopLeftDouble + $HorizontalDouble * ($Size.width - 2) + $TopRightDouble $LineField = $VerticalDouble + ' ' * ($Size.width - 2) + $VerticalDouble $LineBottom = $BottomLeftDouble + $HorizontalDouble * ($Size.width - 2) + $BottomRightDouble } else { # single box $LineTop = $TopLeft + $Horizontal * ($Size.width - 2) + $TopRight $LineField = $Vertical + ' ' * ($Size.width - 2) + $Vertical $LineBottom = $BottomLeft + $Horizontal * ($Size.width - 2) + $BottomRight } $Box = $( $LineTop 1..($Size.Height - 2) | .{process{$LineField}} $LineBottom ) $BoxBuffer = $UI.NewBufferCellArray($Box, $ForegroundColor, $BackgroundColor) , $BoxBuffer } function Get-ContentSize( [object[]]$Content ) { $maxWidth = $GuiCompletionConfig.MinimumTextWidth $end = $Content.Length - 1 for($$ = 0; $$ -le $end; ++$$) { $item = $Content[$$] $text = $_.ListItemText if (($$ -gt 0 -and $text -eq $Content[$$ - 1].ListItemText) -or ($$ -lt $end -and $text -eq $Content[$$ + 1].ListItemText)) { $w = $item.CompletionText.Length } else { $w = $item.ListItemText.Length } if ($maxWidth -lt $w) { $maxWidth = $w } } New-Object System.Drawing.Size $maxWidth, $Content.Length } function New-Position( [int]$X , [int]$Y ) { $Position = $UI.WindowPosition $Position.X += $X $Position.Y += $Y $Position } function New-Buffer( [System.Management.Automation.Host.Coordinates] $Position , [System.Management.Automation.Host.BufferCell[,]] $Buffer ) { $BufferBottom = $BufferTop = $Position $BufferBottom.X += ($Buffer.GetUpperBound(1)) $BufferBottom.Y += ($Buffer.GetUpperBound(0)) $OldTop = New-Object System.Management.Automation.Host.Coordinates 0, $BufferTop.Y $OldBottom = New-Object System.Management.Automation.Host.Coordinates ($UI.BufferSize.Width - 1), $BufferBottom.Y $OldBuffer = $UI.GetBufferContents((New-Object System.Management.Automation.Host.Rectangle $OldTop, $OldBottom)) $UI.SetBufferContents($BufferTop, $Buffer) $Handle = New-Object System.Management.Automation.PSObject -Property @{ Content = $Buffer OldContent = $OldBuffer Location = $BufferTop OldLocation = $OldTop } Add-Member -InputObject $Handle -MemberType ScriptMethod -Name Clear -Value {$UI.SetBufferContents($this.OldLocation, $this.OldContent)} Add-Member -InputObject $Handle -MemberType ScriptMethod -Name Show -Value {$UI.SetBufferContents($this.Location, $this.Content)} $Handle } function New-StatusBufferCellArray( $ListHandle ) { , (New-BufferCellArray " $($ListHandle.SelectedItem + 1) / $($ListHandle.FirstItem + 1)-$($ListHandle.LastItem + 1) / $($ListHandle.Items.Count) " $Colors.BorderTextColor $Colors.BorderBackColor) } function New-BufferCellArray( [string[]]$Content , [System.ConsoleColor]$ForegroundColor = $UI.ForegroundColor , [System.ConsoleColor]$BackgroundColor = $UI.BackgroundColor ) { , $UI.NewBufferCellArray($Content, $ForegroundColor, $BackgroundColor) } function Parse-List( [System.Drawing.Size]$Size , [switch]$NoScroll ) { $WindowPosition = $UI.WindowPosition $WindowSize = $UI.WindowSize $Cursor = $UI.CursorPosition $Center = [int]($WindowSize.Height / 2) $CursorOffset = $Cursor.Y - $WindowPosition.Y $CursorOffsetBottom = $WindowSize.Height - $CursorOffset # vertical size and placement $ListHeight = $Size.Height + 2 $Above = ($CursorOffset -gt $Center) -and ($ListHeight -ge $CursorOffsetBottom) if ($Above) { if (!$NoScroll -and $GuiCompletionConfig.ScrollDisplayDown) { $nScroll = [Math]::Min($CursorOffset - $Center, $ListHeight - $CursorOffsetBottom + 1) [Microsoft.PowerShell.PSConsoleReadLine]::Insert("`n" * ($CursorOffsetBottom + $nScroll)) [Microsoft.PowerShell.PSConsoleReadLine]::Undo() return Parse-List $Size -NoScroll } $MaxListHeight = $CursorOffset - 1 if ($MaxListHeight -lt $ListHeight) {$ListHeight = $MaxListHeight} $Y = $CursorOffset - $ListHeight } else { $MaxListHeight = $CursorOffsetBottom - 2 if ($MaxListHeight -lt $ListHeight) {$ListHeight = $MaxListHeight} $Y = $CursorOffSet + 1 } $MaxItems = $MaxListHeight - 2 # horizontal $ListWidth = $Size.Width + 4 if ($ListWidth -gt $WindowSize.Width) {$ListWidth = $Windowsize.Width} $Max = $ListWidth if (($Cursor.X + $Max) -lt ($WindowSize.Width - 2)) { $X = $Cursor.X } else { if (($Cursor.X - $Max) -gt 0) { $X = $Cursor.X - $Max } else { $X = $windowSize.Width - $Max } } # output @{ TopX = $X TopY = $Y ListHeight = $ListHeight ListWidth = $ListWidth MaxItems = $MaxItems } } function New-ConsoleList( [object[]]$Content , [System.ConsoleColor]$BorderForegroundColor , [System.ConsoleColor]$BorderBackgroundColor , [System.ConsoleColor]$ContentForegroundColor , [System.ConsoleColor]$ContentBackgroundColor ) { $size = Get-ContentSize $Content $minWidth = ([string]$Content.Count).Length * 4 + 7 if ($size.Width -lt $minWidth) {$size.Width = $minWidth} $lines = @( $end = $Content.Length - 1 for($$ = 0; $$ -le $end; ++$$) { $item = $Content[$$] $text = $item.ListItemText if (($$ -gt 0 -and $text -eq $Content[$$ - 1].ListItemText) -or ($$ -lt $end -and $text -eq $Content[$$ + 1].ListItemText)) { $text = $item.CompletionText } "$text ".PadRight($size.Width + 2) } ) $listConfig = Parse-List $size $boxSize = New-Object System.Drawing.Size $listConfig.ListWidth, $listConfig.ListHeight $box = New-Box $boxSize $BorderForegroundColor $BorderBackgroundColor $pos = New-Position $listConfig.TopX $listConfig.TopY $BoxHandle = New-Buffer $pos $box # place content $pos.X += 1 $pos.Y += 1 $contentBuffer = New-BufferCellArray ($lines[0..($listConfig.ListHeight - 3)]) $ContentForegroundColor $ContentBackgroundColor $contentHandle = New-Buffer $pos $contentBuffer $handle = New-Object System.Management.Automation.PSObject -Property @{ Position = New-Position $listConfig.TopX $listConfig.TopY ListConfig = $listConfig ContentSize = $size BoxSize = $boxSize Box = $BoxHandle Content = $contentHandle SelectedItem = 0 SelectedLine = 1 Items = $Content Lines = $lines FirstItem = 0 LastItem = $listConfig.ListHeight - 3 MaxItems = $listConfig.MaxItems } Add-Member -InputObject $handle -MemberType ScriptMethod -Name Clear -Value {$this.Box.Clear()} Add-Member -InputObject $handle -MemberType ScriptMethod -Name Show -Value {$this.Box.Show(); $this.Content.Show()} $handle } function Move-List( [int]$X , [int]$Y , [int]$Width , [int]$Height , [int]$Offset ) { $Position = $ListHandle.Position $Position.X += $X $Position.Y += $Y $Rectangle = New-Object System.Management.Automation.Host.Rectangle $Position.X, $Position.Y, ($Position.X + $Width), ($Position.Y + $Height - 1) $Position.Y += $OffSet $BufferCell = New-Object System.Management.Automation.Host.BufferCell $BufferCell.BackgroundColor = $GuiCompletionConfig.Colors.BackColor $UI.ScrollBufferContents($Rectangle, $Position, $Rectangle, $BufferCell) } function Set-Selection( [int]$X , [int]$Y , [int]$Width , [System.ConsoleColor]$ForegroundColor , [System.ConsoleColor]$BackgroundColor ) { $Position = $ListHandle.Position $Position.X += $X $Position.Y += $Y $Rectangle = New-Object System.Management.Automation.Host.Rectangle $Position.X, $Position.Y, ($Position.X + $Width), $Position.Y $LineBuffer = $UI.GetBufferContents($Rectangle) $LineBuffer = $UI.NewBufferCellArray( @([string]::Join("", ($LineBuffer | .{process{$_.Character}}))), $ForegroundColor, $BackgroundColor ) $UI.SetBufferContents($Position, $LineBuffer) } function Move-Selection( [int]$Count ) { $Colors = $GuiCompletionConfig.Colors $SelectedItem = $ListHandle.SelectedItem $Line = $ListHandle.SelectedLine if ($Count -ge 0) { ## Down in list if ($SelectedItem -eq ($ListHandle.Items.Count - 1)) { return } $One = 1 if ($SelectedItem + $Count -gt $ListHandle.Items.Count - 1) {$Count = $ListHandle.Items.Count - 1 - $SelectedItem} if ($SelectedItem -eq $ListHandle.LastItem) { $Move = $true } else { $Move = $false if (($ListHandle.MaxItems - $Line) -lt $Count) {$Count = $ListHandle.MaxItems - $Line} } } else { if ($SelectedItem -eq 0) { return } $One = -1 if ($SelectedItem -eq $ListHandle.FirstItem) { $Move = $true if ($SelectedItem + $Count -lt 0) {$Count = - $SelectedItem} } else { $Move = $false if ($Line + $Count -lt 1) {$Count = 1 - $Line} } } if ($Move) { Set-Selection 1 $Line ($ListHandle.ListConfig.ListWidth - 3) $Colors.TextColor $Colors.BackColor Move-List 1 1 ($ListHandle.ListConfig.ListWidth - 3) ($ListHandle.ListConfig.ListHeight - 2) ( - $Count) $SelectedItem += $Count $ListHandle.FirstItem += $Count $ListHandle.LastItem += $Count $LinePosition = $ListHandle.Position $LinePosition.X += 1 if ($One -eq 1) { $LinePosition.Y += $Line - ($Count - $One) $ItemLines = $ListHandle.Lines[($SelectedItem - ($Count - $One)) .. $SelectedItem] } else { $LinePosition.Y += 1 $ItemLines = $ListHandle.Lines[($SelectedItem..($SelectedItem - ($Count - $One)))] } $null = New-Buffer $LinePosition (New-BufferCellArray $ItemLines $Colors.TextColor $Colors.BackColor) Set-Selection 1 $Line ($ListHandle.ListConfig.ListWidth - 3) $Colors.SelectedTextColor $Colors.SelectedBackColor } else { Set-Selection 1 $Line ($ListHandle.ListConfig.ListWidth - 3) $Colors.TextColor $Colors.BackColor $SelectedItem += $Count $Line += $Count Set-Selection 1 $Line ($ListHandle.ListConfig.ListWidth - 3) $Colors.SelectedTextColor $Colors.SelectedBackColor } $ListHandle.SelectedItem = $SelectedItem $ListHandle.SelectedLine = $Line # new status buffer $StatusHandle.Clear() $StatusBuffer = New-StatusBufferCellArray $ListHandle $StatusHandle = New-Buffer $StatusHandle.Location $StatusBuffer } function Select-Item( $Content , $Prefix , $Filter ) { $pattern = '^' + [regex]::Escape($Prefix) + '.*?' foreach($c in $Filter.ToCharArray()) { $pattern += [regex]::Escape($c) + '.*?' } $Prefix += $Filter foreach($_ in $Content) { if ($_.ListItemText.StartsWith($Prefix, [StringComparison]::OrdinalIgnoreCase)) { $_ } } foreach($_ in $Content) { $s = $_.ListItemText if (!$s.StartsWith($Prefix, [StringComparison]::OrdinalIgnoreCase) -and $s -match $pattern) { $_ } } } function Get-CommonPrefix( $Content ) { $prefix = $Content[-1].ListItemText for($i = $Content.Count - 2; $i -ge 0 -and $prefix; --$i) { $text = $Content[$i].ListItemText while($prefix -and !$text.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) { $prefix = $prefix.Substring(0, $prefix.Length - 1) } } $prefix } |