Private/Invoke-DFPackageManagerPicker.ps1
|
#Requires -Version 7.0 function Invoke-DFPackageManagerPicker { <# .SYNOPSIS Shared fzf-picker wiring for the scoop/winget/choco package-manager sidecars: delimiter, first-field display, preview debounce prefix, preview window, tab-split parse, and the --expect/--bind pass-through. .DESCRIPTION Each package-manager sidecar (Tools/scoop.ps1, Tools/winget.ps1, Tools/choco.ps1) builds its own item list and its own post-selection action (install/uninstall/update via that package manager's own module or CLI) -- those stay in each sidecar, since they are genuinely different per tool. This helper only collapses the part that was byte-for-byte identical across all nine pickers: the Invoke-DFPicker wiring call itself. .PARAMETER ListItems Scriptblock producing the picker's display lines (tab-delimited: display text first, the parsed key second). .PARAMETER PreviewCommand The package manager's own preview command (e.g. 'scoop info {2}'). The 'ping -n 2 127.0.0.1 >nul &' debounce prefix -- which stops fast scrolling from spawning a preview process per skipped item -- is added here once, instead of at each of the nine call sites. .PARAMETER Header Header text shown at the top of the fzf window. .PARAMETER ExpectKey fzf --expect key name (e.g. 'alt-r', 'alt-c', 'alt-a'). .PARAMETER Bind fzf --bind spec for the in-place execute() key. Omit for update pickers, which have no in-place bind. .PARAMETER Multi Pass -Multi through to Invoke-DFPicker (update pickers only). .OUTPUTS [pscustomobject]@{ Key; Selected } -- see Invoke-DFPicker's -Expect behavior, which every package-manager picker relies on. #> [CmdletBinding()] param( [Parameter(Mandatory)][scriptblock]$ListItems, [Parameter(Mandatory)][string]$PreviewCommand, [Parameter(Mandatory)][string]$Header, [Parameter(Mandatory)][string]$ExpectKey, [string]$Bind, [switch]$Multi ) # Splatted conditionally rather than always passing -Bind $Bind: explicitly # binding a $null scalar to Invoke-DFPicker's [string[]]$Bind parameter gets # coerced into a one-element array containing $null (not a true empty/null # collection), so its `foreach ($b in $Bind)` would iterate once and emit a # bare, argument-less --bind. Omitting the key entirely avoids that. $pickerArgs = @{ List = $ListItems Delimiter = "`t" WithNth = '1' Multi = $Multi Preview = "ping -n 2 127.0.0.1 >nul & $PreviewCommand" PreviewWindow = 'right:60%' Header = $Header Parse = { ($_ -split "`t")[1] } Expect = $ExpectKey } if ($Bind) { $pickerArgs['Bind'] = $Bind } Invoke-DFPicker @pickerArgs } # Each package-manager companion (Tools/winget.ps1, scoop.ps1, choco.ps1) # registers a spec here; its three pickers then call Invoke-DFPackageManagerAction. if (-not (Get-Variable -Name DFPackageManagerSpecs -Scope Script -ErrorAction Ignore)) { $script:DFPackageManagerSpecs = @{} } function Invoke-DFPackageManagerAction { <# .SYNOPSIS Runs one package-manager picker (search and install, uninstall, or update) from that manager's spec. .DESCRIPTION The control flow every package-manager picker shares: check the manager's dependency, build the list, show the picker, then act on the pressed key. Everything that differs between managers (which commands list and act, how a line is formatted, the command strings, the wording) comes from the spec the companion registered in $script:DFPackageManagerSpecs: Name 'winget' Require { $true if usable; warns itself otherwise } SearchPrompt prompt when -Query is empty Preview fzf preview command, {2} is the id Search { param($Query) lines 'display<TAB>id' } Installed { param($Source) lines for installed packages } Outdated { lines for packages with an update } InstallCommand install command, {0} is the id UninstallCommand uninstall command, {0} is the id InPlace { param($command) the command for fzf's execute() keys } Install, Uninstall, Update { param($Id) do it } UpdateAll { update everything } UpdateWord 'upgrade' or 'update' (header text) UpdatingWord 'Upgrading' or 'Updating' (progress text) AllMessage progress text for update-all .PARAMETER Manager The spec name, e.g. 'winget'. .PARAMETER Action Install (search, then return or run the install command), Uninstall, or Update. .PARAMETER Query Search terms for Install; prompted for when empty. .PARAMETER Source Passed to the spec's Installed list (winget filters by it). .OUTPUTS System.String: the install or uninstall command, when the key asks for the command instead of running it. Otherwise none. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Manager, [Parameter(Mandatory)][ValidateSet('Install', 'Uninstall', 'Update')][string]$Action, [string]$Query = '', [string]$Source = '' ) $spec = $script:DFPackageManagerSpecs[$Manager] if (-not (& $spec.Require)) { return } $name = $spec.Name $inPlace = { param($template) & $spec.InPlace ($template -f '{2}') } switch ($Action) { 'Install' { if (-not $Query) { $Query = Read-Host $spec.SearchPrompt } # Resolved here, then closed over as plain data, so the picker's scope # doesn't need $Query. $items = @(& $spec.Search $Query) $sel = Invoke-DFPackageManagerPicker -ListItems { $items }.GetNewClosure() -PreviewCommand $spec.Preview ` -Header "$name search [Enter=command | Alt-R=install | Alt-I=install in place]" ` -ExpectKey 'alt-r' -Bind "alt-i:execute($(& $inPlace $spec.InstallCommand))" if (-not $sel) { return } $id = @($sel.Selected)[0] if (-not $id) { return } # only the in-place Alt-I bind ran; nothing chosen on exit if ($sel.Key -eq 'alt-r') { Write-Host "⚙ Installing $id…" -ForegroundColor Cyan & $spec.Install $id } else { $spec.InstallCommand -f $id # Enter: the command, to review or run } } 'Uninstall' { $items = @(& $spec.Installed $Source) $sel = Invoke-DFPackageManagerPicker -ListItems { $items }.GetNewClosure() -PreviewCommand $spec.Preview ` -Header "$name uninstall [Enter=uninstall | Alt-X=uninstall in place | Alt-C=command]" ` -ExpectKey 'alt-c' -Bind "alt-x:execute($(& $inPlace $spec.UninstallCommand))" if (-not $sel) { return } $id = @($sel.Selected)[0] if (-not $id) { return } if ($sel.Key -eq 'alt-c') { $spec.UninstallCommand -f $id } else { Write-Host "⚙ Uninstalling $id…" -ForegroundColor DarkYellow & $spec.Uninstall $id } } 'Update' { $items = @(& $spec.Outdated) $word = $spec.UpdateWord $sel = Invoke-DFPackageManagerPicker -ListItems { $items }.GetNewClosure() -PreviewCommand $spec.Preview ` -Header "$name $word [Tab=mark | Enter=$word marked | Alt-A=$word all]" -ExpectKey 'alt-a' -Multi if (-not $sel) { return } if ($sel.Key -eq 'alt-a') { Write-Host "⚙ $($spec.AllMessage)" -ForegroundColor Green & $spec.UpdateAll return } foreach ($id in $sel.Selected) { if ($id) { Write-Host "⚙ $($spec.UpdatingWord) $id…" -ForegroundColor Green & $spec.Update $id } } } } } function Register-DFPrefillChord { <# .SYNOPSIS Binds a PSReadLine chord that replaces the command line with the install command a search picker returns. .DESCRIPTION Type a search term, press the chord, pick a package: the term is replaced by the picker's install command, ready to edit or run. Uses the current line as the query and does nothing on an empty line. A no-op when PSReadLine isn't loaded. .PARAMETER Chord The PSReadLine chord, e.g. 'Ctrl+g,w'. .PARAMETER Picker The search picker to run with -Query, e.g. 'Select-WingetPackage'. .PARAMETER Description Shown by Get-PSReadLineKeyHandler. .OUTPUTS None. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Chord, [Parameter(Mandatory)][string]$Picker, [Parameter(Mandatory)][string]$Description ) if (-not (Get-Command Set-PSReadLineKeyHandler -ErrorAction Ignore)) { return } Set-PSReadLineKeyHandler -Chord $Chord -Description $Description -ScriptBlock { $line = $null; $cursor = $null [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor) if ([string]::IsNullOrWhiteSpace($line)) { return } $cmd = & $Picker -Query $line if ($cmd -is [string] -and $cmd.Trim()) { [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine() [Microsoft.PowerShell.PSConsoleReadLine]::Insert($cmd) } }.GetNewClosure() } |