src/UI/60-Gallery.ps1
|
# 60-Gallery.ps1 — search & browse the PowerShell Gallery from the UI (#38); # Enter adds a result straight into a config file. function script:Build-PSMMGalleryView { param( [Parameter(Mandatory)] $State, [Parameter(Mandatory)] $Results, [Parameter(Mandatory)][string]$Query, [string]$StatusMarkup ) if (Test-PSMMWinTooSmall) { return (Get-PSMMTooSmallView) } $n = $Results.Count $win = Get-PSMMWinSize $vp = Get-PSMMViewport -State $State -Count $n -Rows ($win.Height - 11) # Column budget. New-PSMMTable pads every cell by two, and the panel adds # its two borders plus a two-wide cursor slot, so the sum of the COLUMN # WIDTHS has to fit in (window - 4 - 2 per column). Spectre does not # overflow a table that is too wide, it REFLOWS it: every row bursts into # fragments stacked over several lines and the screen becomes unreadable. # The caps here used to be constants (name 40, by 18, description # window-78), which the widest real rows walk straight through - # Microsoft.Online.SharePoint.PowerShell at 16.0.27424.12000 is 53 # characters of name and version on its own. $showDownloads = $win.Width -ge 100 # narrower than that there is no room $showBy = $win.Width -ge 80 $byCap = if ($win.Width -ge 110) { 18 } elseif ($win.Width -ge 90) { 14 } else { 10 } # measure the rows actually on screen. Each header word is its column's # floor, and the version is never truncated (§11: the prerelease label is # part of it) - so the version is what the rest has to fit around. $nameW = 4; $verW = 7; $byW = 2; $dlW = 9 for ($i = $vp.First; $i -le $vp.Last; $i++) { $r = $Results[$i] $nameW = [Math]::Max($nameW, [Math]::Min(40, "$($r.Name)".Length)) $verW = [Math]::Max($verW, (Get-PSMMVersionText -Version $r.Version -Prerelease $r.Prerelease).Length) $byW = [Math]::Max($byW, [Math]::Min($byCap, "$($r.Author)".Length)) } # name and description share what the fixed columns leave. A description # too narrow to say anything is worse than none at all, so in that case it # is dropped and the name gets the space - the context line under the # table carries the full description for the cursor row anyway. $showDesc = $true $nameCap = $nameW $descCap = 0 foreach ($pass in 1, 2) { $count = 2 + [int]$showBy + [int]$showDownloads + [int]$showDesc $budget = [Math]::Max(20, $win.Width - 4 - (2 * $count)) $fixed = $verW + $(if ($showBy) { $byW } else { 0 }) + $(if ($showDownloads) { $dlW } else { 0 }) $room = [Math]::Max(10, $budget - $fixed) if (-not $showDesc) { $nameCap = [int][Math]::Max(8, [Math]::Min($nameW, $room)); break } $nameCap = [int][Math]::Max(10, [Math]::Min($nameW, [Math]::Floor($room * 0.6))) $descCap = [int]($room - $nameCap) if ($descCap -ge 12) { break } $showDesc = $false } $headers = [System.Collections.Generic.List[string]]::new() $headers.Add('name'); $headers.Add('version') if ($showBy) { $headers.Add('by') } if ($showDownloads) { $headers.Add('downloads') } if ($showDesc) { $headers.Add('description') } $rows = [System.Collections.Generic.List[string[]]]::new() for ($i = $vp.First; $i -le $vp.Last; $i++) { $r = $Results[$i] $nm = ConvertTo-PSMMSafe (Get-PSMMTrunc $r.Name $nameCap) if ($i -eq $State.Cursor) { $nm = "[bold $script:PSMM_ColAccent]$nm[/]" } $cells = [System.Collections.Generic.List[string]]::new() $cells.Add($nm) # gallery results can now BE prereleases (Search-PSMMGallery retries # with them when nothing stable matches), so the label has to show $cells.Add((Get-PSMMVersionMarkup -Version $r.Version -Prerelease $r.Prerelease)) if ($showBy) { $cells.Add((ConvertTo-PSMMSafe (Get-PSMMTrunc "$($r.Author)" $byW))) } if ($showDownloads) { $cells.Add("[$script:PSMM_ColDim]$(ConvertTo-PSMMSafe (Format-PSMMDownloadCount $r.Downloads))[/]") } if ($showDesc) { $cells.Add((ConvertTo-PSMMSafe (Get-PSMMTrunc ($r.Description -replace '\s+', ' ') $descCap))) } $rows.Add([string[]]$cells) } $T = New-PSMMTable -Headers ([string[]]$headers) -Rows $rows -CursorRow ($State.Cursor - $vp.First) $pos = Get-PSMMPositionMarkup -State $State -Count $n -Viewport $vp $items = [System.Collections.Generic.List[Spectre.Console.Rendering.IRenderable]]::new() $items.Add([Spectre.Console.Markup]::new((Get-PSMMHeaderBar -Breadcrumb @('home', 'gallery') -CountsMarkup "[$script:PSMM_ColDim]search: $(ConvertTo-PSMMSafe $Query)[/]$pos"))) $items.Add($T) if ($n) { $cur = $Results[$State.Cursor] # §5 context line: the cursor row spelled out, including the two facts # the columns cannot carry - which repository it came from (only worth # saying when it is NOT the public gallery) and the exact download count $bits = [System.Collections.Generic.List[string]]::new() $bits.Add("by $($cur.Author)") if ("$($cur.Repository)" -and "$($cur.Repository)" -ne 'PSGallery') { $bits.Add("from $($cur.Repository)") } if ([long]"0$($cur.Downloads)" -gt 0) { $bits.Add("$('{0:N0}' -f [long]$cur.Downloads) downloads") } $bits.Add(($cur.Description -replace '\s+', ' ')) $items.Add([Spectre.Console.Markup]::new("[$script:PSMM_ColMute]$(ConvertTo-PSMMSafe (Get-PSMMTrunc ($bits -join ' - ') ($win.Width - 6)))[/]")) } $items.Add([Spectre.Console.Markup]::new((Get-PSMMHint -Pairs @('enter=add to config', 'left/right=back / add', '/=new search')))) $items.Add([Spectre.Console.Markup]::new((Get-PSMMPersistentHint -Pairs @("g=goto$([char]0x2026)", '?=help', 'esc=back', '^q=quit')))) if ($StatusMarkup) { $items.Add([Spectre.Console.Markup]::new($StatusMarkup)) } [Spectre.Console.Rows]::new($items) } function script:Show-PSMMGallery { $ui = $script:PSMM_UI $query = '' $results = @() while ($true) { if ($ui.HardQuit -or $ui.Goto) { return } # prompt for a (new) search Clear-PSMMScreen Write-PSMMLine "[$script:PSMM_ColAccent]Search the PowerShell Gallery[/]" Write-PSMMLine "[$script:PSMM_ColMute]a word searches names, descriptions and tags ('excel'); a pattern matches[/]" Write-PSMMLine "[$script:PSMM_ColMute]names only ('Az.*', 'Microsoft.Graph*'); empty cancels[/]" $query = Read-PSMMText -Message 'Search' -AllowEmpty if ([string]::IsNullOrWhiteSpace($query)) { return } Write-PSMMLine "[$script:PSMM_ColAccent]searching the gallery for '$(ConvertTo-PSMMSafe $query)'...[/]" $search = Search-PSMMGallery -Query $query $results = @($search.Results) if (-not $results.Count) { # never a bare "No results." - an empty screen has to say whether # the gallery had nothing or the search itself failed (gh#17) Write-PSMMLine "[$script:PSMM_ColWarn]No results.[/]" if ($search.Note) { Write-PSMMProse -Text $search.Note -Colour $script:PSMM_ColMute } if (-not (Wait-PSMMKey -Message 'search again')) { return } continue } $st = New-PSMMListState $st.Status = "[$script:PSMM_ColOk]$($results.Count) result(s)[/]" # a fallback fired (pattern found nothing, or the endpoint was down): # the results on screen are not the ones that were asked for, so say so if ($search.Note) { $st.Status += " [$script:PSMM_ColMute]$(ConvertTo-PSMMSafe $search.Note)[/]" } $action = @{ Name = $null } Clear-PSMMScreen Invoke-PSMMLive -Body { param($ctx) while ($true) { if ($script:PSMM_UI.HardQuit) { return } $ctx.UpdateTarget((Build-PSMMGalleryView -State $st -Results $results -Query $query -StatusMarkup $st.Status)) $ctx.Refresh() $k = Read-PSMMKeyResize if ($null -eq $k) { continue } if (Test-PSMMHardQuitKey $k) { $script:PSMM_UI.HardQuit = $true; return } if ($k.KeyChar -eq 'g') { $dest = Read-PSMMGotoKey -BaseRenderable (Build-PSMMGalleryView -State $st -Results $results -Query $query -StatusMarkup $st.Status) if ($dest) { $script:PSMM_UI.Goto = $dest; return } continue } if (Test-PSMMHomeKey $k) { $script:PSMM_UI.Goto = 'home'; return } $st.Status = '' if (Invoke-PSMMListNav -State $st -KeyInfo $k -Count $results.Count) { continue } # left/right: same everywhere (gh#7) - right adds, like enter $drill = Get-PSMMDrillKey -KeyInfo $k if ($drill -eq 'out') { $action.Name = 'back'; return } if ($drill -eq 'in') { $action.Name = 'add'; return } switch ($k.Key) { ([ConsoleKey]::Enter) { $action.Name = 'add'; return } ([ConsoleKey]::Escape) { $action.Name = 'back'; return } ([ConsoleKey]::Oem2) { if ($k.KeyChar -eq '?') { $action.Name = 'help'; return } $action.Name = 'newsearch'; return } default { if ($k.KeyChar -eq '/') { $action.Name = 'newsearch'; return } if ($k.KeyChar -eq '?') { $action.Name = 'help'; return } } } } } if ($ui.HardQuit) { return } switch ($action.Name) { 'back' { return } 'help' { Show-PSMMHelpScreen -Topic 'gallery' } 'add' { $pick = $results[$st.Cursor] $pseudo = [pscustomobject]@{ Name = $pick.Name; Description = $pick.Description } $null = Add-PSMMUnmanagedEntry -Entry $pseudo } # 'newsearch' just falls through to the outer loop's prompt } } } |