wingetposh.psm1

class upgradeSoftware {
  [string]$Name
  [string]$Id
  [string]$Version
  [string]$AvailableVersion
  [string]$Source
}

class installSoftware {
  [string]$Name
  [string]$id
  [string]$Version
  [String]$Source
}

class column {
  [string]$Name
  [Int16]$Position
  [Int16]$Len
}

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 window {
  [int]$X
  [int]$Y
  [int]$W
  [int]$H
  [Frame]$frameStyle
  [System.ConsoleColor]$frameColor
  [string]$title = ""
  [System.ConsoleColor]$titleColor
  [string]$footer = ""

  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 - 2, $this.frameStyle.TOP)
    $blank = "".PadLeft($this.W - 2, " ") 
    Write-Host $this.frameStyle.UL -NoNewline -ForegroundColor $this.frameColor
    Write-Host $bloc1 -ForegroundColor $this.frameColor -NoNewline
    Write-Host $this.frameStyle.UR -ForegroundColor $this.frameColor

    for ($i = 1; $i -lt $this.H; $i++) {
      $Y2 = $this.Y + $i
      $X2 = $this.X + $this.W - 1
      $this.setPosition($this.X, $Y2)
      Write-Host $this.frameStyle.LEFT -ForegroundColor $this.frameColor
        
      $X3 = $this.X + 1
      $this.setPosition($X3, $Y2)
      Write-Host $blank 
        
      $this.setPosition($X2, $Y2)
      Write-Host $this.frameStyle.RIGHT -ForegroundColor $this.frameColor
    }

    $Y2 = $this.Y + $this.H
    $this.setPosition( $this.X, $Y2)
    $bloc1 = "".PadLeft($this.W - 2, $this.frameStyle.BOTTOM)
    Write-Host $this.frameStyle.BL -NoNewline -ForegroundColor $this.frameColor
    Write-Host $bloc1 -ForegroundColor $this.frameColor -NoNewline
    Write-Host $this.frameStyle.BR -ForegroundColor $this.frameColor
    $this.drawTitle()
    $this.drawFooter()
  }

  [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.W - ($this.footer.Length + 6))
      $local:Y = $this.Y + $this.h
      $this.setPosition($local:X, $local:Y)
      Write-Host "| " -NoNewline -ForegroundColor $this.frameColor
      $local:X = $local:X + 2
      $this.setPosition($local:X, $local:Y)
      Write-Host $this.footer -NoNewline -ForegroundColor $this.titleColor
      $local:X = $local:X + $this.footer.Length
      $this.setPosition($local:X, $local:Y)
      Write-Host " |" -NoNewline -ForegroundColor $this.frameColor
    }
  }

  [void] clearWindow() {
    $local:blank = "".PadLeft($this.W - 2, " ") 
    for ($i = 1; $i -lt $this.H; $i++) {
      $this.setPosition(($this.X + 1), ($this.Y + $i))
      Write-Host $blank 
    } 
  }
}

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 _wgList {
  [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 
  $command = "winget list"
  
  $SearchResult = Invoke-Expression $command | Out-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
  }

  $InstalledList = @()

  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 = [InstallSoftware]::new()
          $software.Name = $name;
          $software.Id = $id;
          $software.Version = $version
          $software.Source = $source
          $InstalledList += $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
          $InstalledList += $software
        }
      }
    }
  }
  $installedList
}

# function check_ocgv {
# if(-not (Get-Module Microsoft.PowerShell.ConsoleGuiTools -ListAvailable)){
# Install-Module Microsoft.PowerShell.ConsoleGuiTools -Scope CurrentUser -Force
# }
# }

function _wgSearch {
  param(
    [parameter (
      Mandatory,
      HelpMessage = "Package (or part) name to search"
    )]
    [string]$search,
    # Store Filter
    [Parameter(
      HelpMessage = "store to filter on"
    )]
    [string]
    $Store    
  )

  [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 

  $command = "winget search --name ${search} --source winget"

  if ($Store -ne "") {
    $command = $command + " --source " + $Store
  }

  $SearchResult = Invoke-Expression $command | Out-String
  $SearchList = @()
  if ($SearchResult.Contains('----')) {
    $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 ($store -eq "") {
      $sourceStart = $columns[3].Position
    }
    
    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, $line.Length - $versionStart).TrimEnd()
        $source = "Winget"
        $software = [installSoftware]::new()
        $software.Name = $name;
        $software.Id = $id;
        $software.Version = $version
        $software.Source = $source;
        $SearchList += $software
      }
    }
    $SearchList
  }
  else {
    $SearchList
  }
}

function wgUpgradable {
  [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 
  $command = "winget upgrade --include-unknown"
  
  $SearchResult = Invoke-Expression $command | Out-String
  $lines = $SearchResult.Split([Environment]::NewLine)

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

  if ($fl -lt $lines.Length) {

    $columns = getColumnsHeaders -columsLine $lines[$fl - 1]
  
    $idStart = $Columns[1].Position
    $versionStart = $Columns[2].Position
    $availableStart = $columns[3].Position
    $sourceStart = $columns[4].Position

    $upgradeList = @()
    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()
        $software = [upgradeSoftware]::new()
        $software.Name = $name;
        $software.Id = $id;
        $software.Version = $version
        $software.AvailableVersion = $available
        $software.Source = $source
        $upgradeList += $software
      }
    }
    $upgradeList
  }
}

<#
.SYNOPSIS
  Displays a list of installed packages
.DESCRIPTION
  Show-WGList displays a grid view of all the packages installed on the system.
  Each line is selectable. Accepting the selection with Enter returns an array of objects.
  Pressing Escape quits the grid with no furher action.
.EXAMPLE
  Show-WGList | Select-Object -Property Id | Uninstall-WGPackage
  In this example, the selected package in the grid will be uninstalled from the computer.
#>

function Show-WGList {
  param(
    [switch]$Single
  )
  # check_ocgv
  if ($single) {
    $list = _wgList | Out-ConsoleGridView -Title 'Installed Packages' -OutputMode Single
  }
  else {
    $list = _wgList | Out-ConsoleGridView -Title 'Installed Packages'  
  }
  return $list
}

function Show-WGUpdatables {
  # Parameter help description
  param(
    [Switch]
    $Multiple
  )
  # check_ocgv
  if (-not $Multiple) {
    $list = wgUpgradable | Out-ConsoleGridView -Title 'Upgradable Packages' -OutputMode Single
  }
  else {
    $list = wgUpgradable | Out-ConsoleGridView -Title 'Upgradable Packages'
  }
  # Write-Host $list
  return $list
}

function Search-WGPackage {
  param(
    [parameter (
      HelpMessage = "Package (or part) name to search"
    )]
    [string]$search,
    # Store Filter
    [Parameter(
      HelpMessage = "store to filter on"
    )]
    [string]
    $Store,
    [Switch]$Install    
  )
  # check_ocgv
  
  [System.Console]::Clear()
  $search = getSearchTerms
  [System.Console]::Clear()
  if (-not [string]::IsNullOrEmpty($search)) {
    $packages = _wgSearch $search $Store | Out-ConsoleGridView -Title "Search Package" 
    if ($packages.length -gt 0) {
      if ($Install) {
        $packages | Select-Object -Property id | Install-WGPackage
      }
      else {
        return $packages
      }
    }
    else {
      Write-Host "No package found for $search"
    }
  }
}

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 = "WinGetPosh Search"
  $Win.titleColor = "Green"
  $win.footer = "[Enter] : Accept [Ctrl-C] : Abort"
  $win.drawWindow();
  $win.setPosition($X + 2, $Y + 2);
  [System.Console]::Write('Package : ')
  [system.console]::CursorVisible = $true
  $pack = [System.Console]::ReadLine()
  return $pack.Trim()
}

function Install-WGPackage {
  [CmdletBinding()]
  param (
    [Parameter(ValueFromPipeline)]
    [PSCustomObject[]] $inObj
  )

  BEGIN {
    [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 
  }

  PROCESS {
    $id = $inObj.id
    $command = "winget install '$id'"
    # Write-Host $command
    Invoke-Expression $command
  }
  
  
}

function Uninstall-WGPackage {
  [CmdletBinding()]
  param (
    [Parameter(ValueFromPipeline)]
    $inObj,
    [Parameter()]
    [Switch]$Interactive
  )

  [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 

  if ($PSBoundParameters.ContainsKey('inObj')) {
    if ($PSBoundParameters.ContainsKey('Interactive')) {
      Write-Error "-Interactive switch cannot be used with pipeline variable"
      return
    }
  }

  if ($Interactive) {
    $pkg = Show-WGList | Select-Object -Property id, Name
    if (-not $pkg) {
      return
    }
    $ids = $pkg
  }
  else {
    $ids = $inObj
  }

  $ids | ForEach-Object {
    $id = $_.id;
    $name = $_.name
    if (-not [string]::IsNullOrEmpty($id)) {
      Write-Host "Uninstalling : " -NoNewline
      Write-Host "$name ($id)" -ForegroundColor DarkCyan
      $command = "winget uninstall '$id' --Force"
      Invoke-Expression $command
    }
    else {
      Write-Warning "Cannot uninstall unknown package"
    }
  }
}

function Update-WGPackage {
  [CmdletBinding()]
  param (
    [Parameter(ValueFromPipeline)]
    [upgradeSoftware] $inObj,
    [Parameter()]
    [Switch]$Interactive,
    [Switch]$Force
  )

  [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 
  if ($PSBoundParameters.ContainsKey('inObj')) {
    if ($PSBoundParameters.ContainsKey('Interactive')) {
      Write-Error "-Interactive switch cannot be used with pipeline variable"
      return
    }
  }
 
  if ($null -eq $inObj) {
    $pkg = Show-WGUpdatables -Multiple | Select-Object -Property id, Name
    if (-not $pkg) {
      return
    }
    $ids = $pkg
  }
  else {
    $ids = $inObj
  }

  # Write-Host $ids
  $forceinstall = ""
  if ($Force) {
    $forceinstall = "--accept-package-agreements"  
  }
  
  $ids | ForEach-Object {
    $id = $_.id;
    $name = $_.name
    if (-not [string]::IsNullOrEmpty($id)) {
      Write-Host "Updating : " -NoNewline
      Write-Host "$name ($id)" -ForegroundColor DarkCyan
      $command = "winget upgrade '$id' $forceinstall"
      # Write-Host $command
      Invoke-Expression $command
    }
    else {
      Write-Warning "Cannot update unknown package"
    }
  }
  
}