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 {
    <#
    .SYNOPSIS
        Returns $true when the Microsoft.WinGet.Client module is available; otherwise warns and returns $false.
    .DESCRIPTION
        Guard used by the winget pickers (wins, wrm, wup). The warning includes
        the command that installs the module. Defined by DotForge's winget
        companion.
    .EXAMPLE
        if (Assert-DFWingetModule) { Get-WinGetPackage }

        Runs a WinGet.Client 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 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
}

# What differs about winget; the shared flow is Invoke-DFPackageManagerAction.
$script:DFPackageManagerSpecs['winget'] = @{
    Name             = 'winget'
    Require          = { Assert-DFWingetModule }
    SearchPrompt     = 'Search winget packages'
    Preview          = 'winget show --id {2}'
    Search           = { param($Query)
        Find-WinGetPackage $Query 2>$null | ForEach-Object {
            ('{0,-40} {1,-34} {2}' -f $_.Name, $_.Id, $_.Version) + "`t" + $_.Id
        } }
    # Get-WinGetPackage -Source does not actually filter the returned set, so
    # filter on the Source property here (blank = ARP/registry-only entries).
    Installed        = { param($Source)
        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
        } }
    Outdated         = {
        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
        } }
    InstallCommand   = Format-DFInstallCommand -Manager $DFCurrentTool   # from this record's installs block
    UninstallCommand = 'winget uninstall --id {0}'
    InPlace          = { param($command) $command }
    Install          = { param($Id) Install-WinGetPackage -Id $Id -MatchOption Equals }
    Uninstall        = { param($Id) Uninstall-WinGetPackage -Id $Id -MatchOption Equals }
    Update           = { param($Id) Update-WinGetPackage -Id $Id -MatchOption Equals }
    UpdateAll        = { winget upgrade --all }
    UpdateWord       = 'upgrade'
    UpdatingWord     = 'Upgrading'
    AllMessage       = 'Upgrading all packages…'
}

function global:Select-WingetPackage {
    <#
    .SYNOPSIS
        Searches winget, fuzzy-picks a package, and returns or runs its install command.
    .DESCRIPTION
        Runs Find-WinGetPackage for -Query and lists the results (name, id,
        version) in fzf. The preview pane shows winget show for the
        highlighted package after the cursor rests on it for about a second.

        Keys:
          Enter return the command 'winget install --id <id> --exact' (nothing
                  is installed; review it, pipe it, or run it)
          Alt-R close the picker and install the selection now
          Alt-I install the highlighted package without closing the picker

        Bound to Ctrl+G, W when PSReadLine is loaded: type a search term, press
        the chord, pick a package, and the install command replaces the line.

        Defined by DotForge's winget companion. Requires the
        Microsoft.WinGet.Client module (warns and does nothing without it) and
        fzf (or $Env:Picker).
    .PARAMETER Query
        Search terms passed to Find-WinGetPackage. When omitted, you are prompted.
    .EXAMPLE
        wins ripgrep

        Pick ripgrep and press Enter to get:
        winget install --id BurntSushi.ripgrep.MSVC --exact
    .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 winget -Action Install -Query $Query
}
Set-Alias -Name wins -Value Select-WingetPackage -Scope Global -Force

function global:Remove-WingetPackage {
    <#
    .SYNOPSIS
        Fuzzy-picks an installed package and uninstalls it with winget.
    .DESCRIPTION
        Lists installed packages (Get-WinGetPackage) in fzf with a winget show
        preview.

        Keys:
          Enter uninstall the selection (Uninstall-WinGetPackage)
          Alt-X uninstall the highlighted package without closing the picker
          Alt-C return the command 'winget uninstall --id <id>' instead

        Defined by DotForge's winget companion. Requires the
        Microsoft.WinGet.Client module and fzf (or $Env:Picker).
    .PARAMETER Source
        Show only packages from this installed-package source (e.g. winget),
        hiding apps winget only knows from the Windows Apps & Features list.
        Default: all.
    .EXAMPLE
        wrm -Source winget

        Lists only winget-installed packages; Enter uninstalls the selection.
    .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([string]$Source = '')
    Invoke-DFPackageManagerAction -Manager winget -Action Uninstall -Source $Source
}
Set-Alias -Name wrm -Value Remove-WingetPackage -Scope Global -Force

function global:Invoke-WingetUpdate {
    <#
    .SYNOPSIS
        Fuzzy-picks packages with available upgrades and updates them with winget.
    .DESCRIPTION
        Lists installed packages that have an update (installed -> latest
        version) in fzf with a winget show preview.

        Keys:
          Tab mark a package (repeat for several)
          Enter update the marked packages, or the highlighted one
          Alt-A run winget upgrade --all

        Defined by DotForge's winget companion. Requires the
        Microsoft.WinGet.Client module and fzf (or $Env:Picker).
    .EXAMPLE
        wup

        Mark packages with Tab, then press Enter to update them.
    .OUTPUTS
        None. winget writes its own progress.
    .LINK
        https://github.com/simsrw73/DotForge/blob/main/docs/guide/tools.md
    #>

    [CmdletBinding()]
    param()
    Invoke-DFPackageManagerAction -Manager winget -Action Update
}
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.
Register-DFPrefillChord -Chord 'Ctrl+g,w' -Picker 'Select-WingetPackage' `
    -Description 'DotForge: winget search → install command onto the line'