wingetposh.psm1

[Flags()] enum Styles {
  Normal = 0
  Underline = 1
  Bold = 2
  Reversed = 3
}

class upgradeSoftware {
  [boolean]$Selected
  [string]$Name
  [string]$Id
  [string]$Version
  [string]$AvailableVersion
  [string]$Source
}
  
class installSoftware {
  [string]$Name
  [string]$id
  [string]$Version
  [String]$Source
}
  
  
class Frame {
  [char]$UL
  [char]$UR
  [char]$TOP
  [char]$LEFT
  [char]$RIGHT
  [char]$BL
  [char]$BR
  [char]$BOTTOM
  [char]$LEFTSPLIT
  [char]$RIGHTSPLIT
  
  Frame (
    [bool]$Double
  ) {
    if ($Double) {
      $this.UL = "╔"
      $this.UR = "╗"
      $this.TOP = "═"
      $this.LEFT = "║"
      $this.RIGHT = "║"
      $this.BL = "╚"
      $this.BR = "╝"
      $this.BOTTOM = "═"
      $this.LEFTSPLIT = "⊫"
    }
    else {
      $this.UL = "┌"
      $this.UR = "┐"
      $this.TOP = "─"
      $this.LEFT = "│"
      $this.RIGHT = "│"
      $this.BL = "└"
      $this.BR = "┘"
      $this.BOTTOM = "─"
    }
  }
}
  
$Single = [Frame]::new($false)
$Double = [Frame]::new($true)
  
class column {
  [string]$Name
  [Int16]$Position
  [Int16]$Len
}
    
class window {
  [int]$X
  [int]$Y
  [int]$W
  [int]$H
  [Frame]$frameStyle
  [System.ConsoleColor]$frameColor
  [string]$title = ""
  [System.ConsoleColor]$titleColor
  [string]$footer = ""
  [int]$page = 1
  [int]$nbPages = 1
  
  window(
    [int]$X,
    [int]$y,
    [int]$w,
    [int]$h,
    [bool]$Double,
    [System.ConsoleColor]$color = "White"
  ) {
    $this.X = $X
    $this.Y = $y
    $this.W = $W
    $this.H = $H
    $this.frameStyle = [Frame]::new($Double)
    $this.frameColor = $color
      
  }
  
  window(
    [int]$X,
    [int]$y,
    [int]$w,
    [int]$h,
    [bool]$Double,
    [System.ConsoleColor]$color = "White",
    [string]$title = "",
    [System.ConsoleColor]$titlecolor = "Blue"
  ) {
    $this.X = $X
    $this.Y = $y
    $this.W = $W
    $this.H = $H
    $this.frameStyle = [Frame]::new($Double)
    $this.frameColor = $color
    $this.title = $title
    $this.titleColor = $titlecolor
  }
  
  [void] setPosition(
    [int]$X,
    [int]$Y
  ) {
    [System.Console]::SetCursorPosition($X, $Y)
  }
  
  [void] drawWindow() {
    $esc = $([char]0x1b)

    [System.Console]::CursorVisible = $false
    $this.setPosition($this.X, $this.Y)
    $bloc1 = $this.frameStyle.UL, "".PadLeft($this.W - 2, $this.frameStyle.TOP), $this.frameStyle.UR -join ""
    #$blank = "".PadLeft($this.W , " ")
    $blank = "$esc[38;5;15m$($this.frameStyle.LEFT)", "".PadLeft($this.W - 2, " "), "$esc[38;5;15m$($this.frameStyle.RIGHT)" -join ""
    Write-Host $bloc1 -ForegroundColor $this.frameColor -NoNewline
    for ($i = 1; $i -lt $this.H; $i++) {
      $Y2 = $this.Y + $i
      $X3 = $this.X 
      $this.setPosition($X3, $Y2)
      Write-Host $blank     
    }
    $Y2 = $this.Y + $this.H
    $this.setPosition( $this.X, $Y2)
    $bloc1 = $this.frameStyle.BL, "".PadLeft($this.W - 2, $this.frameStyle.BOTTOM), $this.frameStyle.BR -join ""
    Write-Host $bloc1 -ForegroundColor $this.frameColor -NoNewline
    $this.drawTitle()
    $this.drawFooter()
  }
  
  
  [void] drawVersion() {
    $version = $this.frameStyle.LEFT, [string]$(Get-InstalledModule -Name wingetposh -ErrorAction Ignore).version, $this.frameStyle.RIGHT -join ""
    [System.Console]::setcursorposition($this.W - ($version.Length + 6), $this.Y )
    [console]::write($version)
  }
  
  [void] drawTitle() {
    if ($this.title -ne "") {
      $local:X = $this.x + 2
      $this.setPosition($local:X, $this.Y)
      Write-Host "| " -NoNewline -ForegroundColor $this.frameColor
      $local:X = $local:X + 2
      $this.setPosition($local:X, $this.Y)
      Write-Host $this.title -NoNewline -ForegroundColor $this.titleColor
      $local:X = $local:X + $this.title.Length
      $this.setPosition($local:X, $this.Y)
      Write-Host " |" -NoNewline -ForegroundColor $this.frameColor
    }
  }
  
  [void] drawFooter() {
    $Y2 = $this.Y + $this.H
    $this.setPosition( $this.X, $Y2)
    $bloc1 = $this.frameStyle.BL, "".PadLeft($this.W - 2, $this.frameStyle.BOTTOM), $this.frameStyle.BR -join ""
    Write-Host $bloc1 -ForegroundColor $this.frameColor -NoNewline
    if ($this.footer -ne "") {
      $local:x = $this.x + 2
      $local:Y = $this.Y + $this.h
      $this.setPosition($local:X, $local:Y)
      [console]::write($this.footer)
    }
  }
  
  [void] drawPagination() {
    $sPages = ('Page {0}/{1}' -f ($this.page, $this.nbPages))
    [System.Console]::setcursorposition($this.W - ($sPages.Length + 6), $this.Y + $this.H)
    [console]::write($sPages)
  }
  
  [void] clearWindow() {
    $local:blank = "".PadLeft($this.W, " ") 
    for ($i = 1; $i -lt $this.H; $i++) {
      $this.setPosition(($this.X), ($this.Y + $i))
      Write-Host $blank 
    } 
  }
}
  
$columns = [ordered]@{}
  
function getSearchTerms {
  $WinWidth = [System.Console]::WindowWidth
  $X = 0
  $Y = [System.Console]::WindowHeight - 6
  $WinHeigt = 4
  
  $win = [window]::new($X, $Y, $WinWidth, $WinHeigt, $false, "White");
  $win.title = "Search"
  $Win.titleColor = "Green"
  $win.footer = "$(color "[Enter]" "red") : Accept $(color "[Ctrl-C]" "red") : Abort"
  $win.drawWindow();
  $win.setPosition($X + 2, $Y + 2);
  [System.Console]::Write('Package : ')
  [system.console]::CursorVisible = $true
  $pack = [ System.Console]::ReadLine()
  return $pack
}
  

function getColumnsHeaders {
  param(
    [parameter (
      Mandatory
    )]
    [string]$columsLine   
  )
  
  $tempCols = $columsLine.Split(" ")
  $cols = @()
  $result = @()
  foreach ($column in $tempCols) {
    if ($column.Trim() -ne "") {
      $cols += $column
    }
  }
    
  $i = 0
  while ($i -lt $Cols.Length) {
    $pos = $columsLine.IndexOf($Cols[$i])
    if ($i -eq $Cols.Length - 1) {
      #Last Column
      $len = $columsLine.Length - $pos
    }
    else {
      #Not Last Column
      $pos2 = $columsLine.IndexOf($Cols[$i + 1])
      $len = $pos2 - $pos
    }
    $acolumn = [column]::new()
    $acolumn.Name = $Cols[$i]
    $acolumn.Position = $pos
    $acolumn.Len = $len
    $result += $acolumn
    $i++
  }
  $result
}

function color {
  param (
    $Text,
    $ForegroundColor = 'default',
    $BackgroundColor = 'default'
  )
  # Terminal Colors
  $Colors = @{
    "default"    = @(40, 50)
    "black"      = @(30, 0)
    "lightgrey"  = @(33, 43)
    "grey"       = @(37, 47)
    "darkgrey"   = @(90, 100)
    "red"        = @(91, 101)
    "darkred"    = @(31, 41)
    "green"      = @(92, 102)
    "darkgreen"  = @(32, 42)
    "yellow"     = @(93, 103)
    "white"      = @(97, 107)
    "brightblue" = @(94, 104)
    "darkblue"   = @(34, 44)
    "indigo"     = @(35, 45)
    "cyan"       = @(96, 106)
    "darkcyan"   = @(36, 46)
  }
  
  if ( $ForegroundColor -notin $Colors.Keys -or $BackgroundColor -notin $Colors.Keys) {
    Write-Error "Invalid color choice!" -ErrorAction Stop
  }
  
  "$([char]27)[$($colors[$ForegroundColor][0])m$([char]27)[$($colors[$BackgroundColor][1])m$($Text)$([char]27)[0m"    
}
  
function Invoke-Winget {
  param (
    [string]$cmd
  )
  [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 
  #$locals = getWingetLocals
  $TerminalWidth = $Host.UI.RawUI.BufferSize.Width - 2

  $SearchResult = Invoke-Expression $cmd | Out-String -Width $TerminalWidth
  [string[]]$lines = $SearchResult -Split [Environment]::NewLine
  
  $fl = 0
  while (-not $lines[$fl].StartsWith("----")) {
    $fl++
  }
  
  $cols = getColumnsHeaders -columsLine $lines[$fl - 1]
  $lWidth = $lines[$fl].Length
  
  $PackageList = @()
  $columns.Clear()
  foreach ($col in [column[]]$cols) {
    if ($col.name -ne "source") {
    $colPercent = [Math]::Round(($col.Len / $lWidth * 100) - 0.99, 2)
    $colWidth = [System.Math]::Truncate($TerminalWidth / 100 * $colPercent);
    }
    else {
      $colWidth = $col.len  
    }
    $Columns.Add($col.Name, @($col.Position, $colWidth, $col.len))
  }
  
  For ($i = $fl + 1; $i -lt $lines.Length; $i++) {
    $line = $lines[$i]
    if (-not $line.StartsWith('-')) {
      foreach ($column in $columns) {
        $package = [ordered]@{}
        try {
          foreach ($key in $column.keys) {
            $curcol = $column[$key]
            $field = $line.Substring($curcol[0], $curcol[2])
            #Write-Host ("{0} pos {1} len {2} width {3}" -f ($field, $curcol[1], $curcol[2], $curcol[3])) -ForegroundColor Green
            $package.Add($key, $field)  
          }
          $PackageList += $package
        }
        catch {
          <#Do this if a terminating exception happens#>
        }
      }
    }
  }
  
  return $PackageList 
}
  
function makeBlanks {
  param(
    $nblines,
    $win
  )
  if ($iscoreclr) {
    $esc = "`e"
  }
  else {
    $esc = $([char]0x1b)
  }
  $blanks = 1..$nblines | ForEach-Object {
    "$esc[38;5;15m$($Single.LEFT)", "".PadLeft($Win.W - 2, " "), "$esc[38;5;15m$($Single.RIGHT)" -join ""
  }
  $blanks | Out-String
}


function makelines {
  param (
    $list,
    $checked,
    $row,
    $selected,
    $W
  ) 
  if ($iscoreclr) {
    $esc = "`e"
  }
  else {
    $esc = $([char]0x1b)
  }
  [string]$line = ""
  #$w = $host.UI.RawUI.WindowSize.Width - 2
  foreach ($key in $columns.keys) {
    [string]$col = $list.$key
    #$percent = $columns[$key][1]
    $l = $columns[$key][1]
    if ($col.Length -gt $l) {
      $col = $col.Substring(0, $l)
    }
    $line = $line, $col.PadRight($l, " ") -join " "
  }
  if ($row -eq $selected) {
    $line = "$esc[48;5;33m$esc[38;5;15m$($line)"
  }
  if ($row % 2 -eq 0) {
    $line = "$esc[38;5;7m$($line)"
  }
  else {
    $line = "$esc[38;5;8m$($line)"
  }
  if ($checked) {
    $line = "$esc[38;5;46m$('✓')", $line -join ""
  }
  else {
    $line = " ", $line -join ""
  }

  "$esc[38;5;15m$($Single.LEFT)$($line)$esc[0m"
}
  
function displayGrid($title, [scriptblock]$cmd, [ref]$data, $allowSearch = $false) {
  $global:Host.UI.RawUI.FlushInputBuffer()
  $WinWidth = [System.Console]::WindowWidth
  $X = 0
  $Y = 0
  $WinHeigt = [System.Console]::WindowHeight - 1
  $win = [window]::new($X, $Y, $WinWidth, $WinHeigt, $false, "White");
  $win.title = $title
  $Win.titleColor = "Green"
  $win.footer = $Single.LEFT, "$(color "[?]" "red") : Help $(color "[Space]" "red") : Select/Unselect $(color "[Enter]" "red") : Accept $(color "[Esc]" "red") : Quit", $Single.RIGHT -join ""
  $win.drawWindow();
  $win.drawVersion();
  $nbLines = $Win.h - 1
  $blanks = makeBlanks $nblines $win

  $statedata = [System.Collections.Hashtable]::Synchronized([System.Collections.Hashtable]::new())
  $runspace = [runspacefactory]::CreateRunspace()
  $runspace.Open()
  $Runspace.SessionStateProxy.SetVariable("StateData",$StateData)

  $sb = {
    $x = $statedata.X
    $y = $statedata.Y
    $i = 1
    Write-Host $statedata
    while($true) {
      [System.Console]::setcursorposition($X, $Y)
      $str = '⏳ Getting the data ', ''.PadLeft($i,'.') -join ''
      [System.Console]::write($str)
      $i++
      Start-Sleep -Milliseconds 50
    }
  }
  
  #[System.Console]::setcursorposition($win.X + 3, $win.Y + 1)
  #[System.Console]::write('Getting the list.......')
  $session = [powershell]::create()
  $statedata.X = ($win.X + 3)
  $statedata.Y = ($win.Y + 1)
  $session.Runspace = $runspace
  $null = $session.AddScript($sb)
  $handle = $session.BeginInvoke()
  $list = Invoke-Command -ScriptBlock $cmd
  $session.Stop()
  $runspace.Dispose()
  $skip = 0
  $nbPages = [math]::Ceiling($list.count / $nbLines)
  $win.nbpages = $nbPages
  $page = 1
  $selected = 0
  [System.Console]::CursorVisible = $false
  $redraw = $true
  while (-not $stop) {
    $win.page = $page
    [System.Console]::setcursorposition($win.X, $win.Y + 1)
    $row = 0
    $partlist = $list | Select-Object -First $nblines -Skip $skip | ForEach-Object {
      $index = (($page - 1) * $nbLines) + $row
      $checked = $list[$index].Selected
      makelines $list[$index] $checked $row $selected $win.W-2
      $row++
    }
    $nbDisplay = $partlist.Length
    $sText = $partlist | Out-String 
    if ($redraw) {
      [System.Console]::setcursorposition($win.X, $win.Y + 1)
      [system.console]::write($blanks)
      $redraw = $false
    }
    [System.Console]::setcursorposition($win.X, $win.Y + 1)
    [system.console]::write($sText.Substring(0, $sText.Length - 2))
    $win.drawPagination()
    while (-not $stop) {
      if ($global:Host.UI.RawUI.KeyAvailable) { 
        [System.Management.Automation.Host.KeyInfo]$key = $($global:host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'))
        if ($key.Character -eq '?') {
          # Help
          displayHelp $allowSearch
        }
        if ($key.character -eq 'q' -or $key.VirtualKeyCode -eq 27) {
          # Quit
          $stop = $true
        }
        if ($key.VirtualKeyCode -eq 38) {
          # key up
          if ($selected -gt 0) {
            $selected --
          }
        }
        if ($key.VirtualKeyCode -eq 40) {
          # key Down
          if ($selected -lt $nbDisplay - 1) {
            $selected ++
          }
        }
        if ($key.VirtualKeyCode -eq 37) {
          # key Left
          if ($page -gt 1) {
            $skip -= $nbLines
            $page -= 1
            $selected = 0
            $redraw = $true     
          }
        }
        if ($key.VirtualKeyCode -eq 39) {
          # key Right
          if ($page -lt $nbPages) {
            $skip += $nbLines
            $page += 1
            $selected = 0
            $redraw = $true
          }
        }
        if ($key.VirtualKeyCode -eq 32) {
          # key Space
          $index = (($page - 1) * $nbLines) + $selected
          $checked = $list[$index].Selected
          $list[$index].Selected = -not $checked
        }
        if ($key.VirtualKeyCode -eq 13) {
          # key Enter
          Clear-Host
          $data.value = $data.value = $list | Where-Object { $_.Selected }
          $stop = $true
        }
        if ($key.VirtualKeyCode -eq 114) {
          # key F3
          if ($allowSearch) {
            $term = getSearchTerms
            [System.Console]::CursorVisible = $false
            $term = '"', $term, '"' -join ''
            $sb = { Invoke-Winget "winget search --name $term" | Where-Object { $_.source -eq "winget" } }
            $list = Invoke-Command -ScriptBlock $sb
            $skip = 0
            $nbPages = [math]::Ceiling($list.count / $nbLines)
            $win.nbpages = $nbPages
            $page = 1
            $selected = 0
            $redraw = $true
          }
        }
        if ($key.character -eq "+") {
          # key +
          $checked = $true
          $list | ForEach-Object { $_.Selected = $checked }
        }
        if ($key.character -eq "-") {
          # key -
          $checked = $false
          $list | ForEach-Object { $_.Selected = $checked }
        }
        break
      }
      Start-Sleep -Milliseconds 20
    }    
  }
  [System.Console]::CursorVisible = $true
  Clear-Host
}
  
function displayHelp {
  param(
    [boolean]$allowSearch
  )
  $global:Host.UI.RawUI.FlushInputBuffer()
  $WinWidth = [System.Console]::WindowWidth - 4
  $X = 2
  $Y = 10
  $WinHeigt = 6
  $win = [window]::new($X, $Y, $WinWidth, $WinHeigt, $false, "White");
  $win.title = "Help"
  $Win.titleColor = "Blue"
  $win.footer = $Single.LEFT, "$(color "[Esc]" "red") : Close", $Single.RIGHT -join ""
  $win.drawWindow();

  $buffer = "$(color "↑↓" "cyan") : Navigate `t`t`t`t $(color "← →" "cyan") Change page"
  [System.Console]::setcursorposition($win.X + 2, $win.Y + 1)
  [system.console]::write($buffer)
  $buffer = "$(color "Space" "cyan") : Select / Unselect package `t`t $(color "+/-" "cyan") Select All/None "
  [System.Console]::setcursorposition($win.X + 2, $win.Y + 2)
  [system.console]::write($buffer)
  if ($allowSearch) {
    $buffer = "$(color "F3" "cyan") : Enter Package Name"
    [System.Console]::setcursorposition($win.X + 2, $win.Y + 3)
    [system.console]::write($buffer)
  }
  $stop = $false;
  while (-not $stop) {
    if ($global:Host.UI.RawUI.KeyAvailable) { 
      [System.Management.Automation.Host.KeyInfo]$key = $($global:host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'))
      #Write-Host $key.VirtualKeyCode
      if ($key.character -eq 'q' -or $key.VirtualKeyCode -eq 27) {
        $stop = $true
      }
    }
  }
}
  
  
function Show-WGList {
  begin {
    $sb = { Invoke-Winget "winget list" | Where-Object { $_.source -eq "winget" } }
    $data = @{}
  }
  process {
    displayGrid -title "Installed Packages" -cmd $sb -data ([ref]$data)
  }
  end {
    return $data
  } 
}
  
function  Update-WGPackage {
  param (
    [switch]$update
  )
  begin {
    $sb = { Invoke-Winget "winget upgrade --include-unknown" | Where-Object { $_.source -eq "winget" } }
    $data = @()
  }
  process {
    displayGrid -title "Upgradable Packages" -cmd $sb -data ([ref]$data)
    if ($update) {
      if ($data.length -gt 0) {
        $data | Out-Object | ForEach-Object {
          $id = $_.Id
          $expression = "winget upgrade --id $($id)"
          Invoke-Expression $expression
        }
      }
    }
  }
  end {
    return $data
  }
}
  
function Install-WGPackage {
  param (
    [switch]$install,
    [string]$package = ""
    
  )
  begin {
    if ($package -eq "") {
      $term = getSearchTerms
    }
    else {
      $term = $package
    }
  }
  process {
    if ($term.Trim() -ne "") {
      $term = '"', $term, '"' -join ''
      $sb = { Invoke-Winget "winget search $term" | Where-Object { $_.source -eq "winget" } }
      #displayGrid "Install Packages" $sb
      $data = @()
      displayGrid -title "Install Package" -cmd $sb -data ([ref]$data) $true
      if ($install) {
        if ($data.length -gt 0) {
          $data | Out-Object | ForEach-Object {
            $id = $_.Id
            $expression = "winget install --id $($id)"
            Invoke-Expression $expression
          }
        }
      }
    }
  }
  end {
    return $data
  }
}
  
function Uninstall-WGPackage {
  begin {
    $sb = { Invoke-Winget "winget list" | Where-Object { $_.source -eq "winget" } }
    $data = @()
  }
  process {
    displayGrid -title "Remove Packages" -cmd $sb -data ([ref]$data)
    if ($data.length -gt 0) {
      $data | Out-Object | ForEach-Object {
        $id = $_.Id
        $expression = "winget uninstall --id $($id)"
        Invoke-Expression $expression
      }
    }
  }
  end {
    return $data
  }
}
  
function Get-WGList {
  Invoke-Winget "winget list" | Where-Object { $_.source -eq "winget" }
}

function Search-WGPackage {
  param(
    [Parameter(Mandatory=$true)]
    [string]$package
  )
  Invoke-Winget "winget search $package" | Where-Object { $_.source -eq "winget" }
}
  
function Get-WGUpdatables {
  Invoke-Winget "winget upgrade --include-unknown" | Where-Object { $_.source -eq "winget" }
}

function Out-Object {
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
      [hashtable]
      $Data
  )
  begin {
    [PSCustomObject[]]$result = @()
  }
  process {
    foreach($d in $data) {
      $result += [pscustomobject]$d
    }
  }
  end {
    return $result
  }
}
  
function testcolor {
  if ($iscoreclr) {
    $esc = "`e"
  }
  else {
    $esc = $([char]0x1b)
  }
  0..255 | ForEach-Object {
    Write-Host "$esc[4m$esc[38;5;$($_)m'test'$esc[0m"
  } 
}

function getWingetLocals {
  $language = (Get-UICulture).Name
  $version = Invoke-Expression "winget --version" | Out-String -NoNewline
  $languageData = $(
    $hash = @{}

    $(try {
        # We have to trim the leading BOM for .NET's XML parser to correctly read Microsoft's own files - go figure
          ([xml](((Invoke-WebRequest -Uri "https://raw.githubusercontent.com/microsoft/winget-cli/$version/Localization/Resources/$language/winget.resw" -ErrorAction Stop ).Content -replace "\uFEFF", ""))).root.data
      }
      catch {
        # Fall back to English if a locale file doesn't exist
        (
              ('SearchName', 'Name'),
              ('SearchID', 'Id'),
              ('SearchVersion', 'Version'),
              ('AvailableHeader', 'Available'),
              ('SearchSource', 'Source'),
              ('ShowVersion', 'Version'),
              ('GetManifestResultVersionNotFound', 'No version found matching:'),
              ('InstallerFailedWithCode', 'Installer failed with exit code:'),
              ('UninstallFailedWithCode', 'Uninstall failed with exit code:'),
              ('AvailableUpgrades', 'upgrades available.')
        ) | ForEach-Object { [pscustomobject]@{name = $_[0]; value = $_[1] } }
      }) | ForEach-Object {
      # Convert the array into a hashtable
      $hash[$_.name] = $_.value
    }
    $hash
  )
  return $languageData
}