wingetposh.psm1

$include = [System.IO.Path]::GetDirectoryName($myInvocation.MyCommand.Definition) 

. "$include\visuals.ps1"

$widths = @(32, 32, 14, 14  , 8)
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 scoopList {
  [string]$Info
  [string]$Name
  [string]$Version
  [string]$Source
  [datetime]$Updated

}
class scoopSearch {
  [string]$Name
  [string]$Version
  [string]$Source
  [string]$Binaries

}

class scoopBuckets {
  [string]$Name
  [string]$Source
  [string]$Updated
  [int]$Manifests
}

class wingetSource {
  [string]$Name
  [string]$Argument
}
  
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)
  }
}
 

$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
}
    
  
$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
    removeLastPSRealineHistoryItem
    [console]::CursorVisible = $false
  }
  
  return $pack
}

function removeLastPSRealineHistoryItem {
  $psreadline_history = (Get-PSReadLineOption).HistorySavePath
  $h = Get-Content $psreadline_history
  $output = $h[0..($h.count - 2)]
  Clear-History -Newest
}

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 | Select-String -Pattern "(?:\S+)" -AllMatches).Matches
  $result = @()
  
  $w = $columsLine.Length
  $i = 0
  while ($i -lt $tempCols.Count) {
    $pos = $tempCols[$i].Index
    if ($i -eq $tempCols.Count - 1) {
      # Last Column
      $len = $width - $pos
    }
    else {
      # Not last Column
      $len = $tempCols[$i + 1].Index - $pos
    }
    $acolumn = [column]::new()
    # get EN Name
    $base = $script:fields.psobject.Properties | Where-Object { $_.Value -eq $tempCols[$i].Value }
    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 getColumnsHeaders0 {
  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())
  $stateInstall = [System.Collections.Hashtable]::Synchronized([System.Collections.Hashtable]::new())
  $stateInstall.exp = $exp
  $runspaceInstall = [runspacefactory]::CreateRunspace()
  $runspaceInstall.Open()
  $RunspaceInstall.SessionStateProxy.SetVariable("StateInstall", $StateInstall)
  $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++
    }
  }

  $sbInstall = {
    $result = Invoke-Expression -Command $stateInstall.exp
  }

  $session = [powershell]::create()
  $null = $session.AddScript($sb)
  $session.Runspace = $runspace
  $handle = $session.BeginInvoke()
  
  $sessionInstall = [powershell]::create()
  $null = $sessionInstall.AddScript($sbInstall)
  $sessionInstall.Runspace = $runspaceInstall
  $handleInstall = $sessionInstall.BeginInvoke()
  while (-not $handleInstall.IsCompleted) {
    
  }
  $sessionInstall.stop()
  $runspaceInstall.Dispose()
  $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]@{}
  if (Get-ScoopStatus) {
    $sources.Add("scoop", "")
  }
  $sources.Add("none", "")
  foreach ($line in $result) {
    if ($data) {
      $name, $argument = $line -split "\s+"
      $sources.Add($name, $argument)
    }
    else {
      $data = ($line.Contains('-----')) 
    }
  }
  $sources
}

function Get-ScoopBuckets {
  [scoopBuckets[]]$buckets = Invoke-Scoop "scoop bucket list"
  return $buckets
}

function Get-ScoopStatus {
  Get-WingetposhConfig
  $script:config.IncludeScoop -and (Test-Path -Path "$env:HOMEDRIVE$env:HOMEPATH\Scoop\")
}

function Invoke-Scoop {
  param (
    [string]$cmd
  )
  $stateCmd = [System.Collections.Hashtable]::Synchronized([System.Collections.Hashtable]::new())
  $stateCmd.exp = $cmd
  $stateCmd.SearchResult = ""
  $runspaceCmd = [runspacefactory]::CreateRunspace()
  $runspaceCmd.Open()
  $RunspaceCmd.SessionStateProxy.SetVariable("StateCmd", $StateCmd)

  $sbCmd = {
    $StateCmd.SearchResult = Invoke-Expression $stateCmd.exp
  }

  $sessionCmd = [powershell]::create()
  $null = $sessionCmd.AddScript($sbCmd)
  $sessionCmd.Runspace = $runspaceCmd
  $handleCmd = $sessionCmd.BeginInvoke()
  while (-not $handleCmd.IsCompleted) {
    
  }
  $SearchResult = $StateCmd.SearchResult
  $sessionCmd.stop()
  $runspaceCmd.Dispose() 
  return $SearchResult
}

function Invoke-Winget {
  param (
    [string]$cmd,
    [bool]$quiet

  )
  [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 
  if (-not $quiet) {
    [System.Console]::CursorVisible = $false
  }
  $PackageList = @()
  $stateInstall = [System.Collections.Hashtable]::Synchronized([System.Collections.Hashtable]::new())
  $stateInstall.exp = $cmd
  $stateInstall.SearchResult = ""
  $runspaceInstall = [runspacefactory]::CreateRunspace()
  $runspaceInstall.Open()
  $RunspaceInstall.SessionStateProxy.SetVariable("StateInstall", $StateInstall)

  $sbInstall = {
    $StateInstall.SearchResult = Invoke-Expression $stateInstall.exp | Out-String -Stream
  }

  $sessionInstall = [powershell]::create()
  $null = $sessionInstall.AddScript($sbInstall)
  $sessionInstall.Runspace = $runspaceInstall
  $handleInstall = $sessionInstall.BeginInvoke()
  while (-not $handleInstall.IsCompleted) {
    
  }
  $SearchResult = $StateInstall.SearchResult
  $sessionInstall.stop()
  $runspaceInstall.Dispose() 

  $SearchResult | ForEach-Object -Begin { $i = 0; $data = $false } -Process {
    if ($_.StartsWith('---')) {
      $lWidth = $_.Length
      $cols = getColumnsHeaders -columsLine $SearchResult[$i - 1] -width $lWidth
      $columnWidths = @()
      foreach ($column in [column[]]$cols) {
        $columnWidths += $column.Len
      }

      $totalAvailableSpace = $Host.UI.RawUI.WindowSize.Width - 10  # Subtracting 8 for padding
      $totalColumnWidths = $columnWidths | Measure-Object -Sum | Select-Object -ExpandProperty Sum

      # Calculate adjusted column widths
      $adjustedColumnWidths = @()
      foreach ($width in $columnWidths) {
        $adjustedWidth = [math]::Round(($width / $totalColumnWidths) * $totalAvailableSpace)
        $adjustedColumnWidths += $adjustedWidth
      }

      $columns.Clear()
      $i = 1
      foreach ($col in [column[]]$cols) {
        $colw = [math]::round(($totalAvailableSpace) / 100 * $widths[$i - 1])
        $Columns.Add($col.Name, @($col.Position, $colw, $col.len))
        $i++
      }
      $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
            if ($quiet) {
              $package.Add($col.Name, $field.trim())
            }
            else {
              $package.Add($col.Name, $field)
            }
            
          }
          $PackageList += $package
        }
      }
    }
    $i++
  }
  if (-not $quiet) {
    [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) {
    if ($charcount -gt ($col.Length - 1)) {
      [char]$char = " "
    }
    else {
      [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,
    $allowModifications = $false,
    $build = $false
  )

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

  function makelines {
    param (
      $list,
      $checked,
      $Deleted,
      $Updated,
      $row,
      $selected
    ) 
    
    [string]$line = ""
    if ($script:config.UseNerdFont -eq $true) {
      #$check = [char]::ConvertFromUtf32(0xf05d)
      if ($allowModifications -or $build) {
        if ($build) {
          $check = "📝"
        }
        else {
          $check = "📌"
        }
      }
      else {
        $check = "📦"
      }
      $update = "♻️"
      $delete = "🗑️"
    }
    else {
      $check = "✓ "
      $update = "↺ "
      $delete = "Ⅹ "
      #$delete = $check
    }
    
    foreach ($key in $columns.keys) {
      [string]$col = $list.$key
      $line = $line, $col -join " "
    }

    if ($deleted -or $Updated -or $checked) {
      if ($deleted) {
        $line = "$esc[38;5;46m$delete", $line -join ""
      }

      if ($Updated) {
        $line = "$esc[38;5;46m$Update", $line -join ""
      }
      
      if (-not $deleted -and -not $Updated) {
        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 = $sources.IndexOf("winget");
  $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 {
    $src = @()
    if ($sources[$sourceIdx].trim() -in ("none", "msstore")) {
      $src += ""
      $src += "msstore"
    }
    else {
      $src += $sources[$sourceIdx]
    }
    $displayList = $list | Where-Object { $src.Contains($_.source.trim()) }
    if ($displayList.count -eq 0) {
      $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
      $Deleted = $displayList.Deleted
      $Updated = $displayList.Updated
      $partdisplayList = makelines $displayList $checked $Deleted $Updated $row $selected
    }
    else {
      $partdisplayList = $displayList | Select-Object -First $nblines -Skip $skip | ForEach-Object {
        $index = (($page - 1) * $nbLines) + $row
        $checked = $displayList[$index].Selected
        $deleted = $displayList[$index].Deleted
        $Updated = $displayList[$index].Updated
        makelines $displayList[$index] $checked $deleted $Updated $row $selected
        $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 46) {
          # delete key
          if ($allowModifications -and -not $build) {
            if ($displayList.length -eq 1) {
              $deleted = $displayList.Deleted
              $displayList.deleted = -not $deleted
            }
            else {
              $index = (($page - 1) * $nbLines) + $selected
              $Deleted = $displayList[$index].Deleted
              $displayList[$index].Deleted = -not $Deleted
            }
          }
        }

        if ($key.VirtualKeyCode -eq 85) {
          # "u" key (update)
          if ($allowModifications -and -not $build) {
            if ($displayList.length -eq 1) {
              if ($displayList.Available) {
                $Updated = $displayList.Updated
                $displayList.Updated = -not $deleted
              }
            }
            else {
              $index = (($page - 1) * $nbLines) + $selected
              if ($displayList[$index].Available -and ($displayList[$index].Available.trim() -ne "")) {
                $Updated = $displayList[$index].Updated
                $displayList[$index].Updated = -not $Updated
              }
            }
          }
        }
        if ($key.VirtualKeyCode -eq 85) {
          # "Ctrl-u" key (update)
          if ($allowModifications) {
            if (($key.ControlKeyState -band 8) -ne 0) {
              $displayList | ForEach-Object { 
                $Updated = $_.Updated
                if ($_.Available -and ($_.Available.trim() -ne "")) {
                  $_.Updated = -not $Updated
                }
              }
            }
          }
        }

        if ($key.VirtualKeyCode -eq 13) {
          # key Enter
          Clear-Host
          $data.value = $data.value = $displayList | Where-Object { $_.Selected -or $_.Deleted -or $_.Updated }
          $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 {
            $src = @()
            if ($sources[$sourceIdx].trim() -in ("none", "msstore")) {
              $src += ""
              $src += "msstore"
            }
            else {
              $src += $sources[$sourceIdx]
            }
            $displayList = $list | Where-Object { $src.Contains($_.source.trim()) }
            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 GetVersion {
  $version = [string]$(Get-InstalledModule -Name wingetposh -ErrorAction Ignore).version
  if ([bool]$version) {
    return "Debug"
  }
  return $version
}

function openSpinner {
  $SpinnerWidth = 50
  $DotWidth = $SpinnerWidth - 2
  $statedata = [System.Collections.Hashtable]::Synchronized([System.Collections.Hashtable]::new())
  $statedata.X = [math]::round(($Host.UI.RawUI.BufferSize.Width - $SpinnerWidth) / 2)
  $statedata.Y = [math]::round(($Host.UI.RawUI.BufferSize.Height - 3) / 2)
  $statedata.SpinnerWidth = $SpinnerWidth
  $statedata.DotWidth = $DotWidth
  $stateData.SpinLimit = $SpinnerWidth - 5
  $runspace = [runspacefactory]::CreateRunspace()
  $runspace.Open()
  $Runspace.SessionStateProxy.SetVariable("StateData", $StateData)
  [window]$win = [window]::new($statedata.X, $statedata.Y, $SpinnerWidth, 2, $false, "White")
  $win.titleColor = "Red"
  $win.title = '⏳ Fetching Winget data '
  $win.drawWindow()
  $win.drawTitle()
  $statedata.X ++
  $statedata.Y ++
  $sb = {
    [System.Console]::CursorVisible = $false
    $x = $statedata.X
    $y = $statedata.Y
    
    $i = 1
    $string = "".PadRight($statedata.DotWidth, ".")
    $nav = "oOo"
    while ($true) {
      if ($i -lt $nav.Length) {
        $mobile = $nav.Substring($nav.Length - $i)
        $string = $mobile.PadRight($statedata.DotWidth, '.')
      }
      else {
        if ($i -gt $stateData.SpinLimit) {
          $nb = $statedata.DotWidth - $i
          $mobile = $nav.Substring(1, $nb)
          $string = $mobile.PadLeft($statedata.DotWidth, '.')
        }
        else {
          $left = "".PadLeft($i, '.')
          $right = "".PadRight($stateData.SpinLimit - $i, '.')
          $string = $left, $nav, $right -join ""
        }
      }
      [System.Console]::setcursorposition($X, $Y)
      [System.Console]::write($string)
      $i++
      if ($i -gt $statedata.DotWidth) {
        $i = 1
      }
      Start-Sleep -Milliseconds 100
    }
  }
  $session = [powershell]::create()
  $null = $session.AddScript($sb)
  $session.Runspace = $runspace
  $null = $session.BeginInvoke()
  return $Session, $runspace, $win
}

function closeSpinner {
  param(
    $Session,
    $Runspace
  )
  $null = $session.Stop()
  $null = $runspace.dispose() 
  [System.Console]::CursorVisible = $true
}
  
function Get-WGPackage {
  param(
    [string]$source,
    [switch]$interactive,
    [switch]$uninstall,
    [switch]$update,
    [switch]$apply,
    [switch]$silent,
    [switch]$Build,
    [bool]$quiet
  )
  Get-WingetposhConfig
  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 ⟭ " 
    }
  } 
  if (-not $quiet) {
    $Session, $Runspace, $win = openSpinner
  }
  $list = @(Invoke-Winget $command -quiet $quiet)
  # Include scoop search if configured
  if (Get-ScoopStatus) {
    [scoopList[]]$list2 = Invoke-Scoop -cmd "scoop list"
    if ($list2) {
      $list2 | ForEach-Object {
        if ($quiet) {
          $package = [ordered]@{}
          $package.add("Name", $_.Name.trim())
          $package.add("Id", $_.Name.trim())
          $package.add("Version", $_.Version.trim())
          $package.add("Available", $_.Version.trim())
          $package.add("Source", "scoop".trim())
        }
        else {
          $package = [ordered]@{}
          $package.add("Name", $_.Name.PadRight($columns["Name"][1], " "))
          $package.add("Id", $_.Name.PadRight($columns["Id"][1], " "))
          $package.add("Version", $_.Version.PadRight($columns["Version"][1], " "))
          $package.add("Available", $_.Version.PadRight($columns["Version"][1], " "))
          $package.add("Source", "scoop".PadRight($columns["Source"][1], " "))
        }
     
        $list += $package
      }
    }
  }
  if (-not $quiet) {
    closeSpinner -Session $Session -Runspace $Runspace
  }

  if ($source) {
    $list = $list |  Where-Object { $_.source -eq $source }
  }
  
  if ($interactive) {
    $data = @()
    if ($build) {
      $title = " ⟬ Build Install File ⟭ "
    }
    displayGrid -list $list -title "Packages List $($title)" -data ([ref]$data) -allowSearch $false -allowModifications $true -Build $Build
    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 {
      return $data
    }
  }
  else {
    return $list
  }
}

function Search-WGPackage {
  param(
    [string]$package,
    [string]$source,
    [switch]$interactive,
    [switch]$allowSearch,
    [switch]$install,
    [switch]$silent,
    [bool]$quiet
  )
  begin {
    Get-WingetposhConfig
    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
    }
    $terms = $terms.Replace(" ", "")
  }
  process {
    if ($terms -ne "") {
      $list = @()
      if (-not $quiet) {
        $Session, $Runspace, $win = openSpinner
      }
      $terms -split "," | ForEach-Object { 
        $term = $_
        $command = "winget search '$term'"
        $result = @(Invoke-Winget -quiet $quiet $command)
        $result | ForEach-Object { 
          $list += $_
        }
        if (Get-ScoopStatus) {
          $win.title = "⏳ Fetching Scoop $term data "
          $win.drawWindow()
          $win.drawTitle()
          $ScoopCmd = "scoop search $([regex]::escape($term))"
          [scoopSearch[]]$list2 = Invoke-Scoop -cmd $ScoopCmd
          if ($list2) {
            Get-ScoopBuckets | ForEach-Object { $buckets += $_.Name }
            Clear-Host
            $list2 | ForEach-Object {
              if ($buckets.contains($_.Source)) {
                $pkg = [ordered]@{}
                $pkg.add("Name", $_.Name.PadRight($columns["Name"][1], " "))
                $pkg.add("Id", $_.Name.PadRight($columns["Id"][1], " "))
                $version = $_.Version.PadRight($columns["Version"][1], " ")
                $pkg.add("Version", $version.Substring(0, $columns["Version"][1]))
                $pkg.add("Moniker", "".PadRight($columns["Moniker"][1], " "))
              } 
              else {
                $pkg = [ordered]@{}
                $pkg.add("Name", $_.Name.PadRight($columns["Name"][1], " "))
                $pkg.add("Id", "‼️ Missing bucket ‼️".PadRight($columns["Id"][1], " "))
                $version = $_.Source.PadRight($columns["Version"][1], " ")
                $pkg.add("Version", $version.Substring(0, $columns["Version"][1]))
                $pkg.add("Moniker", "".PadRight($columns["Moniker"][1], " "))
              }
          
              $pkg.add("Source", "scoop".PadRight($columns["Source"][1], " "))
              $list += $pkg
            }
          }
        }
      }
      if (-not $quiet) {
       closeSpinner -Session $Session -Runspace $Runspace
      }
      if ($interactive) {
        Get-ScoopBuckets | ForEach-Object { $buckets += $_.Name }
        $data = @()
        displayGrid -list $list -source $source  -title "Package Search ⟬ Install ⟭ " -data ([ref]$data) -allowSearch $allowSearch 
        if ($install) {
          if ($data.length -gt 0) {
            $data | Out-Object | ForEach-Object {
              if ($_.source.trim() -eq "scoop") {
                $expression = "scoop install "
                $expression = $expression, " $($_.Name)" -join ""
                [System.Console]::CursorVisible = $false
                Invoke-Expression2 -exp $expression -title "⚡Invoking scoop for $($_.Name)"
                [System.Console]::CursorVisible = $true
              }
              else {
                $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)"
                [System.Console]::CursorVisible = $true
              }
            }
          }
        }
      }
      else {
        $list
      }
    }
    else {
      Clear-Host
      Write-Host ""
      Write-Host "🛑 Operation Aborted"
    }
  }
}

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

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

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

}

function Get-WGList {
  param(
    [string]$source,
    [bool]$quiet
  )
  $params = @{
    source = $source
    quiet  = $quiet
  }
  Get-WGPackage @params
}

function Build-WGInstallFile {
  param(
    [string]$file = "WGConfig.json"
  )

  if (Test-Path -Path $file) {
    Remove-Item -Path $file
  }

  $data = Get-WGPackage -interactive -Build
  $data | Out-Object | ConvertTo-Json | Out-File -FilePath $file -Append
  Write-Host "Config file writen in $file"
}

function Show-WGList {
  param(
    [string]$source
  )
  $data = Get-WGPackage -interactive -source $source
  if (($data | Out-Object | Where-Object { $_.updated -or $_.deleted }).count -gt 0) {
    $data | Out-Object | ForEach-Object {
      if ($_.Deleted -or $_.Updated) {
        $id = ($_.Id).Trim()
        if ($_.Deleted) {
          $expression = "winget uninstall "
          if ($silent) {
            $expression = $expression, "--silent --disable-interactivity" -join ""
          }
          $expression = $expression, " --id $($id)" -join ""
          $title = "🗑️ Uninstall $($id)"
          $action = " is Uninstalled"
        }
        if ($_.Updated) {
          $expression = "winget upgrade --id $($id)"
          $title = "⚡ Upgrade $($id)"
          $action = " is Updated"
        }
        [System.Console]::CursorVisible = $false
        Invoke-Expression2 -exp $expression -title $title
        #Write-Host "Exit code : $($LASTEXITCODE)"
        Write-Host "Name $($_.Name) $action"
        [System.Console]::CursorVisible = $true
      } 
    }
  }
  else {
    $data | Out-Object
  }
}

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,
    [bool]$quiet
  )
  $interactive = -not $quiet
  $params = @{
    Source      = $source
    Interactive = $interactive
    Update      = -not $quiet
    Apply       = $apply
    quiet       = $quiet
  }
  
  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-JSON {
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
    [hashtable]
    $Data
  )
  begin {
    [PSCustomObject[]]$result = @()
  }
  process {
    foreach ($d in $data) {
      $result += [pscustomobject]$d
    }
  }
  end {
    return (@{
      "packages"= $result
    }) | ConvertTo-Json
  }
}

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", "IncludeScoop")]
    [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, "IncludeScoop": false }' | Out-File -FilePath ~/.config/.wingetposh/config.json -Force | Out-Null
}

function Start-Gui {
  $path =  (Get-Module -Name wingetposh).path | Split-Path -Parent
  Invoke-Expression "$path\WGGui.exe"
}