One (very boring) locked in Bank Holiday weekend, with my right hand in plaster I decided to write a function/s I had been meaning to write for a long time.
A “simple” on screen menu. You give the function an array and it will display it and let you pick a row/element and return that row.
Because the basic idea spiralled in complexity, I added multiple pages, display specific columns and search options.
All written with my left hand.
Controlling the menu is easy using the following keys:
- Up – move the selected line up
- Down – move the selected line down
- Left – move to the previous page
- Right – move to the next page
- Enter – return the selected item/row/array element
- a-z, ., -, 0-9 – display only the items/rows/array elements that contain that string
- Back Space – remove the last character from the search string.
- Esc – exit and return $null.
I loaded in some test data (I will attach that) to demonstrate its functionality.
The first screen shot shows the menu is its simplest form. I have called the function and only specified the object array. The function will default to display every attribute/column and 10 lines per page.
The above screen shot shows rows from page 2.
The screen shot above shows the menu after entering the string “dc”. Any row with the string “dc” in any of the displayed attributes is shown.
The Code
<#
.SYNOPSIS
Will take an array, display it on screen and allow the user to select one row.
.DESCRIPTION
Generate a screen menu from an array, allows the user to scroll up, down as well as page left/right and dynamic search.
.PARAMETER ObjList
Array to be displayed for the user to select a row
.PARAMETER DisplayCols
array of column names from ObjList to be displayed on screen
.PARAMETER HeaderText
Text to be shown at the top of the page
.PARAMETER MaxLinesPerPage
Number of lines per page.
.INPUTS
None. You cannot pipe objects to Do-Menu.
.OUTPUTS
selected row from input array
.EXAMPLE
$SelectedObj = Do-Menu $TestObs -DisplayCols Col1,Col2,Col4
.EXAMPLE
$SelectedObj = Do-Menu $TestObs -DisplayCols location,client,cluster -MaxLinesPerPage 20
.EXAMPLE
$SelectedObj = Do-Menu $MyObjectArray -DisplayCols Name,ID -MaxLinesPerPage 20
#>
function Do-Menu{
param(
[Parameter(Mandatory = $true)] $ObjList,
$DisplayCols = $null,
$MaxLinesPerPage = 10,
$HeaderText = " "
)
if ($DisplayCols -eq $null)
{
$DisplayCols = (($ObjList | get-member) | where{$_.MemberType -eq "NoteProperty"}).name
}
$OffSetLine = 0
$CurrentLine = 0
# Check and exit if this function is running in the ISE
if($host.name -match "ISE")
{
Write-Host "This function will not work in the ISE (cant read keys)"
return $null
}
$StartCursPos = $host.UI.RawUI.CursorPosition
[string]$SearchString = ''
# create new object array with an extra attribute num that increments (like a line number)
$NumObjList = @()
$num = 0
foreach($Obj in $ObjList)
{
$Obj | add-member -NotePropertyName num -NotePropertyValue $num -Force
$NumObjList += $Obj
$num ++
}
# sort the array by the unique line number
$DisplayObj = $NumObjList | sort num
# loop through each display column and get the max length of each, this is for formatting.
# we will end up with an array called $ColLengths that is the number of charictos that the $DisplayCol array should be.
$ColLengths = @()
foreach ($DisplayCol in $DisplayCols)
{
$MaxCLength = 0
foreach ($Row in $DisplayObj.$DisplayCol)
{
if ($Row.length -gt $MaxCLength){$MaxCLength = $Row.length}
}
# also test the column header length
if ($DisplayCol.length -gt $MaxCLength){$MaxCLength = $DisplayCol.length}
$ColLengths += $MaxCLength
}
# initial display of the array
Do-CLIMenuDisplay -DispArr $DisplayObj -DisplayCols $DisplayCols -ColLengths $ColLengths -MaxLines $MaxLinesPerPage -OffSetLine $OffSetLine -CurrentLine $CurrentLine -CursPos $StartCursPos -TitleString $HeaderText
# now act on key presses, keep looping until enter or Esc is pressed
do{
$key = $Host.UI.RawUI.ReadKey()
if ($key.VirtualKeyCode -eq '13') {
# Pressed return
return $ObjList[$DisplayObj[$CurrentLine].num]
}
if (($key.VirtualKeyCode -ge '65' -and $key.VirtualKeyCode -le '90') -or ($key.VirtualKeyCode -ge '48' -and $key.VirtualKeyCode -le '57') -or $key.VirtualKeyCode -eq '189' -or $key.VirtualKeyCode -eq '190') {
# Pressed a - z or 0 - 9 or - or .
# add new character to SearchString
[string]$SearchString = [string]$SearchString + $key.Character.tostring()
# Search each display column for SearchString
$TempDisplayObj = @()
foreach($Col in $DisplayCols)
{
$TempDisplayObj += $NumObjList | where{$_.$Col -match $SearchString}
}
# join the multiple searches using the rownum added earlier. remove duplicates.
$DisplayObj = $TempDisplayObj | sort num -Unique
# if a search shortens the display list and CurrentLine is "high" it can leave the user on page 3 of 2. so if CurrentLine if greater then $DisplayObj.count set CurrentLinr and Offset to 0
if ($CurrentLine -ge @($DisplayObj).count)
{
$OffSetLine = 0
$CurrentLine = 0
}
# display the updated list
Do-CLIMenuDisplay -DispArr $DisplayObj -DisplayCols $DisplayCols -ColLengths $ColLengths -MaxLines $MaxLinesPerPage -OffSetLine $OffSetLine -CurrentLine $CurrentLine -CursPos $StartCursPos -SearchString $SearchString -TitleString $HeaderText
}
if ($key.VirtualKeyCode -eq '40') {
# Pressed down
# inc CurrentLine if not at the end of the array.
if ($CurrentLine -lt ($DisplayObj.count -1)){$CurrentLine ++}
# work out if we hace just crossed a page. if so inc offsetLine by 1 page ($MaxLinesPerPage)
if ($CurrentLine % $MaxLinesPerPage -eq 0){$OffSetLine = ($CurrentLine / $MaxLinesPerPage) * $MaxLinesPerPage}
Do-CLIMenuDisplay -DispArr $DisplayObj -DisplayCols $DisplayCols -ColLengths $ColLengths -MaxLines $MaxLinesPerPage -OffSetLine $OffSetLine -CurrentLine $CurrentLine -CursPos $StartCursPos -SearchString $SearchString -TitleString $HeaderText
}
if ($key.VirtualKeyCode -eq '38') {
# Pressed up
# dec CurrentLine if not at the start of the array.
if ($CurrentLine -gt 0){$CurrentLine --}
# work out if we have just crossed a page. if so dec offsetLine by 1 page ($MaxLinesPerPage)
if ($CurrentLine % $MaxLinesPerPage -eq ($MaxLinesPerPage -1)){$OffSetLine = (($CurrentLine / $MaxLinesPerPage) * $MaxLinesPerPage) - ($MaxLinesPerPage -1)}
Do-CLIMenuDisplay -DispArr $DisplayObj -DisplayCols $DisplayCols -ColLengths $ColLengths -MaxLines $MaxLinesPerPage -OffSetLine $OffSetLine -CurrentLine $CurrentLine -CursPos $StartCursPos -SearchString $SearchString -TitleString $HeaderText
}
if ($key.VirtualKeyCode -eq '37') {
# Pressed left
# dec $OffSetLine and $CurrentLine by one page ($MaxLinesPerPage) if we are not on page 1
if (($OffSetLine - $MaxLinesPerPage) -ge 0)
{
$OffSetLine = $OffSetLine - $MaxLinesPerPage
$CurrentLine = $CurrentLine - $MaxLinesPerPage
}
Do-CLIMenuDisplay -DispArr $DisplayObj -DisplayCols $DisplayCols -ColLengths $ColLengths -MaxLines $MaxLinesPerPage -OffSetLine $OffSetLine -CurrentLine $CurrentLine -CursPos $StartCursPos -SearchString $SearchString -TitleString $HeaderText
}
if ($key.VirtualKeyCode -eq '39') {
# Pressed right
# inc $OffSetLine and $CurrentLine by one page ($MaxLinesPerPage) if we are not on the last page
if (($OffSetLine + $MaxLinesPerPage) -lt $DisplayObj.count){$OffSetLine = $OffSetLine + $MaxLinesPerPage}
if (($CurrentLine + $MaxLinesPerPage ) -lt $DisplayObj.count)
{
$CurrentLine = $CurrentLine + $MaxLinesPerPage
}else{
$CurrentLine = $OffSetLine
}
Do-CLIMenuDisplay -DispArr $DisplayObj -DisplayCols $DisplayCols -ColLengths $ColLengths -MaxLines $MaxLinesPerPage -OffSetLine $OffSetLine -CurrentLine $CurrentLine -CursPos $StartCursPos -SearchString $SearchString -TitleString $HeaderText
}
if ($key.VirtualKeyCode -eq '27') {
# Pressed Esc
return $null
}
if ($key.VirtualKeyCode -eq '8') {
# Pressed backspace
if ($SearchString.length -ge 1)
{
# remove last character from search string
$SearchString = $SearchString.Substring(0,$SearchString.Length-1)
$TempDisplayObj = @()
foreach($Col in $DisplayCols)
{
# loop through each column and search for searchstring
$TempDisplayObj += $NumObjList | where{$_.$Col -match $SearchString}
}
$DisplayObj = $TempDisplayObj | sort num -Unique
Do-CLIMenuDisplay -DispArr $DisplayObj -DisplayCols $DisplayCols -ColLengths $ColLengths -MaxLines $MaxLinesPerPage -OffSetLine $OffSetLine -CurrentLine $CurrentLine -CursPos $StartCursPos -SearchString $SearchString -TitleString $HeaderText
}
}
}until($ExitLoop -eq $true)
}
function Do-CLIMenuDisplay
{
Param(
$DispArr,
$DisplayCols,
$ColLengths,
$MaxLinesPerPage,
$OffSetLine,
$CurrentLine,
$CursPos,
$SearchString,
$TitleString
)
# put the cursor at the start position, instead of clearing the screen I just overwrite.
$host.UI.RawUI.CursorPosition = $StartCursPos
write-host $TitleString
[string]$LineOutString = ''
# build the column headers
# loop through each $DisplayCol, also $ColLengths to "draw" the headers with consistent spacing.
$LoopNum = 0
foreach ($DisplayCol in $DisplayCols)
{
$LineOutString += ( Do-FuncPadSpace -DisplayText $DisplayCol -MaxSpace $ColLengths[$loopNum] ) + " "
$LoopNum ++
}
write-host $LineOutString
# loop through the number of lines to display ($MaxLinesPerPage)
for($x = 0 ; $x -lt ($MaxLinesPerPage) ; $x++)
{
# check if $DispArr exists (a search could have returned nothing)
if ($DispArr)
{
# start at the current $OffSetLine (first line of the current page) and draw this line if it exists in the DisplayArray
if(($OffSetLine + $x) -le @($DispArr).count)
{
# now loop through each $DisplayCol data end build the line.
[string]$LineOutString = ''
$loopNum = 0
foreach($Col in $DisplayCols)
{
# check if there is anything to display. don't pass an empty string to Do-FuncPadSpace.
if ( ($DispArr[$OffSetLine + $x].$Col).length -ge 1)
{
# pass $Col data to Do-FuncPadSpace to add the required spaces so data lines up in columns.
$LineOutString += ( Do-FuncPadSpace -DisplayText ($DispArr[$OffSetLine + $x].$Col) -MaxSpace $ColLengths[$loopNum]) + " "
$LoopNum ++
}else{
# blank line
$LineOutString = " "
}
}
# add some spaces at the end of the line so we remove old text on the line.
$LineOutString += " "
# set correct colour of the selected line.
if ($OffSetLine + $x -eq $CurrentLine)
{
write-host $LineOutString -ForegroundColor Green
}else{
write-host $LineOutString -ForegroundColor DarkGreen
}
}else{
# blank line
write-host " "
}
}else{
write-host " "
}
}
# work out current page number
$Remander = $CurrentLine % $MaxLinesPerPage
$Div = ($CurrentLine - $Remander) / $MaxLinesPerPage
$MaxPage = [int](@($DispArr).count / $MaxLinesPerPage)
$ThisPage = [int]($Div) + 1
$MaxLine = @($DispArr).count - 1
# write the footer.
write-host " "
write-host " Page $ThisPage of $MaxPage (current item $CurrentLine of $MaxLine) "
write-host " Search string = $SearchString "
write-host " "
write-host " "
}
<#
The function will take a text string and pad the end with spaces to make it $MaxSpace in length
$testStringofLength20 = Do-FuncPadSpace $testStringOfLength10 20
#>
function Do-FuncPadSpace
{
Param(
[Parameter(Mandatory = $true)] $DisplayText,
[Parameter(Mandatory = $true)] $MaxSpace
)
if ($DisplayText.length -le $MaxSpace)
{
# add spaces to text to make it the correct length
# I should do it with a loop but its not very readable (and it didn't work)
$spacesToAdd = $MaxSpace - $DisplayText.length
if ($spacesToAdd -eq 0){}
if ($spacesToAdd -eq 1){$Spaces = " "}
if ($spacesToAdd -eq 2){$Spaces = " "}
if ($spacesToAdd -eq 3){$Spaces = " "}
if ($spacesToAdd -eq 4){$Spaces = " "}
if ($spacesToAdd -eq 5){$Spaces = " "}
if ($spacesToAdd -eq 6){$Spaces = " "}
if ($spacesToAdd -eq 7){$Spaces = " "}
if ($spacesToAdd -eq 8){$Spaces = " "}
if ($spacesToAdd -eq 9){$Spaces = " "}
if ($spacesToAdd -eq 10){$Spaces = " "}
if ($spacesToAdd -eq 11){$Spaces = " "}
if ($spacesToAdd -eq 12){$Spaces = " "}
if ($spacesToAdd -eq 13){$Spaces = " "}
if ($spacesToAdd -eq 14){$Spaces = " "}
if ($spacesToAdd -eq 15){$Spaces = " "}
if ($spacesToAdd -eq 16){$Spaces = " "}
if ($spacesToAdd -eq 17){$Spaces = " "}
if ($spacesToAdd -eq 18){$Spaces = " "}
if ($spacesToAdd -eq 19){$Spaces = " "}
if ($spacesToAdd -eq 20){$Spaces = " "}
if ($spacesToAdd -eq 21){$Spaces = " "}
$Result = $DisplayText + $Spaces
return $Result
}else{
# text to long, error
}
}
Installation
- Download the func_menu.txt file and rename it to func_menu.ps1
- Download the test.csv file to the same directory
- open a powershell prompt (not the ISE)
- CD to the script directory
- dot source the function code
- . .\func_menu.ps1
- import the test data
- $TestData = import-csv “test.csv”
- run the function
- Do-Menu -ObjList $TestData
- or
- $SelectedObj = Do-Menu -ObjList $TestData -MaxLinesPerPage 20
- Do-Menu -ObjList $TestData -DisplayCols name,role
That’s it, have fun.