Tools/winget.ps1

# Companion for winget — fuzzy pickers built on Invoke-DFPicker + fzf.
# Select-WingetPackage (wins) search → install
# Remove-WingetPackage (wrm) installed → uninstall
# Invoke-WingetUpdate (wup) upgradable → update (multi-select)
#
# Dot-sourced by Register-DFTool when winget is registered. Every user-facing
# function/alias is declared `global:` so it survives Register-DFTool's scope.
#
# Data comes from the Microsoft.WinGet.Client module (objects, no table
# scraping). fzf's --preview and --bind execute() run in a cmd subshell that
# cannot call cmdlets, so those steps use the `winget` CLI for display only.
#
# 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 `winget show` for skipped items; it only
# runs once the cursor rests on one for ~1s.

# Guard: the pickers need the Microsoft.WinGet.Client module. Documented
# dependency, so warn clearly (not silently) when it is missing.
function global:Assert-DFWingetModule {
    if (Get-Command Find-WinGetPackage -ErrorAction Ignore) { return $true }
    Write-Warning "DotForge: the 'Microsoft.WinGet.Client' module is required for the winget pickers (wins/wrm/wup). Install it with: Install-Module Microsoft.WinGet.Client -Scope CurrentUser"
    return $false
}

function global:Select-WingetPackage {
    [CmdletBinding()]
    param([string]$Query = '')

    if (-not (Assert-DFWingetModule)) { return }
    if (-not $Query) { $Query = Read-Host 'Search winget packages' }

    # Build the display lines up front so $Query is resolved here (not in the
    # picker's scope) and close over the resulting plain array.
    $items = @(
        Find-WinGetPackage $Query 2>$null | ForEach-Object {
            ('{0,-40} {1,-34} {2}' -f $_.Name, $_.Id, $_.Version) + "`t" + $_.Id
        }
    )

    $sel = Invoke-DFPicker `
        -List          { $items }.GetNewClosure() `
        -Delimiter     "`t" `
        -WithNth       '1' `
        -Preview       'ping -n 2 127.0.0.1 >nul & winget show --id {2}' `
        -PreviewWindow 'right:60%' `
        -Header        'winget search [Enter=command | Alt-R=install | Alt-I=install in place]' `
        -Parse         { ($_ -split "`t")[1] } `
        -Expect        'alt-r' `
        -Bind          'alt-i:execute(winget install --id {2} --exact)'

    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
        Install-WinGetPackage -Id $id -MatchOption Equals
    }
    else {
        # Enter → return the install command for review / piping / running.
        "winget install --id $id --exact"
    }
}
Set-Alias -Name wins -Value Select-WingetPackage -Scope Global -Force

function global:Remove-WingetPackage {
    [CmdletBinding()]
    param(
        # Optionally restrict the list to one installed-package source
        # (e.g. -Source winget) to hide ARP/registry-only entries.
        [string]$Source = ''
    )

    if (-not (Assert-DFWingetModule)) { return }

    # Get-WinGetPackage -Source does not actually filter the returned set, so
    # filter on the Source property here (blank = ARP/registry-only entries).
    $items = @(
        Get-WinGetPackage 2>$null |
            Where-Object { -not $Source -or $_.Source -eq $Source } |
            ForEach-Object {
                ('{0,-40} {1,-34} {2}' -f $_.Name, $_.Id, $_.InstalledVersion) + "`t" + $_.Id
            }
    )

    $sel = Invoke-DFPicker `
        -List          { $items }.GetNewClosure() `
        -Delimiter     "`t" `
        -WithNth       '1' `
        -Preview       'ping -n 2 127.0.0.1 >nul & winget show --id {2}' `
        -PreviewWindow 'right:60%' `
        -Header        'winget uninstall [Enter=uninstall | Alt-X=uninstall in place | Alt-C=command]' `
        -Parse         { ($_ -split "`t")[1] } `
        -Expect        'alt-c' `
        -Bind          'alt-x:execute(winget uninstall --id {2})'

    if (-not $sel) { return }
    $id = @($sel.Selected)[0]
    if (-not $id) { return }

    if ($sel.Key -eq 'alt-c') {
        "winget uninstall --id $id"
    }
    else {
        Write-Host "⚙ Uninstalling $id…" -ForegroundColor DarkYellow
        Uninstall-WinGetPackage -Id $id -MatchOption Equals
    }
}
Set-Alias -Name wrm -Value Remove-WingetPackage -Scope Global -Force

function global:Invoke-WingetUpdate {
    [CmdletBinding()]
    param()

    if (-not (Assert-DFWingetModule)) { return }

    $sel = Invoke-DFPicker `
        -List {
            Get-WinGetPackage 2>$null | Where-Object IsUpdateAvailable | ForEach-Object {
                $latest = @($_.AvailableVersions)[0]
                ('{0,-36} {1,-30} {2} -> {3}' -f $_.Name, $_.Id, $_.InstalledVersion, $latest) + "`t" + $_.Id
            }
        } `
        -Delimiter     "`t" `
        -WithNth       '1' `
        -Multi `
        -Preview       'ping -n 2 127.0.0.1 >nul & winget show --id {2}' `
        -PreviewWindow 'right:60%' `
        -Header        'winget upgrade [Tab=mark | Enter=upgrade marked | Alt-A=upgrade all]' `
        -Parse         { ($_ -split "`t")[1] } `
        -Expect        'alt-a'

    if (-not $sel) { return }

    if ($sel.Key -eq 'alt-a') {
        Write-Host '⚙ Upgrading all packages…' -ForegroundColor Green
        winget upgrade --all
        return
    }

    foreach ($id in $sel.Selected) {
        if ($id) {
            Write-Host "⚙ Upgrading $id…" -ForegroundColor Green
            Update-WinGetPackage -Id $id -MatchOption Equals
        }
    }
}
Set-Alias -Name wup -Value Invoke-WingetUpdate -Scope Global -Force

# ── PSReadLine binding: Ctrl+G, W ───────────────────────────────────────────
# Type a search term, press Ctrl+G then W: 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.
if (Get-Command Set-PSReadLineKeyHandler -ErrorAction Ignore) {
    Set-PSReadLineKeyHandler -Chord 'Ctrl+g,w' `
        -Description 'DotForge: winget search → install command onto the line' `
        -ScriptBlock {
            $line = $null; $cursor = $null
            [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
            if ([string]::IsNullOrWhiteSpace($line)) { return }
            $cmd = Select-WingetPackage -Query $line
            if ($cmd -is [string] -and $cmd.Trim()) {
                [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()
                [Microsoft.PowerShell.PSConsoleReadLine]::Insert($cmd)
            }
        }
}