Shortcut.ps1

<#
.SYNOPSIS
    Simple Shortcut
.DESCRIPTION
    Simple Shortcuts with PowerShell
.EXAMPLE
    # Create a `.url` shortcut (Windows Only)
    Shortcut ./psturtle.com.url "https://psturtle.com"
.EXAMPLE
    # Create a `.lnk` shortcut (Windows Only)
    Shortcut ./pwsh.lnk -TargetPath pwsh
.EXAMPLE
    # Create a `.lnk` shortcut to PowerShell
    Shortcut ./pwsh-fullscreen.lnk -TargetPath pwsh -Fullscreen
.EXAMPLE
    # Create a `.lnk` shortcut to a PowerShell script.
    Shortcut ./pwsh-hello-world.lnk -PowerShell { "hello world" }
.EXAMPLE
    # Create a `.desktop` shortcut to a PowerShell script
    Shortcut ./pwsh.desktop -TargetPath /usr/bin/pwsh
#>

[CmdletBinding(PositionalBinding=$false)]
param(
# The name of the shortcut file.
[Parameter(Position=0,ValueFromPipelineByPropertyName)]
[AllowNull()]
[AllowEmptyString()]
[string]
$ShortcutFile,

# The shortcut url
[Parameter(Position=1,ValueFromPipelineByPropertyName)]
[Alias('Uri', 'Href')]
[uri]
$Url,

# Any PowerShell to run.
# This must be either a ScriptBlock or a .ps1 path
# If it is a ScriptBlock,
# a file will be created in the same location as the link.
[ValidateScript({
    if ($_ -is [ScriptBlock]) { return $true }
    $file = try { Get-Item -Path $_ -ErrorAction Ignore } catch {} 
    if ($file.Extension -eq '.ps1') {
        return $true
    }
    throw "Must be a ScriptBlock or .ps1"
})]
[PSObject]
$PowerShell,

# The content of a simple url shortcut.
# If only a -Url or a -Url and -Content and provided,
# will create an HTML shortcut `<a href>`
[Parameter(ValueFromPipelineByPropertyName)]
[object[]]
$Content,

# A description of the shortcut.
# This is ignored for .url shortcut files.
[Parameter(ValueFromPipelineByPropertyName)]
[Alias('Comment')]
[string]
$Description,

# The target path of the link.
[Parameter(ValueFromPipelineByPropertyName)]
[string]
$TargetPath,

# The hotkey.
# This is theoretically supported by the shortcut object
# but may be ignored by explorer.
[Parameter(ValueFromPipelineByPropertyName)]
[string]
$HotKey,

# The icon location
# The path containing an icon,
# followed by the index in that path.
[Parameter(ValueFromPipelineByPropertyName)]
[string]
$IconLocation,

# The working directory
[Parameter(ValueFromPipelineByPropertyName)]
[string]
$WorkingDirectory,

# The window style.
[Parameter(ValueFromPipelineByPropertyName)]
[int]
$WindowStyle,

# If set, will make a fullscreen shortcut.
# Ignored for url shortcuts
# This is equivalent to using `-WindowStyle 3`
[Parameter(ValueFromPipelineByPropertyName)]
[switch]
$Fullscreen,

# Any arguments to the application
[Parameter(ValueFromPipelineByPropertyName)]
[string[]]
$Arguments,

# If set, will show the PowerShell logo
# This is ignored if `-PowerShell` is not passed
[Parameter(ValueFromPipelineByPropertyName)]
[switch]
$ShowLogo,

# If set, will automatically exit upon completion
# This is ignored if `-PowerShell` is not passed.
[Parameter(ValueFromPipelineByPropertyName)]
[switch]
$AutoExit,

# Any Desktop Entry settings
# Settings provided by other parameters will be automatically mapped
# (where possible)
[Parameter(ValueFromPipelineByPropertyName)]
[Collections.IDictionary]
$DesktopEntry = [Ordered]@{}
)

process {
    # We are not actually using parameter sets
    # because of the binding difficulty they would present.

    # It is, however, still a useful way to think of how we will handle each input

    $parameterSet = 
        # `.desktop` files or any `-DesktopEntry`
        if ($ShortcutFile -match '\.desktop$' -or $DesktopEntry.Count) {
            'DesktopEntry' # will make it a `DesktopEntry`
        } elseif (
            # `.lnk` and `.url` shortcut files
            $ShortcutFile -match '\.(?>lnk|url)$'
        ) {
            'WindowsShortcut' # indicate a `WindowsShortcut`
        } elseif ($url -and -not $shortcutPath) {
            # A url without a path indicates an `Href`
            'Href'
        }

    # Copy our parameters for easier debugging
    $parameters = [Ordered]@{} + $PSBoundParameters    

    # If a Script Block was provided
    if ($PowerShell -is [ScriptBlock] -and $ShortcutFile) {
        # make a .ps1 file for simplicity and security.
        $ps1Path = "$($ShortcutFile).ps1"
        "$PowerShell" > $ps1Path
        # and output that file before creating the shortcut.
        $ps1 = Get-Item -Path $ps1Path
        $ps1
        $PowerShell = $ps1.FullName
    }

    # and switch things up based off of the parameter set.
    switch ($parameterSet) {
        DesktopEntry {            
            # For desktop entries,
            $desktopEntryLines = @(
                "[Desktop Entry]" # construct the file
                # Default to application if no `Type` was provided
                if (-not $DesktopEntry.Type) {"Type=Application"}
                # Use `-Description` for `Comment`
                if ($Description -and -not $DesktopEntry.Comment) {
                    "Comment=$Description"
                }
                # Use `-TargetPath` for `Exec`
                if ($TargetPath -and -not $DesktopEntry.Exec) {
                    "Exec=$TargetPath $arguments" -replace '\s$'
                }
                elseif ($parameters.PowerShell -and -not $DesktopEntry.Exec) {                
                    "Exec=/usr/bin/pwsh $(
                        if (-not $AutoExit) {
                            '-noexit'
                        }
                        if (-not $ShowLogo) {
                            '-nologo'
                        }
                        "-file $PowerShell"
                    )"
 -replace '\s$'
                    "Terminal=true"
                }
                # and output any additional values.
                foreach ($key in $DesktopEntry.Keys) {
                    if ($DesktopEntry[$key] -is [bool] -or 
                        $DesktopEntry[$key] -is [switch]) {
                        "$key=$(($DesktopEntry[$key] -as [bool] -as [string]).ToLower())"
                    } else {
                        "$key=$($DesktopEntry[$key])"
                    }                    
                }
            )
            # If a shortcut file path was provided
            if ($ShortcutFile) {
                # write to that file,
                $desktopEntryLines > $ShortcutFile
                # make it executable,
                
                if ($ExecutionContext.InvokeCommand.GetCommand("chmod", "Application")) {
                    chmod +x $ShortcutFile
                    # mark it as trusted,
                }
                
                if ($ExecutionContext.InvokeCommand.GetCommand("gio", "Application")) {
                    gio set $ShortcutFile metadata::trusted true
                }                
                # and output the file.
                Get-Item $ShortcutFile
            } else {
                # Otherwise, output the lines.
                $desktopEntryLines
            }
            
            return # and return
        }
        Href {
            # For hrefs, just stick the content into a `<a>` tag.
            return "<a href='$($Url)'>$(
                foreach ($item in $Content) {
                    if ($item.OuterXml) {
                        $item.OuterXml
                    } elseif ($($itemHtml = $item.Html; $itemHtml)) {
                        $itemHtml
                    } else {
                        "$item"
                    }
                }
            )</a>"

        }
        default {
            # For windows shortcuts, create a `WScript.Shell` object
            $wsh = New-Object -ComObject WScript.Shell
            # If that did not work, we are most likely not on Windows.
            if (-not $wsh.CreateShortcut) { return }
            # Get our shortcut path
            $shortcutPath = # (it may not exist yet)
                $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath(
                    $ShortcutFile
                )
            # Try to create the shortcut
            # (this will also get an existing shortcut)
            $shortcutObject = $wsh.CreateShortcut($shortcutPath)

            # If no shortcut object is present, return

            if (-not $shortcutObject) { return }

            # If no other parameters were passed, output our shortcut object
            if ($parameters.Count -eq 1) {
                return $shortcutObject
            }

            # If arguments exist, clear them.
            if ($shortcutObject.Arguments) {
                $shortcutObject.Arguments = ''
            }
            # Set any properties
            foreach ($prop in $parameters.Keys) {
                # that exist on the shortcut object.
                if (-not $shortcutObject.psobject.properties[$prop].IsSettable) {
                    continue
                }
                $shortcutObject.$prop = "$($parameters[$prop] -join ' ')"                
            }
            # If we want `-Fullscreen`
            if ($Fullscreen) {
                # use the right window style
                $shortcutObject.WindowStyle = 3
            }
            # If we provided PowerShell
            if ($parameters['PowerShell']) {
                # launch our own exe
                $shortcutObject.TargetPath = 
                    (Get-Process -Id $pid).Path
                # If -AutoExit was not provided
                if (-not $AutoExit) {
                    # add -NoExit
                    $shortcutObject.Arguments += "-noexit "
                }
                # If -ShowLogo was not provided
                if (-not $ShowLogo) {
                    # add -NoLogo
                    $shortcutObject.Arguments += "-nologo "
                }                
                $shortcutObject.Arguments += "-file $PowerShell"                
            }
            # If a Url was provided
            elseif ($parameters['Url']) {
                # that is the TargetPath
                $shortcutObject.TargetPath = $url    
            }

            # Save our shortcut object
            $shortcutObject.Save()
            # and get our shortcut file.
            Get-Item -Path $shortcutObject.Fullname
        }
    }    
}