Tools/choco.ps1

# Companion for choco — fuzzy pickers built on Invoke-DFPicker + fzf.
# Select-ChocoPackage (cins) search → install
# Remove-ChocoPackage (crm) installed → uninstall
# Invoke-ChocoUpdate (cup) outdated → upgrade (multi-select)
#
# Dot-sourced by Register-DFTool when choco is registered. Every user-facing
# function/alias is declared `global:` so it survives Register-DFTool's scope.
#
# Chocolatey has no first-class object module, but its CLI has a machine-readable
# mode (`-r` / `--limit-output`) that emits pipe-delimited rows — so we split on
# '|' instead of scraping aligned columns. install/uninstall/upgrade need
# elevation; they run through gsudo when it is available (see Invoke-DFChocoElevated).
#
# 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 `choco info` for skipped items.

# Guard: choco must be on PATH.
function global:Assert-DFChoco {
    if (Get-Command choco -ErrorAction Ignore) { return $true }
    Write-Warning 'DotForge: choco is not installed. See https://chocolatey.org/install'
    return $false
}

# Run a choco command elevated via gsudo when present; otherwise run directly
# (choco will fail/prompt for elevation itself if the shell is not admin).
function global:Invoke-DFChocoElevated {
    param([Parameter(ValueFromRemainingArguments)][string[]]$ChocoArgs)
    if (Get-Command gsudo -ErrorAction Ignore) { gsudo choco @ChocoArgs }
    else { choco @ChocoArgs }
}

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

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

    $items = @(
        choco search $Query -r 2>$null | Where-Object { $_ -match '\|' } | ForEach-Object {
            $parts = $_ -split '\|'
            ('{0,-38} {1}' -f $parts[0], $parts[1]) + "`t" + $parts[0]
        }
    )

    # gsudo-aware command for the in-place execute() key (runs in a cmd subshell).
    $run = if (Get-Command gsudo -ErrorAction Ignore) { 'gsudo choco' } else { 'choco' }

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

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

    if ($sel.Key -eq 'alt-r') {
        Write-Host "⚙ Installing $id…" -ForegroundColor Cyan
        Invoke-DFChocoElevated install $id -y
    }
    else {
        "choco install $id -y"
    }
}
Set-Alias -Name cins -Value Select-ChocoPackage -Scope Global -Force

function global:Remove-ChocoPackage {
    [CmdletBinding()]
    param()

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

    $items = @(
        choco list -r 2>$null | Where-Object { $_ -match '\|' } | ForEach-Object {
            $parts = $_ -split '\|'
            ('{0,-38} {1}' -f $parts[0], $parts[1]) + "`t" + $parts[0]
        }
    )

    $run = if (Get-Command gsudo -ErrorAction Ignore) { 'gsudo choco' } else { 'choco' }

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

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

    if ($sel.Key -eq 'alt-c') {
        "choco uninstall $id -y"
    }
    else {
        Write-Host "⚙ Uninstalling $id…" -ForegroundColor DarkYellow
        Invoke-DFChocoElevated uninstall $id -y
    }
}
Set-Alias -Name crm -Value Remove-ChocoPackage -Scope Global -Force

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

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

    # `choco outdated -r` → name|current|available|pinned
    $items = @(
        choco outdated -r 2>$null | Where-Object { $_ -match '\|' } | ForEach-Object {
            $parts = $_ -split '\|'
            ('{0,-34} {1} -> {2}' -f $parts[0], $parts[1], $parts[2]) + "`t" + $parts[0]
        }
    )

    $sel = Invoke-DFPicker `
        -List          { $items }.GetNewClosure() `
        -Delimiter     "`t" `
        -WithNth       '1' `
        -Multi `
        -Preview       'ping -n 2 127.0.0.1 >nul & choco info {2}' `
        -PreviewWindow 'right:60%' `
        -Header        'choco 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
        Invoke-DFChocoElevated upgrade all -y
        return
    }

    foreach ($id in $sel.Selected) {
        if ($id) {
            Write-Host "⚙ Upgrading $id…" -ForegroundColor Green
            Invoke-DFChocoElevated upgrade $id -y
        }
    }
}
Set-Alias -Name cup -Value Invoke-ChocoUpdate -Scope Global -Force

# ── PSReadLine binding: Ctrl+G, C ───────────────────────────────────────────
# Type a search term, press Ctrl+G then C: 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,c' `
        -Description 'DotForge: choco 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-ChocoPackage -Query $line
            if ($cmd -is [string] -and $cmd.Trim()) {
                [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()
                [Microsoft.PowerShell.PSConsoleReadLine]::Insert($cmd)
            }
        }
}