functions/Read-UtilsUserOption.ps1
|
function Read-UtilsUserOption { <# .SYNOPSIS Opens an interactive single line menue for user confirmation. .DESCRIPTION Opens an interactive single line menue for user confirmation. .OUTPUTS The selected option by the user. .EXAMPLE Present a simple prompt with two selections: Read-UtilsUserOption "Confirm: " .EXAMPLE Present a simple prompt with two selections and an identation: Read-UtilsUserOption "Confirm: " @("A", "B", "C") -i 2 .LINK #> [CmdletBinding( DefaultParameterSetName = "DefaultIndex" )] param ( # The input prompt to ask the user. [Parameter( Position = 0, Mandatory = $false )] [System.String] $Prompt, # Indentation for the prompt to display. [Parameter( Position = 2, Mandatory = $false )] [System.int32] [Alias('i')] $Indendation = 0, <# The Options to display for selection Input as Strings: @("Yes", "No") Input as Objects: $( @{ display = "Yes" value = $true default = $true }, @{ display = "No" value = $false } ), #> [Parameter( Position = 1, Mandatory = $false, ValueFromPipeline = $true )] [System.Object] $Options = @( @{ display = "No" value = $false default = $true }, @{ display = "Yes" value = $true } ), # The Default selected index, starting from left to right. # Either defaultValue or defaultIndex can be used. [Parameter( Mandatory = $false, ParameterSetName = "DefaultIndex" )] [System.Int32] [Alias('Default')] $DefaultIndex = 0, # The Default selected value. # Either defaultValue or defaultIndex can be used. [Parameter( Mandatory = $false, ParameterSetName = "DefaultValue" )] [System.String] $DefaultValue, # Prevents a new line after finishing the method. [Parameter()] [Alias('SkipLineBreak')] [switch] $NoNewLine, <# Custom colors for the prompt and options. Provide a hashtable with the following keys. Each key is optional and $null will be replaced with the default value. - Prompt - ForeGround - Background - Option - ForeGround - Background - Selected - ForeGround - Background Following Colors are supported: - HEX: #FFFFFF - RGB: 255, 255, 255 - ColorName: Colors in $PSStyle.Foreground or $PSStyle.Background Example: @{ Prompt = @{ ForeGround = "White" # White in HEX BackGround = $null } Option = @{ ForeGround = "BrightBlack" # Light Gray in HEX BackGround = $null } Selected = @{ ForeGround = "BrightWhite" BackGround = "Magenta" } } #> [Parameter()] [System.Collections.Hashtable] $CustomColors = @{} ) BEGIN { function Convert-Color { param( [System.Object] $Color, [System.String] $Type ) if ($null -EQ $Color) { return $null } if ($Color -IS [System.String] -AND $Color.startswith('#')) { $Color = [System.Convert]::FromHexString($Color.substring(1)) } if (-NOT ($Color -IS [System.String])) { $COLORS_24BIT = @{ FOREGROUND = "`e[38;2;{0};{1};{2}m" BACKGROUND = "`e[48;2;{0};{1};{2}m" } return $COLORS_24BIT."$Type" -f $Color[0], $Color[1], $Color[2] } if ($null -EQ $PSStyle."$Type"."$Color") { throw [System.InvalidOperationException]::new("The provided color '$Color' is not a valid color.") } else { return $PSStyle."$Type"."$Color" } } $transformedColors = @{ Prompt = @{ ForeGround = "White" # White in HEX Background = $null } Option = @{ ForeGround = "BrightBlack" # Light Gray in HEX Background = $null } Selected = @{ ForeGround = "BrightWhite" Background = "Magenta" } } foreach ($key in $transformedColors.Keys) { $fg = $CustomColors[$key].ForeGround ?? $transformedColors[$key].ForeGround $transformedColors[$key].ForeGround = Convert-Color -Color $fg -Type 'Foreground' $bg = $CustomColors[$key].BackGround ?? $transformedColors[$key].Background $transformedColors[$key].BackGround = Convert-Color -Color $bg -Type 'Background' } <# Setting up the colors for the prompt and options. #> $_Color_Reset_ = "`e[0m" $_Color_Option_ = '' + $transformedColors.Option.ForeGround + $transformedColors.Option.Background $_Color_Selected_ = '' + $transformedColors.Selected.ForeGround + $transformedColors.Selected.Background $_Color_Prompt_ = '' + $transformedColors.Prompt.ForeGround + $transformedColors.Prompt.Background <# Setting up user prompt and displayed options. #> $userPrompt = (" " * $Indendation) + $Prompt.TrimEnd() $selectedIndex = $DefaultIndex $marginSpace = " " if ($PSBoundParameters.ContainsKey('DefaultValue')) { $selectedIndex = ($Options.display ?? $Options).IndexOf($DefaultValue) if ($selectedIndex -LT 0) { throw [System.InvalidOperationException]::new("The Default Value '$DefaultValue' is not part of the Options.") } } $processedOptions = @() } PROCESS { <# If the user provided the obect as a parameter, we pipe it to a new instance of the function. #> if (-NOT $PSCmdlet.MyInvocation.ExpectingInput) { $null = $PSBoundParameters.Remove('Options') return $Options | Read-UtilsUserOption @PSBoundParameters } <# Convert all provided entries to a list of object: - Strings and ValueTypes: String or Value is displayed on screen as is. - Powershell Objects: A property from the object is display on screen, to identify the object. #> if ( $Options -IS [System.String] -OR $Options -IS [System.ValueType] ) { $processedOptions += @{ display = $Options value = $Options } } else { $processedOptions += @{ display = $Options.display value = $Options.value } if ($null -EQ $Options.display -OR $null -EQ $Options.display) { throw [System.InvalidOperationException]::new("@ The provided option is not a valid object. Please provide a hashtable with the properties 'display' and 'value'. Example: @{ display = 'Option1' value = @{ file = 'test.txt' path = 'C:\temp' } } @") } } if ($Options.default -EQ $true) { $selectedIndex = $processedOptions.Count - 1 } } END { if (-NOT $PSCmdlet.MyInvocation.ExpectingInput) { return } [System.Console]::TreatControlCAsInput = $true $cursorX = [System.Console]::GetCursorPosition().Item1 do { [System.Console]::CursorVisible = $false $uIwidth = $Host.UI.RawUI.BufferSize.Width $cursorY = [System.Console]::GetCursorPosition().Item2 <# Overwrites the previous drawn line with whitespaces. Then resets the cursor and draws the Prompt. #> [System.Console]::SetCursorPosition($cursorX, $cursorY) [System.Console]::Write(" " * $uIwidth) [System.Console]::SetCursorPosition($cursorX, $cursorY) [System.Console]::Write($_Color_Prompt_) [System.Console]::write($userPrompt) [System.Console]::Write($_Color_Reset_) for ($index = 0; $index -LT $processedOptions.Count; $index++) { [System.Console]::Write($marginSpace) if ($index -EQ $selectedIndex) { [System.Console]::Write($_Color_Selected_) [System.Console]::Write($processedOptions[$index].display) [System.Console]::Write($_Color_Reset_) } else { [System.Console]::Write($_Color_Option_) [System.Console]::Write($processedOptions[$index].display) [System.Console]::Write($_Color_Reset_) } } <# //////////////////////////////////////// /// Handle Key Inputs from User. #> $e = [System.Console]::ReadKey($true) <# Cancel the operation if the user presses ESC or CTRL+C. #> if ( $e.Key -EQ [System.ConsoleKey]::Escape -OR ($e.Key -EQ [System.ConsoleKey]::C -AND $e.Modifiers -EQ "Control") ) { throw [System.OperationCanceledException]::new("User canceled the operation.") } elseif ( $e.Key -EQ [System.ConsoleKey]::D -OR $e.Key -EQ [System.ConsoleKey]::RightArrow ) { $selectedIndex = ($selectedIndex + 1) % $processedOptions.Count } elseif ( $e.Key -EQ [System.ConsoleKey]::A -OR $e.Key -EQ [System.ConsoleKey]::LeftArrow ) { $selectedIndex = ($selectedIndex + $processedOptions.Count - 1) % $processedOptions.Count } elseif ( $e.Key -EQ [System.ConsoleKey]::Enter ) { # Write a new Line to set the cursor to the next line. if (-NOT $NoNewLine.IsPresent) { [System.Console]::Write([System.Environment]::NewLine) } return $processedOptions[$selectedIndex].value } } while ($e.Key -NE [System.ConsoleKey]::Enter) } CLEAN { # Make sure to always leave in any case function with a visible cursor again. [System.Console]::CursorVisible = $true } } |