startmenu.ps1

function Get-StartMenuProgramsLocation {
    <#
    .SYNOPSIS
        Returns the path to Start Menu > Programs.

    .DESCRIPTION
        Returns the path to the Start Menu Programs folder, for the current user (the default) or for all users.

    .PARAMETER AllUsers
        Return the All Users (machine) Start Menu Programs folder. Aliases: Machine, All.

    .PARAMETER User
        Return the current user's Start Menu Programs folder. (Default.)

    .OUTPUTS
        string - Path to the Start Menu Programs folder.

    .EXAMPLE
        Get-StartMenuProgramsLocation

    .EXAMPLE
        Get-StartMenuProgramsLocation -AllUsers

    .NOTES
        Default scope is User (current user).
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ParameterSetName = "AllUsers")]
        [Alias("Machine", "All")]
        [switch] $AllUsers,
        [Parameter(ParameterSetName = "User")]
        [switch] $User
    )

    # resolved per call, so a folder relocated during the session is picked up
    return $wshShell.SpecialFolders($AllUsers ? "AllUsersPrograms" : "Programs")
}

function New-StartMenuProgramsFolder {
    <#
    .SYNOPSIS
        Creates a new folder in Start Menu > Programs.

    .DESCRIPTION
        Creates a new folder in the Start Menu Programs folder, for the current user (the default) or for all users.

    .PARAMETER Name
        The name of the folder in the Start Menu > Programs folder.

    .PARAMETER AllUsers
        Create the folder in the All Users (machine) Start Menu Programs folder. Aliases: Machine, All.

    .PARAMETER User
        Create the folder in the current user's Start Menu Programs folder. (Default.)

    .OUTPUTS
        string - Path to the newly created folder in the Start Menu Programs folder.

    .EXAMPLE
        New-StartMenuProgramsFolder -Name MyCompany

    .EXAMPLE
        New-StartMenuProgramsFolder -Name MyCompany -AllUsers

    .NOTES
        Default scope is User (current user).
        An unelevated -AllUsers run prompts for elevation once and creates the folder elevated.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, Position = 0)]
        [string] $Name,
        [Parameter(Mandatory, ParameterSetName = "AllUsers")]
        [Alias("Machine", "All")]
        [switch] $AllUsers,
        [Parameter(ParameterSetName = "User")]
        [switch] $User
    )

    # fail fast: an All Users run that cannot elevate stops before anything is created
    if ($AllUsers -and -not (Test-Elevated)) {
        Assert-SudoAvailable
    }

    $programsLocation = Get-StartMenuProgramsLocation -AllUsers:$AllUsers
    $shortcutFolderName = "$programsLocation\$Name"

    # the All Users Programs folder is writable by administrators alone: when not already elevated,
    # the folder is created in an elevated session instead
    if ($AllUsers -and -not $WhatIfPreference -and -not (Test-Elevated)) {
        Invoke-Elevated (ConvertTo-ElevatedCommand -Name New-StartMenuProgramsFolder -BoundParameters $PSBoundParameters)
        return $shortcutFolderName
    }

    if ($PSCmdlet.ShouldProcess($shortcutFolderName, "Create folder")) {
        New-Item -ItemType Directory $shortcutFolderName -Force | Out-Null
    }
    return $shortcutFolderName
}

function New-StartMenuShortcut {
    <#
    .SYNOPSIS
        Creates a new shortcut in Start Menu > Programs.

    .DESCRIPTION
        Creates a new shortcut in the Start Menu Programs folder, for the current user (the default) or for all users.

    .PARAMETER Name
        The name of the shortcut in the Start Menu > Programs folder.

    .PARAMETER Target
        The location of the file or folder the shortcut opens.

    .PARAMETER Folder
        The name of the folder in Start Menu > Programs to create the shortcut in. If not specified, the shortcut is created directly in Start Menu > Programs.

    .PARAMETER Arguments
        The command-line arguments to pass to the target.

    .PARAMETER RunLocation
        The location the target runs in, shown as "Start in" in the shortcut properties.
        Default: the folder of the target.

    .PARAMETER Description
        The free-text description of the shortcut, shown as its comment / tooltip.

    .PARAMETER Icon
        The icon of the shortcut, as the icon file, e.g. "C:\Program Files\MyApp\MyApp.exe", or as the
        icon file and the index of the icon within it, "file,index", e.g. "C:\Windows\imageres.dll,229".
        Without an index the first icon of the file is taken. An icon file whose own name ends in a
        comma and a number needs ",0" appended.
        Default: no icon.

    .PARAMETER Hotkey
        The keyboard shortcut that opens the shortcut, e.g. "Ctrl+Alt+N".

    .PARAMETER WindowStyle
        The window state the target launches in: Normal, Maximized or Minimized. Default: Normal.

    .PARAMETER Elevated
        Launch the target elevated, ticked as "Run as administrator" in the advanced properties.
        Alias: Administrator.

    .PARAMETER Force
        Overwrite the shortcut if it already exists. Without -Force, a terminating error is reported when the shortcut exists.

    .PARAMETER AllUsers
        Create the shortcut in the All Users (machine) Start Menu Programs folder. Aliases: Machine, All.

    .PARAMETER User
        Create the shortcut in the current user's Start Menu Programs folder. (Default.)

    .OUTPUTS
        Shortcut record describing the created shortcut, as read by Get-Shortcut. Nothing under -WhatIf.

    .EXAMPLE
        New-StartMenuShortcut -Name MyApp -Target "C:\Program Files\MyApp\MyApp.exe"

    .EXAMPLE
        New-StartMenuShortcut -Name MyApp -Folder MyCompany -Target "C:\Program Files\MyApp\MyApp.exe" `
            -Arguments "--profile Default" `
            -Icon "C:\Program Files\MyApp\MyApp.exe,3" `
            -WindowStyle Maximized -Elevated

    .NOTES
        Default scope is User (current user).
        Alias: Administrator for -Elevated.
        An unelevated -AllUsers run prompts for elevation once and creates folder and shortcut elevated.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    # the type name is a string: the Shortcut class lives in another file, unresolvable at definition time
    [OutputType("Shortcut")]
    param (
        [Parameter(Mandatory, Position = 0)]
        [string] $Name,
        [Parameter(Mandatory, Position = 1)]
        [string] $Target,
        [string] $Folder,
        [string] $Arguments,
        [string] $RunLocation,
        [string] $Description,
        [string] $Icon,
        [string] $Hotkey,
        [ShortcutWindowStyle] $WindowStyle = [ShortcutWindowStyle]::Normal,
        [Alias("Administrator")]
        [switch] $Elevated,
        [switch] $Force,
        [Parameter(Mandatory, ParameterSetName = "AllUsers")]
        [Alias("Machine", "All")]
        [switch] $AllUsers,
        [Parameter(ParameterSetName = "User")]
        [switch] $User
    )

    # fail fast: an All Users run that cannot elevate stops before anything is created
    if ($AllUsers -and -not (Test-Elevated)) {
        Assert-SudoAvailable
    }

    # elevate before the folder is touched, so folder and shortcut are created in one elevated session
    # behind one prompt; the record comes from reading back what that session wrote
    if ($AllUsers -and -not $WhatIfPreference -and -not (Test-Elevated)) {
        Invoke-Elevated (ConvertTo-ElevatedCommand -Name New-StartMenuShortcut -BoundParameters $PSBoundParameters)

        $programsLocation = Get-StartMenuProgramsLocation -AllUsers
        return Get-Shortcut -Location ($Folder `
                ? "$programsLocation\$Folder\$Name.lnk" `
                : "$programsLocation\$Name.lnk")
    }

    $shortcutFolder = $Folder `
        ? (New-StartMenuProgramsFolder -Name $Folder -AllUsers:$AllUsers) `
        : (Get-StartMenuProgramsLocation -AllUsers:$AllUsers)

    # an omitted run location is left to New-Shortcut, which defaults it to the folder of the target.
    # the name differs from the parameter: a [string] parameter would coerce the hashtable to its type name
    $runLocationArgument = $PSBoundParameters.ContainsKey("RunLocation") ? @{ RunLocation = $RunLocation } : @{}

    # New-Shortcut gates the creation behind its own ShouldProcess, inheriting -WhatIf / -Confirm from here.
    # -CreateFolder states that the folder is there: under -WhatIf New-StartMenuProgramsFolder only reports it.
    return New-Shortcut `
        -Location "$shortcutFolder\$Name.lnk" `
        -Target $Target `
        -Arguments $Arguments `
        @runLocationArgument `
        -Description $Description `
        -Icon $Icon `
        -Hotkey $Hotkey `
        -WindowStyle $WindowStyle `
        -Elevated:$Elevated `
        -CreateFolder `
        -Force:$Force
}

function Remove-StartMenuShortcut {
    <#
    .SYNOPSIS
        Removes a shortcut from Start Menu > Programs.

    .DESCRIPTION
        Removes a shortcut from the Start Menu Programs folder, for the current user (the default) or for all users.
        Mirrors New-StartMenuShortcut: the shortcut is expected at <Programs>\<Folder>\<Name>.lnk, or directly at
        <Programs>\<Name>.lnk when Folder is not specified. After removing the shortcut, its containing folder is
        removed too if it is now empty. If the shortcut does not exist, a terminating error is reported.

    .PARAMETER Name
        The name of the shortcut to remove from the Start Menu > Programs folder.

    .PARAMETER Folder
        The name of the folder in Start Menu > Programs that contains the shortcut. If not specified, the shortcut is expected directly in Start Menu > Programs.

    .PARAMETER AllUsers
        Remove the shortcut from the All Users (machine) Start Menu Programs folder. Aliases: Machine, All.

    .PARAMETER User
        Remove the shortcut from the current user's Start Menu Programs folder. (Default.)

    .EXAMPLE
        Remove-StartMenuShortcut MyApp

    .EXAMPLE
        Remove-StartMenuShortcut -Name MyApp -Folder MyCompany

    .EXAMPLE
        Remove-StartMenuShortcut -Name MyApp -AllUsers

    .NOTES
        Default scope is User (current user).
        An unelevated -AllUsers run prompts for elevation once and removes the shortcut elevated.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, Position = 0)]
        [string] $Name,
        [string] $Folder,
        [Parameter(Mandatory, ParameterSetName = "AllUsers")]
        [Alias("Machine", "All")]
        [switch] $AllUsers,
        [Parameter(ParameterSetName = "User")]
        [switch] $User
    )

    # fail fast: an All Users run that cannot elevate stops before anything is removed
    if ($AllUsers -and -not (Test-Elevated)) {
        Assert-SudoAvailable
    }

    $programsLocation = Get-StartMenuProgramsLocation -AllUsers:$AllUsers
    $shortcutFolder = $Folder ? "$programsLocation\$Folder" : $programsLocation
    $shortcutLocation = "$shortcutFolder\$Name.lnk"

    if (-not (Test-Path -LiteralPath $shortcutLocation)) {
        Write-Error "Shortcut not found: '$shortcutLocation'" `
            -ErrorId "ShortcutNotFound" `
            -Category ObjectNotFound `
            -TargetObject $shortcutLocation `
            -ErrorAction Stop
    }

    # as when creating: an All Users removal that is not already elevated runs in an elevated session
    if ($AllUsers -and -not $WhatIfPreference -and -not (Test-Elevated)) {
        Invoke-Elevated (ConvertTo-ElevatedCommand -Name Remove-StartMenuShortcut -BoundParameters $PSBoundParameters)
        return
    }

    if ($PSCmdlet.ShouldProcess($shortcutLocation, "Remove shortcut")) {
        Remove-Item -LiteralPath $shortcutLocation -Force

        # remove the containing folder if it is now empty, but never the Programs root
        if ($shortcutFolder -ne $programsLocation -and -not (Get-ChildItem -LiteralPath $shortcutFolder -Force)) {
            Remove-Item -LiteralPath $shortcutFolder -Force
        }
    }
}

function New-PowershellStartMenuShortcut {
    <#
    .SYNOPSIS
        Creates a new shortcut that runs a PowerShell command in Start Menu > Programs.

    .DESCRIPTION
        Creates a new shortcut that runs a PowerShell command in the Start Menu Programs folder, for the current user (the default) or for all users.

    .PARAMETER Name
        The name of the shortcut in the Start Menu > Programs folder.

    .PARAMETER Command
        The PowerShell command to run.

    .PARAMETER Folder
        The name of the folder in Start Menu > Programs to create the shortcut in. If not specified, the shortcut is created directly in Start Menu > Programs.

    .PARAMETER KeepOpen
        Whether to keep the PowerShell window open after the command has finished running. Alias: NoExit.

    .PARAMETER RunLocation
        The location the command runs in, shown as "Start in" in the shortcut properties.

    .PARAMETER Description
        The free-text description of the shortcut, shown as its comment / tooltip.

    .PARAMETER Icon
        The icon of the shortcut, as the icon file, e.g. "C:\Program Files\MyApp\MyApp.exe", or as the
        icon file and the index of the icon within it, "file,index", e.g. "C:\Windows\imageres.dll,229".
        Without an index the first icon of the file is taken. An icon file whose own name ends in a
        comma and a number needs ",0" appended.
        Default: no icon.

    .PARAMETER Hotkey
        The keyboard shortcut that opens the shortcut, e.g. "Ctrl+Alt+N".

    .PARAMETER WindowStyle
        The window state the PowerShell window launches in: Normal, Maximized or Minimized. Default: Minimized.

    .PARAMETER Elevated
        Run the PowerShell command elevated, ticked as "Run as administrator" in the advanced properties.
        Alias: Administrator.

    .PARAMETER Force
        Overwrite the shortcut if it already exists. Without -Force, a terminating error is reported when the shortcut exists.

    .PARAMETER AllUsers
        Create the shortcut in the All Users (machine) Start Menu Programs folder. Aliases: Machine, All.

    .PARAMETER User
        Create the shortcut in the current user's Start Menu Programs folder. (Default.)

    .OUTPUTS
        Shortcut record describing the created shortcut, as read by Get-Shortcut. Nothing under -WhatIf.

    .EXAMPLE
        New-PowershellStartMenuShortcut -Name "Kill Node.js" -Command "Stop-Process -Name node -Force"

    .EXAMPLE
        New-PowershellStartMenuShortcut -Name "Run System Update" -Script "C:\Scripts\system-update.ps1" `
            -KeepOpen -WindowStyle Maximized -Elevated

    .NOTES
        Default scope is User (current user).
        Alias: Script for -Command, NoExit for -KeepOpen, Administrator for -Elevated.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    # the type name is a string: the Shortcut class lives in another file, unresolvable at definition time
    [OutputType("Shortcut")]
    param (
        [Parameter(Mandatory, Position = 0)]
        [string] $Name,
        [Parameter(Mandatory, Position = 1)]
        [Alias("Script")]
        [string] $Command,
        [string] $Folder,
        [Alias("NoExit")]
        [switch] $KeepOpen,
        [string] $RunLocation,
        [string] $Description,
        [string] $Icon,
        [string] $Hotkey,
        [ShortcutWindowStyle] $WindowStyle = [ShortcutWindowStyle]::Minimized,
        [Alias("Administrator")]
        [switch] $Elevated,
        [switch] $Force,
        [Parameter(Mandatory, ParameterSetName = "AllUsers")]
        [Alias("Machine", "All")]
        [switch] $AllUsers,
        [Parameter(ParameterSetName = "User")]
        [switch] $User
    )

    $shortcutFolder = $Folder `
        ? (New-StartMenuProgramsFolder -Name $Folder -AllUsers:$AllUsers) `
        : (Get-StartMenuProgramsLocation -AllUsers:$AllUsers)

    $arguments = @()
    if ($KeepOpen) {
        $arguments += "-NoExit"
    }
    $arguments += "-Command `"$Command`""

    # New-Shortcut gates the creation behind its own ShouldProcess, inheriting -WhatIf / -Confirm from here.
    # -CreateFolder states that the folder is there: under -WhatIf New-StartMenuProgramsFolder only reports it.
    # an omitted run location stays empty: the bare pwsh target has no folder to fall back to.
    return New-Shortcut `
        -Location "$shortcutFolder\$Name.lnk" `
        -Target "pwsh" `
        -Arguments ($arguments -join ' ') `
        -RunLocation $RunLocation `
        -Description $Description `
        -Icon $Icon `
        -Hotkey $Hotkey `
        -WindowStyle $WindowStyle `
        -Elevated:$Elevated `
        -CreateFolder `
        -Force:$Force
}