Examples/Invoke-WidgetShowcaseDemo.ps1
|
Import-Module "$PSScriptRoot/../Elm.psd1" -Force # --------------------------------------------------------------------------- # Widget Showcase demo # Demonstrates all five Phase 9 widgets with live-adjustable configuration. # Each panel exposes widget params as keybindings — cycle options and see # the widget update in real time. # # Navigation: # P / N — previous / next panel # Q — quit # # Panel 1 — Animation (New-ElmSpinner + New-ElmProgressBar) # W cycle -Width (20/30/40) # F cycle -FilledChar (#/=/*/+) # E cycle -EmptyChar (-/./spc) # # Panel 2 — List (New-ElmList) # Up/Down navigate M cycle -MaxVisible (5/8/10) # V cycle -Prefix (> / * / | / ->) # # Panel 3 — Viewport (New-ElmViewport) # Up/Down scroll M cycle -MaxVisible (4/8/12/16) # # Panel 4 — TextInput (New-ElmTextInput) # Left/Right cursor F toggle -Focused (on/off) # Backspace delete C cycle -CursorChar (|/_/#) # --------------------------------------------------------------------------- $script:PANEL_COUNT = 7 # Config option arrays — indices stored in the model $script:BAR_WIDTHS = @(20, 30, 40) $script:FILLED_CHARS = @('#', '=', '*', '+') $script:EMPTY_CHARS = @('-', '.', ' ') $script:LIST_MAX_VIS = @(5, 8, 10) $script:LIST_PREFIXES = @( [PSCustomObject]@{ Sel = '> '; Unsel = ' ' } [PSCustomObject]@{ Sel = '* '; Unsel = ' ' } [PSCustomObject]@{ Sel = '| '; Unsel = ' ' } [PSCustomObject]@{ Sel = '->'; Unsel = ' ' } ) $script:VP_MAX_VIS = @(4, 8, 12, 16) $script:CURSOR_CHARS = @('|', '_', '#') # Phase 10 config $script:TA_MAX_VIS = @(4, 8, 12, 16) $script:PAGER_MODES = @('Numeric', 'Tabs', 'Dots') $script:PAGER_PAGES = 7 $script:PAGER_TAB_LABELS = @('Animate','List','Viewport','Input','Textarea','Table','Paginator') # Table widget reference data $script:TABLE_HEADERS = @('Widget', 'Returns', 'Key Params') $script:TABLE_ROWS = @( ,@('New-ElmProgressBar', 'Text', '-Value/-Percent, -Width') ,@('New-ElmSpinner', 'Text', '-Frame, -Variant') ,@('New-ElmList', 'Box/Vertical','-Items, -SelectedIndex') ,@('New-ElmViewport', 'Box/Vertical','-Lines, -ScrollOffset') ,@('New-ElmTextInput', 'Text/Box', '-Value, -CursorPos, -FocusedStyle, -FocusedBoxStyle') ,@('New-ElmTextarea', 'Box/Vertical','-Lines, -CursorRow/-Col, -FocusedStyle, -FocusedBoxStyle') ,@('New-ElmTable', 'Box/Vertical','-Headers, -Rows') ,@('New-ElmPaginator', 'Text/Box', '-CurrentPage / -Tabs / -Dots') ) # Viewport content: one line per Phase 9 widget description $script:VIEWPORT_LINES = @( 'Phase 9 Widget Library — Elm for PowerShell' '============================================' '' 'New-ElmProgressBar' ' Horizontal progress bar.' ' -Value 0.0..1.0 or -Percent 0..100' ' -Width (min 4), -FilledChar, -EmptyChar' ' Returns a Text node: [###-------]' '' 'New-ElmSpinner' ' Animated spinner driven by a frame counter.' ' -Frame (caller increments), -Variant:' ' Dots | / - \ (default)' ' Braille 10 braille chars' ' Bounce . o O o' ' Arrow > >> >>>' ' -Frames for a fully custom sequence.' '' 'New-ElmList' ' Scrollable, selectable list of strings.' ' -Items, -SelectedIndex, -MaxVisible' ' -Prefix (selected), -UnselectedPrefix' ' -Style, -SelectedStyle' ' Returns a Box (Vertical) of Text nodes.' '' 'New-ElmViewport' ' Fixed-height window into a string array.' ' -Lines, -ScrollOffset, -MaxVisible, -Style' ' Clamps scroll offset automatically.' ' Returns a Box (Vertical) of Text nodes.' '' 'New-ElmTextInput' ' Single-line text input with cursor.' ' -Value, -CursorPos, -Focused (switch)' ' -Placeholder shown when empty+unfocused.' ' -CursorChar (default |)' ' -Style, -FocusedStyle' ' Returns a Text node.' '' 'New-ElmTextarea' ' Multi-line text area with cursor.' ' -Lines [string[]], -CursorRow, -CursorCol' ' -Focused (switch), -MaxVisible, -ScrollOffset' ' -Placeholder, -CursorChar, -Style, -FocusedStyle' ' Returns a Box (Vertical) of Text nodes.' '' 'New-ElmTable' ' Data table with optional headers and row selection.' ' -Headers [string[]], -Rows [object[]]' ' -SelectedRow (-1 = no selection), -ColumnWidths' ' -Style, -HeaderStyle, -SelectedStyle' ' Returns a Box (Vertical) of Text nodes.' '' 'New-ElmPaginator' ' Numeric page indicator or named tab bar.' ' Numeric: -CurrentPage, -PageCount' ' renders: < 3 / 7 >' ' Tabs: -Tabs [string[]], -ActiveTab' ' renders: Tab1 | [Tab2] | Tab3' ' -Style, -ActiveStyle' ' Returns Text (Numeric) or Box/Horizontal (Tabs).' ) # List panel items $script:COLOR_NAMES = @( 'BrightRed' 'BrightGreen' 'BrightYellow' 'BrightBlue' 'BrightMagenta' 'BrightCyan' 'BrightWhite' 'Red' 'Green' 'Yellow' 'Blue' 'Magenta' 'Cyan' 'White' 'BrightBlack' 'Black' ) # Helper — shallow-copy model, optionally overriding specific fields function New-ShowcaseModel { param($Model, [hashtable]$Overrides = @{}) $m = [PSCustomObject]@{ Tab = $Model.Tab Frame = $Model.Frame Progress = $Model.Progress ListCursor = $Model.ListCursor ViewOffset = $Model.ViewOffset InputValue = $Model.InputValue InputCursor = $Model.InputCursor InputFocused = $Model.InputFocused BarWidthIdx = $Model.BarWidthIdx FilledCharIdx = $Model.FilledCharIdx EmptyCharIdx = $Model.EmptyCharIdx ListMaxVisIdx = $Model.ListMaxVisIdx ListPrefixIdx = $Model.ListPrefixIdx VpMaxVisIdx = $Model.VpMaxVisIdx CursorCharIdx = $Model.CursorCharIdx # Phase 10 fields TextareaLines = $Model.TextareaLines TextareaRow = $Model.TextareaRow TextareaCol = $Model.TextareaCol TextareaOffset = $Model.TextareaOffset TextareaFocused = $Model.TextareaFocused TextareaCursorIdx = $Model.TextareaCursorIdx TextareaMaxVisIdx = $Model.TextareaMaxVisIdx TableCursor = $Model.TableCursor PagerPage = $Model.PagerPage PagerTabIdx = $Model.PagerTabIdx PagerModeIdx = $Model.PagerModeIdx } foreach ($key in $Overrides.Keys) { $m.$key = $Overrides[$key] } $m } $initFn = { [PSCustomObject]@{ Model = [PSCustomObject]@{ Tab = 0 # 0=Animation 1=List 2=Viewport 3=TextInput Frame = 0 # spinner frame counter Progress = 0.0 # 0.0..1.0 progress bar value ListCursor = 0 # selected list item ViewOffset = 0 # viewport scroll offset InputValue = '' InputCursor = 0 InputFocused = $false # TextInput -Focused state BarWidthIdx = 1 # index into BAR_WIDTHS (default: 30) FilledCharIdx = 0 # index into FILLED_CHARS (default: #) EmptyCharIdx = 0 # index into EMPTY_CHARS (default: -) ListMaxVisIdx = 2 # index into LIST_MAX_VIS (default: 10) ListPrefixIdx = 0 # index into LIST_PREFIXES (default: '> ') VpMaxVisIdx = 2 # index into VP_MAX_VIS (default: 12) CursorCharIdx = 0 # index into CURSOR_CHARS (default: |) # Phase 10 fields TextareaLines = [string[]]@('') TextareaRow = 0 # cursor row (0-based) TextareaCol = 0 # cursor col (0-based) TextareaOffset = 0 # scroll offset TextareaFocused = $false TextareaCursorIdx = 0 # index into CURSOR_CHARS TextareaMaxVisIdx = 1 # index into TA_MAX_VIS (default: 8) TableCursor = 0 # selected row in table panel PagerPage = 1 # current page (1-based, max = PAGER_PAGES) PagerTabIdx = 0 # active tab in tab-mode paginator PagerModeIdx = 0 # 0 = Numeric, 1 = Tabs } Cmd = $null } } $updateFn = { param($msg, $model) switch -Wildcard ($msg) { 'Tick' { $newProgress = $model.Progress + 0.02 if ($newProgress -gt 1.0) { $newProgress = 0.0 } return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ Frame = $model.Frame + 1; Progress = $newProgress } Cmd = $null } } 'TabNext' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ Tab = [math]::Min($model.Tab + 1, $script:PANEL_COUNT - 1) } Cmd = $null } } 'TabPrev' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ Tab = [math]::Max($model.Tab - 1, 0) } Cmd = $null } } 'ListUp' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ ListCursor = [math]::Max($model.ListCursor - 1, 0) } Cmd = $null } } 'ListDown' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ ListCursor = [math]::Min($model.ListCursor + 1, $script:COLOR_NAMES.Count - 1) } Cmd = $null } } 'ViewUp' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ ViewOffset = [math]::Max($model.ViewOffset - 1, 0) } Cmd = $null } } 'ViewDown' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ ViewOffset = [math]::Min($model.ViewOffset + 1, $script:VIEWPORT_LINES.Count - 1) } Cmd = $null } } 'InputLeft' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ InputCursor = [math]::Max($model.InputCursor - 1, 0) } Cmd = $null } } 'InputRight' { $maxPos = ([string]$model.InputValue).Length return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ InputCursor = [math]::Min($model.InputCursor + 1, $maxPos) } Cmd = $null } } 'Backspace' { $val = [string]$model.InputValue $curPos = [math]::Max(0, [math]::Min($model.InputCursor, $val.Length)) if ($curPos -eq 0) { return [PSCustomObject]@{ Model = $model; Cmd = $null } } $newVal = $val.Substring(0, $curPos - 1) + $val.Substring($curPos) return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ InputValue = $newVal; InputCursor = $curPos - 1 } Cmd = $null } } # --- config cycling --- 'BarWidthCycle' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ BarWidthIdx = ($model.BarWidthIdx + 1) % $script:BAR_WIDTHS.Count } Cmd = $null } } 'FilledCharCycle' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ FilledCharIdx = ($model.FilledCharIdx + 1) % $script:FILLED_CHARS.Count } Cmd = $null } } 'EmptyCharCycle' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ EmptyCharIdx = ($model.EmptyCharIdx + 1) % $script:EMPTY_CHARS.Count } Cmd = $null } } 'ListMaxVisCycle' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ ListMaxVisIdx = ($model.ListMaxVisIdx + 1) % $script:LIST_MAX_VIS.Count } Cmd = $null } } 'ListPrefixCycle' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ ListPrefixIdx = ($model.ListPrefixIdx + 1) % $script:LIST_PREFIXES.Count } Cmd = $null } } 'VpMaxVisCycle' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ VpMaxVisIdx = ($model.VpMaxVisIdx + 1) % $script:VP_MAX_VIS.Count } Cmd = $null } } 'InputToggleFocus' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ InputFocused = (-not $model.InputFocused) } Cmd = $null } } 'CursorCharCycle' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ CursorCharIdx = ($model.CursorCharIdx + 1) % $script:CURSOR_CHARS.Count } Cmd = $null } } 'Input:*' { $typedChar = $msg.Substring(6) # strip 'Input:' prefix $val = [string]$model.InputValue $curPos = [math]::Max(0, [math]::Min($model.InputCursor, $val.Length)) $newVal = $val.Substring(0, $curPos) + $typedChar + $val.Substring($curPos) return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ InputValue = $newVal; InputCursor = $curPos + 1 } Cmd = $null } } # --- textarea --- 'TextareaUp' { $newRow = [math]::Max($model.TextareaRow - 1, 0) $newLine = [string]$model.TextareaLines[$newRow] $newCol = [math]::Min($model.TextareaCol, $newLine.Length) $maxVis = $script:TA_MAX_VIS[$model.TextareaMaxVisIdx] $newOff = $model.TextareaOffset if ($newRow -lt $newOff) { $newOff = $newRow } return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ TextareaRow = $newRow; TextareaCol = $newCol; TextareaOffset = $newOff } Cmd = $null } } 'TextareaDown' { $maxRow = $model.TextareaLines.Count - 1 $newRow = [math]::Min($model.TextareaRow + 1, $maxRow) $newLine = [string]$model.TextareaLines[$newRow] $newCol = [math]::Min($model.TextareaCol, $newLine.Length) $maxVis = $script:TA_MAX_VIS[$model.TextareaMaxVisIdx] $newOff = $model.TextareaOffset if ($newRow -ge $newOff + $maxVis) { $newOff = $newRow - $maxVis + 1 } return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ TextareaRow = $newRow; TextareaCol = $newCol; TextareaOffset = $newOff } Cmd = $null } } 'TextareaLeft' { $newCol = [math]::Max($model.TextareaCol - 1, 0) return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ TextareaCol = $newCol } Cmd = $null } } 'TextareaRight' { $line = [string]$model.TextareaLines[$model.TextareaRow] $newCol = [math]::Min($model.TextareaCol + 1, $line.Length) return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ TextareaCol = $newCol } Cmd = $null } } 'TextareaBackspace' { $lines = [System.Collections.Generic.List[string]]::new([string[]]$model.TextareaLines) $row = [math]::Max(0, [math]::Min($model.TextareaRow, $lines.Count - 1)) $line = $lines[$row] $col = [math]::Max(0, [math]::Min($model.TextareaCol, $line.Length)) if ($col -gt 0) { $lines[$row] = $line.Substring(0, $col - 1) + $line.Substring($col) return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ TextareaLines = $lines.ToArray(); TextareaCol = $col - 1 } Cmd = $null } } elseif ($row -gt 0) { $prevLine = $lines[$row - 1] $newCol = $prevLine.Length $lines[$row - 1] = $prevLine + $line $lines.RemoveAt($row) return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ TextareaLines = $lines.ToArray(); TextareaRow = $row - 1; TextareaCol = $newCol } Cmd = $null } } return [PSCustomObject]@{ Model = $model; Cmd = $null } } 'TextareaEnter' { $lines = [System.Collections.Generic.List[string]]::new([string[]]$model.TextareaLines) $row = [math]::Max(0, [math]::Min($model.TextareaRow, $lines.Count - 1)) $line = $lines[$row] $col = [math]::Max(0, [math]::Min($model.TextareaCol, $line.Length)) $lines[$row] = $line.Substring(0, $col) $lines.Insert($row + 1, $line.Substring($col)) $maxVis = $script:TA_MAX_VIS[$model.TextareaMaxVisIdx] $newRow = $row + 1 $newOff = $model.TextareaOffset if ($newRow -ge $newOff + $maxVis) { $newOff = $newRow - $maxVis + 1 } return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ TextareaLines = $lines.ToArray(); TextareaRow = $newRow; TextareaCol = 0; TextareaOffset = $newOff } Cmd = $null } } 'TextareaFocusToggle' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ TextareaFocused = (-not $model.TextareaFocused) } Cmd = $null } } 'TextareaCursorCycle' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ TextareaCursorIdx = ($model.TextareaCursorIdx + 1) % $script:CURSOR_CHARS.Count } Cmd = $null } } 'TextareaMaxVisCycle' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ TextareaMaxVisIdx = ($model.TextareaMaxVisIdx + 1) % $script:TA_MAX_VIS.Count } Cmd = $null } } 'TextareaInput:*' { $typedChar = $msg.Substring(14) # strip 'TextareaInput:' prefix (14 chars) $lines = [System.Collections.Generic.List[string]]::new([string[]]$model.TextareaLines) $row = [math]::Max(0, [math]::Min($model.TextareaRow, $lines.Count - 1)) $line = $lines[$row] $col = [math]::Max(0, [math]::Min($model.TextareaCol, $line.Length)) $lines[$row] = $line.Substring(0, $col) + $typedChar + $line.Substring($col) return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ TextareaLines = $lines.ToArray(); TextareaCol = $col + 1 } Cmd = $null } } # --- table --- 'TableUp' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ TableCursor = [math]::Max($model.TableCursor - 1, 0) } Cmd = $null } } 'TableDown' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ TableCursor = [math]::Min($model.TableCursor + 1, $script:TABLE_ROWS.Count - 1) } Cmd = $null } } # --- paginator --- 'PagerLeft' { if ($model.PagerModeIdx -eq 1) { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ PagerTabIdx = [math]::Max($model.PagerTabIdx - 1, 0) } Cmd = $null } } else { # Numeric (0) and Dots (2) both navigate page return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ PagerPage = [math]::Max($model.PagerPage - 1, 1) } Cmd = $null } } } 'PagerRight' { if ($model.PagerModeIdx -eq 1) { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ PagerTabIdx = [math]::Min($model.PagerTabIdx + 1, $script:PAGER_TAB_LABELS.Count - 1) } Cmd = $null } } else { # Numeric (0) and Dots (2) both navigate page return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ PagerPage = [math]::Min($model.PagerPage + 1, $script:PAGER_PAGES) } Cmd = $null } } } 'PagerModeCycle' { return [PSCustomObject]@{ Model = New-ShowcaseModel $model @{ PagerModeIdx = ($model.PagerModeIdx + 1) % $script:PAGER_MODES.Count } Cmd = $null } } 'Quit' { return [PSCustomObject]@{ Model = $model Cmd = [PSCustomObject]@{ Type = 'Quit' } } } } [PSCustomObject]@{ Model = $model; Cmd = $null } } $viewFn = { param($model) # Shared styles $titleStyle = New-ElmStyle -Foreground 'BrightCyan' -Bold $hintStyle = New-ElmStyle -Foreground 'BrightBlack' $activeTab = New-ElmStyle -Foreground 'BrightWhite' -Bold -Underline $inactiveTab = New-ElmStyle -Foreground 'BrightBlack' $accentStyle = New-ElmStyle -Foreground 'BrightYellow' $labelStyle = New-ElmStyle -Foreground 'BrightWhite' $configStyle = New-ElmStyle -Foreground 'BrightCyan' $inputStyle = New-ElmStyle -Foreground 'White' $inputFocStyle = New-ElmStyle -Foreground 'Black' -Background 'White' $listStyle = New-ElmStyle -Foreground 'White' $selStyle = New-ElmStyle -Foreground 'BrightYellow' -Bold $vpStyle = New-ElmStyle -Foreground 'BrightGreen' $tabNames = @('[1] Animate', '[2] List', '[3] Viewport', '[4] Input', '[5] Textarea', '[6] Table', '[7] Paginator') $tabRow = [System.Collections.Generic.List[object]]::new() for ($i = 0; $i -lt $tabNames.Count; $i++) { $ts = if ($i -eq $model.Tab) { $activeTab } else { $inactiveTab } $tabRow.Add((New-ElmText -Content " $($tabNames[$i]) " -Style $ts)) } $children = [System.Collections.Generic.List[object]]::new() $children.Add((New-ElmText -Content 'Elm Widget Showcase' -Style $titleStyle)) $children.Add((New-ElmRow -Children $tabRow.ToArray())) $children.Add((New-ElmText -Content ('-' * 50) -Style $hintStyle)) switch ($model.Tab) { 0 { # ----- Animation: New-ElmSpinner + New-ElmProgressBar ----- $barWidth = $script:BAR_WIDTHS[$model.BarWidthIdx] $filledChar = $script:FILLED_CHARS[$model.FilledCharIdx] $emptyChar = $script:EMPTY_CHARS[$model.EmptyCharIdx] $children.Add((New-ElmText -Content '' )) $children.Add((New-ElmText -Content 'New-ElmSpinner' -Style $labelStyle)) $spinVariants = @('Dots', 'Braille', 'Bounce', 'Arrow') foreach ($variant in $spinVariants) { $spinNode = New-ElmSpinner -Frame $model.Frame -Variant $variant -Style $accentStyle $label = New-ElmText -Content " -Variant $($variant.PadRight(7)): " -Style $hintStyle $children.Add((New-ElmRow -Children @($label, $spinNode))) } $children.Add((New-ElmText -Content '' )) $children.Add((New-ElmText -Content 'New-ElmProgressBar' -Style $labelStyle)) $pct = [int]([math]::Round($model.Progress * 100)) $barNode = New-ElmProgressBar -Value $model.Progress -Width $barWidth ` -FilledChar $filledChar -EmptyChar $emptyChar ` -Style $accentStyle $children.Add($barNode) $children.Add((New-ElmText -Content " $pct%" -Style $hintStyle)) $children.Add((New-ElmText -Content '' )) $emptyDisplay = if ($emptyChar -eq ' ') { 'spc' } else { $emptyChar } $widthOpts = $script:BAR_WIDTHS -join '/' $filledOpts = $script:FILLED_CHARS -join '/' $emptyOpts = ($script:EMPTY_CHARS | ForEach-Object { if ($_ -eq ' ') { 'spc' } else { $_ } }) -join '/' $children.Add((New-ElmText -Content " -Width $barWidth [W: $widthOpts] -FilledChar '$filledChar' [F: $filledOpts] -EmptyChar '$emptyDisplay' [E: $emptyOpts]" -Style $configStyle)) $children.Add((New-ElmText -Content '' )) $children.Add((New-ElmText -Content '[W] Width [F] FilledChar [E] EmptyChar [P] prev [N] next [Q] quit' -Style $hintStyle)) } 1 { # ----- List: New-ElmList ----- $maxVis = $script:LIST_MAX_VIS[$model.ListMaxVisIdx] $prefix = $script:LIST_PREFIXES[$model.ListPrefixIdx] $children.Add((New-ElmText -Content '' )) $children.Add((New-ElmText -Content 'New-ElmList — ANSI Color Names' -Style $labelStyle)) $listNode = New-ElmList -Items $script:COLOR_NAMES ` -SelectedIndex $model.ListCursor ` -MaxVisible $maxVis ` -Prefix $prefix.Sel ` -UnselectedPrefix $prefix.Unsel ` -Style $listStyle ` -SelectedStyle $selStyle $children.Add($listNode) $itemName = $script:COLOR_NAMES[$model.ListCursor] $maxVisOpts = $script:LIST_MAX_VIS -join '/' $prefixOpts = ($script:LIST_PREFIXES | ForEach-Object { "'$($_.Sel)'" }) -join '/' $children.Add((New-ElmText -Content '' )) $children.Add((New-ElmText -Content " Selected: $itemName ($($model.ListCursor + 1)/$($script:COLOR_NAMES.Count))" -Style (New-ElmStyle -Foreground $itemName))) $children.Add((New-ElmText -Content " -MaxVisible $maxVis [M: $maxVisOpts] -Prefix '$($prefix.Sel)' [V: $prefixOpts]" -Style $configStyle)) $children.Add((New-ElmText -Content '' )) $children.Add((New-ElmText -Content '[Up/Down] navigate [M] MaxVisible [V] Prefix [P] prev [N] next [Q] quit' -Style $hintStyle)) } 2 { # ----- Viewport: New-ElmViewport ----- $maxVis = $script:VP_MAX_VIS[$model.VpMaxVisIdx] $children.Add((New-ElmText -Content '' )) $children.Add((New-ElmText -Content 'New-ElmViewport — Widget Documentation' -Style $labelStyle)) $vpNode = New-ElmViewport -Lines $script:VIEWPORT_LINES ` -ScrollOffset $model.ViewOffset ` -MaxVisible $maxVis ` -Style $vpStyle $children.Add($vpNode) $maxOff = [math]::Max(0, $script:VIEWPORT_LINES.Count - $maxVis) $maxVisOpts = $script:VP_MAX_VIS -join '/' $children.Add((New-ElmText -Content '' )) $children.Add((New-ElmText -Content " Line $($model.ViewOffset + 1)/$($script:VIEWPORT_LINES.Count) (scroll range: 0-$maxOff)" -Style $hintStyle)) $children.Add((New-ElmText -Content " -MaxVisible $maxVis [M: $maxVisOpts]" -Style $configStyle)) $children.Add((New-ElmText -Content '' )) $children.Add((New-ElmText -Content '[Up/Down] scroll [M] MaxVisible [P] prev [N] next [Q] quit' -Style $hintStyle)) } 3 { # ----- TextInput: New-ElmTextInput ----- $cursorChar = $script:CURSOR_CHARS[$model.CursorCharIdx] $isFocused = $model.InputFocused $focusedBoxStyle = New-ElmStyle -Border 'Rounded' $tiParams = @{ Value = $model.InputValue CursorPos = $model.InputCursor Placeholder = 'Press F to focus here!' CursorChar = $cursorChar Style = $inputStyle FocusedStyle = $inputFocStyle FocusedBoxStyle = $focusedBoxStyle } $tiNode = if ($isFocused) { New-ElmTextInput @tiParams -Focused } else { New-ElmTextInput @tiParams } $children.Add((New-ElmText -Content '' )) $children.Add((New-ElmText -Content 'New-ElmTextInput' -Style $labelStyle)) $label = New-ElmText -Content ' > ' -Style $hintStyle $children.Add((New-ElmRow -Children @($label, $tiNode))) $val = [string]$model.InputValue $focusedStr = if ($isFocused) { 'on' } else { 'off' } $cursorOpts = $script:CURSOR_CHARS -join '/' $children.Add((New-ElmText -Content '' )) $children.Add((New-ElmText -Content " Length: $($val.Length) Cursor: $($model.InputCursor)" -Style $hintStyle)) $children.Add((New-ElmText -Content " -Focused $focusedStr [F: on/off] -CursorChar '$cursorChar' [C: $cursorOpts]" -Style $configStyle)) $children.Add((New-ElmText -Content ' -FocusedStyle (Black/White bg) -FocusedBoxStyle (Rounded border)' -Style $configStyle)) $children.Add((New-ElmText -Content '' )) if ($isFocused) { $children.Add((New-ElmText -Content '[type] input [Left/Right] cursor [Backspace] delete [Esc] unfocus to quit' -Style $hintStyle)) } else { $children.Add((New-ElmText -Content '[F] Focused [C] CursorChar [Left/Right] cursor [P] prev [N] next [Q] quit' -Style $hintStyle)) } } 4 { # ----- Textarea: New-ElmTextarea ----- $cursorChar = $script:CURSOR_CHARS[$model.TextareaCursorIdx] $maxVis = $script:TA_MAX_VIS[$model.TextareaMaxVisIdx] $isFocused = $model.TextareaFocused $focusedBoxStyle = New-ElmStyle -Border 'Rounded' $taParams = @{ Lines = $model.TextareaLines CursorRow = $model.TextareaRow CursorCol = $model.TextareaCol MaxVisible = $maxVis ScrollOffset = $model.TextareaOffset Placeholder = 'Press F to focus here!' CursorChar = $cursorChar Style = $inputStyle FocusedStyle = $inputFocStyle FocusedBoxStyle = $focusedBoxStyle } $taNode = if ($isFocused) { New-ElmTextarea @taParams -Focused } else { New-ElmTextarea @taParams } $children.Add((New-ElmText -Content '' )) $children.Add((New-ElmText -Content 'New-ElmTextarea — Multi-line editor' -Style $labelStyle)) $children.Add($taNode) $focusedStr = if ($isFocused) { 'on' } else { 'off' } $taMaxOpts = $script:TA_MAX_VIS -join '/' $cursorOpts = $script:CURSOR_CHARS -join '/' $lineCount = $model.TextareaLines.Count $children.Add((New-ElmText -Content '' )) $children.Add((New-ElmText -Content " Row: $($model.TextareaRow + 1)/$lineCount Col: $($model.TextareaCol)" -Style $hintStyle)) $children.Add((New-ElmText -Content " -Focused $focusedStr [F] -MaxVisible $maxVis [M: $taMaxOpts] -CursorChar '$cursorChar' [C: $cursorOpts]" -Style $configStyle)) $children.Add((New-ElmText -Content ' -FocusedStyle (Black/White bg) -FocusedBoxStyle (Rounded border)' -Style $configStyle)) $children.Add((New-ElmText -Content '' )) if ($isFocused) { $children.Add((New-ElmText -Content '[type] input [Enter] new line [Backspace] delete [Arrow keys] navigate [Esc] unfocus' -Style $hintStyle)) } else { $children.Add((New-ElmText -Content '[F] Focus [M] MaxVisible [C] CursorChar [P] prev [N] next [Q] quit' -Style $hintStyle)) } } 5 { # ----- Table: New-ElmTable ----- $headerStyle = New-ElmStyle -Foreground 'BrightCyan' -Bold $tableSelStyle = New-ElmStyle -Foreground 'BrightYellow' -Bold $children.Add((New-ElmText -Content '' )) $children.Add((New-ElmText -Content 'New-ElmTable — Widget Reference' -Style $labelStyle)) $tableNode = New-ElmTable -Headers $script:TABLE_HEADERS ` -Rows $script:TABLE_ROWS ` -SelectedRow $model.TableCursor ` -Style $listStyle ` -HeaderStyle $headerStyle ` -SelectedStyle $tableSelStyle $children.Add($tableNode) $selected = $script:TABLE_ROWS[$model.TableCursor] $children.Add((New-ElmText -Content '' )) $children.Add((New-ElmText -Content " Row $($model.TableCursor + 1)/$($script:TABLE_ROWS.Count) — $($selected[0])" -Style $hintStyle)) $children.Add((New-ElmText -Content '' )) $children.Add((New-ElmText -Content '[Up/Down] select row [P] prev [N] next [Q] quit' -Style $hintStyle)) } 6 { # ----- Paginator: New-ElmPaginator ----- $pagerMode = $script:PAGER_MODES[$model.PagerModeIdx] $numericStyle = New-ElmStyle -Foreground 'BrightMagenta' $tabStyle = New-ElmStyle -Foreground 'BrightBlack' $activeTabStyle = New-ElmStyle -Foreground 'BrightWhite' -Bold $children.Add((New-ElmText -Content '' )) $children.Add((New-ElmText -Content 'New-ElmPaginator — Navigation widgets' -Style $labelStyle)) $children.Add((New-ElmText -Content '' )) $dotStyle = New-ElmStyle -Foreground 'BrightMagenta' $activeDotStyle = New-ElmStyle -Foreground 'BrightWhite' -Bold # Numeric mode row $numNode = New-ElmPaginator -CurrentPage $model.PagerPage -PageCount $script:PAGER_PAGES -Style $numericStyle $label = New-ElmText -Content ' Numeric: ' -Style $hintStyle $arrow = if ($pagerMode -eq 'Numeric') { New-ElmText -Content ' <- active' -Style $accentStyle } else { New-ElmText -Content '' } $children.Add((New-ElmRow -Children @($label, $numNode, $arrow))) # Dots mode row $dotsNode = New-ElmPaginator -Dots -CurrentPage $model.PagerPage -PageCount $script:PAGER_PAGES ` -Style $dotStyle -ActiveStyle $activeDotStyle $label3 = New-ElmText -Content ' Dots: ' -Style $hintStyle $arrow3 = if ($pagerMode -eq 'Dots') { New-ElmText -Content ' <- active' -Style $accentStyle } else { New-ElmText -Content '' } $children.Add((New-ElmRow -Children @($label3, $dotsNode, $arrow3))) # Tabs mode row $tabNode = New-ElmPaginator -Tabs $script:PAGER_TAB_LABELS -ActiveTab $model.PagerTabIdx ` -Style $tabStyle -ActiveStyle $activeTabStyle $label2 = New-ElmText -Content ' Tabs: ' -Style $hintStyle $arrow2 = if ($pagerMode -eq 'Tabs') { New-ElmText -Content ' <- active' -Style $accentStyle } else { New-ElmText -Content '' } $children.Add((New-ElmRow -Children @($label2, $tabNode, $arrow2))) $modeOpts = $script:PAGER_MODES -join '/' $children.Add((New-ElmText -Content '' )) $children.Add((New-ElmText -Content " Active mode: $pagerMode [M: $modeOpts]" -Style $configStyle)) $children.Add((New-ElmText -Content '' )) $children.Add((New-ElmText -Content '[Left/Right] navigate [M] Mode [P] prev [N] next [Q] quit' -Style $hintStyle)) } } New-ElmBox -Children $children.ToArray() } $subFn = { param($model) $subs = [System.Collections.Generic.List[object]]::new() # Suppress Q/N/P when a text widget is focused so those chars can be typed $textFocused = ($model.Tab -eq 3 -and $model.InputFocused) -or ($model.Tab -eq 4 -and $model.TextareaFocused) if (-not $textFocused) { $subs.Add((New-ElmKeySub -Key 'Q' -Handler { 'Quit' })) $subs.Add((New-ElmKeySub -Key 'N' -Handler { 'TabNext' })) $subs.Add((New-ElmKeySub -Key 'P' -Handler { 'TabPrev' })) } # Timer — always runs (spinner animation + progress auto-advance) $subs.Add((New-ElmTimerSub -IntervalMs 120 -Handler { 'Tick' })) # Panel-specific keys switch ($model.Tab) { 0 { $subs.Add((New-ElmKeySub -Key 'W' -Handler { 'BarWidthCycle' })) $subs.Add((New-ElmKeySub -Key 'F' -Handler { 'FilledCharCycle' })) $subs.Add((New-ElmKeySub -Key 'E' -Handler { 'EmptyCharCycle' })) } 1 { $subs.Add((New-ElmKeySub -Key 'UpArrow' -Handler { 'ListUp' })) $subs.Add((New-ElmKeySub -Key 'DownArrow' -Handler { 'ListDown' })) $subs.Add((New-ElmKeySub -Key 'M' -Handler { 'ListMaxVisCycle' })) $subs.Add((New-ElmKeySub -Key 'V' -Handler { 'ListPrefixCycle' })) } 2 { $subs.Add((New-ElmKeySub -Key 'UpArrow' -Handler { 'ViewUp' })) $subs.Add((New-ElmKeySub -Key 'DownArrow' -Handler { 'ViewDown' })) $subs.Add((New-ElmKeySub -Key 'M' -Handler { 'VpMaxVisCycle' })) } 3 { $subs.Add((New-ElmKeySub -Key 'LeftArrow' -Handler { 'InputLeft' })) $subs.Add((New-ElmKeySub -Key 'RightArrow' -Handler { 'InputRight' })) $subs.Add((New-ElmKeySub -Key 'Backspace' -Handler { 'Backspace' })) if ($model.InputFocused) { # Focused: char sub captures all printable input; Esc unfocuses $subs.Add((New-ElmKeySub -Key 'Escape' -Handler { 'InputToggleFocus' })) $subs.Add((New-ElmCharSub -Handler { param($e) "Input:$([string]$e.Char)" })) } else { # Unfocused: expose config keys $subs.Add((New-ElmKeySub -Key 'F' -Handler { 'InputToggleFocus' })) $subs.Add((New-ElmKeySub -Key 'C' -Handler { 'CursorCharCycle' })) } } 4 { $subs.Add((New-ElmKeySub -Key 'UpArrow' -Handler { 'TextareaUp' })) $subs.Add((New-ElmKeySub -Key 'DownArrow' -Handler { 'TextareaDown' })) $subs.Add((New-ElmKeySub -Key 'LeftArrow' -Handler { 'TextareaLeft' })) $subs.Add((New-ElmKeySub -Key 'RightArrow'-Handler { 'TextareaRight' })) $subs.Add((New-ElmKeySub -Key 'Backspace' -Handler { 'TextareaBackspace' })) if ($model.TextareaFocused) { $subs.Add((New-ElmKeySub -Key 'Enter' -Handler { 'TextareaEnter' })) $subs.Add((New-ElmKeySub -Key 'Escape' -Handler { 'TextareaFocusToggle' })) $subs.Add((New-ElmCharSub -Handler { param($e) "TextareaInput:$([string]$e.Char)" })) } else { $subs.Add((New-ElmKeySub -Key 'F' -Handler { 'TextareaFocusToggle' })) $subs.Add((New-ElmKeySub -Key 'M' -Handler { 'TextareaMaxVisCycle' })) $subs.Add((New-ElmKeySub -Key 'C' -Handler { 'TextareaCursorCycle' })) } } 5 { $subs.Add((New-ElmKeySub -Key 'UpArrow' -Handler { 'TableUp' })) $subs.Add((New-ElmKeySub -Key 'DownArrow' -Handler { 'TableDown' })) } 6 { $subs.Add((New-ElmKeySub -Key 'LeftArrow' -Handler { 'PagerLeft' })) $subs.Add((New-ElmKeySub -Key 'RightArrow' -Handler { 'PagerRight' })) $subs.Add((New-ElmKeySub -Key 'M' -Handler { 'PagerModeCycle' })) } } return $subs.ToArray() } function Invoke-WidgetShowcaseDemo { <# .SYNOPSIS Interactive showcase of the Elm Phase 9 widget library. .DESCRIPTION Cycles through four panels demonstrating all five Phase 9 widgets with live-adjustable configuration options. Global keys: P / N — previous / next panel Q — quit Panel 1 — Animation: W / F / E cycle Width / FilledChar / EmptyChar Panel 2 — List: Up/Down navigate M / V cycle MaxVisible / Prefix Panel 3 — Viewport: Up/Down scroll M cycle MaxVisible Panel 4 — TextInput: Left/Right cursor Backspace delete F / C Focused / CursorChar .NOTES Requires the Elm module. Run from Examples: . .\Invoke-WidgetShowcaseDemo.ps1; Invoke-WidgetShowcaseDemo #> [CmdletBinding()] param() Start-ElmProgram -InitFn $initFn -UpdateFn $updateFn -ViewFn $viewFn -SubscriptionFn $subFn } Invoke-WidgetShowcaseDemo |