cliHelper.core.psm1

#!/usr/bin/env pwsh
using namespace System
using namespace System.IO
using namespace System.Web
using namespace System.Text
using namespace System.Threading
using namespace System.Collections
using namespace System.ComponentModel
using namespace System.Threading.workers
using namespace System.Collections.Generic
using namespace System.Management.Automation
using namespace System.Collections.Concurrent
using namespace System.Collections.Specialized
using namespace System.Text.Json.Serialization
using namespace System.Runtime.InteropServices
using namespace System.Management.Automation.Runspaces

#region Classes
#Requires -RunAsAdministrator
#Requires -Modules cliHelper.xconvert, cliHelper.xcrypt
#Requires -Psedition Core


#region Config_classes
class PsRecord {
  hidden [uri] $Remote # usually a gist uri
  hidden [string] $File
  hidden [datetime] $LastWriteTime = [datetime]::Now

  PsRecord() {
    $this._init_()
  }
  PsRecord([hashtable]$hashtable) {
    $this.Add(@($hashtable)); $this._init_()
  }
  PsRecord([hashtable[]]$array) {
    $this.Add($array); $this._init_()
  }
  hidden [void] _init_() {
    $this.PsObject.Methods.Add([PSScriptMethod]::new('GetCount', [ScriptBlock]::Create({ ($this | Get-Member -Type *Property).count })))
    $this.PsObject.Methods.Add([PSScriptMethod]::new('GetKeys', [ScriptBlock]::Create({ ($this | Get-Member -Type *Property).Name })))
  }
  [void] Edit() {
    $this.Set([PsRecord]::EditFile([IO.FileInfo]::new($this.File)))
    $this.Save()
  }
  [void] Add([hashtable]$table) {
    [ValidateNotNullOrEmpty()][hashtable]$table = $table
    $Keys = $table.Keys | Where-Object { !$this.HasNoteProperty($_) -and ($_.GetType().FullName -eq 'System.String' -or $_.GetType().BaseType.FullName -eq 'System.ValueType') }
    foreach ($key in $Keys) {
      if ($key -notin ('File', 'Remote', 'LastWriteTime')) {
        $this | Add-Member -MemberType NoteProperty -Name $key -Value $table[$key]
      } else {
        $this.$key = $table[$key]
      }
    }
  }
  [void] Add([hashtable[]]$items) {
    foreach ($item in $items) { $this.Add($item) }
  }
  [void] Add([string]$key, [System.Object]$value) {
    [ValidateNotNullOrEmpty()][string]$key = $key
    if (!$this.HasNoteProperty($key)) {
      $htab = [hashtable]::new(); $htab.Add($key, $value); $this.Add($htab)
    } else {
      Write-Warning "Config.Add() Skipped $Key. Key already exists."
    }
  }
  [void] Add([List[hashtable]]$items) {
    foreach ($item in $items) { $this.Add($item) }
  }
  [void] Set([OrderedDictionary]$dict) {
    $dict.Keys.Foreach({ $this.Set($_, $dict["$_"]) });
  }
  [void] Set([hashtable]$table) {
    [ValidateNotNullOrEmpty()][hashtable]$table = $table
    $Keys = $table.Keys | Where-Object { $_.GetType().FullName -eq 'System.String' -or $_.GetType().BaseType.FullName -eq 'System.ValueType' } | Sort-Object -Unique
    foreach ($key in $Keys) {
      if (!$this.HasNoteProperty($key)) {
        $this | Add-Member -MemberType NoteProperty -Name $key -Value $table[$key] -Force
      } else {
        $this.$key = $table[$key]
      }
    }
  }
  [void] Set([hashtable[]]$items) {
    foreach ($item in $items) { $this.Set($item) }
  }
  [void] Set([string]$key, [System.Object]$value) {
    $htab = [hashtable]::new(); $htab.Add($key, $value)
    $this.Set($htab)
  }
  # work in progress
  static [hashtable[]] Read([string]$FilePath) {
    $cfg = $null
    # $pass = $null;
    # try {
    # [ValidateNotNullOrEmpty()][string]$FilePath = [AesGCM]::GetUnResolvedPath($FilePath)
    # if (![IO.File]::Exists($FilePath)) { throw [System.IO.FileNotFoundException]::new("File '$FilePath' was not found") }
    # if ([string]::IsNullOrWhiteSpace([AesGCM]::caller)) { [AesGCM]::caller = [PsRecord]::caller }
    # Set-Variable -Name pass -Scope Local -Visibility Private -Option Private -Value $(if ([xcrypt]::EncryptionScope.ToString() -eq "User") { Read-Host -Prompt "$([PsRecord]::caller) Paste/write a Password to decrypt configs" -AsSecureString }else { [AesGCM]::GetUniqueMachineId() | xconvert ToSecurestring })
    # $_ob = [AesGCM]::Decrypt(([IO.File]::ReadAllText($FilePath) | xconvert FromBase85), $pass) | xconvert FromCompressed, FromBytes
    # $cfg = [hashtable[]]$_ob.Keys.ForEach({ @{ $_ = $_ob.$_ } })
    # } catch {
    # throw $_.Exeption
    # } finally {
    # Remove-Variable Pass -Force -ErrorAction SilentlyContinue
    # }
    return $cfg
  }
  # static [hashtable[]] EditFile([IO.FileInfo]$File) {
  # $result = @(); $private:config_ob = $null; $fswatcher = $null; $process = $null;
  # [ValidateScript({ if ([IO.File]::Exists($_)) { return $true } ; throw [System.IO.FileNotFoundException]::new("File '$_' was not found") })][IO.FileInfo]$File = $File;
  # $OutFile = [IO.FileInfo][IO.Path]::GetTempFileName()
  # $UseVerbose = [bool]$((Get-Variable verbosePreference -ValueOnly) -eq "continue")
  # try {
  # [NetworkManager]::BlockAllOutbound()
  # if ($UseVerbose) { "[+] Edit Config started .." | Write-Host -ForegroundColor Magenta }
  # [PsRecord]::Read($File.FullName) | ConvertTo-Json | Out-File $OutFile.FullName -Encoding utf8BOM
  # Set-Variable -Name OutFile -Value $(Rename-Item $outFile.FullName -NewName ($outFile.BaseName + '.json') -PassThru)
  # $process = [System.Diagnostics.Process]::new()
  # $process.StartInfo.FileName = 'nvim'
  # $process.StartInfo.Arguments = $outFile.FullName
  # $process.StartInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Maximized
  # $process.Start(); $fswatcher = [FileMonitor]::MonitorFile($outFile.FullName, [ScriptBlock]::Create("Stop-Process -Id $($process.Id) -Force"));
  # if ($null -eq $fswatcher) { Write-Warning "Failed to start FileMonitor"; Write-Host "Waiting nvim process to exit..." $process.WaitForExit() }
  # $private:config_ob = [IO.FILE]::ReadAllText($outFile.FullName) | ConvertFrom-Json
  # } finally {
  # [NetworkManager]::UnblockAllOutbound()
  # if ($fswatcher) { $fswatcher.Dispose() }
  # if ($process) {
  # "[+] Neovim process {0} successfully" -f $(if (!$process.HasExited) {
  # $process.Kill($true)
  # "closed"
  # } else {
  # "exited"
  # }
  # ) | Write-Host -ForegroundColor Green
  # $process.Close()
  # $process.Dispose()
  # }
  # Remove-Item $outFile.FullName -Force
  # if ($UseVerbose) { "[+] FileMonitor Log saved in variable: `$$([fileMonitor]::LogvariableName)" | Write-Host -ForegroundColor Magenta }
  # if ($null -ne $config_ob) { $result = $config_ob.ForEach({ $_ | xconvert ToHashTable }) }
  # if ($UseVerbose) { "[+] Edit Config completed." | Write-Host -ForegroundColor Magenta }
  # }
  # return $result
  # }
  # [void] Save() {
  # $pass = $null;
  # try {
  # Write-Host "$([PsRecord]::caller) Save records to file: $($this.File) ..." -ForegroundColor Blue
  # Set-Variable -Name pass -Scope Local -Visibility Private -Option Private -Value $(if ([xcrypt]::EncryptionScope.ToString() -eq "User") { Read-Host -Prompt "$([PsRecord]::caller) Paste/write a Password to encrypt configs" -AsSecureString } else { [AesGCM]::GetUniqueMachineId() | xconvert ToSecurestring })
  # $this.LastWriteTime = [datetime]::Now; [IO.File]::WriteAllText($this.File, ([AesGCM]::Encrypt($($this.ToByte() | xconvert ToCompressed), $pass) | xconvert ToBase85), [System.Text.Encoding]::UTF8)
  # Write-Host "$([PsRecord]::caller) Save records " -ForegroundColor Blue -NoNewline; Write-Host "Completed." -ForegroundColor Green
  # } catch {
  # throw $_.Exeption
  # } finally {
  # Remove-Variable Pass -Force -ErrorAction SilentlyContinue
  # }
  # }
  hidden [bool] HasNoteProperty([object]$Name) {
    [ValidateNotNullOrEmpty()][string]$Name = $($Name -as 'string')
    return (($this | Get-Member -Type NoteProperty | Select-Object -ExpandProperty name) -contains "$Name")
  }
  [void] Import([String]$FilePath) {
    Write-Host "Import records: $FilePath ..." -ForegroundColor Green
    $this.Set([PsRecord]::Read($FilePath))
    Write-Host "Import records Complete" -ForegroundColor Green
  }
  [byte[]] ToByte() {
    return $this | xconvert ToBytes
  }
  [void] Import([uri]$raw_uri) {
    # try {
    # $pass = $null;
    # Set-Variable -Name pass -Scope Local -Visibility Private -Option Private -Value $(if ([xcrypt]::EncryptionScope.ToString() -eq "User") { Read-Host -Prompt "$([PsRecord]::caller) Paste/write a Password to decrypt configs" -AsSecureString }else { [xconvert]::ToSecurestring([AesGCM]::GetUniqueMachineId()) })
    # $_ob = [xconvert]::Deserialize([xconvert]::ToDeCompressed([AesGCM]::Decrypt([base85]::Decode($(Invoke-WebRequest $raw_uri -Verbose:$false).Content), $pass)))
    # $this.Set([hashtable[]]$_ob.Keys.ForEach({ @{ $_ = $_ob.$_ } }))
    # } catch {
    # throw $_.Exeption
    # } finally {
    # Remove-Variable Pass -Force -ErrorAction SilentlyContinue
    # }
  }
  [void] Upload() {
    if ([string]::IsNullOrWhiteSpace($this.Remote)) { throw [System.ArgumentException]::new('remote') }
    # $gisturi = 'https://gist.github.com/' + $this.Remote.Segments[2] + $this.Remote.Segments[2].replace('/', '')
    # [GitHub]::UpdateGist($gisturi, $content)
  }
  [array] ToArray() {
    $array = @(); $props = $this | Get-Member -MemberType NoteProperty
    if ($null -eq $props) { return @() }
    $props.name | ForEach-Object { $array += @{ $_ = $this.$_ } }
    return $array
  }
  [string] ToJson() {
    return [string]($this | Select-Object -ExcludeProperty count | ConvertTo-Json -Depth 3)
  }
  [System.Collections.Specialized.OrderedDictionary] ToOrdered() {
    $dict = [System.Collections.Specialized.OrderedDictionary]::new(); $Keys = $this.PsObject.Properties.Where({ $_.Membertype -like "*Property" }).Name
    if ($Keys.Count -gt 0) {
      $Keys | ForEach-Object { [void]$dict.Add($_, $this."$_") }
    }
    return $dict
  }
  [string] ToString() {
    $r = $this.ToArray(); $s = ''
    $shortnr = [ScriptBlock]::Create({
        param([string]$str, [int]$MaxLength)
        while ($str.Length -gt $MaxLength) {
          $str = $str.Substring(0, [Math]::Floor(($str.Length * 4 / 5)))
        }
        return $str
      }
    )
    if ($r.Count -gt 1) {
      $b = $r[0]; $e = $r[-1]
      $0 = $shortnr.Invoke("{'$($b.Keys)' = '$($b.values.ToString())'}", 40)
      $1 = $shortnr.Invoke("{'$($e.Keys)' = '$($e.values.ToString())'}", 40)
      $s = "@($0 ... $1)"
    } elseif ($r.count -eq 1) {
      $0 = $shortnr.Invoke("{'$($r[0].Keys)' = '$($r[0].values.ToString())'}", 40)
      $s = "@($0)"
    } else {
      $s = '@()'
    }
    return $s
  }
}

class RGB {
  [ValidateRange(0, 255)]
  [int]$Red
  [ValidateRange(0, 255)]
  [int]$Green
  [ValidateRange(0, 255)]
  [int]$Blue
  RGB() {}
  RGB([string]$Name) {
    $IsValid = [color].GetProperties().Name.Contains($Name)
    if (!$IsValid) { throw [System.InvalidCastException]::new("Color name '$Name' is not valid. See: [color].GetProperties().Name") }
    [RGB]$c = [color]::$Name
    $this.Red = $c.Red
    $this.Green = $c.Green
    $this.Blue = $c.Blue
  }
  RGB([int]$r, [int]$g, [int]$b) {
    $this.Red = $r
    $this.Green = $g
    $this.Blue = $b
  }
  [string] ToString() {
    return [color].GetProperties().Name.Where({ [color]::$_ -eq $this })
  }
}

class Color : System.ValueType {
  static [rgb] $Red = [rgb]::new(255, 0, 0)
  static [rgb] $DarkRed = [rgb]::new(128, 0, 0)
  static [rgb] $Green = [rgb]::new(0, 255, 0)
  static [rgb] $DarkGreen = [rgb]::new(0, 128, 0)
  static [rgb] $Blue = [rgb]::new(0, 0, 255)
  static [rgb] $DarkBlue = [rgb]::new(0, 0, 128)
  static [rgb] $White = [rgb]::new(255, 255, 255)
  static [rgb] $Black = [rgb]::new(0, 0, 0)
  static [rgb] $Yellow = [rgb]::new(255, 255, 0)
  static [rgb] $DarkGray = [rgb]::new(128, 128, 128)
  static [rgb] $Gray = [rgb]::new(192, 192, 192)
  static [rgb] $LightGray = [rgb]::new(238, 237, 240)
  static [rgb] $Cyan = [rgb]::new(0, 255, 255)
  static [rgb] $DarkCyan = [rgb]::new(0, 128, 128)
  static [rgb] $Magenta = [rgb]::new(255, 0, 255)
  static [rgb] $PSBlue = [rgb]::new(1, 36, 86)
  static [rgb] $AliceBlue = [rgb]::new(240, 248, 255)
  static [rgb] $AntiqueWhite = [rgb]::new(250, 235, 215)
  static [rgb] $AquaMarine = [rgb]::new(127, 255, 212)
  static [rgb] $Azure = [rgb]::new(240, 255, 255)
  static [rgb] $Beige = [rgb]::new(245, 245, 220)
  static [rgb] $Bisque = [rgb]::new(255, 228, 196)
  static [rgb] $BlanchedAlmond = [rgb]::new(255, 235, 205)
  static [rgb] $BlueViolet = [rgb]::new(138, 43, 226)
  static [rgb] $Brown = [rgb]::new(165, 42, 42)
  static [rgb] $Burlywood = [rgb]::new(222, 184, 135)
  static [rgb] $CadetBlue = [rgb]::new(95, 158, 160)
  static [rgb] $Chartreuse = [rgb]::new(127, 255, 0)
  static [rgb] $Chocolate = [rgb]::new(210, 105, 30)
  static [rgb] $Coral = [rgb]::new(255, 127, 80)
  static [rgb] $CornflowerBlue = [rgb]::new(100, 149, 237)
  static [rgb] $CornSilk = [rgb]::new(255, 248, 220)
  static [rgb] $Crimson = [rgb]::new(220, 20, 60)
  static [rgb] $DarkGoldenrod = [rgb]::new(184, 134, 11)
  static [rgb] $DarkKhaki = [rgb]::new(189, 183, 107)
  static [rgb] $DarkMagenta = [rgb]::new(139, 0, 139)
  static [rgb] $DarkOliveGreen = [rgb]::new(85, 107, 47)
  static [rgb] $DarkOrange = [rgb]::new(255, 140, 0)
  static [rgb] $DarkOrchid = [rgb]::new(153, 50, 204)
  static [rgb] $DarkSalmon = [rgb]::new(233, 150, 122)
  static [rgb] $DarkSeaGreen = [rgb]::new(143, 188, 143)
  static [rgb] $DarkSlateBlue = [rgb]::new(72, 61, 139)
  static [rgb] $DarkSlateGray = [rgb]::new(47, 79, 79)
  static [rgb] $DarkTurquoise = [rgb]::new(0, 206, 209)
  static [rgb] $DarkViolet = [rgb]::new(148, 0, 211)
  static [rgb] $DeepPink = [rgb]::new(255, 20, 147)
  static [rgb] $DeepSkyBlue = [rgb]::new(0, 191, 255)
  static [rgb] $DimGray = [rgb]::new(105, 105, 105)
  static [rgb] $DodgerBlue = [rgb]::new(30, 144, 255)
  static [rgb] $FireBrick = [rgb]::new(178, 34, 34)
  static [rgb] $FloralWhite = [rgb]::new(255, 250, 240)
  static [rgb] $ForestGreen = [rgb]::new(34, 139, 34)
  static [rgb] $GainsBoro = [rgb]::new(220, 220, 220)
  static [rgb] $GhostWhite = [rgb]::new(248, 248, 255)
  static [rgb] $Gold = [rgb]::new(255, 215, 0)
  static [rgb] $Goldenrod = [rgb]::new(218, 165, 32)
  static [rgb] $GreenYellow = [rgb]::new(173, 255, 47)
  static [rgb] $HoneyDew = [rgb]::new(240, 255, 240)
  static [rgb] $HotPink = [rgb]::new(255, 105, 180)
  static [rgb] $IndianRed = [rgb]::new(205, 92, 92)
  static [rgb] $Indigo = [rgb]::new(75, 0, 130)
  static [rgb] $Ivory = [rgb]::new(255, 255, 240)
  static [rgb] $Khaki = [rgb]::new(240, 230, 140)
  static [rgb] $Lavender = [rgb]::new(230, 230, 250)
  static [rgb] $LavenderBlush = [rgb]::new(255, 240, 245)
  static [rgb] $LawnGreen = [rgb]::new(124, 252, 0)
  static [rgb] $LemonChiffon = [rgb]::new(255, 250, 205)
  static [rgb] $LightBlue = [rgb]::new(173, 216, 230)
  static [rgb] $LightCoral = [rgb]::new(240, 128, 128)
  static [rgb] $LightCyan = [rgb]::new(224, 255, 255)
  static [rgb] $LightGoldenrodYellow = [rgb]::new(250, 250, 210)
  static [rgb] $LightPink = [rgb]::new(255, 182, 193)
  static [rgb] $LightSalmon = [rgb]::new(255, 160, 122)
  static [rgb] $LightSeaGreen = [rgb]::new(32, 178, 170)
  static [rgb] $LightSkyBlue = [rgb]::new(135, 206, 250)
  static [rgb] $LightSlateGray = [rgb]::new(119, 136, 153)
  static [rgb] $LightSteelBlue = [rgb]::new(176, 196, 222)
  static [rgb] $LightYellow = [rgb]::new(255, 255, 224)
  static [rgb] $LimeGreen = [rgb]::new(50, 205, 50)
  static [rgb] $Linen = [rgb]::new(250, 240, 230)
  static [rgb] $MediumAquaMarine = [rgb]::new(102, 205, 170)
  static [rgb] $MediumOrchid = [rgb]::new(186, 85, 211)
  static [rgb] $MediumPurple = [rgb]::new(147, 112, 219)
  static [rgb] $MediumSeaGreen = [rgb]::new(60, 179, 113)
  static [rgb] $MediumSlateBlue = [rgb]::new(123, 104, 238)
  static [rgb] $MediumSpringGreen = [rgb]::new(0, 250, 154)
  static [rgb] $MediumTurquoise = [rgb]::new(72, 209, 204)
  static [rgb] $MediumVioletRed = [rgb]::new(199, 21, 133)
  static [rgb] $MidnightBlue = [rgb]::new(25, 25, 112)
  static [rgb] $MintCream = [rgb]::new(245, 255, 250)
  static [rgb] $MistyRose = [rgb]::new(255, 228, 225)
  static [rgb] $Moccasin = [rgb]::new(255, 228, 181)
  static [rgb] $NavajoWhite = [rgb]::new(255, 222, 173)
  static [rgb] $OldLace = [rgb]::new(253, 245, 230)
  static [rgb] $Olive = [rgb]::new(128, 128, 0)
  static [rgb] $OliveDrab = [rgb]::new(107, 142, 35)
  static [rgb] $Orange = [rgb]::new(255, 165, 0)
  static [rgb] $OrangeRed = [rgb]::new(255, 69, 0)
  static [rgb] $Orchid = [rgb]::new(218, 112, 214)
  static [rgb] $PaleGoldenrod = [rgb]::new(238, 232, 170)
  static [rgb] $PaleGreen = [rgb]::new(152, 251, 152)
  static [rgb] $PaleTurquoise = [rgb]::new(175, 238, 238)
  static [rgb] $PaleVioletRed = [rgb]::new(219, 112, 147)
  static [rgb] $PapayaWhip = [rgb]::new(255, 239, 213)
  static [rgb] $PeachPuff = [rgb]::new(255, 218, 185)
  static [rgb] $Peru = [rgb]::new(205, 133, 63)
  static [rgb] $Pink = [rgb]::new(255, 192, 203)
  static [rgb] $Plum = [rgb]::new(221, 160, 221)
  static [rgb] $PowderBlue = [rgb]::new(176, 224, 230)
  static [rgb] $Purple = [rgb]::new(128, 0, 128)
  static [rgb] $RosyBrown = [rgb]::new(188, 143, 143)
  static [rgb] $RoyalBlue = [rgb]::new(65, 105, 225)
  static [rgb] $SaddleBrown = [rgb]::new(139, 69, 19)
  static [rgb] $Salmon = [rgb]::new(250, 128, 114)
  static [rgb] $SandyBrown = [rgb]::new(244, 164, 96)
  static [rgb] $SeaGreen = [rgb]::new(46, 139, 87)
  static [rgb] $SeaShell = [rgb]::new(255, 245, 238)
  static [rgb] $Sienna = [rgb]::new(160, 82, 45)
  static [rgb] $SkyBlue = [rgb]::new(135, 206, 235)
  static [rgb] $SlateBlue = [rgb]::new(106, 90, 205)
  static [rgb] $SlateGray = [rgb]::new(112, 128, 144)
  static [rgb] $Snow = [rgb]::new(255, 250, 250)
  static [rgb] $SpringGreen = [rgb]::new(0, 255, 127)
  static [rgb] $SteelBlue = [rgb]::new(70, 130, 180)
  static [rgb] $Tan = [rgb]::new(210, 180, 140)
  static [rgb] $Thistle = [rgb]::new(216, 191, 216)
  static [rgb] $Tomato = [rgb]::new(255, 99, 71)
  static [rgb] $Turquoise = [rgb]::new(64, 224, 208)
  static [rgb] $Violet = [rgb]::new(238, 130, 238)
  static [rgb] $Wheat = [rgb]::new(245, 222, 179)
  static [rgb] $WhiteSmoke = [rgb]::new(245, 245, 245)
  static [rgb] $YellowGreen = [rgb]::new(154, 205, 50)
}

# .SYNOPSIS
# shell config.
# .LINK
# https://effective-shell.com
# .NOTES
# Props reference: https://learn.microsoft.com/en-us/windows-hardware/customize/desktop/unattend/microsoft-windows-shell-setup
class ShellConfig : PsRecord {
  [string]$Title = 'Terminal'
  [Encoding]$Encoding = 'UTF8' # Default is utf-8 Encoding
  [bool]$AutoLogon #Specifies credentials for an account that is used to automatically log on to the computer.
  [bool]$BluetoothTaskbarIconEnabled #Specifies whether to enable the Bluetooth taskbar icon.
  [string]$ComputerName #Specifies the name of the computer.
  [bool]$ConvertibleSlateModePromptPreference # Configure to support prompts triggered by changes to ConvertibleSlateMode. OEMs must make sure that ConvertibleSlateMode is always accurate for their devices.
  [dotProfile]$dotProfile = $( { ConvertFrom-Json '
        {
            "path": "PathTo\\Microsoft.PowerShell_profile.ps1",
            "autoUpdate": true,
            "autoLoad": true,
            "ExecutionPolicy": "RemoteSigned",
            "CopyProfile": true
        }
    '
}.Invoke())
  [bool]$DisableAutoDaylightTimeSet #Specifies whether to enable the destination computer to automatically change between daylight saving time and standard time.
  [array]$Display # Specifies display settings to apply to a destination computer.
  [string[]]$FirstLogonCommands #Specifies commands to run the first time that an end user logs on to the computer. This setting is not supported in Windows 10 in S mode.
  [string[]]$FolderLocations #Specifies the location of the user profile and program data folders.
  [string[]]$LogonCommands #Specifies commands to run when an end user logs on to the computer.
  [array]$NotificationArea #Specifies settings that are related to the system notification area at the far right of the taskbar.
  [string]$SignInMode #Specifies whether users switch to tablet mode by default after signing in.
  [string[]]$TaskbarLinks #Specifies shortcuts to display on the taskbar. You can specify up to three links.
  [string[]]$Themes #Specifies custom elements of the Windows visual style.
  [TimeZone]$TimeZone = [TimeZone]::CurrentTimeZone #Specifies the computer's time zone.
  [string[]]$VisualEffects #Specifies additional display settings.
  [string[]]$WindowsFeatures
  [PSObject]$RequiredModules = { ConvertFrom-Json '
        [
            {
                "name": "posh-git",
                "version": "1.0"
            },
            {
                "name": "oh-my-posh",
                "version": "1.0"
            },
            {
                "name": "PSReadLine",
                "version": "2.0.4"
            }
        ]
    '
}.Invoke()
  [string]$OmpConfig #Path to omp.json

  ShellConfig() {}

  [xml] ToXML() {
    return $this.PsObject.Properties | Export-Clixml
  }
  [string] ToJSON() {
    return $this.PsObject.Properties | Select-Object Name, value | ConvertTo-Json
  }
  [string] ToString() {
    return [string]::Empty
  }
}
class dotProfile {
  [string]$path = [IO.Path]::Combine([Environment]::GetFolderPath('MyDocuments'), 'WindowsPowershell', 'Microsoft.PowerShell_profile.ps1')
  [Microsoft.PowerShell.ExecutionPolicy]$ExecutionPolicy
  [cliart]$Banner = [cliart]::Create("H4sIAAAAAAAAA22SQQ+CMAyFf5AHTVwiV2UHCVFMDOyMiS4kHHA4iP/edmW2EA4v27r27XsE1ZmP6u4nlvEq1IzP0qMNKvLKPfPSsirnQ61y1jVtdmvaeg/rGdYDnYMuMK/prlboU9rN3ws96AzqC+EFc8CiWZKR+fhMfZM/eUa+OTPv41sPPXFvBTfm2QWGFzOTJ3p89bw3uyaCSfCaIfWrmUMNM4fcMT/fyR6am9e4F3zfsg9zYaY6AX4zIgOtxCO+4yA0Lu48z6zN8T5tpn9koej5A1txgllgAgAA", ("Quick productive tech"))
  [bool]$CopyProfile
  [bool]$autoLoad = $true
  [bool]$autoUpdate = $true
  [bool]$IsLoaded = $false
  dotProfile() { }
  dotProfile([string]$Json) {
    $this.Import($Json)
  }
  dotProfile([System.IO.FileInfo]$JsonPath) {
    $this.Import($(Get-Content $JsonPath.FullName))
  }
  [void] Install() {
    # Copies the profile Script to all ProfilePaths
    [dotProfile]::Install($(Get-Variable -Name PROFILE -Scope global).Value)
  }
  [object] static Install([string]$Path) {
    # Installs all necessary cli stuff as configured.
    if ($null -eq [dotProfile]::config) {
      throw [System.ArgumentNullException]::new('Config')
    }
    $IsSuccess = $true; $ErrorRecord = $Result = $Output = $null
    try {
      #region Self_Update
      if ([dotProfile]::config.dotProfile.autoUpdate) {
        # Make $profile Auto update itself to the latest version gist
        Invoke-Command -ScriptBlock $([ScriptBlock]::Create({
              While ($true) {
                Start-Sleep 3 #seconds
                #Run this script, change the text below, and save this script
                #and the PowerShell window stays open and starts running the new version without a hitch
                # "Hi"
                $lastWriteTimeOfThisScriptNow = [datetime](Get-ItemProperty -Path $PSCommandPath -Name LastWriteTime).LastWriteTime
                if ($lastWriteTimeOfThisScriptWhenItFirstStarted -ne $lastWriteTimeOfThisScriptNow) {
                  Get-LatestdotProfile
                  exit
                }
              }
            }
          )
        )
      }
      #endregion Self_Update
      if ([dotProfile]::config.dotProfile.autoLoad) {
        if (![dotProfile]::config.dotProfile.IsLoaded) {
          # Invoke-Command -ScriptBlock $Load_Profile_Functions
          $IsSuccess = $? -and $([dotProfile]::SetHostUI())
          $IsSuccess = $? -and $([dotProfile]::Set_PowerlinePrompt())
        } else {
          Write-Debug "dotProfile is already Initialized, Skipping This step ..."
        }
      } else {
        # Invoke-Command -ScriptBlock $Load_Profile_Functions
        $IsSuccess = $? -and $([dotProfile]::SetHostUI())
        $IsSuccess = $? -and $([dotProfile]::Set_PowerlinePrompt())
      }
      # show Banner # or 'Message Of The Day' ...
      if (($IsSuccess -eq $true) -and [dotProfile]::config.dotProfile.IsLoaded) {
        Write-Host ''
        Write-Console $([dotProfile]::Banner) -ForegroundColor SlateBlue -BackgroundColor Black;
        Write-Host ''
      }
    } catch {
      $IsSuccess = $false
      $ErrorRecord = [System.Management.Automation.ErrorRecord]$_
      # Write-Log -ErrorRecord $_
    } finally {
      $Result = [PSCustomObject]@{
        Output      = $Output
        IsSuccess   = $IsSuccess
        ErrorRecord = $ErrorRecord
      }
    }
    return $Result
  }
  [bool] static SetHostUI() {
    $Title = [dotProfile]::config.Title
    $user = [Security.Principal.WindowsIdentity]::GetCurrent()
    [void][Security.Principal.WindowsIdentity]::GetAnonymous()
    $ob = [Security.Principal.WindowsPrincipal]::new($user)
    $UserRole = [PSCustomObject]@{
      'HasUserPriv'  = [bool]$ob.IsInRole([Security.Principal.WindowsBuiltinRole]::User)
      'HasAdminPriv' = [bool]$ob.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
      'HasSysPriv'   = [bool]$ob.IsInRole([Security.Principal.WindowsBuiltinRole]::SystemOperator)
      'IsPowerUser'  = [bool]$ob.IsInRole([Security.Principal.WindowsBuiltinRole]::PowerUser)
      'IsGuest'      = [bool]$ob.IsInRole([Security.Principal.WindowsBuiltinRole]::Guest)
    }
    $UserRole.PSObject.TypeNames.Insert(0, 'Security.User.RoleProperties')
    $b1 = [string][char]91
    $b2 = [string][char]93
    if ($UserRole.IsPowerUser) {
      $Title += $($b1 + 'E' + $b2) # IsElevated
    }
    if ($UserRole.HasSysPriv) {
      $Title += $($b1 + 'S' + $b2) # IsSYSTEM
    }
    # Admin indicator maybe not neded, since Windows11 build 22557.1
    if ($UserRole.HasAdminPriv) { $Title += ' (Admin)' }
    if ($UserRole.HasUserPriv -and !($UserRole.HasAdminPriv)) { $Title += ' (User)' }
    $h = Get-Variable -Name Host -Scope Global -ErrorAction SilentlyContinue
    if ([bool]$h) {
      $h.UI.RawUI.WindowTitle = "$Title"
      $h.UI.RawUI.ForegroundColor = "White"
      $h.PrivateData.ErrorForegroundColor = "DarkGray"
    }
    # Write-Verbose "Set Encoding"
    $c = "[System.Text.Encoding]::Get_{0}()" -f [dotProfile]::config.Encoding
    [console]::InputEncoding = [console]::OutputEncoding = [scriptblock]::Create($c).Invoke()
    #Banner was Generated using an online tool, then converted to base64 for easyUsage
    [dotProfile]::config.dotProfile.IsLoaded = $true
    return $true -and $([dotProfile]::Set_PowerlinePrompt())
  }
  [bool] static hidden Set_PowerlinePrompt() {
    # Sets Custom prompt if nothing goes wrong then shows a welcome Ascii Art
    $IsSuccess = $true
    try {
      $null = New-Item -Path function:prompt -Value $([dotProfile]::config.dotProfile.PSObject.Methods['ShowPrompt']) -Force
      $IsSuccess = $IsSuccess -and $?
    } catch {
      $IsSuccess = $false
      # Write-Log -ErrorRecord $_
    } finally {
      Set-Variable -Name dotProfileIsLoaded -Value $IsSuccess -Visibility Public -Scope Global;
    }
    return $IsSuccess
  }
  [void] Import([string]$Json) {
    $props = { ConvertFrom-Json $Json }.Invoke()
    $this.PsObject.Properties.Name | ForEach-Object {
      $val = $props[$_]
      if ($null -ne $val) {
        $this.PsObject.Properties[$_] = $val
      }
    }
    # $this.IsLoaded = $true
  }
  [string]ShowPrompt() {
    $realLASTEXITCODE = $LASTEXITCODE
    $nl = [Environment]::NewLine;
    # UTF8 Characters from https://www.w3schools.com/charsets/ref_utf_box.asp
    $dt = [string][char]8230
    $b1 = [string][char]91
    $b2 = [string][char]93
    $swiglyChar = [char]126
    $Home_Indic = $swiglyChar + [IO.Path]::DirectorySeparatorChar
    # $leadingChar = [char]9581 + [char]9472 + [char]9592 # long
    # $trailngChar = [char]9584 + [char]9472 + [char]9588 # long
    $leadingChar = [char]9581 + [char]9592;
    $trailngChar = [char]9584 + [char]9588; if ($NestedPromptLevel -ge 1) { $trailngChar += [string][char]9588 }
    $location = $shortLoc = [string]::Empty
    # Grab current loaction
    try {
      $location = "$((Get-Variable ExecutionContext -Scope local).SessionState.Path.CurrentLocation.Path)";
      $shortLoc = Invoke-PathShortener $location -TruncateChar $dt;
    } catch {
      Set-Location ..
    }
    $IsGitRepo = if ([bool]$(try { Test-Path .git -ErrorAction silentlyContinue }catch { $false })) { $true }else { $false }
    try {
      $(Get-Variable Host -Scope local).UI.Write($leadingChar)
      Write-Host -NoNewline $b1;
      Write-Host $([Environment]::UserName).ToLower() -NoNewline -ForegroundColor Magenta;
      Write-Host $([string][char]64) -NoNewline -ForegroundColor Gray;
      Write-Host $([System.Net.Dns]::GetHostName().ToLower()) -NoNewline -ForegroundColor DarkYellow;
      Write-Host -NoNewline "$b2 ";
      if ($location -eq "$env:UserProfile") {
        Write-Host $Home_Indic -NoNewline -ForegroundColor DarkCyan;
      } elseif ($location.Contains("$env:UserProfile")) {
        $location = $($location.replace("$env:UserProfile", "$swiglyChar"));
        if ($location.Length -gt 25) {
          $location = $(Invoke-PathShortener $location -TruncateChar $dt)
        }
        Write-Host $location -NoNewline -ForegroundColor DarkCyan
      } else {
        Write-Host $shortLoc -NoNewline -ForegroundColor DarkCyan
      }
      if ($IsGitRepo) {
        Write-Host $(Write-VcsStatus)
      } else {
        # No alternate display, just send a newline
        Write-Host ''
      }
      # $vs = Write-VcsStatus
      # [char[]]$vs.count
      # ' ' * $($Host.UI.RawUI.WindowSize.Width - 19) + 'Time______goes_Here'
      # $dt = [datetime]::Now; $day = $dt.ToLongDateString().split(',')[1].trim()
      # $CurrentTime = "$day, $($dt.Year) $($dt.Hour):$($dt.Minute)"
    } catch {
      <#Do this if a terminating exception happens#>
      # if ($_.Exception.WasThrownFromThrowStatement) {
      # [System.Management.Automation.ErrorRecord]$_ | Write-Log $LogFile
      # }
      $(Get-Variable Host -Scope local).UI.WriteErrorLine("[PromptError] [$($_.FullyQualifiedErrorId)] $($_.Exception.Message) # see the Log File : `$LogFile $nl")
    } finally {
      #Do this after the try block regardless of whether an exception occurred or not#
      $LASTEXITCODE = $realLASTEXITCODE
    }
    return $trailngChar
  }
}

#endregion Config_classes

#region console_art
# .SYNOPSIS
# A console writeline helper
# .EXAMPLE
# Write-AnimatedHost "Hello world" -f magenta
class cli {
  static hidden [ValidateNotNull()][string]$Preffix # .EXAMPLE Try this: # [cli]::Preffix = '@:'; [void][cli]::Write('animations and stuff', [ConsoleColor]::Magenta)
  static hidden [ValidateNotNull()][scriptblock]$textValidator # ex: if $text does not match a regex throw 'erro~ ..'
  static [string] write([string]$text) {
    return [cli]::Write($text, 20, 1200)
  }
  static [string] Write([string]$text, [bool]$AddPreffix) {
    return [cli]::Write($text, 20, 1200, $AddPreffix)
  }
  static [string] Write([string]$text, [int]$Speed, [int]$Duration) {
    return [cli]::Write($text, 20, 1200, $true)
  }
  static [string] write([string]$text, [ConsoleColor]$color) {
    return [cli]::Write($text, $color, $true)
  }
  static [string] write([string]$text, [ConsoleColor]$color, [bool]$Animate) {
    return [cli]::Write($text, [cli]::Preffix, 20, 1200, $color, $Animate, $true)
  }
  static [string] write([string]$text, [int]$Speed, [int]$Duration, [bool]$AddPreffix) {
    return [cli]::Write($text, [cli]::Preffix, $Speed, $Duration, [ConsoleColor]::White, $true, $AddPreffix)
  }
  static [string] write([string]$text, [ConsoleColor]$color, [bool]$Animate, [bool]$AddPreffix) {
    return [cli]::Write($text, [cli]::Preffix, 20, 1200, $color, $Animate, $AddPreffix)
  }
  static [string] write([string]$text, [string]$Preffix, [System.ConsoleColor]$color) {
    return [cli]::Write($text, $Preffix, $color, $true)
  }
  static [string] write([string]$text, [string]$Preffix, [System.ConsoleColor]$color, [bool]$Animate) {
    return [cli]::Write($text, $Preffix, 20, 1200, $color, $Animate, $true)
  }
  static [string] write([string]$text, [string]$Preffix, [int]$Speed, [int]$Duration, [bool]$AddPreffix) {
    return [cli]::Write($text, $Preffix, $Speed, $Duration, [ConsoleColor]::White, $true, $AddPreffix)
  }
  static [string] write([string]$text, [string]$Preffix, [int]$Speed, [int]$Duration, [ConsoleColor]$color, [bool]$Animate, [bool]$AddPreffix) {
    return [cli]::Write($text, $Preffix, $Speed, $Duration, $color, $Animate, $AddPreffix, [cli]::textValidator)
  }
  static [string] write([string]$text, [string]$Preffix, [int]$Speed, [int]$Duration, [ConsoleColor]$color, [bool]$Animate, [bool]$AddPreffix, [scriptblock]$textValidator) {
    if ($null -ne $textValidator) {
      $textValidator.Invoke($text)
    }
    if ([string]::IsNullOrWhiteSpace($text)) {
      return $text
    }
    [int]$length = $text.Length; $delay = 0
    # Check if delay time is required:
    $delayIsRequired = if ($length -lt 50) { $false } else { $delay = $Duration - $length * $Speed; $delay -gt 0 }
    if ($AddPreffix -and ![string]::IsNullOrEmpty($Preffix)) {
      [void][cli]::Write($Preffix, [string]::Empty, 1, 100, [ConsoleColor]::Green, $false, $false);
    }
    $FgColr = [Console]::ForegroundColor
    [Console]::ForegroundColor = $color
    if ($Animate) {
      for ($i = 0; $i -lt $length; $i++) {
        [void][Console]::Write($text[$i]);
        Start-Sleep -Milliseconds $Speed;
      }
    } else {
      [void][Console]::Write($text);
    }
    if ($delayIsRequired) {
      Start-Sleep -Milliseconds $delay
    }
    [Console]::ForegroundColor = $FgColr
    return $text
  }
  static [string] ReadHost(
    [string]$Default = { throw 'Please enter what the default value will be if user just hits [Enter]' }.Invoke(),
    [string]$Prompt = { throw 'Enter a password value or accept default of' }.Invoke()
  ) {
    $Response = Read-Host -Prompt ($Prompt + " [$Default]")
    if ('' -eq $response) {
      return $Default
    } else {
      return $Response
    }
  }
}

# .SYNOPSIS
# cliart helper.
# .DESCRIPTION
# A class to convert dot ascii arts to b64string & vice versa
# .LINK
# https://getcliart.vercel.app
# .EXAMPLE
# $art = [cliart]::Create((Get-Item ./ascii))
# .EXAMPLE
# $a = [cliart]"/home/alain/Documents/GitHub/clihelper_modules/cliHelper.core/Tests/cliart_test.txt/hacker"
# Write-Console -Text $a -f SpringGreen
# $print_expression = $a.GetPrinter()
# Now instead of hard coding the content of the art file, you can use $print_expression anywhere in your script
class cliart {
  hidden [list[string]] $taglines = @()
  hidden [ValidateNotNullOrWhiteSpace()][string] $cstr
  cliart() { }
  cliart([byte[]]$bytes) { [void][cliart]::_init_($bytes, [ref]$this) }
  cliart([string]$string) { [void][cliart]::_init_($string, [string[]]@(), [ref]$this) }
  cliart([IO.FileInfo]$file) { [void][cliart]::_init_($file, [ref]$this) }

  static [cliart] Create([byte[]]$bytes) { return [cliart]::_init_($bytes, [ref][cliart]::new()) }
  static [cliart] Create([string]$string) { return [cliart]::_init_($string, [ref][cliart]::new()) }
  static [cliart] Create([IO.FileInfo]$file) { return [cliart]::_init_($file, [ref][cliart]::new()) }
  static [cliart] Create([byte[]]$bytes, [string[]]$taglines) { return [cliart]::_init_($bytes, $taglines, [ref][cliart]::new()) }
  static [cliart] Create([string]$string, [string[]]$taglines) { return [cliart]::_init_($string, $taglines, [ref][cliart]::new()) }

  static hidden [cliart] _init_([string]$s, $o) { $o.Value.cstr = $s; return $o.Value }

  static hidden [cliart] _init_([string]$s, [string[]]$taglines, $o) {
    $use_verbose = (Get-Variable VerbosePreference -Scope global -ValueOnly) -eq 'Continue'
    $i = switch ($true) {
      ([xcrypt]::IsValidUrl($s)) { Get-Item (Start-DownloadWithRetry -Url $s -Message "download cliart" -Verbose:$use_verbose).FullName; break }
      ([xcrypt]::IsBase64String($s)) { $s; break }
      ([IO.Path]::IsPathFullyQualified($s)) { (Get-Item $s); break }
      Default {
        throw [ArgumentException]::new('Invalid input string. [cliart]::Create() requires a valid url, base64 string or path.')
      }
    }
    $o.Value.cstr = [cliart]::_init_($i, $o).cstr;
    if ([IO.File]::Exists($i)) { Remove-Item $i -Verbose:$false -Force -ea Ignore }
    if ($taglines.Count -gt 0) { $taglines.ForEach({ [void]$o.Value.taglines.Add($_) }) }
    return $o.Value
  }
  static hidden [cliart] _init_([IO.FileInfo]$file, $o) {
    return [cliart]::_init_([IO.File]::ReadAllBytes($file.FullName), @(), $o)
  }
  static hidden [cliart] _init_([byte[]]$bytes, [string[]]$taglines, $o) {
    $o.Value.cstr = [convert]::ToBase64String($bytes) | xconvert ToCompressed;
    if ($taglines.Count -gt 0) { $taglines.ForEach({ [void]$o.Value.taglines.Add($_) }) }
    return $o.Value
  }
  static [string] Print([string]$cstr) {
    if ([string]::IsNullOrWhiteSpace($cstr)) { return [string]::Empty }
    return [System.Text.Encoding]::UTF8.GetString([convert]::FromBase64String(($cstr | xconvert FromCompressed)))
  }
  hidden [string] GetPrinter() {
    return '[cliart]::Print("{0}")' -f $this.cstr
  }
  [void] Write() {
    $this.Write($true)
  }
  [void] Write([bool]$Animate) {
    $this.Write(0, $true, $Animate)
  }
  [void] Write([string]$AdditionalText) {
    $this.Write('LimeGreen', 0, $true, $AdditionalText, $true)
  }
  [void] Write([int]$SpaceBeforeTagline, [bool]$Nonewline, [bool]$Animate) {
    $this.Write('LimeGreen', $SpaceBeforeTagline, $Nonewline, [string]::Empty, $Animate)
  }
  [void] Write([string]$asciicolor, [int]$SpaceBeforeTagline, [bool]$Nonewline, [string]$AdditionalText, [bool]$Animate) {
    $this.Write($asciicolor, $SpaceBeforeTagline, $asciicolor, $Nonewline, $AdditionalText, $Animate)
  }
  [void] Write([string]$asciicolor, [int]$SpaceBeforeTagline, [string]$TaglineColor, [bool]$Nonewline, [string]$AdditionalText, [bool]$Animate) {
    $this.ToString() | Write-Console -f $asciicolor; $last_line = '{0}{1}' -f $([string][char]32 * $SpaceBeforeTagline), $this.GetTagline()
    if (![string]::IsNullOrWhiteSpace($last_line)) { $last_line | Write-Console -f $TaglineColor -NoNewLine:$Nonewline -Animate:$Animate }
    if (![string]::IsNullOrWhiteSpace($AdditionalText)) { $AdditionalText | Write-Console -f LightCyan -Animate:$Animate }
  }
  [string] GetTagline() { return $this.taglines | Get-Random }

  [string] ToString() { return [cliart]::Print($this.cstr) }
}

#endregion console_art

class NetworkManager {
  [string] $HostName
  static [System.Net.IPAddress[]] $IPAddresses
  static [PsRecord] $DownloadOptions = [PsRecord]::New(@{
      ShowProgress      = $true
      ProgressBarLength = [int]([Console]::WindowWidth * 0.7)
      ProgressMessage   = [string]::Empty
      RetryTimeout      = 1000 #(milliseconds)
      Headers           = @{}
      Proxy             = $null
      Force             = $false
    }
  )
  static [string] $caller

  NetworkManager ([string]$HostName) {
    $this.HostName = $HostName
    $this::IPAddresses = [System.Net.Dns]::GetHostAddresses($HostName)
  }
  static [string] GetResponse ([string]$URL) {
    [System.Net.HttpWebRequest]$Request = [System.Net.HttpWebRequest]::Create($URL)
    $Request.Method = "GET"
    $Request.Timeout = 10000 # 10 seconds
    [System.Net.HttpWebResponse]$Response = [System.Net.HttpWebResponse]$Request.GetResponse()
    if ($Response.StatusCode -eq [System.Net.HttpStatusCode]::OK) {
      [System.IO.Stream]$ReceiveStream = $Response.GetResponseStream()
      [System.IO.StreamReader]$ReadStream = [System.IO.StreamReader]::new($ReceiveStream)
      [string]$Content = $ReadStream.ReadToEnd()
      $ReadStream.Close()
      $Response.Close()
      return $Content
    } else {
      throw "The request failed with status code: $($Response.StatusCode)"
    }
  }
  static [void] BlockAllOutbound() {
    $HostOs = [xcrypt]::Get_Host_Os()
    if ($HostOs -eq "Linux") {
      sudo iptables -P OUTPUT DROP
    } else {
      netsh advfirewall set allprofiles firewallpolicy blockinbound, blockoutbound
    }
  }
  static [void] UnblockAllOutbound() {
    $HostOs = [xcrypt]::Get_Host_Os()
    if ($HostOs -eq "Linux") {
      sudo iptables -P OUTPUT ACCEPT
    } else {
      netsh advfirewall set allprofiles firewallpolicy blockinbound, allowoutbound
    }
  }
  static [IO.FileInfo] DownloadFile([uri]$url) {
    # No $outFile so we create ones ourselves, and use suffix to prevent duplicaltes
    $randomSuffix = [Guid]::NewGuid().Guid.subString(15).replace('-', [string]::Join('', (0..9 | Get-Random -Count 1)))
    return [NetworkManager]::DownloadFile($url, "$(Split-Path $url.AbsolutePath -Leaf)_$randomSuffix");
  }
  static [IO.FileInfo] DownloadFile([uri]$url, [string]$outFile) {
    return [NetworkManager]::DownloadFile($url, $outFile, $false)
  }
  static [IO.FileInfo] DownloadFile([uri]$url, [string]$outFile, [bool]$Force) {
    [ValidateNotNullOrEmpty()][uri]$url = $url; [ValidateNotNull()][bool]$Force = ($Force -as [bool])
    [ValidateNotNullOrEmpty()][string]$outFile = $outFile; $stream = $null;
    $fileStream = $null; $name = Split-Path $url -Leaf;
    $request = [System.Net.HttpWebRequest]::Create($url)
    $request.UserAgent = "Mozilla/5.0"
    $response = $request.GetResponse()
    $contentLength = $response.ContentLength
    $stream = $response.GetResponseStream()
    $buffer = New-Object byte[] 1024
    $outPath = [xcrypt]::GetUnResolvedPath($outFile)
    if ([System.IO.Directory]::Exists($outFile)) {
      if (!$Force) { throw [ArgumentException]::new("Please provide valid file path, not a directory.", "outFile") }
      $outPath = Join-Path -Path $outFile -ChildPath $name
    }
    $Outdir = [IO.Path]::GetDirectoryName($outPath)
    if (![System.IO.Directory]::Exists($Outdir)) { [void][System.IO.Directory]::CreateDirectory($Outdir) }
    if ([IO.File]::Exists($outPath)) {
      if (!$Force) { throw "$outFile already exists" }
      Remove-Item $outPath -Force -ErrorAction Ignore | Out-Null
    }
    $fileStream = [System.IO.FileStream]::new($outPath, [IO.FileMode]::Create, [IO.FileAccess]::ReadWrite, [IO.FileShare]::None)
    $totalBytesReceived = 0
    $totalBytesToReceive = $contentLength
    $OgForeground = (Get-Variable host).Value.UI.RawUI.ForegroundColor
    $Progress_Msg = [NetworkManager]::DownloadOptions.ProgressMessage
    if ([string]::IsNullOrWhiteSpace($Progress_Msg)) { $Progress_Msg = "[+] Downloading $name to $Outfile" }
    Write-Host $Progress_Msg -ForegroundColor Magenta
    $(Get-Variable host).Value.UI.RawUI.ForegroundColor = [ConsoleColor]::Green
    while ($totalBytesToReceive -gt 0) {
      $bytesRead = $stream.Read($buffer, 0, 1024)
      $totalBytesReceived += $bytesRead
      $totalBytesToReceive -= $bytesRead
      $fileStream.Write($buffer, 0, $bytesRead)
      if ([NetworkManager]::DownloadOptions.ShowProgress) {
        [ProgressUtil]::WriteProgressBar([int]($totalBytesReceived / $contentLength * 100), $true, [NetworkManager]::DownloadOptions.progressBarLength);
      }
    }
    $(Get-Variable host).Value.UI.RawUI.ForegroundColor = $OgForeground
    try { Invoke-Command -ScriptBlock { $stream.Close(); $fileStream.Close() } -ErrorAction SilentlyContinue } catch { $null }
    return (Get-Item $outFile)
  }
  static [void] UploadFile ([string]$SourcePath, [string]$DestinationURL) {
    Invoke-RestMethod -Uri $DestinationURL -Method Post -InFile $SourcePath
  }
  static [bool] TestConnection ([string]$HostName) {
    [ValidateNotNullOrEmpty()][string]$HostName = $HostName
    if (![bool]("System.Net.NetworkInformation.Ping" -as 'type')) { Add-Type -AssemblyName System.Net.NetworkInformation };
    $cs = $null; $cc = [NetworkManager]::caller; $re = @{ true = @{ m = "Success"; c = "Green" }; false = @{ m = "Failed"; c = "Red" } }
    Write-Host "$cc Testing Connection ... " -ForegroundColor Blue -NoNewline
    try {
      [System.Net.NetworkInformation.PingReply]$PingReply = [System.Net.NetworkInformation.Ping]::new().Send($HostName);
      $cs = $PingReply.Status -eq [System.Net.NetworkInformation.IPStatus]::Success
    } catch [System.Net.Sockets.SocketException], [System.Net.NetworkInformation.PingException] {
      $cs = $false
    } catch {
      $cs = $false;
      Write-Error $_
    }
    $re = $re[$cs.ToString()]
    Write-Host $re.m -ForegroundColor $re.c
    return $cs
  }
  static [bool] IsIPv6AddressValid([string]$IP) {
    $IPv4Regex = '(((25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})\.){3}(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))'
    $G = '[a-f\d]{1,4}'
    $Tail = @(":",
      "(:($G)?|$IPv4Regex)",
      ":($IPv4Regex|$G(:$G)?|)",
      "(:$IPv4Regex|:$G(:$IPv4Regex|(:$G){0,2})|:)",
      "((:$G){0,2}(:$IPv4Regex|(:$G){1,2})|:)",
      "((:$G){0,3}(:$IPv4Regex|(:$G){1,2})|:)",
      "((:$G){0,4}(:$IPv4Regex|(:$G){1,2})|:)")
    [string] $IPv6RegexString = $G
    $Tail | ForEach-Object { $IPv6RegexString = "${G}:($IPv6RegexString|$_)" }
    $IPv6RegexString = ":(:$G){0,5}((:$G){1,2}|:$IPv4Regex)|$IPv6RegexString"
    $IPv6RegexString = $IPv6RegexString -replace '\(' , '(?:' # make all groups non-capturing
    [regex] $IPv6Regex = $IPv6RegexString
    if ($IP -imatch "^$IPv6Regex$") {
      return $true
    } else {
      return $false
    }
  }
  static [bool] IsMACAddressValid([string]$mac) {
    $RegEx = "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})|([0-9A-Fa-f]{2}){6}$"
    if ($mac -match $RegEx) {
      return $true
    } else {
      return $false
    }
  }
  static [bool] IsSubNetMaskValid([string]$IP) {
    $RegEx = "^(254|252|248|240|224|192|128).0.0.0$|^255.(254|252|248|240|224|192|128|0).0.0$|^255.255.(254|252|248|240|224|192|128|0).0$|^255.255.255.(255|254|252|248|240|224|192|128|0)$"
    if ($IP -match $RegEx) {
      return $true
    } else {
      return $false
    }
  }
  static [bool] IsIPv4AddressValid([string]$IP) {
    $RegEx = "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
    if ($IP -match $RegEx) {
      return $true
    } else {
      return $false
    }
  }
}


<#
.SYNOPSIS
  A simple progress utility class
.EXAMPLE
  for ($i = 0; $i -le 100; $i++) {
    [ProgressUtil]::WriteProgressBar($i, "doing stuff")
  }
.EXAMPLE
  [progressUtil]::WaitJob("waiting", { Start-Sleep -Seconds 3 });
.EXAMPLE
  $j = [ProgressUtil]::WaitJob("waiting", { Param($ob) Start-Sleep -Seconds 3; return $ob }, (Get-Process pwsh));
  $j | Receive-Job
 
  NPM(K) PM(M) WS(M) CPU(s) Id SI ProcessName
  ------ ----- ----- ------ -- -- -----------
        0 0.00 559.55 94.22 53184 …84 pwsh
        0 0.00 253.84 6.91 55195 …23 pwsh
.EXAMPLE
  Wait-Task -ScriptBlock { Start-Sleep -Seconds 3; $input | Out-String } -InputObject (Get-Process pwsh)
.EXAMPLE
  $RequestParams = @{
    Uri = "placeholderuri"
    Method = "Post"
    Body = "jsonbody"
  }
  $r = [progressUtil]::WaitJob($progressmsg, { $p = $input; Invoke-RestMethod @p }, $RequestParams)
#>

class ProgressUtil {
  static hidden [string] $_block = '■';
  static hidden [string] $_back = "`b";
  static hidden [string[]] $_twirl = @(
    "◰◳◲◱",
    "◇◈◆",
    "◐◓◑◒",
    "←↖↑↗→↘↓↙",
    "┤┘┴└├┌┬┐",
    "⣾⣽⣻⢿⡿⣟⣯⣷",
    "|/-\\",
    "-\\|/",
    "|/-\\"
  );
  static hidden [int] $_twirlIndex = 0
  static hidden [string]$frames
  static [void] WriteProgressBar([int]$percent) {
    [ProgressUtil]::WriteProgressBar($percent, $true, "")
  }
  static [void] WriteProgressBar([int]$percent, [string]$message) {
    [ProgressUtil]::WriteProgressBar($percent, $true, $message)
  }
  static [void] WriteProgressBar([int]$percent, [bool]$update, [string]$message) {
    [ProgressUtil]::WriteProgressBar($percent, $update, [int]([Console]::WindowWidth * 0.7), $message)
  }
  static [void] WriteProgressBar([int]$percent, [bool]$update, [string]$message, [bool]$Completed) {
    [ProgressUtil]::WriteProgressBar($percent, $update, [int]([Console]::WindowWidth * 0.7), $message, $Completed)
  }
  static [void] WriteProgressBar([int]$percent, [bool]$update, [int]$PBLength, [string]$message) {
    [ProgressUtil]::WriteProgressBar($percent, $update, $PBLength, $message, $false)
  }
  static [void] WriteProgressBar([int]$percent, [bool]$update, [int]$PBLength, [string]$message, [bool]$Completed) {
    [ValidateNotNull()][int]$PBLength = $PBLength
    [ValidateNotNull()][int]$percent = $percent
    [ValidateNotNull()][bool]$update = $update
    [ValidateNotNull()][string]$message = $message
    [ProgressUtil]::_back = "`b" * [Console]::WindowWidth
    if ($update) { [Console]::Write([ProgressUtil]::_back) }
    Write-Console ("{0} [" -f $message) -f SlateBlue -NoNewLine
    $p = [int](($percent / 100.0) * $PBLength + 0.5)
    for ($i = 0; $i -lt $PBLength; $i++) {
      if ($i -ge $p) {
        Write-Console ' ' -NoNewLine
      } else {
        Write-Console ([ProgressUtil]::_block) -f SlateBlue -NoNewLine
      }
    }
    Write-Console ("] {0,3:##0}%" -f $percent) -f SlateBlue -NoNewLine:(!$Completed)
  }
  static [System.Management.Automation.Job] WaitJob([string]$progressMsg, [scriptblock]$sb) {
    return [ProgressUtil]::WaitJob($progressMsg, $sb, $null)
  }
  static [System.Management.Automation.Job] WaitJob([string]$progressMsg, [System.Management.Automation.Job]$Job) {
    [Console]::CursorVisible = $false;
    [ProgressUtil]::frames = [ProgressUtil]::_twirl[0]
    [int]$length = [ProgressUtil]::frames.Length;
    $originalY = [Console]::CursorTop
    while ($Job.JobStateInfo.State -notin ('Completed', 'failed')) {
      for ($i = 0; $i -lt $length; $i++) {
        [ProgressUtil]::frames.Foreach({ [Console]::Write("$progressMsg $($_[$i])") })
        [System.Threading.Thread]::Sleep(50)
        [Console]::Write(("`b" * ($length + $progressMsg.Length)))
        [Console]::CursorTop = $originalY
      }
    }
    Write-Host "`b$progressMsg ... " -NoNewline -ForegroundColor Magenta
    [System.Management.Automation.Runspaces.RemotingErrorRecord[]]$Errors = $Job.ChildJobs.Where({
        $null -ne $_.Error
      }
    ).Error;
    if ($Job.JobStateInfo.State -eq "Failed" -or $Errors.Count -gt 0) {
      $errormessages = [string]::Empty
      if ($null -ne $Errors) {
        $errormessages = $Errors.Exception.Message -join "`n"
      }
      Write-Host "Completed with errors.`n`t$errormessages" -ForegroundColor Red
    } else {
      Write-Host "Done" -ForegroundColor Green
    }
    [Console]::CursorVisible = $true;
    return $Job
  }
  static [System.Management.Automation.Job] WaitJob([string]$progressMsg, [scriptblock]$sb, [Object[]]$ArgumentList) {
    $Job = ($null -ne $ArgumentList) ? (Start-ThreadJob -ScriptBlock $sb -ArgumentList $ArgumentList ) : (Start-ThreadJob -ScriptBlock $sb)
    return [ProgressUtil]::WaitJob($progressMsg, $Job)
  }
}

class StackTracer {
  static [System.Collections.Concurrent.ConcurrentStack[string]]$stack = [System.Collections.Concurrent.ConcurrentStack[string]]::new()
  static [System.Collections.Generic.List[hashtable]]$CallLog = @()
  static [void] Push([string]$class) {
    $str = "[{0}]" -f $class
    if ([StackTracer]::Peek() -ne "$class") {
      [StackTracer]::stack.Push($str)
      $LAST_ERROR = $(Get-Variable -Name Error -ValueOnly)[0]
      [StackTracer]::CallLog.Add(@{ ($str + ' @ ' + [datetime]::Now.ToShortTimeString()) = $(if ($null -ne $LAST_ERROR) { $LAST_ERROR.ScriptStackTrace } else { [System.Environment]::StackTrace }).Split("`n").Replace("at ", "# ").Trim() })
    }
  }
  static [type] Pop() {
    $result = $null
    if ([StackTracer]::stack.TryPop([ref]$result)) {
      return $result
    } else {
      throw [System.InvalidOperationException]::new("Stack is empty!")
    }
  }
  static [string] Peek() {
    $result = $null
    if ([StackTracer]::stack.TryPeek([ref]$result)) {
      return $result
    } else {
      return [string]::Empty
    }
  }
  static [int] GetSize() {
    return [StackTracer]::stack.Count
  }
  static [bool] IsEmpty() {
    return [StackTracer]::stack.IsEmpty
  }
}

<#
.SYNOPSIS
  PsRunner : Main class of the module
.DESCRIPTION
  Provides simple multithreading implementation in powerhsell
.NOTES
  Author : Alain Herve
  Created : <release_date>
  Register-EngineEvent -SourceIdentifier ([System.Management.Automation.PsEngineEvent]::Exiting) -Action ([PsRunner]::GetOnRemovalScript())
 
 .EXAMPLE
  $r = [PsRunner]::new();
  $worker1 = { $res = "worker running on thread $([Threading.Thread]::CurrentThread.ManagedThreadId)"; Start-Sleep -Seconds 5; return $res };
  $worker2 = { return (Get-Variable PsRunner_* -ValueOnly) };
  $worker3 = { $res = "worker running on thread $([Threading.Thread]::CurrentThread.ManagedThreadId)"; 10..20 | Get-Random | ForEach-Object { Start-Sleep -Milliseconds ($_ * 100) }; return $res };
  $r.Add_BGWorker($worker1); $r.Add_BGWorker($worker2); $r.Add_BGWorker($worker3);
  $r.Run()
 
 .EXAMPLE
  $ps = $r.RunAsync()
  $or = $ps.Instance.EndInvoke($ps.AsyncHandle)
 
 .EXAMPLE
  $ps = [powershell]::Create([PsRunner]::CreateRunspace())
  $ps = $ps.AddScript({
    $(Get-Variable SyncHash -ValueOnly)["JobsCleanup"] = "hello from rs manager"
    return [PSCustomObject]@{
      Setup = Get-Variable RunSpace_Setup -ValueOnly
      SyncHash = Get-Variable SyncHash -ValueOnly
    }
  })
 $h = $ps.BeginInvoke()
 $ps.EndInvoke($h)
#>

class PsRunner {
  hidden [int] $_MinThreads = 2
  hidden [int] $_MaxThreads = [PsRunner]::GetThreadCount()
  static hidden [string] $SyncId = { [void][PsRunner]::SetSyncHash(); return [PsRunner].SyncId }.Invoke() # Unique ID for each runner instance

  static PsRunner() {
    # set properties (Once-time)
    "PsRunner" | Update-TypeData -MemberName Id -MemberType ScriptProperty -Value { return [PsRunner]::SyncId } -SecondValue { throw [SetValueException]::new('Id is read-only') } -Force;
    "PsRunner" | Update-TypeData -MemberName MinThreads -MemberType ScriptProperty -Value { return $this._MinThreads } -SecondValue {
      param($value) if ($value -lt 2) { throw [ArgumentOutOfRangeException]::new("MinThreads must be greater than or equal to 2") };
      $this._MinThreads = $value
    } -Force;
    "PsRunner" | Update-TypeData -MemberName MaxThreads -MemberType ScriptProperty -Value { return $this._MaxThreads } -SecondValue {
      param($value) $m = [PsRunner]::GetThreadCount(); if ($value -gt $m) { throw [ArgumentOutOfRangeException]::new("MaxThreads must be less than or equal to $m") }
      $this._MaxThreads = $value
    } -Force;
    [PsRunner].PsObject.Properties.Add([PSScriptProperty]::new('Instance', {
          if (![PsRunner]::GetSyncHash()["Instance"]) { [PsRunner].SyncHash["Instance"] = [PsRunner]::Get_runspace_manager() };
          return [PsRunner].SyncHash["Instance"]
        }, {
          throw [SetValueException]::new('Instance is read-only')
        }
      )
    )
    "PsRunner" | Update-TypeData -MemberName GetOutput -MemberType ScriptMethod -Value {
      $o = [PsRunner]::GetSyncHash()["Output"]; [PsRunner].SyncHash["Output"] = [PSDataCollection[PsObject]]::new()
      [PsRunner].SyncHash["Runspaces"] = [ConcurrentDictionary[int, powershell]]::new()
      return $o
    } -Force;
  }
  [void] Add_BGWorker([ScriptBlock]$Worker) {
    $this.Add_BGWorker([PsRunner]::GetWorkerId(), $Worker, @())
  }
  [void] Add_BGWorker([int]$Id, [ScriptBlock]$Worker, [object[]]$Arguments) {
    [void][PsRunner]::Isvalid_NewRunspaceId($Id)
    $ps = [powershell]::Create([PsRunner]::CreateRunspace())
    $ps = $ps.AddScript($Worker)
    if ($Arguments.Count -gt 0) { $Arguments.ForEach({ [void]$ps.AddArgument($_) }) }
    # $ps.RunspacePool = [PsRunner].SyncHash["RunspacePool"] # https://github.com/PowerShell/PowerShell/issues/18934
    # Save each Worker in a dictionary, ie: {Int_Id, Powershell_instance_on_different_thread}
    if (![PsRunner]::GetSyncHash()["Runspaces"].TryAdd($Id, $ps)) { throw [System.InvalidOperationException]::new("worker $Id already exists.") }
    [PsRunner].SyncHash["Jobs"][$Id] = @{
      __PS   = ([ref]$ps).Value
      Status = ([ref]$ps).Value.Runspace.RunspaceStateInfo.State
      Result = $null
    }
  }
  [PsObject[]] Run() { $o = [PsRunner]::Run([PsRunner]::GetSyncHash()["Runspaces"]); [PsRunner].SyncHash["Output"] += $o; return $o }
  static [PsObject[]] Run([ConcurrentDictionary[int, powershell]]$Runspaces) {
    if (![PsRunner]::HasPendingJobs()) { return $null }
    [PsRunner].SyncHash["Runspaces"] = $Runspaces
    $Handle = [PsRunner].Instance.BeginInvoke()
    Write-Host ">>> Started background jobs" -f Green
    $i = 0
    while ($Handle.IsCompleted -eq $false) {
      Start-Sleep -Milliseconds 500
      Write-Progress -Activity "Running" -Status "In Progress" -PercentComplete $($i % 100)
      $i += 5
    }
    Write-Host "`n>>> All background jobs are complete." -f Green
    return [PsRunner].Instance.EndInvoke($Handle)
  }
  [PsObject] RunAsync() { return [PsRunner]::RunAsync([PsRunner]::GetSyncHash()["Runspaces"]) }
  static [PsObject] RunAsync([ConcurrentDictionary[int, powershell]]$Runspaces) {
    if (![PsRunner]::HasPendingJobs()) { return $null }
    [PsRunner]::GetSyncHash()["Runspaces"] = $Runspaces
    return [PSCustomObject]@{
      Instance    = [PsRunner].Instance
      AsyncHandle = [IAsyncResult][PsRunner].Instance.BeginInvoke()
    }
  }
  static [Runspace] CreateRunspace() {
    $defaultvars = @(
      [PSVariable]::new("RunSpace_Setup", [PsRunner]::GetRunSpace_Setup())
      [PSVariable]::new("SyncHash", [PsRunner]::GetSyncHash())
    )
    return [PsRunner]::CreateRunspace($defaultvars)
  }
  static [Runspace] CreateRunspace([PSVariable[]]$variables) {
    # https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_automatic_variables
    $automatic_variables = @('$', '?', '^', '_', 'args', 'ConsoleFileName', 'EnabledExperimentalFeatures', 'Error', 'Event', 'EventArgs', 'EventSubscriber', 'ExecutionContext', 'false', 'foreach', 'HOME', 'Host', 'input', 'IsCoreCLR', 'IsLinux', 'IsMacOS', 'IsWindows', 'LASTEXITCODE', 'Matches', 'MyInvocation', 'NestedPromptLevel', 'null', 'PID', 'PROFILE', 'PSBoundParameters', 'PSCmdlet', 'PSCommandPath', 'PSCulture', 'PSDebugContext', 'PSEdition', 'PSHOME', 'PSItem', 'PSScriptRoot', 'PSSenderInfo', 'PSUICulture', 'PSVersionTable', 'PWD', 'Sender', 'ShellId', 'StackTrace', 'switch', 'this', 'true')
    $_variables = [PsRunner]::GetVariables().Where({ $_.Name -notin $automatic_variables }); $r = [runspacefactory]::CreateRunspace()
    if ((Get-Variable PSVersionTable -ValueOnly).PSEdition -ne "Core") { $r.ApartmentState = "STA" }
    $r.ThreadOptions = "ReuseThread"; $r.Open()
    $variables.ForEach({ $r.SessionStateProxy.SetVariable($_.Name, $_.Value) })
    [void]$r.SessionStateProxy.Path.SetLocation((Resolve-Path ".").Path)
    $_variables.ForEach({ $r.SessionStateProxy.PSVariable.Set($_.Name, $_.Value) })
    return $r
  }
  static [RunspacePool] CreateRunspacePool([int]$minRunspaces, [int]$maxRunspaces, [initialsessionstate]$initialSessionState, [Host.PSHost]$PsHost) {
    Write-Verbose "Creating runspace pool and session states"
    #If specified, add variables and modules/snapins to session state
    $sessionstate = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
    # if ($UserVariables.count -gt 0) {
    # foreach ($Variable in $UserVariables) {
    # $sessionstate.Variables.Add((New-Object -TypeName System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList $Variable.Name, $Variable.Value, $null) )
    # }
    # }
    # if ($UserModules.count -gt 0) {
    # foreach ($ModulePath in $UserModules) {
    # $sessionstate.ImportPSModule($ModulePath)
    # }
    # }
    # if ($UserSnapins.count -gt 0) {
    # foreach ($PSSnapin in $UserSnapins) {
    # [void]$sessionstate.ImportPSSnapIn($PSSnapin, [ref]$null)
    # }
    # }
    # if ($UserFunctions.count -gt 0) {
    # foreach ($FunctionDef in $UserFunctions) {
    # $sessionstate.Commands.Add((New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList $FunctionDef.Name, $FunctionDef.ScriptBlock))
    # }
    # }
    $runspacepool = [runspacefactory]::CreateRunspacePool($minRunspaces, $maxRunspaces, $sessionstate, $PsHost)
    if ((Get-Variable PSVersionTable -ValueOnly).PSEdition -ne "Core") { $runspacepool.ApartmentState = "STA" }
    $runspacepool.Open()
    return $runspacepool
  }
  static [bool] Isvalid_NewRunspaceId([int]$RsId) {
    return [PsRunner]::Isvalid_NewRunspaceId($RsId, $true)
  }
  static [bool] Isvalid_NewRunspaceId([int]$RsId, [bool]$ThrowOnFail) {
    $v = $null -eq (Get-Runspace -Id $RsId)
    if (!$v -and $ThrowOnFail) {
      throw [System.InvalidOperationException]::new("Runspace with ID $RsId already exists.")
    }; return $v
  }
  static [PSVariable[]] GetVariables() {
    # Set Environment Variables
    # if ($EnvironmentVariablesToForward -notcontains '*') {
    # $EnvVariables = foreach ($obj in $EnvVariables) {
    # if ($EnvironmentVariablesToForward -contains $obj.Name) {
    # $obj
    # }
    # }
    # }
    # Write-Verbose "Setting SyncId <=> (OneTime/Session)"
    return (Get-Variable).Where({ $o = $_.Options.ToString(); $o -notlike "*ReadOnly*" -and $o -notlike "*Constant*" })
  }
  static [Object[]] GetCommands() {
    # if ($FunctionsToForward -notcontains '*') {
    # $Functions = foreach ($FuncObj in $Functions) {
    # if ($FunctionsToForward -contains $FuncObj.Name) {
    # $FuncObj
    # }
    # }
    # }
    # $res = [PsRunner].SyncHash["Commands"]; if ($res) { return $res }
    return (Get-ChildItem Function:).Where({ ![System.String]::IsNullOrWhiteSpace($_.Name) })
  }
  static [string[]] GetModuleNames() {
    # if ($ModulesToForward -notcontains '*') {
    # $Modules = foreach ($obj in $Modules) {
    # if ($ModulesToForward -contains $obj.Name) {
    # $obj
    # }
    # }
    # }
    return (Get-Module).Name
  }
  static [ArrayList] GetRunSpace_Setup() {
    return [PsRunner]::GetRunSpace_Setup((Get-ChildItem Env:), [PsRunner]::GetModuleNames(), [PsRunner]::GetCommands());
  }
  static [ArrayList] GetRunSpace_Setup([DictionaryEntry[]]$EnvVariables, [string[]]$ModuleNames, [Object[]]$Functions) {
    [ArrayList]$RunSpace_Setup = @(); $EnvVariables = Get-ChildItem Env:\
    $SetEnvVarsPrep = foreach ($obj in $EnvVariables) {
      if ([char[]]$obj.Name -contains '(' -or [char[]]$obj.Name -contains ' ') {
        $strr = @(
          'try {'
          $(' ${env:' + $obj.Name + '} = ' + "@'`n$($obj.Value)`n'@")
          'catch {'
          " Write-Debug 'Unable to forward environment variable $($obj.Name)'"
          '}'
        )
      } else {
        $strr = @(
          'try {'
          $(' $env:' + $obj.Name + ' = ' + "@'`n$($obj.Value)`n'@")
          '} catch {'
          " Write-Debug 'Unable to forward environment variable $($obj.Name)'"
          '}'
        )
      }
      [string]::Join("`n", $strr)
    }
    [void]$RunSpace_Setup.Add([string]::Join("`n", $SetEnvVarsPrep))
    $Modules = Get-Module -Name $ModuleNames | Select-Object Name, @{ l = "Manifest"; e = { [IO.Path]::Combine($_.ModuleBase, $_.Name + ".psd1") } }
    $SetModulesPrep = foreach ($obj in $Modules) {
      $strr = @(
        '$tempfile = [IO.Path]::Combine([IO.Path]::GetTempPath(), [IO.Path]::GetRandomFileName())'
        "if (![bool]('$($obj.Name)' -match '\.WinModule')) {"
        ' try {'
        " Import-Module '$($obj.Name)' -NoClobber -ErrorAction Stop 2>`$tempfile"
        '} catch {'
        ' try {'
        " Import-Module '$($obj.Manifest)' -NoClobber -ErrorAction Stop 2>`$tempfile"
        ' } catch {'
        " Write-Debug 'Unable to Import-Module $($obj.Name)'"
        ' }'
        ' }'
        '}'
        'if ([IO.File]::Exists($tempfile)) {'
        ' Remove-Item $tempfile -Force'
        '}'
      )
      [string]::Join("`n", $strr)
    }
    [void]$RunSpace_Setup.Add([string]::Join("`n", $SetModulesPrep))
    $SetFunctionsPrep = foreach ($obj in $Functions) {
      $_src_txt = '@(${Function:' + $obj.Name + '}.Ast.Extent.Text)'
      $FunctionText = [scriptblock]::Create($_src_txt).InvokeReturnAsIs()
      if ($($FunctionText -split "`n").Count -gt 1) {
        if ($($FunctionText -split "`n")[0] -match "^function ") {
          if ($($FunctionText -split "`n") -match "^'@") {
            Write-Debug "Unable to forward function $($obj.Name) due to heredoc string: '@"
          } else {
            'Invoke-Expression ' + "@'`n$FunctionText`n'@"
          }
        }
      } elseif ($($FunctionText -split "`n").Count -eq 1) {
        if ($FunctionText -match "^function ") {
          'Invoke-Expression ' + "@'`n$FunctionText`n'@"
        }
      }
    }
    [void]$RunSpace_Setup.Add([string]::Join("`n", $SetFunctionsPrep))
    return $RunSpace_Setup
  }
  static [Hashtable] SetSyncHash() {
    return [PsRunner]::SetSyncHash($False)
  }
  static [Hashtable] SetSyncHash([bool]$Force) {
    if (![PsRunner].SyncHash -or $Force) {
      $Id = [string]::Empty; $sv = Get-Variable PsRunner_* -Scope Global; if ($sv.Count -gt 0) { $Id = $sv[0].Name }
      if ([string]::IsNullOrWhiteSpace($Id)) { $Id = "PsRunner_{0}" -f [Guid]::NewGuid().Guid.substring(0, 21).replace('-', [string]::Join('', (0..9 | Get-Random -Count 1))) };
      [PsRunner].PsObject.Properties.Add([PSScriptProperty]::new('SyncId', [scriptblock]::Create("return '$Id'"), { throw [SetValueException]::new('SyncId is read-only') }))
      [PsRunner].PsObject.Properties.Add([PsNoteProperty]::new('SyncHash', [Hashtable]::Synchronized(@{
              Id          = [string]::Empty
              Jobs        = [Hashtable]::Synchronized(@{})
              Runspaces   = [ConcurrentDictionary[int, powershell]]::new()
              JobsCleanup = @{}
              Output      = [PSDataCollection[PsObject]]::new()
            }
          )
        )
      );
      New-Variable -Name $Id -Value $([ref][PsRunner].SyncHash).Value -Option AllScope -Scope Global -Visibility Public -Description "PID_$(Get-Variable PID -ValueOnly)_PsRunner_variables" -Force
      [PsRunner].SyncHash["Id"] = $Id;
    }
    return [PsRunner].SyncHash
  }
  static [PowerShell] Get_runspace_manager() {
    Write-Verbose "Creating Runspace Manager (OneTime) ..."
    $i = [powershell]::Create([PsRunner]::CreateRunspace())
    $i.AddScript({
        $Runspaces = $SyncHash["Runspaces"]
        $Jobs = $SyncHash["Jobs"]
        if ($RunSpace_Setup) {
          foreach ($obj in $RunSpace_Setup) {
            if ([string]::IsNullOrWhiteSpace($obj)) { continue }
            try {
              Invoke-Expression -Command $obj
            } catch {
              throw ("Error {0} `n{1}" -f $_.Exception.Message, $_.ScriptStackTrace)
            }
          }
        }
        $Runspaces.Keys.ForEach({
            $Jobs[$_]["Handle"] = [IAsyncResult]$Jobs[$_]["__PS"].BeginInvoke()
            Write-Host "Started worker $_" -f Yellow
          }
        )
        # Monitor workers until they complete
        While ($Runspaces.ToArray().Where({ $Jobs[$_.Key]["Handle"].IsCompleted -eq $false }).count -gt 0) {
          # Write-Host "$(Get-Date) Still running..." -f Green
          # foreach ($worker in $Runspaces.ToArray()) {
          # $Id = $worker.Key
          # $status = $Jobs[$Id]["Status"]
          # Write-Host "worker $Id Status: $status"
          # }
          Start-Sleep -Milliseconds 500
        }
        Write-Host "All workers are complete." -f Yellow
        $SyncHash["Results"] = @(); foreach ($i in $Runspaces.Keys) {
          $__PS = $Jobs[$i]["__PS"]
          try {
            $Jobs[$i] = @{
              Result = $__PS.EndInvoke($Jobs[$i]["Handle"])
              Status = "Completed"
            }
          } catch {
            $Jobs[$i] = @{
              Result = $_.Exception.Message
              Status = "Failed"
            }
          } finally {
            # Dispose of the PowerShell instance
            $__PS.Runspace.Close()
            $__PS.Runspace.Dispose()
            $__PS.Dispose()
          }
          # Store results
          $SyncHash["Results"] += [pscustomobject]@{
            Id     = $i
            Result = $Jobs[$i]["Result"]
            Status = $Jobs[$i]["Status"]
          }
        }
        return $SyncHash["Results"]
      }
    )
    return $i
  }
  static [hashtable] GetSyncHash() {
    return (Get-Variable -Name $([PsRunner]::SyncId) -ValueOnly -Scope Global)
  }
  static [bool] HasPendingJobs() {
    $hpj = [PsRunner]::GetSyncHash()["Jobs"].Values.Keys.Contains("__PS")
    if (!$hpj) { Write-Warning "There are no pending jobs. Please run GetOutput(); and Add_BGWorker(); then try again." }
    return $hpj
  }
  static [int] GetWorkerId() {
    $Id = 0; do {
      $Id = ((Get-Runspace).Id)[-1] + 1
    } until ([PsRunner]::Isvalid_NewRunspaceId($Id, $false))
    return $Id
  }
  static [int] GetThreadCount() {
    $_PID = Get-Variable PID -ValueOnly
    return $(if ((Get-Variable IsLinux, IsMacOs).Value -contains $true) { &ps huH p $_PID | wc --lines } else { $(Get-Process -Id $_PID).Threads.Count })
  }
}

# Types that will be available to users when they import the module.
$typestoExport = @(
  [NetworkManager], [RGB], [color],
  [ProgressUtil], [StackTracer], [ShellConfig], [dotProfile],
  [PsRunner], [PsRecord], [cli], [cliart]
)
$TypeAcceleratorsClass = [PsObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')
foreach ($Type in $typestoExport) {
  if ($Type.FullName -in $TypeAcceleratorsClass::Get.Keys) {
    $Message = @(
      "Unable to register type accelerator '$($Type.FullName)'"
      'Accelerator already exists.'
    ) -join ' - '

    [System.Management.Automation.ErrorRecord]::new(
      [System.InvalidOperationException]::new($Message),
      'TypeAcceleratorAlreadyExists',
      [System.Management.Automation.ErrorCategory]::InvalidOperation,
      $Type.FullName
    ) | Write-Warning
  }
}
# Add type accelerators for every exportable type.
foreach ($Type in $typestoExport) {
  $TypeAcceleratorsClass::Add($Type.FullName, $Type)
}
# Remove type accelerators when the module is removed.
$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
  foreach ($Type in $typestoExport) {
    $TypeAcceleratorsClass::Remove($Type.FullName)
  }
}.GetNewClosure();

$scripts = @(); $Public = Get-ChildItem "$PSScriptRoot/Public/" -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue
$scripts += Get-ChildItem "$PSScriptRoot/Private/" -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue
$scripts += $Public

foreach ($file in $scripts) {
  Try {
    if ([string]::IsNullOrWhiteSpace($file.fullname)) { continue }
    . "$($file.fullname)"
  } Catch {
    Write-Warning "Failed to import function $($file.BaseName): $_"
    $host.UI.WriteErrorLine($_)
  }
}

$Param = @{
  Function = $Public.BaseName
  Cmdlet   = '*'
  Alias    = '*'
  Verbose  = $false
}
Export-ModuleMember @Param