New-StreamDeckAction.ps1

function New-StreamDeckAction
{
    <#
    .Synopsis
        Creates a StreamDeck action
    .Description
        Creates a StreamDeck action, to be used as part of a profile.
    .Example
        New-StreamDeckAction -HotKey "CTRL+F4" -Title "Close"
    .Example
        New-StreamDeckAction -Uri https://github.com/ -Title "GitHub"
    .Link
        Get-StreamDeckAction
    #>

    [OutputType('StreamDeck.Action')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("Test-ForParameterSetAmbiguity", "", Justification="Ambiguity Desired")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="Does not change state")]
    param(
    # The name of the plugin.
    [Parameter(Mandatory,ParameterSetName='PluginName',ValueFromPipelineByPropertyName)]
    [string]
    $Name,

    # The StreamDeck Plugin UUID.
    [Parameter(ParameterSetName='PluginName',ValueFromPipelineByPropertyName)]
    [string]
    $UUID,

    # A sequence of hotkeys
    [Parameter(Mandatory,ParameterSetName='Hotkey',ValueFromPipelineByPropertyName)]
    [string[]]
    $HotKey,

    # A URI.
    [Parameter(Mandatory,ParameterSetName='OpenUri',ValueFromPipelineByPropertyName)]
    [alias('Url')]
    [uri]
    $Uri,

    # A PowerShell ScriptBlock.
    # Currently, this will run using pwsh by default, and use -WindowsPowerShell if provided.
    [Parameter(Mandatory,ParameterSetName='ScriptBlock',ValueFromPipelineByPropertyName)]
    [ScriptBlock]
    $ScriptBlock,

    # If set, will run the ScriptBlock in Windows PowerShell.
    # By default, will run the Scriptblock in PowerShell core.
    # This option is obviously not supported on MacOS.
    [Parameter(ValueFromPipelineByPropertyName)]
    [switch]
    $WindowsPowerShell,

    # The path to an application.
    [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='ApplicationPath')]
    [string]
    $ApplicationPath,

    # The name of a StreamDeck profile.
    [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='ProfileName')]
    [string]
    $ProfileName,

    # The device UUID, when switching profiles.
    [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='ProfileName')]
    [string]
    $DeviceUUID,

    # The next page. This should be created by New-StreamDeckProfile, passing -IsNextPage
    [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='NextPage')]
    [PSTypeName('StreamDeck.Profile')]
    [PSObject]
    $NextPage,

    # A Child Profile. These should be created by New-StreamDeckProfile, passing -IsChildProfile
    [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='ChildProfile')]
    [PSTypeName('StreamDeck.Profile')]
    [PSObject]
    $ChildProfile,

    # If set, will create an action that will navigate back to the parent folder.
    [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='BackToParent')]
    [switch]
    $BackToParent,

    # If set, will create an action that will navigate back to the previous page.
    [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='PreviousPage')]
    [switch]
    $PreviousPage,

    # The text that should be automatically typed
    [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='Text')]
    [string]
    $Text,

    # If set, will send an enter key after typing the text.
    [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='Text')]
    [Alias('Enter','Return', 'SendReturn')]
    [switch]
    $SendEnter,

    # The settings passed to the plugin.
    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias('Settings')]
    [PSObject]
    $Setting = @{},

    # The title of the action
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]
    $Title,

    # The image used for the action
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]
    $Image,

    # The font size
    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias('FSize')]
    [string]
    $FontSize,

    # The font family
    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias('FFamily')]
    [string]
    $FontFamily,

    # If set, will underline the action title
    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias('FontUnderline')]
    [switch]
    $Underline,

    # The possible states of the plugin.
    [Parameter(ValueFromPipelineByPropertyName)]
    [PSObject[]]
    $States = @(),

    # The state index.
    [Parameter(ValueFromPipelineByPropertyName)]
    [uint32]
    $State = 0,

    # If set, will not show a title.
    [Parameter(ValueFromPipelineByPropertyName)]
    [switch]
    $HideTitle
    )

    begin {
        $streamDeckActions = Get-StreamDeckAction
    }

    process {
            switch ($PSCmdlet.ParameterSetName) {
                PluginName {

                    if (-not $UUID) { # If UUID has not been provided
                        $uuid =
                            $streamDeckActions |
                                Where-Object Name -EQ $name |
                                Select-Object -ExpandProperty UUID # Attempt to find the plugin by name
                        if (-not $uuid) {
                            # If we could not, assume a base name
                            Write-Warning "Could not find UUID for Name '$name', assuming com.elgato.streamdeck.system.$name"
                            $uuid = "com.elgato.streamdeck.system.$name"
                        }
                    }


                    $matchingPlugin =
                        $streamDeckActions | Where-Object UUID -EQ $UUID
                    $uuid = $matchingPlugin.uuid
                }
                ChildProfile {
                    $name = 'Create Folder'
                    $uuid = 'com.elgato.streamdeck.profile.openchild'
                    $Setting = $ChildProfile
                }
                NextPage {
                    $name = 'Next Page'
                    $uuid = 'com.elgato.streamdeck.page.next'
                    $Setting = $NextPage
                }
                PreviousPage {
                    $name = 'Previous'
                    $uuid = 'com.elgato.streamdeck.page.previous'
                }
                BackToParent {
                    $name = 'BackToParent'
                    $uuid = 'com.elgato.streamdeck.profile.backtoparent'
                }
                ProfileName {
                    #region Switch Profile
                    # If switching profiles, find the profile
                    $streamDeckProfile = Get-StreamDeckProfile -Name $ProfileName
                    # If we could not, error out.
                    if (-not $streamDeckProfile) { Write-Error "Could not find profile named '$ProfileName'"; return}
                    $name = 'Switch Profile'
                    $UUID = 'com.elgato.streamdeck.profile.rotate'
                    $Setting = @{
                        DeviceUUID= $DeviceUUID
                        ProfileUUID = $streamDeckProfile.Guid
                    }
                    #endregion Switch Profile
                }
                OpenURI {
                    #region Website
                    $name = 'Website'
                    $uuid = 'com.elgato.streamdeck.system.website'
                    $Setting = [Ordered]@{
                        openInBrowser = $true
                        path = "$Uri"
                    }
                    #endregion Website
                }
                ApplicationPath {
                    #region Application
                    $name = 'open'
                    $uuid = 'com.elgato.streamdeck.system.open'
                    $Setting = [Ordered]@{
                        openInBrowser =  $true
                        path = $ApplicationPath
                    }
                    #endregion Application
                }
                Text {
                    #region Text
                    $name ='text'
                    $uuid = 'com.elgato.streamdeck.system.text'
                    $setting = @{
                        isSendingEnter = ($SendEnter -as [bool])
                        pastedText = $Text
                    }
                    #region Text
                }
                HotKey {
                    #region HotKey
                    $name = 'HotKey'
                    $uuid = 'com.elgato.streamdeck.system.hotkey'
                    # Hotkeys are tricky. First we need a RegEx to get the sequence parts out.
                    $HotKeyRegex = [Regex]::new(@'
(?<Modifier>
    (?>
        (?<Control>Control|Ctrl|LeftCtrl|RightCtrl)
        |
        (?<Command>Command|Cmd|LeftCmd|RightCmd|Windows|Win|Apple|OpenApple)
        |
        (?<Alt>Option|Alt|LeftAlt|RightAlt)
        |
        (?<Shift>Shift|LeftShift|RightShift)
    )
\+){0,3}
(?<Key>.+$)
'@
, 'IgnoreCase,IgnorePatternWhitespace')
                    $Setting =
                        [Ordered]@{
                            Hotkeys = @(
                                foreach ($hot in $HotKey) { # Then we need to walk over each -HotKey
                                    $matched =  $HotKeyRegex.Match($Hot)

                                    # Find what matched
                                    $keyCmd = $matched.Groups["Command"].Success
                                    $keyControl = $matched.Groups["Control"].Success
                                    $keyOption = $matched.Groups["Alt"].Success
                                    $keyShift = $matched.Groups["Shift"].Success
                                    $modifiers = 0
                                    # Set the right modifiers
                                    if ($keyShift) { $modifiers = $modifiers -bor 1 }
                                    if ($keyControl) { $modifiers = $modifiers -bor 2 }
                                    if ($keyOption ) { $modifiers = $modifiers -bor 4 }
                                    if ($keyCmd) { $modifiers = $modifiers -bor 8 }
                                    $nativeCode = $vKey = $qtKey = # and map the key.
                                        if ($matched.Groups["Key"].Length -eq 1) {
                                            # If it's a single char, it's
                                            # easy mode, turn the key into a char to get the v-key.
                                            [char]$matched.Groups["Key"].Value
                                        } else {
                                            # Otherwise, it's lookup table time.
                                            $matchedKey = $matched.Groups["Key"].Value
                                            switch -Regex ($matchedKey)  {
                                                'F(?>[1-2][0-9]|[0-9])' { # F1-F24
                                                    [int]($matchedKey -replace 'F') + 0x6f
                                                }
                                                '(?>#|Num|NumPad)[0-9]' { # Numpad
                                                    [int]($matchedKey -replace '\D') + 0x60
                                                }
                                                'Back|Backspace' {
                                                    0x8
                                                }
                                                Tab { 0x9 }
                                                Clear { 0xc }
                                                End {0x23}
                                                Home {0x24}
                                                Left {0x25}
                                                Up { 0x26}
                                                Right { 0x27}
                                                Down { 0x28}
                                                Select { 0x29}
                                                Print { 0x2a}
                                                Execute { 0x2b}
                                                'Insert|Ins' { 0x2d}
                                                'Delete|Del' { 0x2e}
                                                Help { 0x2f}
                                                'Space|Spacebar|\s' { 0x20}
                                                'Prior|PageUp' {0x21}
                                                'Next|PageDown' {0x22}
                                                'Enter|Return' { 0xd}
                                                'Caps|CapsLock' { 0x14}
                                                'Kana|Hangul|Hanguel'{ 0x15}
                                                'Junja' { 0x17}
                                                'Final' { 0x18}
                                                'Hanja|Kanji' {0x19}
                                                Sleep {0x5f}
                                                Multiply { 0x6a}
                                                Add { 0x6b}
                                                Subtract { 0x6d}
                                                Decimal { 0x6e}
                                                Divide { 0x6f}
                                                'Num|NumLock' {0x90}
                                                BrowserBack {0xa6}
                                                BrowserForward {0xa7}
                                                BrowserRefresh {0xa8}
                                                BrowserStop {0xa9}
                                                BrowserSearch { 0xaa}
                                                BrowserHome {0xac}
                                                'Mute|VolumeMute|MediaMute' {0xad}
                                                'VolumeDown|MediaDown' {0xae}
                                                'VolumeUp|MediaUp' {0xaf}
                                                'NextTrack|MediaNextTrack' { 0xb0}
                                                'PreviousTrack|MediaPreviousTrack' { 0xb1}
                                                'Stop|MediaStop' { 0xb2 }
                                                'Play|Pause|MediaPlayPause|TogglePlay' {0xb3}
                                                'Mail|LaunchMail' {0xb4}
                                                '\+|Plus|OEMPlus' {0xbb}
                                                ',|Comma|OEMComma' {0xbc}
                                                '\-|Minus|OEMMinus' {0xbd}
                                                '\.|Period|OEMPeriod' {0xbe}
                                            }
                                        }

                                    # If we could not find a match, error and return
                                    if (-not $nativeCode) { Write-Error "Could not map key code for HotKey '$hot'"; return}

                                    # Otherwise, create the type of object StreamDeck will want.
                                    [PSCustomObject][Ordered]@{
                                        KeyCmd = $keyCmd
                                        KeyCtrl = $keyControl
                                        KeyModifiers = $modifiers
                                        KeyOption = $keyOption
                                        KeyShift = $keyShift
                                        NativeCode = $nativeCode -as [int]
                                        QTKeyCode = $qtKey -as [int]
                                        VKeyCode = $vKey -as [int]
                                    }
                                }
                                # All hotkeys must be followed by this "non-key".
                                [PSCustomObject][Ordered]@{
                                    KeyCmd = $false
                                    KeyCtrl = $false
                                    KeyModifiers = 0
                                    KeyOption = $false
                                    KeyShift = $false
                                    NativeCode = 146
                                    QTKeyCode = 33554431
                                    VKeyCode = -1
                                }
                            )
                        }
                    #region HotKey
                }

                ScriptBlock {
                    #region ScriptBlock
                    $name = 'Open'
                    $uuid = 'com.elgato.streamdeck.system.open'

                    $cmdSequence =
                        @(
                        if ($WindowsPowerShell) {
                            'powershell'
                        }
                        else {
                            'pwsh'
                        }
                        '-noexit'
                        '-noprofile'
                        '-nologo'
                        '-encodedCommand'
                        [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($ScriptBlock.ToString()))
                        ) -join ' '

                    $Setting = [Ordered]@{
                        openInBrowser = $true
                        path = $cmdSequence
                    }
                    #endregion ScriptBlock
                }
            }


        if (-not $States) {
            $States =
                @(if ($matchingPlugin.states) {
                    foreach ($s in $matchingPlugin.states) { # If the plugin had a default state
                        $sc = [Ordered]@{} # copy it so we don't change that data.
                        foreach ($p in $s.psobject.properties) {
                            $sc[$p.Name] = $p.Value
                        }
                        [PSCustomObject]$sc
                    }
                } else {
                    [PSCustomObject][Ordered]@{ # Otherwise, create a state object based off of built-in parameters
                        FFamily= $fontFamily
                        FSize = $fontSize
                        FUnderline = $Underline -as [bool]
                        Image = $Image
                        Title = $title
                        TitleAlignment = $titleAlignment
                        TitleColor = $titleColor
                        TitleShow = (-not $HideTitle)
                    }
                })
        }
        foreach ($s in $States) {
            if ($Image) { $s | Add-Member NoteProperty Image $Image -Force }
            if ($Title) { $s | Add-Member NoteProperty Title $Title -Force }
            if ($FontSize) { $s | Add-Member NotePropety FSize $FontSize -Force}
            if ($Underline) { $s | Add-Member NotePropety FUnderline ($Underline -as [bool]) -Force }
            if ($FontFamily) { $s | Add-Member NoteProperty FFamily $FontFamily -Force}
            if ($titleColor) { $s | Add-Member NoteProperty TitleColor $titleColor -Force }
            if ($titleAlignment) { $s | Add-Member NoteProperty TitleAlignment $titleAlignment -Force }
            if ($HideTitle) { $s | Add-Member TitleShow (-not $HideTitle) -Force  }
        }


        [PSCustomobject]([Ordered]@{
            Name = $Name
            Settings = $Setting
            State = $State
            States = @($States)            
            UUID = $UUID
            PSTypeName = 'StreamDeck.Action'
        })
    }
}