wingetposh.psm1

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() {
    [System.Console]::CursorVisible = $false
    $this.setPosition($this.X, $this.Y)
    $bloc1 = "".PadLeft($this.W , $this.frameStyle.TOP)
    $blank = "".PadLeft($this.W , " ") 
    Write-Host $bloc1 -ForegroundColor $this.frameColor -NoNewline
    for ($i = 1; $i -lt $this.H; $i++) {
      $Y2 = $this.Y + $i
      #$X2 = $this.X + $this.W - 1
      $this.setPosition($this.X, $Y2)
        
      $X3 = $this.X 
      $this.setPosition($X3, $Y2)
      Write-Host $blank 
    }
    $Y2 = $this.Y + $this.H
    $this.setPosition( $this.X, $Y2)
    $bloc1 = "".PadLeft($this.W , $this.frameStyle.BOTTOM)
    Write-Host $bloc1 -ForegroundColor $this.frameColor -NoNewline
    $this.drawTitle()
    $this.drawFooter()
  }


  [void] drawVersion() {
    $version = $this.frameStyle.LEFT, [string]$(Get-InstalledModule -Name wingetposh).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() {
    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]@{
  "Name"             = @(0, 33)
  "Id"               = @(1, 33)
  "Version"          = @(2, 12)
  "AvailableVersion" = @(3, 12)
  "Source"           = @(4, 7)
}

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) {
      #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 
  
  $SearchResult = Invoke-Expression $cmd | Out-String -Width $Host.UI.RawUI.WindowSize.Width
  [string[]]$lines = $SearchResult -Split [Environment]::NewLine

  $fl = 0
  while (-not $lines[$fl].StartsWith("----")) {
    $fl++
  }

  $columns = getColumnsHeaders -columsLine $lines[$fl - 1]

  $idStart = $Columns[1].Position
  $versionStart = $Columns[2].Position
 

 
  if ($columns.Length -eq 5) {
    $availableStart = $columns[3].Position
    $sourceStart = $columns[4].Position
  }
  else {
    $sourceStart = $columns[3].Position
  }

  $PackageList = @()

  if ($Columns.Length -eq 4) {
    For ($i = $fl + 1; $i -le $lines.Length; $i++) {
      $line = $lines[$i]
      if ($line.Length -gt ($sourceStart + 1) -and -not $line.StartsWith('-')) {
        $name = $line.Substring(0, $idStart).TrimEnd()
        $id = $line.Substring($idStart, $versionStart - $idStart).TrimEnd()
        $version = $line.Substring($versionStart, $sourceStart - $versionStart).TrimEnd()
        $source = $line.Substring($sourceStart, $line.Length - $sourceStart).TrimEnd()
        if ($source -ne "") {
          $software = [upgradeSoftware]::new()
          $software.Name = $name;
          $software.Id = $id;
          $software.Version = $version
          $software.Source = $source
          $software.Selected = $false
          $PackageList += $software
        }
      }
    }
  }
  else {
    For ($i = $fl + 1; $i -le $lines.Length; $i++) {
      $line = $lines[$i]
      if ($line.Length -gt ($availableStart + 1) -and -not $line.StartsWith('-')) {
        $name = $line.Substring(0, $idStart).TrimEnd()
        $id = $line.Substring($idStart, $versionStart - $idStart).TrimEnd()
        $version = $line.Substring($versionStart, $availableStart - $versionStart).TrimEnd()
        $available = $line.Substring($availableStart, $sourceStart - $availableStart).TrimEnd()
        $source = $line.Substring($sourceStart, $line.Length - $sourceStart).TrimEnd()
        if ($source -ne "") {
          $software = [UpgradeSoftware]::new()
          $software.Name = $name;
          $software.Id = $id;
          $software.Version = $version
          $software.AvailableVersion = $available
          $software.Source = $Source
          $software.Selected = $false
          $PackageList += $software
        }
      }
    }
  }
  return $PackageList 
}

function makelines {
  param (
    $list,
    $checked
  ) 
  if ($checked) {
    [string]$line = "✓"
  }
  else {
    [string]$line = " "
  }
  $w = $host.UI.RawUI.WindowSize.Width
  foreach ($key in $columns.keys) {
    [string]$col = $list.$key
    $percent = $columns[$key][1]
    $l = [math]::floor($w / 100 * $percent)
    if ($col.Length -gt $l) {
      $col = $col.Substring(0, $l)
    }
    if ($key -eq "AvailableVersion") {
      $line = $line, $col.PadRight($l, " ") -join " "
    }
    else {
      $line = $line, $col.PadRight($l, " ") -join " "
    }
  }
  $line
}

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 - 2
  $win = [window]::new($X, $Y, $WinWidth, $WinHeigt, $false, "White");
  $win.title = $title
  $Win.titleColor = "Green"
  $win.footer = $Single.LEFT, "$(color "[Space]" "red") : Select/Unselect $(color "[Enter]" "red") : Accept $(color "[?]" "red") : Help $(color "[Esc]" "red") : Quit", $Single.RIGHT -join ""
  $win.drawWindow();
  $win.drawVersion();
  $nbLines = $Win.h - 2
  $blanks = ' '.PadRight($global:Host.UI.RawUI.WindowSize.Width * ($nbLines + 1))
  [System.Console]::setcursorposition($win.X, $win.Y + 1)
  [System.Console]::write('Getting the list.......')
  $list = Invoke-Command -ScriptBlock $cmd
  $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
    $win.drawPagination()
    [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
      $line = makelines $list[$index] $checked 
      if ($row -eq $selected) {
        $(color $line "black" "white")
      }
      else {
        if ($row % 2 -eq 0) {
          $(color $line "darkgrey")
        }
        else {
          $(color $line "white")
        }
      }
      $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)
    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 '?') { # 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 = $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
}

function displayHelp {
  param (
    [boolean]$allowSearch
  )
  $global:Host.UI.RawUI.FlushInputBuffer()
  $Width = [System.Console]::WindowWidth - 2
  $Height = [System.Console]::WindowHeight -2
  $WinHeigt = 4
  $X = 2
  $Y = [math]::Ceiling(($Height - $WinHeigt) / 2 )
  $win = [window]::new($X, $Y, $Width, $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"
  $buffer = $buffer, " $(color "Space" "cyan") : Select / Unselect package `t`t $(color "+/-" "cyan") Select All/None " -join "`n"
  if ($allowSearch) {
    $buffer = $buffer, " $(color "F3" "cyan") : Search" -join "`n"
  }
  [System.Console]::setcursorposition($win.X + 1, $win.Y + 1)
  $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'))
      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" } }
    [upgradeSoftware[]]$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" } }
    [upgradeSoftware[]]$data = @()
  }
  process {
    displayGrid -title "Upgradable Packages" -cmd $sb -data ([ref]$data)
    if ($update) {
      if ($data.length -gt 0) {
        foreach ($package in $data) {
          $id = $package.id
          Invoke-Expression "winget upgrade --id $id"
        }
      }
    }
  }
  end {
    return $data
  }
}

function Install-WGPackage {
  param (
    [switch]$install 
  )
  begin {
    $term = getSearchTerms
  }
  process {
    if ($term.Trim() -ne "") {
      $term = '"', $term, '"' -join ''
      $sb = { invoke-Winget "winget search --name $term" | Where-Object { $_.source -eq "winget" } }
      #displayGrid "Install Packages" $sb
      [upgradeSoftware[]]$data = @()
      displayGrid -title "Install Package" -cmd $sb -data ([ref]$data) $true
      if ($install) {
        if ($data.length -gt 0) {
          foreach ($package in $data) {
            $id = $package.id
            Invoke-Expression "winget install --id $id"
          }
        }
      }
    }
  }
  end {
    return $data
  }
}

function Uninstall-WGPackage {
  begin {
    $sb = { invoke-Winget "winget list" | Where-Object { $_.source -eq "winget" } }
    [upgradeSoftware[]]$data = @()
  }
  process {
    displayGrid -title "Remove Packages" -cmd $sb -data ([ref]$data)
    if ($data.length -gt 0) {
      foreach ($package in $data) {
        $id = $package.id
        Invoke-Expression "winget uninstall --id $id"
      }
    }
  }
  end {
    return $data 
  }
}

function Get-WGList {
  invoke-Winget "winget list" | Where-Object { $_.source -eq "winget" }
}

function Get-WGUpdatables {
  invoke-Winget "winget upgrade --include-unknown" | Where-Object { $_.source -eq "winget" }
}