Private/Console/Prompts.psm1
|
using namespace System using namespace System.Collections.Generic using module ..\Enums.psm1 using module ..\Abstracts.psm1 using module .\Ansi.psm1 using module .\Colors.psm1 using module .\Rendering.psm1 using module .\Widgets.psm1 using module .\Progress.psm1 class ValidationResult { [bool]$Successful [string]$Message ValidationResult([bool]$success, [string]$message) { $this.Successful = $success $this.Message = $message } static [ValidationResult] Success() { return [ValidationResult]::new($true, $null) } static [ValidationResult] Error([string]$msg) { return [ValidationResult]::new($false, $msg) } } class IPrompt { [object] Show([object]$consoleInput) { return $null } } class TextPrompt : IPrompt { [Type]$ResultType [string]$Title [object]$DefaultValue [bool]$ShowDefaultValue = $true [bool]$IsSecret = $false [Nullable[char]]$Mask = '*' [bool]$AllowEmpty = $false [string]$ValidationErrorMessage = '[red]Invalid input[/]' [Style]$PromptStyle = [Style]::Plain [ScriptBlock]$Validator TextPrompt([Type]$type, [string]$title) { $this.ResultType = $type $this.Title = $title } [object] Show([object]$consoleInput) { $console = [ConsoleResolver]::ResolveConsole($consoleInput) $writer = [ConsoleResolver]::ResolveWriter($consoleInput) while ($true) { $this.WritePrompt($console) $_input = $this.ReadLine($writer) $writer.WriteLine() if ([string]::IsNullOrWhiteSpace($_input)) { if ($null -ne $this.DefaultValue) { return $this.DefaultValue } if (!$this.AllowEmpty) { continue } return $null } $converted = $null try { if ($this.ResultType -eq [string]) { $converted = $_input } else { $converted = [System.Management.Automation.LanguagePrimitives]::ConvertTo($_input, $this.ResultType) } } catch { $console.MarkupLine($this.ValidationErrorMessage) continue } if ($null -ne $this.Validator) { $res = $this.Validator.InvokeReturnAsIs($converted) if ($res -is [ValidationResult] -and !$res.Successful) { $console.MarkupLine($res.Message) continue } elseif ($res -is [bool] -and !$res) { $console.MarkupLine($this.ValidationErrorMessage) continue } } return $converted } return $null } hidden [void] WritePrompt([object]$console) { $sb = [Text.StringBuilder]::new() $sb.Append($this.Title) if ($this.ShowDefaultValue -and $null -ne $this.DefaultValue) { $sb.Append(" [green]($($this.DefaultValue))[/]") } $markup = $sb.ToString().TrimEnd() if (!$markup.EndsWith('?') -and !$markup.EndsWith(':')) { $markup += ':' } $console.Markup($markup + ' ') } hidden [string] ReadLine([AnsiWriter]$writer) { $input = [Text.StringBuilder]::new() while ($true) { $key = [Console]::ReadKey($true) if ($key.Key -eq [ConsoleKey]::Enter) { break } elseif ($key.Key -eq [ConsoleKey]::Backspace) { if ($input.Length -gt 0) { $input.Length-- $writer.Write("`b `b", [Style]::Plain) } } elseif ($key.KeyChar -ne 0 -and (-not [char]::IsControl($key.KeyChar))) { $input.Append($key.KeyChar) if ($this.IsSecret -and $null -ne $this.Mask) { $writer.Write($this.Mask.ToString(), $this.PromptStyle) } elseif (!$this.IsSecret) { $writer.Write($key.KeyChar.ToString(), $this.PromptStyle) } } } return $input.ToString() } } class ConfirmationPrompt : IPrompt { [string]$Prompt [Style]$PromptStyle = [Style]::Plain [bool]$DefaultValue = $true [bool]$ShowDefaultValue = $true [string]$Yes = 'y' [string]$No = 'n' ConfirmationPrompt([string]$prompt) { $this.Prompt = $prompt } [object] Show([object]$consoleInput) { $console = [ConsoleResolver]::ResolveConsole($consoleInput) $writer = [ConsoleResolver]::ResolveWriter($consoleInput) while ($true) { $sb = [Text.StringBuilder]::new() $sb.Append($this.Prompt) if ($this.ShowDefaultValue) { $yesText = if ($this.DefaultValue) { $this.Yes.ToUpper() } else { $this.Yes.ToLower() } $noText = if (!$this.DefaultValue) { $this.No.ToUpper() } else { $this.No.ToLower() } $sb.Append(" [blue]($yesText/$noText)[/]") } $markup = $sb.ToString().TrimEnd() if (!$markup.EndsWith('?') -and !$markup.EndsWith(':')) { $markup += '?' } $console.Markup($markup + ' ') $key = [Console]::ReadKey($true) if ($key.Key -eq [ConsoleKey]::Enter) { $writer.WriteLine() return $this.DefaultValue } $char = $key.KeyChar.ToString() if ($char -eq $this.Yes -or $char -eq $this.Yes.ToLower() -or $char -eq $this.Yes.ToUpper()) { $console.WriteLine($this.Yes) return $true } elseif ($char -eq $this.No -or $char -eq $this.No.ToLower() -or $char -eq $this.No.ToUpper()) { $console.WriteLine($this.No) return $false } $writer.WriteLine() $console.MarkupLine("[red]Please enter '$($this.Yes)' or '$($this.No)'[/]") } return $null } } class SelectionChoice { [string]$Title [object]$Data SelectionChoice([string]$title, [object]$data) { $this.Title = $title $this.data = $data } } class SelectionPrompt : IPrompt { [string]$Title [List[SelectionChoice]]$Choices [int]$PageSize = 10 [Style]$HighlightStyle = [Style]::new([Color]::Blue) [string]$MoreChoicesText = '(Move up and down to reveal more choices)' hidden [int]$_index = 0 SelectionPrompt([string]$title) { $this.Title = $title $this.Choices = [List[SelectionChoice]]::new() } [void] AddChoice([string]$title, [object]$data) { $this.Choices.Add([SelectionChoice]::new($title, $data)) } [object] Show([object]$consoleInput) { if ($null -eq $consoleInput) { throw 'Console cannot be null' } $writer = [ConsoleResolver]::ResolveWriter($consoleInput) $region = [LiveDisplayRegion]::new($writer) $region.Begin() try { while ($true) { $region.Render($this.RenderLines($writer)) $key = [Console]::ReadKey($true) if ($key.Key -eq [ConsoleKey]::UpArrow) { $this._index-- if ($this._index -lt 0) { $this._index = $this.Choices.Count - 1 } } elseif ($key.Key -eq [ConsoleKey]::DownArrow) { $this._index++ if ($this._index -ge $this.Choices.Count) { $this._index = 0 } } elseif ($key.Key -eq [ConsoleKey]::Enter) { if ($this.Choices.Count -gt 0) { return $this.Choices[$this._index].data } return $null } } } finally { $region.Complete($this.RenderFinal($writer)) } return $null } hidden [string[]] RenderLines([AnsiWriter]$console) { $start = [Math]::Max(0, $this._index - [Math]::Floor($this.PageSize / 2)) if ($start + $this.PageSize -gt $this.Choices.Count) { $start = [Math]::Max(0, $this.Choices.Count - $this.PageSize) } $end = [Math]::Min($start + $this.PageSize, $this.Choices.Count) $lines = [List[string]]::new() if ($null -ne $this.Title) { $lines.Add(($this.MarkupToAnsi($console, $this.Title))) } for ($i = $start; $i -lt $end; $i++) { $choice = $this.Choices[$i] $isCurrent = $i -eq $this._index $prefix = if ($isCurrent) { '> ' } else { ' ' } $style = if ($isCurrent) { $this.HighlightStyle } else { [Style]::Plain } $text = $prefix + $choice.Title $lines.Add($this.MarkupToAnsi($console, $text, $style)) } if ($this.Choices.Count -gt $this.PageSize) { $lines.Add(($this.MarkupToAnsi($console, "[grey]$($this.MoreChoicesText)[/]"))) } return $lines.ToArray() } hidden [string[]] RenderFinal([AnsiWriter]$console) { if ($this.Choices.Count -gt 0 -and $null -ne $this.Title) { $choice = $this.Choices[$this._index] $text = "$($this.Title) [green]$($choice.Title)[/]" return @($this.MarkupToAnsi($console, $text, [Style]::Plain)) } return [string[]]@() } hidden [string] MarkupToAnsi([AnsiWriter]$console, [string]$text) { return $this.MarkupToAnsi($console, $text, [Style]::Plain) } hidden [string] MarkupToAnsi([AnsiWriter]$console, [string]$text, [Style]$overrideStyle) { $m = [Markup]::new($text, $overrideStyle) $options = [RenderOptions]::Create($console, $console.Capabilities) $segs = $m.Render($options, 80) $sb = [Text.StringBuilder]::new() foreach ($seg in $segs) { $hasColor = $console.Capabilities.Ansi -and $null -ne $seg.Style -and $seg.Style -ne [Style]::Plain if ($hasColor) { [void]$sb.Append("`e[" + [AnsiCodeBuilder]::GetAnsi($seg.Style, $console.Capabilities.ColorSystem) + 'm') } [void]$sb.Append($seg.Text) if ($hasColor) { [void]$sb.Append("`e[0m") } } return $sb.ToString() } } class MultiSelectionPrompt : SelectionPrompt { [List[object]]$SelectedItems [string]$InstructionsText = '(Press <space> to select, <enter> to accept)' [Style]$SelectedStyle = [Style]::new([Color]::Green) MultiSelectionPrompt([string]$title) : base($title) { $this.SelectedItems = [List[object]]::new() } [object] Show([object]$consoleInput) { if ($null -eq $consoleInput) { throw 'Console cannot be null' } $writer = [ConsoleResolver]::ResolveWriter($consoleInput) $region = [LiveDisplayRegion]::new($writer) $region.Begin() try { while ($true) { $region.Render($this.RenderLines($writer)) $key = [Console]::ReadKey($true) if ($key.Key -eq [ConsoleKey]::UpArrow) { $this._index-- if ($this._index -lt 0) { $this._index = $this.Choices.Count - 1 } } elseif ($key.Key -eq [ConsoleKey]::DownArrow) { $this._index++ if ($this._index -ge $this.Choices.Count) { $this._index = 0 } } elseif ($key.Key -eq [ConsoleKey]::Spacebar) { if ($this.Choices.Count -gt 0) { $item = $this.Choices[$this._index].data if ($this.SelectedItems.Contains($item)) { [void]$this.SelectedItems.Remove($item) } else { $this.SelectedItems.Add($item) } } } elseif ($key.Key -eq [ConsoleKey]::Enter) { return $this.SelectedItems.ToArray() } } } finally { $region.Complete($this.RenderFinal($writer)) } return $null } hidden [string[]] RenderLines([AnsiWriter]$console) { $start = [Math]::Max(0, $this._index - [Math]::Floor($this.PageSize / 2)) if ($start + $this.PageSize -gt $this.Choices.Count) { $start = [Math]::Max(0, $this.Choices.Count - $this.PageSize) } $end = [Math]::Min($start + $this.PageSize, $this.Choices.Count) $lines = [List[string]]::new() if ($null -ne $this.Title) { $lines.Add(($this.MarkupToAnsi($console, $this.Title))) $lines.Add(($this.MarkupToAnsi($console, "[grey]$($this.InstructionsText)[/]"))) } for ($i = $start; $i -lt $end; $i++) { $choice = $this.Choices[$i] $isCurrent = $i -eq $this._index $isSelected = $this.SelectedItems.Contains($choice.data) $prefix = if ($isCurrent) { '> ' } else { ' ' } $box = if ($isSelected) { '[[X]]' } else { '[[ ]]' } $style = if ($isCurrent) { $this.HighlightStyle } elseif ($isSelected) { $this.SelectedStyle } else { [Style]::Plain } $text = "$prefix$box $($choice.Title)" $lines.Add($this.MarkupToAnsi($console, $text, $style)) } if ($this.Choices.Count -gt $this.PageSize) { $lines.Add(($this.MarkupToAnsi($console, "[grey]$($this.MoreChoicesText)[/]"))) } return $lines.ToArray() } hidden [string[]] RenderFinal([AnsiWriter]$console) { if ($null -ne $this.Title) { $titles = [List[string]]::new() foreach ($choice in $this.Choices) { if ($this.SelectedItems.Contains($choice.data)) { $titles.Add($choice.Title) } } $joined = $titles -join ', ' $text = "$($this.Title) [green]$joined[/]" return @($this.MarkupToAnsi($console, $text)) } return [string[]]@() } } |