DSCResources/cShortcut/cShortcut.psm1

# Import ShellLink class
$ShellLinkPath = Join-Path $PSScriptRoot '..\..\Libs\ShellLink\ShellLink.dll'
if (Test-Path -LiteralPath $ShellLinkPath -PathType Leaf) {
    Add-Type -Path $ShellLinkPath -ErrorAction Stop
}

# Import VKeyUtil class
$VKeyUtilPath = Join-Path $PSScriptRoot '..\..\Libs\VKeyUtil\VKeyUtil.dll'
if (Test-Path -LiteralPath $VKeyUtilPath -PathType Leaf) {
    Add-Type -Path $VKeyUtilPath -ErrorAction Stop
}

Enum Ensure {
    Absent
    Present
}

Enum WindowStyle {
    undefined = 0
    normal = 1
    maximized = 3
    minimized = 7
}

function Get-TargetResource {
    [CmdletBinding()]
    [OutputType([Hashtable])]
    param
    (
        [Parameter()]
        [ValidateSet('Present', 'Absent')]
        [string]
        $Ensure = [Ensure]::Present,

        [Parameter(Mandatory)]
        [string]
        $Path,

        [Parameter(Mandatory)]
        [string]
        $Target,

        [Parameter()]
        [string]
        $WorkingDirectory,

        [Parameter()]
        [string]
        $Arguments,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        [string]
        $Icon,

        [Parameter()]
        [string]
        $HotKey,

        [Parameter()]
        [uint16]
        $HotKeyCode = 0x0000,

        [ValidateSet('normal', 'maximized', 'minimized')]
        [string]
        $WindowStyle = [WindowStyle]::normal,

        [Parameter()]
        [string]$AppUserModelID
    )

    if (-not $Path.EndsWith('.lnk')) {
        Write-Verbose ("File extension is not 'lnk'. Automatically add extension")
        $Path = $Path + '.lnk'
    }

    $Ensure = [Ensure]::Present

    try {
        # check file exists
        if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
            Write-Verbose 'File not found.'
            $Ensure = [Ensure]::Absent
        }
        else {
            $Shortcut = Get-Shortcut -Path $Path -ReadOnly -ErrorAction Continue
        }
        $returnValue = @{
            Ensure           = $Ensure
            Path             = $Path
            Target           = $(if ($Shortcut.TargetPath) { $Shortcut.TargetPath }else { $Shortcut.IDList })
            WorkingDirectory = $Shortcut.WorkingDirectory
            Arguments        = $Shortcut.Arguments
            Description      = $Shortcut.Description
            Icon             = $Shortcut.IconLocation
            HotKey           = ConvertTo-HotKeyString -HotKeyCode $Shortcut.Hotkey
            HotKeyCode       = $Shortcut.Hotkey
            WindowStyle      = [WindowStyle]::undefined
            AppUserModelID   = $Shortcut.AppUserModelID
        }

        if ($Shortcut.WindowStyle -as [WindowStyle]) {
            $returnValue.WindowStyle = [WindowStyle]$Shortcut.WindowStyle
        }

        $returnValue
    }
    finally {
        if ($Shortcut -is [IDisposable]) {
            $Shortcut.Dispose()
            $Shortcut = $null
        }
    }
} # end of Get-TargetResource


function Set-TargetResource {
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [ValidateSet('Present', 'Absent')]
        [string]
        $Ensure = [Ensure]::Present,

        [Parameter(Mandatory)]
        [string]
        $Path,

        [Parameter(Mandatory)]
        [string]
        $Target,

        [Parameter()]
        [string]
        $WorkingDirectory,

        [Parameter()]
        [string]
        $Arguments,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        [string]
        $Icon,

        [Parameter()]
        [string]
        $HotKey,

        [Parameter()]
        [uint16]
        $HotKeyCode = 0x0000,

        [ValidateSet('normal', 'maximized', 'minimized')]
        [string]
        $WindowStyle = [WindowStyle]::normal,

        [Parameter()]
        [string]$AppUserModelID
    )

    $arg = [HashTable]$PSBoundParameters

    if (-not $Path.EndsWith('.lnk')) {
        Write-Verbose ("File extension is not 'lnk'. Automatically add extension")
        $arg.Path = $Path + '.lnk'
    }

    if ($Icon -and ($Icon -notmatch ',\d+$')) {
        $arg.Icon = $Icon + ',0'
    }

    # Ensure = "Absent"
    if ($Ensure -eq [Ensure]::Absent) {
        Write-Verbose ('Remove shortcut file "{0}"' -f $arg.Path)
        Remove-Item -LiteralPath $arg.Path -Force
    }
    else {
        # Ensure = "Present"
        $null = $arg.Remove('Ensure')
        Update-Shortcut @arg -Force
    }

} # end of Set-TargetResource


function Test-TargetResource {
    [CmdletBinding()]
    [OutputType([bool])]
    param
    (
        [Parameter()]
        [ValidateSet('Present', 'Absent')]
        [string]
        $Ensure = [Ensure]::Present,

        [Parameter(Mandatory)]
        [string]
        $Path,

        [Parameter(Mandatory)]
        [string]
        $Target,

        [Parameter()]
        [string]
        $WorkingDirectory,

        [Parameter()]
        [string]
        $Arguments,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        [string]
        $Icon = ',0',

        [Parameter()]
        [string]
        $HotKey,

        [Parameter()]
        [uint16]
        $HotKeyCode = 0x0000,

        [ValidateSet('normal', 'maximized', 'minimized')]
        [string]
        $WindowStyle = [WindowStyle]::normal,

        [Parameter()]
        [string]$AppUserModelID
    )

    <# 想定される状態パターンと返却するべき値
        1. ショートカットがあるべき(Present)
            1-A. ショートカットなし => 更新の必要あり($false)
            1-B. ショートカットはあるがプロパティが正しくない => 更新の必要あり($false)
            1-C. ショートカットあり、プロパティ一致 => 何もする必要なし($true)
        2. ショートカットはあるべきではない(Absent)
            2-A. ショートカットなし => 何もする必要なし($true)
            2-B. ショートカットあり => 削除の必要あり($false)
    #>


    # 拡張子つける
    if (-not $Path.EndsWith('.lnk')) {
        Write-Verbose ("File extension is not 'lnk'. Automatically add extension")
        $Path = $Path + '.lnk'
    }

    if ($Icon -and ($Icon -notmatch ',\d+$')) {
        $Icon = $Icon + ',0'
    }

    # HotKey文字列からHotKeyCode(数値表現)を取得
    if ($HotKey) {
        # $HotKeyStr = Format-HotKeyString $HotKey
        $HotKeyCode = ConvertFrom-HotKeyString -HotKey $HotKey
    }
    else {
        # $HotKeyStr = [string]::Empty
        $HotKeyCode = 0x0000
    }

    $ReturnValue = $false
    switch ($Ensure) {
        'Absent' {
            # ファイルがなければ$true あれば$false
            $ReturnValue = (-not (Test-Path -LiteralPath $Path -PathType Leaf))
        }
        'Present' {
            $Info = Get-TargetResource -Ensure $Ensure -Path $Path -Target $Target
            if ($Info.Ensure -eq [Ensure]::Absent) {
                $ReturnValue = $false
            }
            else {
                # Tests whether the shortcut property is the same as the specified parameter.
                $NotMatched = @()
                if ($Info.Target -ne $Target) {
                    $NotMatched += 'Target'
                }

                if ($PSBoundParameters.ContainsKey('WorkingDirectory') -and ($Info.WorkingDirectory -ne $WorkingDirectory)) {
                    $NotMatched += 'WorkingDirectory'
                }

                if ($PSBoundParameters.ContainsKey('Arguments') -and ($Info.Arguments -ne $Arguments)) {
                    $NotMatched += 'Arguments'
                }

                if ($PSBoundParameters.ContainsKey('Description') -and ($Info.Description -ne $Description)) {
                    $NotMatched += 'Description'
                }

                if ($PSBoundParameters.ContainsKey('Icon') -and ($Info.Icon -ne $Icon)) {
                    $NotMatched += 'Icon'
                }

                if ($PSBoundParameters.ContainsKey('HotKey') -and ($Info.HotKeyCode -ne $HotKeyCode)) {
                    $NotMatched += 'HotKey'
                }

                if ($PSBoundParameters.ContainsKey('WindowStyle') -and ($Info.WindowStyle -ne $WindowStyle)) {
                    $NotMatched += 'WindowStyle'
                }

                if ($PSBoundParameters.ContainsKey('AppUserModelID') -and ($Info.AppUserModelID -ne $AppUserModelID)) {
                    $NotMatched += 'AppUserModelID'
                }

                $ReturnValue = ($NotMatched.Count -eq 0)
                if (-not $ReturnValue) {
                    $NotMatched | ForEach-Object {
                        Write-Verbose ('{0} property does not match!' -f $_)
                    }
                }
            }
        }
    }
    Write-Verbose "Test returns $ReturnValue"
    return $ReturnValue
} # end of Test-TargetResource


function Get-Shortcut {
    [CmdletBinding()]
    [OutputType([ShellLink])]
    param
    (
        # Path of shortcut files
        [Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('FullName')]
        [ValidateScript( { Test-Path -LiteralPath $_ -PathType Leaf } )]
        [string]$Path,

        [switch]$ReadOnly
    )

    Begin {
        if ($ReadOnly) {
            [int]$flag = 0x00000000 #STGM_READ
        }
        else {
            [int]$flag = 0x00000002 #STGM_READWRITE
        }
    }

    Process {
        try {
            $Shortcut = New-Object -TypeName ShellLink
            $Shortcut.Load($Path, $flag)
            return $Shortcut
        }
        catch {
            if ($Shortcut -is [IDisposable]) {
                $Shortcut.Dispose()
                $Shortcut = $null
            }

            Write-Error -Exception $_.Exception
            return $null
        }
    }
}


function New-Shortcut {
    [CmdletBinding()]
    [OutputType([System.IO.FileSystemInfo])]
    param
    (
        # set file path to create shortcut. If the path not ends with '.lnk', extension will be add automatically.
        [Parameter(
            Position = 0,
            Mandatory,
            ValueFromPipelineByPropertyName)]
        [Alias('FilePath')]
        [string]$Path,

        # Set Target full path to create shortcut
        [Parameter(
            Position = 1,
            Mandatory,
            ValueFromPipelineByPropertyName)]
        [Alias('Target')]
        [string]$TargetPath,

        # Set Description for shortcut.
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias('Comment')]
        [string]$Description,

        # Set Arguments for shortcut.
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$Arguments,

        # Set WorkingDirectory for shortcut.
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$WorkingDirectory,

        # Set IconLocation for shortcut.
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$Icon,

        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$HotKey,

        # Set WindowStyle for shortcut.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('normal', 'maximized', 'minimized')]
        [string]$WindowStyle = [WindowStyle]::normal,

        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$AppUserModelID,

        # set if you want to show create shortcut result
        [switch]$PassThru,

        [switch]$Force
    )

    begin {
        $extension = '.lnk'
    }

    process {
        # Set Path of a Shortcut
        if (-not $Path.EndsWith($extension)) {
            $Path = $Path + $extension
        }

        if ($HotKey) {
            $local:HotKeyCode = ConvertFrom-HotKeyString -HotKey $HotKey -ErrorAction Stop
        }
        else {
            $local:HotKeyCode = 0x0000
        }

        if (-not (Test-Path -LiteralPath (Split-Path $Path -Parent))) {
            Write-Verbose 'Create a parent folder'
            $null = New-Item -Path (Split-Path $Path -Parent) -ItemType Directory -Force -ErrorAction Stop
        }

        $fileName = Split-Path $Path -Leaf  # Filename of shortcut
        $Directory = Resolve-Path -Path (Split-Path $Path -Parent) # Directory of shortcut
        $Path = Join-Path $Directory $fileName  # Fullpath of shortcut

        #Remove existing shortcut (when the Force switch is specified)
        if (Test-Path -LiteralPath $Path -PathType Leaf) {
            if ($Force) {
                Write-Verbose 'Remove existing shortcut file'
                Remove-Item $Path -Force -ErrorAction SilentlyContinue
            }
            else {
                Write-Error -Exception ([System.IO.IOException]::new("The file '$Path' is already exists."))
                return
            }
        }

        # Call IShellLink to create Shortcut
        Write-Verbose ("Trying to create Shortcut to '{0}'" -f $Path)
        try {
            $Shortcut = New-Object -TypeName ShellLink
            $Shortcut.TargetPath = $TargetPath
            $Shortcut.Description = $Description
            $Shortcut.WindowStyle = [int][WindowStyle]$WindowStyle
            $Shortcut.Arguments = $Arguments
            $Shortcut.WorkingDirectory = $WorkingDirectory
            if ($PSBoundParameters.ContainsKey('Icon')) {
                $Shortcut.IconLocation = $Icon
            }
            if ($PSBoundParameters.ContainsKey('AppUserModelID')) {
                $Shortcut.AppUserModelID = $AppUserModelID
            }
            if ($PSBoundParameters.ContainsKey('Hotkey')) {
                $Shortcut.Hotkey = $local:HotKeyCode
            }
            $Shortcut.Save($Path)
            Write-Verbose 'Shortcut file created successfully.'
        }
        catch {
            Write-Error -Exception $_.Exception
            return
        }
        finally {
            if ($Shortcut -is [System.IDisposable]) {
                $Shortcut.Dispose()
                $Shortcut = $null
            }
        }

        if ($PassThru) {
            Get-Item -LiteralPath $Path
        }
    }

    end {}
}

function Update-Shortcut {
    [CmdletBinding(DefaultParameterSetName = 'ShellLink')]
    [OutputType([System.IO.FileSystemInfo])]
    param
    (
        # Set file path to update shortcut. If the path not ends with '.lnk', extension will be add automatically.
        [Parameter(
            Position = 0,
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName,
            ParameterSetName = 'FilePath')]
        [Alias('FilePath')]
        [string]$Path,

        [Parameter(
            Position = 0,
            Mandatory,
            ValueFromPipeline,
            ParameterSetName = 'ShellLink')]
        [ShellLink]$InputObject,

        # Set Target full path for shortcut
        [Parameter(ParameterSetName = 'FilePath', ValueFromPipelineByPropertyName)]
        [Parameter(ParameterSetName = 'ShellLink')]
        [Alias('Target')]
        [string]$TargetPath,

        # Set Description for shortcut.
        [Parameter(ParameterSetName = 'FilePath', ValueFromPipelineByPropertyName)]
        [Parameter(ParameterSetName = 'ShellLink')]
        [Alias('Comment')]
        [string]$Description,

        # Set Arguments for shortcut.
        [Parameter(ParameterSetName = 'FilePath', ValueFromPipelineByPropertyName)]
        [Parameter(ParameterSetName = 'ShellLink')]
        [string]$Arguments,

        # Set WorkingDirectory for shortcut.
        [Parameter(ParameterSetName = 'FilePath', ValueFromPipelineByPropertyName)]
        [Parameter(ParameterSetName = 'ShellLink')]
        [string]$WorkingDirectory,

        # Set IconLocation for shortcut.
        [Parameter(ParameterSetName = 'FilePath', ValueFromPipelineByPropertyName)]
        [Parameter(ParameterSetName = 'ShellLink')]
        [string]$Icon,

        [Parameter(ParameterSetName = 'FilePath', ValueFromPipelineByPropertyName)]
        [Parameter(ParameterSetName = 'ShellLink')]
        [string]$HotKey,

        # Set WindowStyle for shortcut.
        [Parameter(ParameterSetName = 'FilePath', ValueFromPipelineByPropertyName)]
        [Parameter(ParameterSetName = 'ShellLink')]
        [ValidateSet('normal', 'maximized', 'minimized')]
        [string]$WindowStyle = [WindowStyle]::normal,

        [Parameter(ParameterSetName = 'FilePath', ValueFromPipelineByPropertyName)]
        [Parameter(ParameterSetName = 'ShellLink')]
        [string]$AppUserModelID,

        # set if you want to show create shortcut result
        [switch]$PassThru,

        [switch]$Force
    )

    begin {
        $extension = '.lnk'
    }

    process {
        if ($PSCmdlet.ParameterSetName -eq 'FilePath') {
            if (-not $Path.EndsWith($extension)) {
                $Path = $Path + $extension
            }

            if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
                if ($Force -and $TargetPath) {
                    New-Shortcut @PSBoundParameters
                    return
                }
                else {
                    Write-Error -Exception ([System.IO.FileNotFoundException]::new("The file '$Path' does not exists."))
                    return
                }
            }
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'ShellLink') {
            if (-not ($InputObject.FilePath)) {
                Write-Error -Exception ([System.ArgumentException]::new('The InputObject does not valid.'))
                return
            }
        }

        if ($HotKey) {
            $local:HotKeyCode = ConvertFrom-HotKeyString -HotKey $HotKey -ErrorAction Stop
        }
        else {
            $local:HotKeyCode = 0x0000
        }

        # Call IShellLink to update Shortcut
        Write-Verbose ("Updating Shortcut for '{0}'" -f $Path)
        try {
            if ($PSCmdlet.ParameterSetName -eq 'FilePath') {
                $InputObject = New-Object -TypeName ShellLink
                $InputObject.Load($Path)
            }
            elseif ($PSCmdlet.ParameterSetName -eq 'ShellLink') {
                $Path = $InputObject.FilePath
            }

            $Shortcut = $InputObject
            if ($PSBoundParameters.ContainsKey('TargetPath')) {
                $Shortcut.TargetPath = $TargetPath
            }
            if ($PSBoundParameters.ContainsKey('Description')) {
                $Shortcut.Description = $Description
            }
            if ($PSBoundParameters.ContainsKey('WindowStyle')) {
                $Shortcut.WindowStyle = [int][WindowStyle]$WindowStyle
            }
            if ($PSBoundParameters.ContainsKey('Arguments')) {
                $Shortcut.Arguments = $Arguments
            }
            if ($PSBoundParameters.ContainsKey('WorkingDirectory')) {
                $Shortcut.WorkingDirectory = $WorkingDirectory
            }
            if ($PSBoundParameters.ContainsKey('Icon')) {
                $Shortcut.IconLocation = $Icon
            }
            if ($PSBoundParameters.ContainsKey('AppUserModelID')) {
                $Shortcut.AppUserModelID = $AppUserModelID
            }
            if ($PSBoundParameters.ContainsKey('Hotkey')) {
                $Shortcut.Hotkey = $local:HotKeyCode
            }

            $Shortcut.Save($Path)
            Write-Verbose 'Shortcut file updated successfully.'
        }
        catch {
            Write-Error -Exception $_.Exception
            return
        }
        finally {
            if ($Shortcut -is [System.IDisposable]) {
                $Shortcut.Dispose()
                $Shortcut = $null
            }
        }

        if ($PassThru) {
            Get-Item -LiteralPath $Path
        }
    }

    end {}
}


function Format-HotKeyString {
    [CmdletBinding()]
    [OutputType([string])]
    Param(
        [Parameter(Mandatory, Position = 0)]
        [AllowEmptyString()]
        [string]$HotKey
    )

    if ([string]::IsNullOrWhiteSpace($HotKey)) {
        return [string]::Empty
    }

    [string[]]$local:HotKeyArray = $HotKey.split('+').Trim()

    if ($local:HotKeyArray.Count -eq 1 -and $local:HotKeyArray[0] -match '^F([1-9]|1[0-9]|2[0-4])$') {
        # F1~F24は修飾キーを伴わず単体でもOK
    }
    elseif ($local:HotKeyArray.Count -notin (2..4)) {
        #最短で修飾+キーの2要素、最長でAlt+Ctrl+Shift+キーの4要素
        Write-Error 'HotKey is not valid format.'
        return [string]::Empty
    }
    elseif ($local:HotKeyArray[0] -notmatch '^(Ctrl|Alt|Shift)$') {
        #修飾キーから始まっていないとダメ
        Write-Error 'HotKey is not valid format.'
        return [string]::Empty
    }

    #優先順位付きソート
    $local:sort = $local:HotKeyArray | ForEach-Object {
        switch ($_) {
            'Ctrl' { 1 }
            'Shift' { 2 }
            'Alt' { 3 }
            Default { 4 }
        }
    }
    [Array]::Sort($local:sort, $local:HotKeyArray)
    $local:HotKeyArray -join '+'
}


function ConvertFrom-HotKeyString {
    [CmdletBinding()]
    [OutputType([uint16])]
    Param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [AllowEmptyString()]
        [string]$HotKey
    )

    begin {
        [uint16]$HOTKEYF_SHIFT = 0x0100
        [uint16]$HOTKEYF_CONTROL = 0x0200
        [uint16]$HOTKEYF_ALT = 0x0400
        # [uint16]$HOTKEYF_EXT = 0x0800 #?

        Add-Type -AssemblyName System.Windows.Forms
        $KeysConverter = New-Object -TypeName 'System.Windows.Forms.KeysConverter'
    }

    Process {
        if ([string]::IsNullOrWhiteSpace($HotKey)) {
            return 0x0000
        }

        [uint16]$local:HotKeyCode = 0x0000
        $HotKey = Format-HotKeyString -HotKey $HotKey
        [string[]]$local:HotKeyArray = $HotKey.split('+').Trim()

        switch ($local:HotKeyArray) {
            'Shift' {
                $local:HotKeyCode = $local:HotKeyCode -bor $HOTKEYF_SHIFT
                continue
            }

            'Ctrl' {
                $local:HotKeyCode = $local:HotKeyCode -bor $HOTKEYF_CONTROL
                continue
            }

            'Alt' {
                $local:HotKeyCode = $local:HotKeyCode -bor $HOTKEYF_ALT
                continue
            }

            Default {
                $local:KeyString = $_
                $local:KeyCode = $null
                try {
                    $local:KeyCode = $KeysConverter.ConvertFromString($local:KeyString.ToUpper())
                }
                catch [ArgumentException] {
                    try {
                        $local:KeyCode = [VKeyUtil]::GetKeyCodeFromChar($local:KeyString) -band 0x00ff
                    }
                    catch {
                        Write-Error 'HotKey is not valid format.'
                        return
                    }
                }
                catch {
                    Write-Error -Exception $_.Exception
                    return
                }

                if ($null -ne $local:KeyCode) {
                    $local:HotKeyCode = $local:HotKeyCode -bor $local:KeyCode
                }
            }
        }

        $local:HotKeyCode
    }

    End {
        $KeysConverter = $null
    }
}


function ConvertTo-HotKeyString {
    [CmdletBinding()]
    [OutputType([string])]
    Param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [uint16]$HotKeyCode
    )

    begin {
        [uint16]$HOTKEYF_SHIFT = 0x0100
        [uint16]$HOTKEYF_CONTROL = 0x0200
        [uint16]$HOTKEYF_ALT = 0x0400
        # [uint16]$HOTKEYF_EXT = 0x0800 #?

        Add-Type -AssemblyName System.Windows.Forms
        $KeysConverter = New-Object -TypeName 'System.Windows.Forms.KeysConverter'
    }

    Process {
        if ($HotKeyCode -eq 0x0000) {
            return [string]::Empty
        }

        [string[]]$local:HotKeyArray = @()

        # Modifier Keys
        if ($HotKeyCode -band $HOTKEYF_SHIFT) {
            $local:HotKeyArray += 'Shift'
        }
        if ($HotKeyCode -band $HOTKEYF_CONTROL) {
            $local:HotKeyArray += 'Ctrl'
        }
        if ($HotKeyCode -band $HOTKEYF_ALT) {
            $local:HotKeyArray += 'Alt'
        }

        # Key
        [string]$local:Key = $null
        try { $local:Key = [VKeyUtil]::GetCharsFromKeys($HotKeyCode -band 0x00ff) } catch {}

        if ([string]::IsNullOrWhiteSpace($local:Key)) {
            try {
                $local:Key = $KeysConverter.ConvertToString($HotKeyCode -band 0x00ff)
            }
            catch {
                Write-Error -Exception $_.Exception
                return
            }
        }

        if (-not [string]::IsNullOrWhiteSpace($local:Key)) {
            $local:HotKeyArray += $local:Key.ToUpper()
            # return formatted string
            Format-HotKeyString -HotKey ([string]::Join('+', $local:HotKeyArray))
        }
        else {
            [string]::Empty
        }
    }

    End {
        $KeysConverter = $null
    }
}


Export-ModuleMember -Function *-TargetResource
Export-ModuleMember -Function @(
    'Get-TargetResource',
    'Set-TargetResource',
    'Test-TargetResource',
    'Get-Shortcut',
    'New-Shortcut',
    'Update-Shortcut'
)