Skip to content

Instantly share code, notes, and snippets.

@mikl0s
Created August 18, 2025 08:36
Show Gist options
  • Save mikl0s/4e9fa611386e4b75efb3a47b37bf7e12 to your computer and use it in GitHub Desktop.
Save mikl0s/4e9fa611386e4b75efb3a47b37bf7e12 to your computer and use it in GitHub Desktop.
Winget terminal UI using fzf
# ======================= PowerShell profile (only 'wi') =======================
function wgi {
[CmdletBinding()]
param(
[Parameter(Mandatory=$false)][string]$Query = "",
[switch]$ShowOnly,
[switch]$u # uninstall mode
)
try { [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false) } catch {}
# Check if this is uninstall mode
$isUninstall = [bool]$u
# Validate parameters
if (-not $isUninstall -and [string]::IsNullOrWhiteSpace($Query)) {
Write-Error "Query parameter is required when not in uninstall mode. Use 'wi <query>' to search or 'wi -u' to list installed packages."
return
}
# --- column widths (auto-shrink if terminal is narrow) ---
$total = $Host.UI.RawUI.WindowSize.Width
if (($null -eq $total) -or ($total -lt 60)) { $total = 120 }
$wName=50; $wId=34; $wVer=16; $wMatch=18; $wSrc=10
$padCols = 2*4
$used = $wName + $wId + $wVer + $wMatch + $wSrc + $padCols
if ($used -gt ($total - 8)) {
$scale = [math]::Max(0.6, ($total - 8 - $padCols) / ($wName + $wId + $wVer + $wMatch + $wSrc))
$wName=[int]([math]::Floor($wName*$scale))
$wId =[int]([math]::Floor($wId *$scale))
$wVer =[int]([math]::Floor($wVer *$scale))
$wMatch=[int]([math]::Floor($wMatch*$scale))
$wSrc =[int]([math]::Floor($wSrc *$scale))
}
function Short([string]$s, [int]$n) {
if ([string]::IsNullOrEmpty($s)) { return "" }
if ($s.Length -le $n) { return $s }
if ($n -le 3) { return $s.Substring(0,[math]::Max(0,$n)) }
return $s.Substring(0, $n-3) + '...'
}
# --- winget search or list (table output) ---
if ($isUninstall) {
$lines = winget list 2>&1 | Out-String -Stream
} else {
$lines = winget search $Query 2>&1 | Out-String -Stream
}
if (($null -eq $lines) -or ($lines.Count -eq 0)) {
$action = if ($isUninstall) { "installed packages" } else { "results for '$Query'" }
Write-Host "No $action."; return
}
# find dashed separator; header is the line above it
$sep = $null
for ($i=0; $i -lt $lines.Count; $i++) {
if ($lines[$i] -match '^\s*-{5,}\s*$') { $sep = $i; break }
}
if (($null -eq $sep) -or ($sep -lt 1)) { Write-Warning "Couldn't locate header/separator in winget output."; return }
$headerIdx = $sep - 1
$cols = ($lines[$headerIdx].TrimEnd() -split '\s{2,}') | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }
# map columns case-insensitively
$map = @{}
for ($i=0; $i -lt $cols.Count; $i++) { $map[$cols[$i].ToLower()] = $i }
if (-not $map.ContainsKey('id')) { Write-Warning "Winget header missing 'Id' column."; return }
# build rows for fzf
$start = $sep + 1
$rows = @()
for ($i=$start; $i -lt $lines.Count; $i++) {
$line = $lines[$i].TrimEnd()
if ([string]::IsNullOrWhiteSpace($line)) { continue }
if ($line -match '^\s*-{5,}\s*$') { continue }
$parts = @()
# Try to identify ID patterns more robustly
# Common patterns: text.text, msstore alphanumeric, version numbers
if ($line -match '^(.+?)\s+([^\s]*\.[^\s]*(?:\.[^\s]*)*|[A-Z0-9]{10,}|9[A-Z0-9]{9,})\s+(.*)$') {
# Pattern found: name, ID, rest
$name = $matches[1].Trim()
$id = $matches[2].Trim()
$remaining = $matches[3].Trim()
$parts = @($name, $id)
if ($remaining) {
# Split remaining fields by 1+ spaces (more flexible)
$remainingParts = $remaining -split '\s+' | Where-Object { $_ -ne '' }
$parts += $remainingParts
}
} else {
# Fallback: try original 2+ space method
$spaceSplit = $line -split '\s{2,}'
if ($spaceSplit.Count -gt 1) {
$parts = $spaceSplit
} else {
# Last resort: single space split (risky but better than nothing)
Write-Verbose "Using single-space fallback for: $line"
$parts = $line -split '\s+' | Where-Object { $_ -ne '' }
}
}
if ($parts.Count -le $map['id']) { continue }
$name = if ($map.ContainsKey('name')) { $parts[$map['name']] } else { '' }
$id = $parts[$map['id']]
$ver = if ($map.ContainsKey('version')) { $parts[$map['version']] } else { '' }
$match = if ($map.ContainsKey('match')) { $parts[$map['match']] } else { '' }
$src = if ($map.ContainsKey('source')) { $parts[$map['source']] } else { '' }
if (-not $id) { continue }
$colName = "{0,-$wName}" -f (Short $name $wName)
$colId = "{0,-$wId}" -f (Short $id $wId)
$colVer = "{0,-$wVer}" -f (Short $ver $wVer)
$colMatch = "{0,-$wMatch}" -f (Short $match $wMatch)
$colSrc = "{0,-$wSrc}" -f (Short $src $wSrc)
$pretty = @(
$colName
$colId
$colVer
$colMatch
$colSrc
) -join ' '
# pretty display + hidden fields: 2=id, 3=source
$rows += "$pretty`t$id`t$src"
}
if (-not $rows) { Write-Host "No results."; return }
# fzf header
$action = if ($isUninstall) { "UNINSTALL" } else { "INSTALL" }
$hdr = "$action - " + ('Name'.PadRight($wName) + ' ' +
'Id'.PadRight($wId) + ' ' +
'Version'.PadRight($wVer)+ ' ' +
'Match'.PadRight($wMatch)+ ' ' +
'Source'.PadRight($wSrc))
# PREVIEW: inline PowerShell; build argument array; pass -s <source> when present;
# capture output into an array and print from line 2 onward (skip banner).
# Double-double-quotes keep the whole -Command string intact through fzf -> cmd.
$previewCmd = 'powershell -NoProfile -Command "& {param($i,$s); $a=@(\"show\",\"--id\",$i,\"-e\"); if($s){$a+=@(\"-s\",$s)}; $o=winget @a 2>$null; $o | Select-Object -Skip 4}" {2} {3}'
$fzfOpts = @(
'--with-nth','1',
'--delimiter',"`t",
'--height','100%',
'--margin','0',
'--border','rounded',
'--header',$hdr,
'--preview-window','right,50%,wrap',
'--preview',$previewCmd,
'--cycle','--reverse'
)
$sel = $rows | fzf @fzfOpts
if (-not $sel) { return }
$fields = $sel -split "`t"
$chosenId = $fields[1].Trim()
$chosenSrc = if ($fields.Count -gt 2) { $fields[2].Trim() } else { '' }
if ($ShowOnly) { $chosenId; return }
if ($isUninstall) {
$actionArgs = @('uninstall','--id',$chosenId,'-e')
} else {
$actionArgs = @('install','--id',$chosenId,'-e')
}
if ($chosenSrc) { $actionArgs += @('-s',$chosenSrc) }
& winget @actionArgs
}
Set-Alias wi wgi
# ===================== end profile (only 'wi') =====================
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment