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 wingetSource {
  [string]$Name
  [string]$Argument
}
  
  
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.UL = [char]::ConvertFromUtf32(0x256d)
      #$this.UR = "┐"
      $this.UR = [char]::ConvertFromUtf32(0x256e)
      $this.TOP = "─"
      $this.LEFT = "│"
      $this.RIGHT = "│"
      $this.BL = [char]::ConvertFromUtf32(0x2570)
      #$this.BL = "└"
      $this.BR = [char]::ConvertFromUtf32(0x256f)
      #$this.BR = "┘"
      $this.BOTTOM = "─"
      $this.LEFTSPLIT = [char]::ConvertFromUtf32(0x2524)
      $this.RIGHTSPLIT = [char]::ConvertFromUtf32(0x251c)
    }
  }
}

class Keys {
  static [string] $enter
  static [string] $space 
  static [string] $escape

  static Keys() {
    [Keys]::enter = [char]::ConvertFromUtf32(0xf0311)
    [Keys]::space = [char]::ConvertFromUtf32(0xf1050)
    [Keys]::escape = [char]::ConvertFromUtf32(0xf12b7)
  }
}
  
$Single = [Frame]::new($false)
$Double = [Frame]::new($true)
  

$baseFields = @{
  'SearchName'        = 'Name'
  'SearchID'          = 'Id'
  'SearchVersion'     = 'Version'
  'AvailableHeader'   = 'Available'
  'SearchSource'      = 'Source'
  'ShowVersion'       = 'Version'
  'AvailableUpgrades' = 'upgrades available.'
  "SearchMatch"       = "Moniker"
  "SourceListName"    = "Name"
  "SourceListArg"     = "Argument"
}
  
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 = $this.frameStyle.LEFT, "".PadLeft($this.W - 2, " "), $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 -ForegroundColor $this.frameColor    
    }
    $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.LEFTSPLIT, [string]$(Get-InstalledModule -Name wingetposh -ErrorAction Ignore).version, $this.frameStyle.RIGHTSPLIT -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 ($this.frameStyle.LEFTSPLIT, " " -join "") -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 (" ",$this.frameStyle.RIGHTSPLIT -join "") -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)
      $foot = $this.frameStyle.LEFTSPLIT, " ", $this.footer, " ", $this.frameStyle.RIGHTSPLIT -join ""
      [console]::write($foot)
    }
  }
  
  [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 "[Esc]" "red") : Abort"
  $win.drawWindow();
  $win.setPosition($X + 2, $Y + 2);
  [System.Console]::Write('Package : ')
  [system.console]::CursorVisible = $true
  try {
    [Microsoft.PowerShell.PSConsoleReadLineOptions]$option = Get-PSReadLineOption
    $save = $option.PredictionSource
    Set-PSReadLineKeyHandler -key Escape -Function CancelLine
    Set-PSReadLineOption -PredictionSource None
    $pack = PSConsoleHostReadLine  
  }
  finally {
    Remove-PSReadlineKeyHandler -Key Escape
    Set-PSReadLineOption -PredictionSource $save
    [console]::CursorVisible = $false
  }
  
  return $pack
}

function getFilterSource {
  $WinWidth = [System.Console]::WindowWidth
  $X = 0
  $Y = [System.Console]::WindowHeight - 6
  $WinHeigt = 4
  
  $win = [window]::new($X, $Y, $WinWidth, $WinHeigt, $false, "White");
  $win.title = "Source Filter"
  $Win.titleColor = "Green"
  $win.footer = "$(color "[Enter]" "red") : Accept $(color "[Esc]" "red") : Abort"
  $win.drawWindow();
  $win.setPosition($X + 2, $Y + 2);
  [System.Console]::Write('Source : ')
  [system.console]::CursorVisible = $true
  try {
    [Microsoft.PowerShell.PSConsoleReadLineOptions]$option = Get-PSReadLineOption
    $save = $option.PredictionSource
    Set-PSReadLineKeyHandler -key Escape -Function CancelLine
    Set-PSReadLineOption -PredictionSource None
    $pack = PSConsoleHostReadLine  
  }
  finally {
    Remove-PSReadlineKeyHandler -Key Escape
    Set-PSReadLineOption -PredictionSource $save
    [console]::CursorVisible = $false
  }
  
  return $pack
}
  

function getColumnsHeaders {
  param(
    [parameter (
      Mandatory
    )]
    [string]$columsLine,
    [int]$width
  )

  $script:fields = Get-Content $env:USERPROFILE\.config\.wingetposh\locals.json | ConvertFrom-Json
  
  $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 = $width - $pos
    }
    else {
      #Not Last Column
      $pos2 = $columsLine.IndexOf($Cols[$i + 1])
      $len = $pos2 - $pos
    }
    $acolumn = [column]::new()
    # get EN Name
    $base = $script:fields.psobject.Properties | Where-Object { $_.Value -eq $cols[$i] }
    if ($base.count -eq 1) {
      $BaseName = $base.Name
    }
    else {
      $BaseName = ($base | Where-Object { $_.Name.StartsWith("Search") }).Name
    }
    $acolumn.Name = $baseFields[$BaseName]
    $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-Expression2 {
  param(
    [string]$exp,
    [string]$title
  )
  $statedata = [System.Collections.Hashtable]::Synchronized([System.Collections.Hashtable]::new())
  $statedata.X = 0
  $statedata.Y = $Host.UI.RawUI.CursorPosition.Y
  $statedata.title = $title
  $runspace = [runspacefactory]::CreateRunspace()
  $runspace.Open()
  $Runspace.SessionStateProxy.SetVariable("StateData", $StateData)
  $sb = {
    $x = $statedata.X
    $y = $statedata.Y
    $spinner = '{"aesthetic": {
      "interval": 80,
      "frames": [
        "▰▱▱▱▱▱▱",
        "▰▰▱▱▱▱▱",
        "▰▰▰▱▱▱▱",
        "▰▰▰▰▱▱▱",
        "▰▰▰▰▰▱▱",
        "▰▰▰▰▰▰▱",
        "▰▰▰▰▰▰▰",
        "▰▱▱▱▱▱▱"
      ]
    }}'

    $spinners = $spinner | ConvertFrom-Json 
    $frameCount = $spinners.aesthetic.frames.count
    $frameInterval = $spinners.aesthetic.interval

    $i = 1
    $string = "".PadRight(30, ".")
    $nav = "oOo"
    while ($true) {
      $e = "$([char]27)"
      [System.Console]::setcursorposition($X, $Y)
      $frame = $spinners.aesthetic.frames[$i % $frameCount]
      $string = "$($e)[s", "$e[u$frame", " $($statedata.title)" -join ""
      [System.Console]::write($string)
      Start-Sleep -Milliseconds $frameInterval
      $i++
    }
  }

  $session = [powershell]::create()
  $null = $session.AddScript($sb)
  $session.Runspace = $runspace
  $handle = $session.BeginInvoke()
  $result = Invoke-Expression -Command $exp

  $session.Stop()
  $runspace.Dispose()
  [System.Console]::setcursorposition($statedata.X, $statedata.Y)
  [System.Console]::write("".PadRight($Host.UI.RawUI.BufferSize.Width, " "))
}

function Get-WGSources {
  $cmd = "winget source list"
  $result = Invoke-Expression -Command $cmd
  $data = $false
  $sources = [ordered]@{}
  foreach ($line in $result) {
    if ($data) {
      $name, $argument = $line -split "\s+"
      $sources.Add($name, $argument)
    }
    else {
      $data = ($line.Contains('-----')) 
    }
  }
  $sources
}

function Invoke-Winget {
  param (
    [string]$cmd
  )
  [console]::clear()
  [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 
  $TerminalWidth = $Host.UI.RawUI.BufferSize.Width - 2
  $statedata = [System.Collections.Hashtable]::Synchronized([System.Collections.Hashtable]::new())
  $statedata.X = [math]::round(($Host.UI.RawUI.BufferSize.Width - 32) / 2)
  #$statedata.Y = $Host.UI.RawUI.CursorPosition.Y
  $statedata.Y = [math]::round(($Host.UI.RawUI.BufferSize.Height - 3) / 2)
  
  $runspace = [runspacefactory]::CreateRunspace()
  $runspace.Open()
  $Runspace.SessionStateProxy.SetVariable("StateData", $StateData)
  [window]$win = [window]::new($statedata.X, $statedata.Y, 32, 2, $false, "White")
  $win.titleColor = "Red"
  $win.title = '⏳ Getting the data '
  $win.drawWindow()
  $win.drawTitle()
  $statedata.X ++
  $statedata.Y ++
  $sb = {
    $x = $statedata.X
    $y = $statedata.Y
    
    $i = 1
    $string = "".PadRight(30, ".")
    $nav = "oOo"
    while ($true) {
      if ($i -lt $nav.Length) {
        $mobile = $nav.Substring($nav.Length - $i)
        $string = $mobile.PadRight(30, '.')
      }
      else {
        if ($i -gt 27) {
          $nb = 30 - $i
          $mobile = $nav.Substring(1, $nb)
          $string = $mobile.PadLeft(30, '.')
        }
        else {
          $left = "".PadLeft($i, '.')
          $right = "".PadRight(27 - $i, '.')
          $string = $left, $nav, $right -join ""
        }
      }
      [System.Console]::setcursorposition($X, $Y)
      [System.Console]::write($string)
      $i++
      if ($i -gt 30) {
        $i = 1
      }
      Start-Sleep -Milliseconds 100
    }
  
  }
  $session = [powershell]::create()
  $null = $session.AddScript($sb)
  $session.Runspace = $runspace
  [System.Console]::CursorVisible = $false
  $handle = $session.BeginInvoke()
  
  $PackageList = @()
  $SearchResult = Invoke-Expression $cmd | Out-String -Width $TerminalWidth -Stream 

  $SearchResult | ForEach-Object -Begin { $i = 0; $data = $false } -Process {
    if ($_.StartsWith('---')) {
      $lWidth = $_.Length
      $cols = getColumnsHeaders -columsLine $SearchResult[$i - 1] -width $lWidth
      $columns.Clear()
      $i = 1
      foreach ($col in [column[]]$cols) {
        if ($i -lt $cols.length) {
          $colPercent = [Math]::Round(($col.Len / $lWidth * 100) - 0.99, 2)
          $colWidth = [System.Math]::Truncate($TerminalWidth / 100 * $colPercent);
        }
        else {
          $colWidth = $col.len  
        }
        $i++
        $Columns.Add($col.Name, @($col.Position, $colWidth, $col.len))
      }
      $data = $true
    }
    else {
      if ($data) {
        $s = [string]$_
        $regex = $script:Fields.AvailableUpgrades.Replace("{0}", "")
        if (-not $s.Contains($regex)) {
          $package = [ordered]@{}
          $i2 = 0
          foreach ($col in $cols) {
            [System.Text.StringBuilder]$sb = New-Object System.Text.StringBuilder $col.Len
            $charcount = 0
            while ($charcount -lt $col.Len) {
              [char]$char = $s[$i2]
              if (-not ([bool]$char)) {
                $char = " "
              }
              [void]$sb.Append($char)
              $nbBytes = [Text.Encoding]::UTF8.GetByteCount($char)
              if ($nbBytes -gt 1) {
                $charcount += ($nbBytes - 1)
              }
              else {
                $charcount += $nbBytes
              }
              $i2++
            }
            $field = $sb.ToString()
            if ($field.Contains("…")) {
              $i2++
            }
            $field = adjustCol -len $columns.$($col.Name)[1] -col $field
          
            $sb = $null
            $package.Add($col.Name, $field)
          }
          $PackageList += $package
        }
      }
    }
    $i++
  }
  $session.Stop()
  $runspace.Dispose()
  [System.Console]::CursorVisible = $true
  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)", "".PadRight($Win.W - 2, " "), "$esc[38;5;15m$($Single.RIGHT)" -join ""
  }
  $blanks | Out-String
}

function adjustCol {
  param(
    [int]$len,
    [string]$col
  )
  
  $charcount = 0
  $i = 0
  $field = ""
  while ($charcount -lt $len) {
    [char]$char = $col[$i]
    $field = $field + $char
    $nbBytes = [Text.Encoding]::UTF8.GetByteCount($char)
    if ($nbBytes -gt 1) {
      $charcount += ($nbBytes - 1)
    }
    else {
      $charcount += $nbBytes
    }
    $i++
  }
  
  if ($field.Contains("…")) {
    $field = $field, " " -join ""
  }
  
  return $field
}


  

function displayGrid {
  param (
    [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
    $list,
    [string]$source,
    [string]$title, 
    [ref]$data, 
    $allowSearch = $false
  )

  if ($iscoreclr) {
    $esc = "`e"
  }
  else {
    $esc = $([char]0x1b)
  }

  function makelines {
    param (
      $list,
      $checked,
      $row,
      $selected,
      $W
    ) 
    
    [string]$line = ""
    if ($script:config.UseNerdFont -eq $true) {
      $check = [char]::ConvertFromUtf32(0xf05d)
    }
    else {
      $check = "✓"
    }
    
    foreach ($key in $columns.keys) {
      [string]$col = $list.$key
      $line = $line, $col -join " "
    }
    if ($checked) {
      $line = "$esc[38;5;46m$check", $line -join ""
    }
    else {
      $line = " ", $line -join ""
    }
    if ($row -eq $selected) {
      $line = "$esc[48;5;33m$esc[38;5;15m$($line)"
    }
    if ($row % 2 -eq 0) {
      $line = "$esc[38;5;252m$($line)"
    }
    else {
      $line = "$esc[38;5;244m$($line)"
    }
    
  
    "$esc[38;5;15m$($Single.LEFT)$($line)$esc[0m"
  }

  function  drawHeader {
    [System.Console]::setcursorposition($win.X+1, $win.Y + 1)
    $H = " "
    foreach ($key in $columns.keys) {
      $len = $columns[$key][1]
      [string]$col = $key.PadRight($len," ")
      $H = $H, $col -join " "
    }
    $header = $H.PadRight($win.w-2,' ')
    [System.Console]::write("$esc[4m$esc[38;5;11m$($header)$esc[0m")
  }

  function drawFooter {

    [System.Console]::setcursorposition($win.X+1, $win.H -1)
    
    if ($sourceIdx -eq -1) {
      $s = $sources -join ","
    } else {
      $s = $sources[$sourceIdx]
    }
    $footerL = " Selected : $nbChecked"
    $footerR = "Source : [ $s ] "
    $fill = $win.w-2 - $footerL.Length - $footerR.Length
    $f = $footerL,"".PadRight($fill,' '),$footerR -join ""
    [System.Console]::write("$esc[48;5;19m$esc[38;5;15m$($f)$esc[0m")
  }

  $sources = $(Get-WGSources).keys
  $sourceIdx = -1
  $global:Host.UI.RawUI.FlushInputBuffer()
  Get-WingetposhConfig
  $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 = "$(color "[?]" "red") Help $(color "[F2]" "red") Source $(color "[Space]" "red") Select/Unselect $(color "[Enter]" "red") Accept $(color "[Esc]" "red") Quit"
  $win.drawWindow();
  $win.drawVersion();
  $nbLines = $Win.h - 3
  $blanks = makeBlanks $nblines $win

  $statedata = [System.Collections.Hashtable]::Synchronized([System.Collections.Hashtable]::new())
  
  $statedata.X = ($win.X + 3)
  $statedata.Y = ($win.Y + 1)
 
  if ($source) {
    $displayList = $list | Where-Object {$_.source.trim() -eq $source}
    if ($displayList.count -eq 0) {
      $displayList = $list
    }
  }
  else {
    $displayList = $list
  }

  $skip = 0
  $nbPages = [math]::Ceiling($displayList.count / $nbLines)
  $win.nbpages = $nbPages
  $page = 1
  $selected = 0
  $nbChecked = 0
  [System.Console]::CursorVisible = $false
  $redraw = $true
  while (-not $stop) {
    $win.page = $page
    [System.Console]::setcursorposition($win.X, $win.Y + 2)
    $row = 0
    if ($displayList.length -eq 1) {
      $checked = $displayList.Selected
      $partdisplayList = makelines $displayList $checked $row $selected $win.W-2
    }
    else {
      $partdisplayList = $displayList | Select-Object -First $nblines -Skip $skip | ForEach-Object {
        $index = (($page - 1) * $nbLines) + $row
        $checked = $displayList[$index].Selected
        makelines $displayList[$index] $checked $row $selected $win.W-2
        $row++
      }
    }
    $nbDisplay = $partdisplayList.Length
    $sText = $partdisplayList | Out-String 
    if ($redraw) {
      [System.Console]::setcursorposition($win.X, $win.Y + 2)
      [system.console]::write($blanks)
      $redraw = $false
    }
    [System.Console]::setcursorposition($win.X, $win.Y + 2)
    [system.console]::write($sText.Substring(0, $sText.Length - 2))
    drawHeader
    drawFooter
    $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
          $redraw = $true
        }
        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
          if ($displayList.length -eq 1) {
            $checked = $displayList.Selected
            $displayList.Selected = -not $checked
          }
          else {
            $index = (($page - 1) * $nbLines) + $selected
            $checked = $displayList[$index].Selected
            $displayList[$index].Selected = -not $checked
          }
          if ($checked) {$nbChecked--} else {$nbChecked++}
        }

        if ($key.VirtualKeyCode -eq 13) {
          # key Enter
          Clear-Host
          $data.value = $data.value = $displayList | Where-Object { $_.Selected }
          $stop = $true
        }
        if ($key.VirtualKeyCode -eq 114) {
          # key F3
          if ($allowSearch) {
            $term = getSearchTerms
            [System.Console]::CursorVisible = $false
            $term = '"', $term, '"' -join ''
            # Todo : re-run original search
            $sb = { Invoke-Winget "winget search --name $term" | Where-Object { $_.source -eq "winget" } }
            $displayList = Invoke-Command -ScriptBlock $sb
            $skip = 0
            $nbPages = [math]::Ceiling($displayList.count / $nbLines)
            $win.nbpages = $nbPages
            $page = 1
            $selected = 0
            $redraw = $true
          }
        }
        if ($key.VirtualKeyCode -eq 113) {
          # key F2
          $sourceIdx ++
          if ($sourceIdx -gt $sources.count -1) {
            $displayList = $list
            $sourceIdx = -1
          } else {
            $displayList = $list | Where-Object {$_.source.trim() -eq $sources[$sourceIdx]}
            if ($displayList.count -eq 0) {
              $displayList = $list
            }
          }
          $skip = 0
            $nbPages = [math]::Ceiling($displayList.count / $nbLines)
            $win.nbpages = $nbPages
            $page = 1
            $selected = 0
            $redraw = $true
        }
        if ($key.character -eq "+") {
          # key +
          $checked = $true
          $nbChecked = 0
          $displayList | ForEach-Object { $_.Selected = $checked; $nbChecked++ }
        }
        if ($key.character -eq "-") {
          # key -
          $checked = $false
          $displayList | ForEach-Object { $_.Selected = $checked }
          $nbChecked = 0
        }
        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, "red");
  $win.title = "Help"
  $Win.titleColor = "Blue"
  $win.footer = "$(color "[Esc]" "red") : Close"
  $win.drawWindow();

  $buffer = "$(color "↑↓" "cyan") : Navigate `t`t`t`t $(color "← →" "cyan") Change page"
  $y = 11
  [System.Console]::setcursorposition($win.X + 2, $Y)
  [system.console]::write($buffer)
  $buffer = "$(color "Space" "cyan") : Select / Unselect package `t`t $(color "+/-" "cyan") Select All/None "
  $Y ++
  [System.Console]::setcursorposition($win.X + 2, $Y)
  [system.console]::write($buffer)
  $Y ++
  $buffer = "$(color "F2" "cyan") Cycle Sources"
  [System.Console]::setcursorposition($win.X + 2, $Y)
  [system.console]::write($buffer)
  if ($allowSearch) {
    $y++
    $buffer = "$(color "F3" "cyan") : Enter Package Name"
    [System.Console]::setcursorposition($win.X + 2, $Y)
    [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'))
      if ($key.character -eq 'q' -or $key.VirtualKeyCode -eq 27) {
        $stop = $true
      }
    }
  }
}
  
function Get-WGPackage {
  param(
    [string]$source,
    [switch]$interactive,
    [switch]$uninstall,
    [switch]$update,
    [switch]$apply,
    [switch]$silent
  )
  if ($source) {
    $sources = Get-WGSources 
    if (-not $sources.Contains($source)) {
      Clear-Host
      Write-Host "⚠️ Source Unknown." -ForegroundColor DarkYellow
      Write-Host "".PadRight($Host.UI.RawUI.BufferSize.Width, "-") -ForegroundColor DarkYellow
      Write-Host "Valid sources are : " -ForegroundColor Blue
      $sources.keys | ForEach-Object {
        Write-Host " 🔹 $($_)"
      }
      Write-Host ""
      Write-Host "🛑 Operation Aborted"
      return $null
    }
  }

  $title = ""
  if ($update) {
    $command = "winget update --include-unknown"
  }
  else {
    $command = "winget list"
  }
  
  if ($apply) {
    if (-not $interactive) {
      Write-Warning "🚫 -apply can only be used with -interactive"
      return $null
    }
    if ((-not $update) -and (-not $uninstall)) {
      Write-Warning "🚫 -apply can only be used with -update or -uninstall"
      return $null
    }
  }

  if ($update -or $uninstall) {
    if (-not $interactive) {
      Write-Warning "🚫 -update and -uninstall can only be used with -interactive"
      return $null
    }
  }

  if ($update) {
    $title = "⫷ Update ⫸"
  }
  else {
    if ($uninstall) {
      $title = "⫷ Uninstall ⫸"
    }
  }

  $list = Invoke-Winget $command

  if ($source) {
    $list = $list |  Where-Object { $_.source -eq $source }
  }
  
  if ($interactive) {
    $data = @()
    displayGrid -list $list -title "Packages List $($title)" -data ([ref]$data) -allowSearch $false
    if ($apply) {
      $title = ""
      if ($data.length -gt 0) {
        $data | Out-Object | ForEach-Object {
          $id = ($_.Id).Trim()
          if ($uninstall) {
            $expression = "winget uninstall "
            if ($silent) {
              $expression = $expression, "--silent --disable-interactivity" -join ""
            }
            $expression = $expression, " --id $($id)" -join ""
            $title = "🗑️ Uninstall $($id)"
          }
          else {
            $expression = "winget upgrade --id $($id)"
            $title = "⚡ Upgrade $($id)"
          }
          [System.Console]::CursorVisible = $false
          Invoke-Expression2 -exp $expression -title $title
          #Write-Host "Exit code : $($LASTEXITCODE)"
          Write-Host "Name $($_.Name)"
          [System.Console]::CursorVisible = $true
        }
      }
      # display summary.

    }
    else {
      $data
    }
  }
  else {
    $list
  }
}

function Search-WGPackage {
  param(
    [string]$package,
    [string]$source,
    [switch]$interactive,
    [switch]$allowSearch,
    [switch]$install,
    [switch]$silent
  )
  begin {
    if ($source) {
      $sources = Get-WGSources 
      if (-not $sources.Contains($source)) {
        Clear-Host
        Write-Host "⚠️ Source Unknown." -ForegroundColor DarkYellow
        Write-Host "".PadRight($Host.UI.RawUI.BufferSize.Width, "-") -ForegroundColor DarkYellow
        Write-Host "Valid sources are : " -ForegroundColor Blue
        $sources.keys | ForEach-Object {
          Write-Host " 🔹 $($_)"
        }
        $terms = ""
        return $null
      }
    }
    $terms = $package
    if ($package.Trim() -eq "") {
      $terms = getSearchTerms
    }
  }
  process {
    if ($terms -ne "") {
      $command = "winget search '$terms'"
      $list = Invoke-Winget $command
      if ($interactive) {
        $data = @()
        displayGrid -list $list -source $source  -title "Package Search" -data ([ref]$data) -allowSearch $allowSearch
        if ($install) {
          if ($data.length -gt 0) {
            $data | Out-Object | ForEach-Object {
              $expression = "winget install "
              if ($silent) {
                $expression = $expression, "--silent --disable-interactivity" -join ""
              }
              $id = ($_.Id).Trim()
              $expression = $expression, " --id $($id)" -join ""
              [System.Console]::CursorVisible = $false
              Invoke-Expression2 -exp $expression -title "⚡ Installation of $($id)"
              #Write-Host "Exit code : $($LASTEXITCODE)"
              [System.Console]::CursorVisible = $true
            }
          }
        }
        $data
      }
      else {
        $list
      }
    }
    else {
      Clear-Host
      Write-Host ""
      Write-Host "🛑 Operation Aborted"
    }
  }
}

function Get-WGPVersion {
  param(
    [ValidateSet("Winget", "WGP", "All")]
    [String]$param="WGP"
  )

  if ($param -in ("Winget","All")) {
    $v = Invoke-Expression "winget -v" | Out-String -NoNewline
    Write-Host "Winget version : $v"
  }

  if ($param -in ("WGP","All")) {
    $v = $(Get-InstalledModule -Name wingetposh -ErrorAction Ignore).version
    Write-Host "Wingetposh version : $v"
  }

}

function Get-WGList {
  param(
    [string]$source
  )
  Get-WGPackage -source $source
}

function Show-WGList {
  param(
    [string]$source
  )
  Get-WGPackage -interactive -source $source
}

function Install-WGPackage {
  param(
    [string]$package,
    [string]$source,
    [switch]$silent,
    [switch]$acceptpackageagreements,
    [switch]$acceptsourceagreements
  )
  $params = @{
    interactive = $true
    package     = $package
    source      = $source
    install     = $true
  }
  Get-WingetposhConfig
  if ($silent) {
    $params.add("silent", $true)
  }
  else {
    $params.Add("silent", $script:config.SilentInstall)
  }
  Search-WGPackage @params
}

function Update-WGPackage {
  param(
    [string]$source,
    [switch]$apply
  )
  $params = @{
    Source      = $source
    Interactive = $true
    Update      = $true
    Apply       = $apply
  }
  
  Get-WGPackage @params
}

function Uninstall-WGPackage {
  param(
    [string]$source,
    [switch]$apply,
    [switch]$silent
  )
  $params = @{
    Interactive = $true
    Source      = $source
    Uninstall   = $true
    Apply       = $apply
  }
  Get-WingetposhConfig
  if ($silent) {
    $params.add("silent", $true)
  }
  else {
    $params.Add("silent", $script:config.SilentInstall)
  }
  Get-WGPackage @params
}

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 Get-WingetposhConfig {
  param(
    [switch]$display
  )
  $script:config = Get-Content $env:USERPROFILE/.config/.wingetposh/config.json | ConvertFrom-Json
  if ($display) {
    $script:config
  }
}

function Set-WingetposhConfig {
  param(
    [ValidateSet("UseNerdFont", "SilentInstall", "AcceptPackageAgreements", "AcceptSourceAgreements", "Force")]
    [String]$param,
    $value
  )
  Get-WingetposhConfig
  $script:config.$param = $value
  $script:config | ConvertTo-Json | Out-File -FilePath ~/.config/.wingetposh/config.json -Force | Out-Null
}

function Reset-WingetposhConfig {
  '{ "UseNerdFont" : false, "SilentInstall": false, "AcceptPackageAgreements" : true, "AcceptSourceAgreements" : true,"Force": false }' | Out-File -FilePath ~/.config/.wingetposh/config.json -Force | Out-Null
}