Created
August 18, 2025 08:36
-
-
Save mikl0s/4e9fa611386e4b75efb3a47b37bf7e12 to your computer and use it in GitHub Desktop.
Winget terminal UI using fzf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ======================= 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