helpers.ps1

<#
    .Synopsis
        Kick off a new window from PowerShell of a Visual Studio created XAML file and attach handlers - the easy way 🚀
 
    .Description
        Version 1.1.0
        License MIT
        (c) Nabil Redmann 2019 - 2026
        Supports: Powershell 5+ (including pwsh 7)
 
    .LINK
        https://gist.github.com/BananaAcid/0484b11a03c03f172740096e213d1d82
 
    .Notes
        based on https://stackoverflow.com/a/52416973/1644202
#>


[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingCmdletAliases', '', Scope = 'Function', Target = '*')]
Param()

# Enable visual styles, in case there will be a message box or alike
Function Enable-VisualStyles
{
    Add-Type -AssemblyName System.Drawing,System.Windows.Forms
    [System.Windows.Forms.Application]::EnableVisualStyles()
}


# .Net methods for hiding/showing the console in the background, https://stackoverflow.com/a/40621143/1644202
Add-Type -Name Window -Namespace XAMLgui_Console -MemberDefinition '
[DllImport("Kernel32.dll")]
public static extern IntPtr GetConsoleWindow();
 
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow);
'


Function Show-Console
{
    Param( [Parameter(Mandatory=$false)]$state=4 )

    $consolePtr = [XAMLgui_Console.Window]::GetConsoleWindow()

    # https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-showwindow
    # Hide = 0,
    # ShowNormal = 1,
    # ShowMinimized = 2,
    # ShowMaximized = 3,
    # Maximize = 3,
    # ShowNormalNoActivate = 4,
    # Show = 5,
    # Minimize = 6,
    # ShowMinNoActivate = 7,
    # ShowNoActivate = 8,
    # Restore = 9,
    # ShowDefault = 10,
    # ForceMinimized = 11

    [XAMLgui_Console.Window]::ShowWindow($consolePtr, $state)
}

Function Hide-Console
{
    # return true/false
    $consolePtr = [XAMLgui_Console.Window]::GetConsoleWindow()
    #0 hide
    [XAMLgui_Console.Window]::ShowWindow($consolePtr, 0)
}

Function Get-PropOrNull
{
    param( $thing, [string]$prop )

    Try {
        $thing.$prop
    } Catch {}
}

# https://gist.github.com/nwolverson/8003100
Function Get-VisualChildren($item)
{
    for ($i = 0; $i -lt [System.Windows.Media.VisualTreeHelper]::GetChildrenCount($item); $i++) {
        $child = [System.Windows.Media.VisualTreeHelper]::GetChild($item, $i)
        Get-VisualChildren($child)
    }
    $item
}

Function Get-CellItemByName
{
    Param
    (
        [ref]$Parent,
        $ItemNo,
        $Name
    )

    [System.Windows.Forms.Application]::DoEvents()

    if ($DebugPreference -ne 'SilentlyContinue') { $Parent.value | Write-Host }

    $items = (Get-VisualChildren ($Parent.Value) |? { $_.GetType().Name -eq "ListViewItem" })

    if ($DebugPreference -ne 'SilentlyContinue') { $items | Write-Host }
    if ($DebugPreference -ne 'SilentlyContinue') { $ItemNo | Write-Host }
    if ($DebugPreference -ne 'SilentlyContinue') { $items[$ItemNo] | Write-Host }

    if ($DebugPreference -ne 'SilentlyContinue') { Get-VisualChildren $items[$ItemNo] | Write-Host }


    return (Get-VisualChildren $items[$ItemNo] |? { $_.Name -eq $Name} | Select-Object -First 1)
}

Function Wait-AwaitJob
{
    Param ( [Parameter(Mandatory=$true)]$Job )

    while ($Job.state -eq 'Running') {
        [System.Windows.Forms.Application]::DoEvents()  # keep form responsive
    }

    # Captures and throws any exception in the job output -> '-ErrorAction stop' --- otherwise returns result
    return Receive-Job $Job -ErrorAction Continue
}

# start and await a job
Function Start-AwaitJob
{
    Param
    (
        [Scriptblock][Parameter(Mandatory=$true)] $ScriptBlock,
        [string[]][Parameter(Mandatory=$false)] $ArgumentList=@(),
        [string][Parameter(Mandatory=$false)] $Dir = "", # sets the current working directory (use it to set the subfolder) !
        [boolean][Parameter(Mandatory=$false)] $Await = $True,
        [string][Parameter(Mandatory=$false)] $InitBlock = ""
    )

    $useDir = $PWD
    If ($Dir -ne "") { $useDir = Resolve-Path $Dir }

    $job = Start-Job -Init ([ScriptBlock]::Create("Set-Location '$($useDir -replace "'", "''")'`n" + $InitBlock)) -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList

    If ($Await) {
        return Wait-AwaitJob $job    
    }
    Else {
        return $job    
    }
}


Function Show-MessageBox
{
    Param
    (
        [string]$Message = "This is a default Message.", 
        [string]$Title = "Default Title", 
        [ValidateSet("Asterisk","Error","Exclamation","Hand","Information","None","Question","Stop","Warning")] 
        [string]$Type = "Error", 
        [ValidateSet("AbortRetryIgnore","OK","OKCancel","RetryCancel","YesNo","YesNoCancel")] 
        [string]$Buttons = "OK" 
    )

    Add-Type -AssemblyName System.Windows.Forms
    [System.Windows.Forms.Application]::EnableVisualStyles()
    $MsgBoxResult = [System.Windows.Forms.MessageBox]::Show($Message,$Title,[Windows.Forms.MessageBoxButtons]::$Buttons,[Windows.Forms.MessageBoxIcon]::$Type) 

    Return $MsgBoxResult 
}

Function Invoke-BalloonTip
{
    <#
    .Synopsis
        Display a balloon tip message in the system tray.
    .Description
        This function displays a user-defined message as a balloon popup in the system tray. This function
        requires Windows Vista or later.
    .Parameter Message
        The message text you want to display. Recommended to keep it short and simple.
    .Parameter Title
        The title for the message balloon.
    .Parameter MessageType
        The type of message. This value determines what type of icon to display. Valid values are
    .Parameter SysTrayIcon
        The path to a file that you will use as the system tray icon. Default is the PowerShell ISE icon.
    .Parameter Duration
        The number of seconds to display the balloon popup. The default is 1000.
    .Inputs
        None
    .Outputs
        None
    .Notes
         NAME: Invoke-BalloonTip
         VERSION: 1.0
         AUTHOR: Boe Prox
 
         Modified by Nabil Redmann
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory=$True,HelpMessage="The message text to display. Keep it short and simple.")]
        [string]$Message,
        [Parameter(HelpMessage="The message title")]
        [string]$Title="Attention $env:username",
        [Parameter(HelpMessage="The message type: Info,Error,Warning,None")]
        [System.Windows.Forms.ToolTipIcon]$MessageType="Info",
     
        [Parameter(HelpMessage="The path to a file to use its icon in the system tray")]
        [string]$SysTrayIconPath="",
        [Parameter(HelpMessage="The number of milliseconds to display the message.")]
        [int]$Duration=1000
    )
    
    Add-Type -AssemblyName System.Windows.Forms

    If (-NOT $global:balloon) {
        Write-Debug 'Initiating creation of balloon'
        $global:balloon = New-Object System.Windows.Forms.NotifyIcon
        #Mouse double click on icon to dispose
        [void](Register-ObjectEvent -InputObject $balloon -EventName MouseDoubleClick -SourceIdentifier IconClicked -Action {
            #Perform cleanup actions on balloon tip
            Write-Debug 'Disposing of balloon'
            $global:balloon.dispose()
            Unregister-Event -SourceIdentifier IconClicked
            Remove-Job -Name IconClicked
            Remove-Variable -Name balloon -Scope Global
        })
    }

    If ($SysTrayIconPath -ne "") {
        $SysTrayIcon = Get-IconFromFile -FilePath $SysTrayIconPath
    }

    # Need an icon for the tray - $SysTrayIcon is null if Get-IconFromFile failed
    If ($SysTrayIconPath -eq "" -or $null -eq $SysTrayIcon) {
        $SysTrayIconPath = Get-Process -id $PID | Select-Object -ExpandProperty Path
        $SysTrayIcon = [System.Drawing.Icon]::ExtractAssociatedIcon($SysTrayIconPath)
    }

    #Extract the icon from the file
    $balloon.Icon = $SysTrayIcon
    #Can only use certain TipIcons: [System.Windows.Forms.ToolTipIcon] | Get-Member -Static -Type Property
    $balloon.BalloonTipIcon  = [System.Windows.Forms.ToolTipIcon]$MessageType
    $balloon.BalloonTipText  = $Message
    $balloon.BalloonTipTitle = $Title
    $balloon.Visible = $true
    #Display the tip and specify in milliseconds on how long balloon will stay visible
    $balloon.ShowBalloonTip($Duration)
    Write-Debug "Ending function"
}

Function Get-IconFromFile
{
    param(
        [string][Parameter(Mandatory=$True)]$FilePath,
        [string][Parameter(Mandatory=$False)]$Type
    )

    If (-not (Test-Path $FilePath)) {
        Write-Warning "Icon file not found at: $FilePath"
        exit 7
    }

    If ($Type -eq "") {
        $Type = (Get-Item $FilePath).Extension.Substring(1)
    }

    If ($Type -eq "bmp" -or $Type -eq "jpg" -or $Type -eq "jpeg" -or $Type -eq "png") {
        $picture = New-Object System.Drawing.Bitmap($FilePath)
    }
    elseif ($Type -eq "ico") {
        return $FilePath
    }
    elseif ($Type -eq "exe") {
        return [System.Drawing.Icon]::ExtractAssociatedIcon($FilePath)
    }
    else {
        return $null
    }

    $iconHandle = $picture.GetHicon()
    # Create a System.Drawing.Icon object from the handle
    $icon = [System.Drawing.Icon]::FromHandle($iconHandle)
    $picture.Dispose()

    return $icon
}


Function Select-FolderDialog
{
    Param
    (
        [string]$Title = "Select a Folder",
        [string]$Description = "",
        [string]$Path = [Environment]::GetFolderPath("Desktop"),
        [string]$SelectedPath = "",
        [boolean]$Multiselect = $false,
        [boolean]$ShowNewFolderButton = $false
    )

    Add-Type -AssemblyName System.Windows.Forms  

    if ($Title -ne "" -and $Description -eq "") {
        $Description = $Title
        $UseDescriptionForTitle = $true
    }
    else {
        $UseDescriptionForTitle = $false
    }

    $objForm = New-Object System.Windows.Forms.FolderBrowserDialog -Property @{
        # https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.folderbrowserdialog?view=windowsdesktop-10.0
        InitialDirectory = $Path
        SelectedPath = $SelectedPath
        Description = $Description
        Multiselect = $Multiselect
        ShowNewFolderButton = $ShowNewFolderButton

        UseDescriptionForTitle = $UseDescriptionForTitle
    }

    $Show = $objForm.ShowDialog()

    If ($Show -eq "OK") {
        Return $objForm.SelectedPaths
    }
    Else {
        Write-Verbose "Select-FolderDialog cancelled by user."
        Return ''
    }
}
# $folder = Select-FolderDialog # the variable contains user folder selection

Function Select-FileDialog
{
    Param
    (
        [string]$Title="Select Folder",
        [string]$Path="Desktop",
        [string]$Filter='Images (*.jpg, *.png)|*.jpg;*.png',
        [boolean]$Multiselect=$false
    )

    Add-Type -AssemblyName System.Windows.Forms

    $FileBrowser = New-Object System.Windows.Forms.OpenFileDialog -Property @{
        Filter = $Filter # Specified file types
        Multiselect = $Multiselect # Multiple files can be chosen
        Title = $Title
        InitialDirectory = $Path
    }
 
    [void]$FileBrowser.ShowDialog()

    If ($FileBrowser.FileNames -like "*\*") {
        # even with multiselect, if only 1 file was selected, it will NOT return an array
        Return $FileBrowser.FileNames
    }
    Else {
        #if ($DebugPreference -ne 'SilentlyContinue') { Write-Host "Cancelled by user" }
        Return ""
    }
}

Function Get-PowershellInterpreter
{
    <#
    .SYNOPSIS
    Returns the powershell interpreter used in the current session, and a list if `poershell.exe` or `pwsh.exe` are available.
    .EXAMPLE
    $current, $available = Get-PowershellInterpreter
    #>

    If ($PSVersionTable.PSEdition -eq "Desktop") {
        $current = "powershell.exe"
    }
    Else { #"Core"
        $current = "pwsh.exe"
    }

    $available = @{
        "powershell.exe" = (Command powershell.exe)
        "pwsh.exe" = (Command pwsh.exe)
    }

    return $current, $available
}

Function Set-RunOnce
{
    <#
    .SYNOPSIS
    Sets a Runonce-Registry Key
 
    .DESCRIPTION
    Sets a Runonce-Key in the Computer-Registry. Every Program which will be added will run once at system startup.
    This Command can be used to configure a computer at startup.
 
    .EXAMPLE
    Set-Runonce -command '%systemroot%\System32\WindowsPowerShell\v1.0\powershell.exe -executionpolicy bypass -file c:\Scripts\start.ps1'
    Sets a Key to run Powershell at startup and execute C:\Scripts\start.ps1
 
    .NOTES
    Author: Holger Voges
    Version: 1.0
    Date: 2018-08-17
 
    # modified by Nabil Redmann
 
    .LINK
    https://www.netz-weise-it.training/
    #>

    [CmdletBinding()]
    param
    (
        #The Name of the Registry Key in the Autorun-Key.
        [string]$KeyName = "Run",


        #Command to run
        [string]$Command = "-executionpolicy bypass -file `"$($MyInvocation.ScriptName)`"",

        #Command params to add
        [String]$Params = "",

        #Interpreter to use
        [String]$Interpreter = "powershell.exe"
    )

    $cmdStr = "$($Interpreter) $($Command) $($Params)"

    
    If (-not ((Get-Item -Path HKLM:\Software\Microsoft\Windows\CurrentVersion\RunOnce).$KeyName )) {
        New-ItemProperty -Path 'HKLM:\Software\Microsoft\Windows\CurrentVersion\RunOnce' -Name $KeyName -Value $cmdStr -PropertyType ExpandString -Force
    }
    else {
        Set-ItemProperty -Path 'HKLM:\Software\Microsoft\Windows\CurrentVersion\RunOnce' -Name $KeyName -Value $cmdStr -PropertyType ExpandString -Force
    }

    return $cmdStr
}

Function New-ClonedObjectDeep
{
    #param([PSCustomObject]$srcObject)
    #return $srcObject.psobject.copy()

    #param([PSCustomObject]$srcObject)
    #deep copy: return $srcObject | ConvertTo-Json -depth 100 | ConvertFrom-Json

    # deep copy!
    param($srcObject)
    $ms = New-Object System.IO.MemoryStream
    $bf = New-Object System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
    $bf.Serialize($ms, $srcObject)
    $ms.Position = 0
    $copyObject = $bf.Deserialize($ms)
    $ms.Close()

    return $copyObject
}

Function New-ClonedObject
{
    param( [PSCustomObject]$srcObject )

    return $srcObject.psobject.copy() # | ConvertTo-Json -depth 100 | ConvertFrom-Json
}

# To be able to use a local module, because deploying the app on an USB stick
# will need it and might not have internet access
function Find-LocalModulePath {
    param(
        [Parameter(Mandatory=$true, Position=0)]
        [String] $Name,

        [String] $Path = ".\ps_modules"
    )

    return ls "$Path\$Name" -ErrorAction SilentlyContinue | select -Last 1 |% FullName
}
function Import-LocalModule {
    param(
        [Parameter(Mandatory=$true, Position=0)]
        [String] $Name,

        [String] $Path = ".\ps_modules",

        [Boolean] $Download = $True
    )

    if (-not (Find-LocalModulePath $Name -Path $Path) -and $Download) { Save-Module -Name $Name -Path $Path }
    $fullPath = Find-LocalModulePath $Name -Path $Path
    if (-not $fullPath) { Write-Error "Unable to find $Name module, could not download. Aborting."; Exit 99 }
    Import-Module (Join-Path $fullPath $Name)
}

# Usefull for Start-AwaitJob -InitBlock (@( Get-FnAsString "fn1", Get-FnAsString "fn2" ) -Join "`n")
Function Get-FnAsString {
    param( [String]$FnName )

    $fn = Get-Command -Name $FnName -CommandType Function

    return "Function {0} {{{1}}}" -f $fn.Name, $fn.ScriptBlock
}