Tools/scoop.ps1
|
# Companion for scoop — dot-sourced by Register-DFTool when scoop is registered. # Invoke-Expression is required by scoop-search's hook registration pattern. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '')] param() # Scoop requires git for bucket operations (scoop bucket add, scoop update, etc.) if (-not (Get-Command git -ErrorAction Ignore)) { Write-Warning 'DotForge: git is not installed — scoop bucket operations will fail. Fix: scoop install git' } # If scoop-search is installed, hook it as the default search provider. # It is dramatically faster than built-in scoop search. # To install: scoop install scoop-search if (Get-Command scoop-search -ErrorAction Ignore) { # scoop-search --hook emits `function scoop { ... }` with no scope modifier. # This companion is dot-sourced from inside Register-DFTool, so an unqualified # function definition lands in that function's scope and is discarded when it # returns — the hook never reaches the prompt. (zoxide avoids this because its # init emits global:-scoped functions.) Promote the hook's scoop function to # global scope so it survives. # # The hook script is a pure function of scoop-search's own build (verified # byte-identical across runs) -- cached keyed to the binary's own file # identity so a scoop-search upgrade regenerates it. See # docs/superpowers/specs/2026-09-05-startup-perf-audit.md. $_scoopHook = Get-DFCachedCommandOutput -Name 'scoop-search-hook' -Executable 'scoop-search' -Generate { & scoop-search --hook | Out-String } # If scoop-search ever emits a global-scoped function itself (proposed # upstream), leave it untouched. Otherwise the Replace forces global scope; # it no-ops (leaving the function local, i.e. the pre-fix behavior) if the # codegen changes in some other way. if ($_scoopHook -notmatch 'function\s+global:scoop\b') { $_scoopHook = $_scoopHook.Replace('function scoop {', 'function global:scoop {') } Invoke-Expression $_scoopHook } else { Write-Verbose 'DotForge: scoop-search not found — install for faster search: scoop install scoop-search' } # ── Fuzzy pickers (sins / srm / sup) ───────────────────────────────────────── # Built on Invoke-DFPicker + fzf, with a live `scoop info` preview pane. # Search uses scoop-search (fast; matches names AND binaries) when present, # else the Scoop module's Find-ScoopApp wildcard. Installed list + typed # install/uninstall/update actions come from the Scoop module (object output, # no `scoop list` table scraping). fzf --bind execute() runs in a cmd subshell # that cannot call cmdlets, so the in-place keys use the `scoop` CLI. # Previews are prefixed with `ping -n 2 127.0.0.1 >nul &` — a ~1s cmd sleep # that debounces the preview: fzf kills the running preview command when the # cursor moves, so scrolling fast never spawns `scoop info` for skipped items. # Guard: the pickers need the Scoop module (Get-ScoopApp + *-ScoopApp actions). function global:Assert-DFScoopModule { <# .SYNOPSIS Returns $true when the Scoop PowerShell module is available; otherwise warns and returns $false. .DESCRIPTION Guard used by the scoop pickers (sins, srm, sup). The warning includes the command that installs the module. Defined by DotForge's scoop companion. .EXAMPLE if (Assert-DFScoopModule) { Get-ScoopApp } Runs a Scoop module cmdlet only when the module is present. .OUTPUTS System.Boolean. .LINK https://github.com/simsrw73/DotForge/blob/main/docs/guide/tools.md #> if (Get-Command Get-ScoopApp -ErrorAction Ignore) { return $true } Write-Warning "DotForge: the 'Scoop' module is required for the scoop pickers (sins/srm/sup). Install it with: Install-Module Scoop -Scope CurrentUser" return $false } # What differs about scoop; the shared flow is Invoke-DFPackageManagerAction. $script:DFPackageManagerSpecs['scoop'] = @{ Name = 'scoop' Require = { Assert-DFScoopModule } SearchPrompt = 'Search scoop apps' Preview = 'scoop info {2}' # Prefer scoop-search (fast, matches binaries); fall back to the module. Search = { param($Query) if (Get-Command scoop-search -ErrorAction Ignore) { $bucket = '' & scoop-search $Query 2>$null | ForEach-Object { if ($_ -match "^'([^']+)' bucket:") { $bucket = $Matches[1]; return } if ($_ -match '^\s+(\S+)\s+\(([^)]+)\)') { ('{0,-34} {1,-14} {2}' -f $Matches[1], $Matches[2], $bucket) + "`t" + $Matches[1] } } } else { Find-ScoopApp -Name "*$Query*" 2>$null | ForEach-Object { ('{0,-34} {1,-14} {2}' -f $_.Name, $_.Version, $_.Source) + "`t" + $_.Name } } } Installed = { param($Source) Get-ScoopApp 2>$null | ForEach-Object { ('{0,-34} {1,-18} {2}' -f $_.Name, $_.Version, $_.Source) + "`t" + $_.Name } } # The Scoop module has no "outdated" query, so offer every installed app. Outdated = { Get-ScoopApp 2>$null | ForEach-Object { ('{0,-34} {1,-18} {2}' -f $_.Name, $_.Version, $_.Source) + "`t" + $_.Name } } InstallCommand = Format-DFInstallCommand -Manager $DFCurrentTool # from this record's installs block UninstallCommand = 'scoop uninstall {0}' InPlace = { param($command) $command } Install = { param($Id) Install-ScoopApp -Name $Id } Uninstall = { param($Id) Uninstall-ScoopApp -Name $Id } Update = { param($Id) Update-ScoopApp -Name $Id } UpdateAll = { scoop update '*' } UpdateWord = 'update' UpdatingWord = 'Updating' AllMessage = 'Updating all apps…' } function global:Select-ScoopPackage { <# .SYNOPSIS Searches scoop buckets, fuzzy-picks an app, and returns or runs its install command. .DESCRIPTION Searches with scoop-search when it is installed (fast, and matches binary names as well as app names), otherwise with the Scoop module's Find-ScoopApp, and lists the results in fzf with a scoop info preview. Only buckets you have added are searched. Keys: Enter return the command 'scoop install <name>' (nothing is installed) Alt-R close the picker and install the selection now Alt-I install the highlighted app without closing the picker Bound to Ctrl+G, S when PSReadLine is loaded: type a search term, press the chord, pick an app, and the install command replaces the line. Defined by DotForge's scoop companion. Requires the Scoop PowerShell module (Install-Module Scoop -Scope CurrentUser; warns and does nothing without it) and fzf (or $Env:Picker). .PARAMETER Query Search terms. When omitted, you are prompted. .EXAMPLE sins ripgrep Pick ripgrep and press Enter to get: scoop install ripgrep .OUTPUTS System.String (the install command) on Enter; otherwise none. .LINK https://github.com/simsrw73/DotForge/blob/main/docs/guide/tools.md #> [CmdletBinding()] param([string]$Query = '') Invoke-DFPackageManagerAction -Manager scoop -Action Install -Query $Query } Set-Alias -Name sins -Value Select-ScoopPackage -Scope Global -Force function global:Remove-ScoopPackage { <# .SYNOPSIS Fuzzy-picks an installed scoop app and uninstalls it. .DESCRIPTION Lists installed apps (Get-ScoopApp) in fzf with a scoop info preview. Keys: Enter uninstall the selection (Uninstall-ScoopApp) Alt-X uninstall the highlighted app without closing the picker Alt-C return the command 'scoop uninstall <name>' instead Defined by DotForge's scoop companion. Requires the Scoop PowerShell module (Install-Module Scoop -Scope CurrentUser; warns and does nothing without it) and fzf (or $Env:Picker). .EXAMPLE srm Pick an installed app and press Enter to uninstall it. .OUTPUTS System.String (the uninstall command) on Alt-C; otherwise none. .LINK https://github.com/simsrw73/DotForge/blob/main/docs/guide/tools.md #> [CmdletBinding()] param() Invoke-DFPackageManagerAction -Manager scoop -Action Uninstall } Set-Alias -Name srm -Value Remove-ScoopPackage -Scope Global -Force function global:Invoke-ScoopUpdate { <# .SYNOPSIS Fuzzy-picks installed scoop apps and updates them. .DESCRIPTION Lists every installed app (the Scoop module cannot list only outdated ones) in fzf with a scoop info preview. Updating an app that is already current does nothing. Keys: Tab mark an app (repeat for several) Enter update the marked apps, or the highlighted one Alt-A run scoop update '*' (every app) Defined by DotForge's scoop companion. Requires the Scoop PowerShell module (Install-Module Scoop -Scope CurrentUser; warns and does nothing without it) and fzf (or $Env:Picker). .EXAMPLE sup Mark apps with Tab, then press Enter to update them. .OUTPUTS None. scoop writes its own progress. .LINK https://github.com/simsrw73/DotForge/blob/main/docs/guide/tools.md #> [CmdletBinding()] param() Invoke-DFPackageManagerAction -Manager scoop -Action Update } Set-Alias -Name sup -Value Invoke-ScoopUpdate -Scope Global -Force # ── PSReadLine binding: Ctrl+G, S ─────────────────────────────────────────── # Type a search term, press Ctrl+G then S: pick in fzf, and the install command # lands on the command line (editable — press Enter to run). Uses the current # line as the query. Guarded so it is a no-op when PSReadLine is unavailable. Register-DFPrefillChord -Chord 'Ctrl+g,s' -Picker 'Select-ScoopPackage' ` -Description 'DotForge: scoop search → install command onto the line' |