completers/psshutdown_completer/psshutdown_completer.ps1

# psshutdown tab completion for PowerShell
# Native completer for PsShutdown actions, switches, and value-aware placeholders.

Set-StrictMode -Version Latest

function New-PsShutdownCompletionResult {
    param(
        [string]$CompletionText,
        [string]$ResultType,
        [string]$ToolTip,
        [string]$ListItemText
    )

    if ([string]::IsNullOrWhiteSpace($ListItemText)) {
        $ListItemText = $CompletionText
    }

    if ([string]::IsNullOrWhiteSpace($ToolTip)) {
        $ToolTip = $CompletionText
    }

    [System.Management.Automation.CompletionResult]::new(
        $CompletionText,
        $ListItemText,
        $ResultType,
        $ToolTip
    )
}

function Remove-PsShutdownOuterQuotes {
    param([string]$Value)

    if ([string]::IsNullOrEmpty($Value)) {
        return ''
    }

    if ($Value.Length -ge 2 -and $Value.StartsWith('"') -and $Value.EndsWith('"')) {
        return $Value.Substring(1, $Value.Length - 2)
    }

    $Value.TrimStart('"')
}

function Get-PsShutdownCurrentToken {
    param(
        [string]$Line,
        [int]$CursorPosition,
        [string]$Fallback
    )

    if ([string]::IsNullOrEmpty($Line)) {
        return $Fallback
    }

    $safeCursor = [Math]::Min([Math]::Max($CursorPosition, 0), $Line.Length)
    $prefix = $Line.Substring(0, $safeCursor)
    if ($prefix -match '\s$') {
        return ''
    }

    $parts = @([regex]::Matches($prefix, '"[^"]*"|\S+') | ForEach-Object { $_.Value })
    if ($parts.Count -gt 0) {
        return $parts[-1]
    }

    $Fallback
}

function Get-PsShutdownActionSpecs {
    @(
        [pscustomobject]@{ Token = '-s'; Description = 'Shutdown without poweroff.' }
        [pscustomobject]@{ Token = '-r'; Description = 'Reboot after shutdown.' }
        [pscustomobject]@{ Token = '-h'; Description = 'Hibernate the computer.' }
        [pscustomobject]@{ Token = '-d'; Description = 'Suspend the computer.' }
        [pscustomobject]@{ Token = '-k'; Description = 'Power off the computer.' }
        [pscustomobject]@{ Token = '-a'; Description = 'Abort a shutdown already in progress.' }
        [pscustomobject]@{ Token = '-l'; Description = 'Lock the computer.' }
        [pscustomobject]@{ Token = '-o'; Description = 'Log off the console user.' }
        [pscustomobject]@{ Token = '-x'; Description = 'Turn the monitor off.' }
    )
}

function Get-PsShutdownSwitchSpecs {
    @(
        [pscustomobject]@{ Token = '-f'; Description = 'Force running applications to close.'; TakesValue = $false }
        [pscustomobject]@{ Token = '-c'; Description = 'Allow the shutdown to be aborted by the interactive user.'; TakesValue = $false }
        [pscustomobject]@{ Token = '-t'; Description = 'Countdown seconds or shutdown time in h:m format.'; TakesValue = $true; ValueKind = 'Time' }
        [pscustomobject]@{ Token = '-v'; Description = 'Display the shutdown dialog for the specified number of seconds.'; TakesValue = $true; ValueKind = 'DisplaySeconds' }
        [pscustomobject]@{ Token = '-e'; Description = 'Shutdown reason code in [u|p]:xx:yy form.'; TakesValue = $true; ValueKind = 'ReasonCode' }
        [pscustomobject]@{ Token = '-m'; Description = 'Message shown to logged on users.'; TakesValue = $true; ValueKind = 'Message' }
        [pscustomobject]@{ Token = '-u'; Description = 'User name for the remote connection.'; TakesValue = $true; ValueKind = 'UserName' }
        [pscustomobject]@{ Token = '-p'; Description = 'Optional password for the remote connection user name.'; TakesValue = $true; ValueKind = 'Password' }
        [pscustomobject]@{ Token = '-n'; Description = 'Timeout in seconds for connecting to remote computers.'; TakesValue = $true; ValueKind = 'ConnectTimeout' }
        [pscustomobject]@{ Token = '-nobanner'; Description = 'Suppress the startup banner and copyright message.'; TakesValue = $false }
        [pscustomobject]@{ Token = '-accepteula'; Description = 'Accept the Sysinternals license agreement without the first-run dialog.'; TakesValue = $false }
    )
}

if (-not (Get-Variable -Name PsShutdownCompletionCatalog -Scope Script -ErrorAction Ignore)) {
    $script:PsShutdownCompletionCatalog = @{
        Initialized         = $false
        Actions             = @()
        ActionLookup        = @{}
        Switches            = @()
        SwitchLookup        = @{}
        ValueTakingSwitches = @{}
    }
}

function Initialize-PsShutdownCompletionCatalog {
    if ($script:PsShutdownCompletionCatalog.Initialized) {
        return
    }

    $script:PsShutdownCompletionCatalog.Actions = @(Get-PsShutdownActionSpecs)
    $script:PsShutdownCompletionCatalog.ActionLookup = @{}
    $script:PsShutdownCompletionCatalog.Switches = @(Get-PsShutdownSwitchSpecs)
    $script:PsShutdownCompletionCatalog.SwitchLookup = @{}
    $script:PsShutdownCompletionCatalog.ValueTakingSwitches = @{}

    foreach ($action in $script:PsShutdownCompletionCatalog.Actions) {
        $script:PsShutdownCompletionCatalog.ActionLookup[$action.Token.ToLowerInvariant()] = $action
    }

    foreach ($switchSpec in $script:PsShutdownCompletionCatalog.Switches) {
        $lowerToken = $switchSpec.Token.ToLowerInvariant()
        $script:PsShutdownCompletionCatalog.SwitchLookup[$lowerToken] = $switchSpec
        if ($switchSpec.TakesValue) {
            $script:PsShutdownCompletionCatalog.ValueTakingSwitches[$lowerToken] = $switchSpec.ValueKind
        }
    }

    $script:PsShutdownCompletionCatalog.Initialized = $true
}

function Get-PsShutdownCommandState {
    param([string[]]$TokensBeforeCurrent)

    $usedTokens = @{}
    $selectedAction = $null
    $valueContext = $null
    $hasRemoteTarget = $false
    $remoteTargetToken = $null

    for ($i = 0; $i -lt $TokensBeforeCurrent.Count; $i++) {
        $token = $TokensBeforeCurrent[$i]
        $lookup = $token.ToLowerInvariant()

        if ($script:PsShutdownCompletionCatalog.ActionLookup.ContainsKey($lookup)) {
            $usedTokens[$lookup] = $true
            if (-not $selectedAction) {
                $selectedAction = $lookup
            }
            continue
        }

        if ($script:PsShutdownCompletionCatalog.SwitchLookup.ContainsKey($lookup)) {
            $usedTokens[$lookup] = $true
            if ($script:PsShutdownCompletionCatalog.ValueTakingSwitches.ContainsKey($lookup)) {
                if ($i -eq ($TokensBeforeCurrent.Count - 1)) {
                    $valueContext = $lookup
                    break
                }

                $i++
            }
            continue
        }

        if (-not $hasRemoteTarget) {
            $hasRemoteTarget = $true
            $remoteTargetToken = $token
        }
    }

    [pscustomobject]@{
        UsedTokens        = $usedTokens
        SelectedAction    = $selectedAction
        ValueContext      = $valueContext
        HasRemoteTarget   = $hasRemoteTarget
        RemoteTargetToken = $remoteTargetToken
    }
}

function New-PsShutdownLiteralValueResults {
    param(
        [string]$CurrentValue,
        [string]$Placeholder,
        [string]$ToolTip
    )

    if ([string]::IsNullOrWhiteSpace($CurrentValue)) {
        return @(
            New-PsShutdownCompletionResult -CompletionText $Placeholder -ListItemText $Placeholder -ResultType 'ParameterValue' -ToolTip $ToolTip
        )
    }

    @(
        New-PsShutdownCompletionResult -CompletionText $CurrentValue -ListItemText $CurrentValue -ResultType 'ParameterValue' -ToolTip $ToolTip
    )
}

function Get-PsShutdownSampleValueResults {
    param(
        [string]$CurrentValue,
        [object[]]$Samples,
        [string]$Placeholder,
        [string]$PlaceholderToolTip
    )

    $typedValue = Remove-PsShutdownOuterQuotes -Value $CurrentValue
    $results = New-Object System.Collections.Generic.List[object]

    foreach ($sample in $Samples) {
        if (-not [string]::IsNullOrWhiteSpace($typedValue) -and
            -not $sample.CompletionText.StartsWith($typedValue, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }

        [void]$results.Add((
            New-PsShutdownCompletionResult `
                -CompletionText $sample.CompletionText `
                -ListItemText $sample.ListItemText `
                -ResultType 'ParameterValue' `
                -ToolTip $sample.ToolTip
        ))
    }

    if ([string]::IsNullOrWhiteSpace($typedValue) -or
        $Placeholder.StartsWith($typedValue, [System.StringComparison]::OrdinalIgnoreCase)) {
        [void]$results.Add((
            New-PsShutdownCompletionResult `
                -CompletionText $Placeholder `
                -ListItemText $Placeholder `
                -ResultType 'ParameterValue' `
                -ToolTip $PlaceholderToolTip
        ))
    }

    if ($results.Count -eq 0 -and -not [string]::IsNullOrWhiteSpace($CurrentValue)) {
        [void]$results.Add((
            New-PsShutdownCompletionResult `
                -CompletionText $CurrentValue `
                -ListItemText $CurrentValue `
                -ResultType 'ParameterValue' `
                -ToolTip $PlaceholderToolTip
        ))
    }

    @($results.ToArray())
}

function ConvertTo-PsShutdownQuotedTarget {
    param(
        [string]$Value,
        [bool]$AlwaysQuote = $false
    )

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

    if (($AlwaysQuote -or $Value -match '\s') -and -not ($Value.StartsWith('"') -and $Value.EndsWith('"'))) {
        return '"' + $Value + '"'
    }

    $Value
}

function Get-PsShutdownAtFileCompletions {
    param([string]$CurrentWord)

    $rawValue = if ([string]::IsNullOrEmpty($CurrentWord)) { '@' } else { $CurrentWord }
    $startedQuoted = $rawValue.StartsWith('"')
    $trimmedValue = Remove-PsShutdownOuterQuotes -Value $rawValue
    if (-not $trimmedValue.StartsWith('@')) {
        return @()
    }

    $inputPath = $trimmedValue.Substring(1)
    $cleanInput = $inputPath
    if ([string]::IsNullOrWhiteSpace($cleanInput)) {
        $parent = '.'
        $leaf = ''
    } else {
        $parent = Split-Path -Path $cleanInput -Parent
        if ([string]::IsNullOrWhiteSpace($parent)) {
            $parent = '.'
        }

        $leaf = Split-Path -Path $cleanInput -Leaf
    }

    $filter = if ([string]::IsNullOrWhiteSpace($leaf)) { '*' } else { "$leaf*" }
    $items = @(Get-ChildItem -Path $parent -Filter $filter -ErrorAction SilentlyContinue)
    $results = New-Object System.Collections.Generic.List[object]

    foreach ($item in $items) {
        $pathText = if ($parent -eq '.') {
            $item.Name
        } elseif (-not [System.IO.Path]::IsPathRooted($cleanInput)) {
            Join-Path -Path $parent -ChildPath $item.Name
        } else {
            $item.FullName
        }

        if ($item.PSIsContainer -and -not $pathText.EndsWith([System.IO.Path]::DirectorySeparatorChar)) {
            $pathText += [System.IO.Path]::DirectorySeparatorChar
        }

        $completionText = ConvertTo-PsShutdownQuotedTarget -Value ('@' + $pathText) -AlwaysQuote $startedQuoted
        [void]$results.Add((
            New-PsShutdownCompletionResult `
                -CompletionText $completionText `
                -ListItemText ('@' + $item.Name) `
                -ResultType 'ParameterValue' `
                -ToolTip 'Path to a file containing remote computer names.'
        ))
    }

    if ([string]::IsNullOrWhiteSpace($inputPath) -or
        '@file'.StartsWith($trimmedValue, [System.StringComparison]::OrdinalIgnoreCase)) {
        [void]$results.Add((
            New-PsShutdownCompletionResult `
                -CompletionText '@file' `
                -ListItemText '@file' `
                -ResultType 'ParameterValue' `
                -ToolTip 'Path to a file containing remote computer names.'
        ))
    }

    if ($results.Count -eq 0 -and -not [string]::IsNullOrWhiteSpace($CurrentWord)) {
        [void]$results.Add((
            New-PsShutdownCompletionResult `
                -CompletionText $CurrentWord `
                -ListItemText $CurrentWord `
                -ResultType 'ParameterValue' `
                -ToolTip 'Remote target file in @file form.'
        ))
    }

    @($results.ToArray())
}

function Get-PsShutdownRemoteTargetCompletions {
    param([string]$CurrentWord)

    $typedValue = if ($null -eq $CurrentWord) { '' } else { Remove-PsShutdownOuterQuotes -Value $CurrentWord }
    if ($typedValue.StartsWith('@') -or ($CurrentWord -and $CurrentWord.StartsWith('"@'))) {
        return @(Get-PsShutdownAtFileCompletions -CurrentWord $CurrentWord)
    }

    $results = New-Object System.Collections.Generic.List[object]

    # PowerShell splits a native argument at ',' before the completer runs, so a
    # '\\a,b' list is never seen as one word; only the single-target forms are modelled.
    $candidates = @(
        [pscustomobject]@{ CompletionText = '\\computer'; ToolTip = 'Remote computer target in \\computer form.' }
        [pscustomobject]@{ CompletionText = '\\*'; ToolTip = 'Broadcast shutdown target placeholder.' }
        [pscustomobject]@{ CompletionText = '@file'; ToolTip = 'Path to a file containing remote computer names.' }
    )

    foreach ($candidate in $candidates) {
        if (-not [string]::IsNullOrWhiteSpace($typedValue) -and
            -not $candidate.CompletionText.StartsWith($typedValue, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }

        [void]$results.Add((
            New-PsShutdownCompletionResult `
                -CompletionText $candidate.CompletionText `
                -ListItemText $candidate.CompletionText `
                -ResultType 'ParameterValue' `
                -ToolTip $candidate.ToolTip
        ))
    }

    if ($results.Count -eq 0 -and -not [string]::IsNullOrWhiteSpace($CurrentWord)) {
        [void]$results.Add((
            New-PsShutdownCompletionResult `
                -CompletionText $CurrentWord `
                -ListItemText $CurrentWord `
                -ResultType 'ParameterValue' `
                -ToolTip 'Remote target in \\computer[,computer[,...]] or @file form.'
        ))
    }

    @($results.ToArray())
}

function Get-PsShutdownOptionCompletions {
    param(
        [string]$CurrentWord,
        [psobject]$State
    )

    $prefix = if ($null -eq $CurrentWord) { '' } else { $CurrentWord }
    $results = New-Object System.Collections.Generic.List[object]

    if (-not $State.SelectedAction) {
        foreach ($action in $script:PsShutdownCompletionCatalog.Actions) {
            if ($State.UsedTokens.ContainsKey($action.Token.ToLowerInvariant())) {
                continue
            }

            if (-not [string]::IsNullOrWhiteSpace($prefix) -and
                -not $action.Token.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
                continue
            }

            [void]$results.Add((
                New-PsShutdownCompletionResult `
                    -CompletionText $action.Token `
                    -ListItemText $action.Token `
                    -ResultType 'ParameterName' `
                    -ToolTip $action.Description
            ))
        }
    }

    foreach ($switchSpec in $script:PsShutdownCompletionCatalog.Switches) {
        $lowerToken = $switchSpec.Token.ToLowerInvariant()
        if ($State.UsedTokens.ContainsKey($lowerToken)) {
            continue
        }

        if (-not [string]::IsNullOrWhiteSpace($prefix) -and
            -not $switchSpec.Token.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }

        [void]$results.Add((
            New-PsShutdownCompletionResult `
                -CompletionText $switchSpec.Token `
                -ListItemText $switchSpec.Token `
                -ResultType 'ParameterName' `
                -ToolTip $switchSpec.Description
        ))
    }

    @($results.ToArray())
}

function Get-PsShutdownValueCompletions {
    param(
        [string]$OptionToken,
        [string]$CurrentWord
    )

    switch ($OptionToken.ToLowerInvariant()) {
        '-t' {
            return @(Get-PsShutdownSampleValueResults -CurrentValue $CurrentWord -Placeholder '<seconds-or-h:mm>' -PlaceholderToolTip 'Countdown seconds or shutdown time in h:m format.' -Samples @(
                    [pscustomobject]@{ CompletionText = '20'; ListItemText = '20'; ToolTip = 'Default countdown in seconds.' }
                    [pscustomobject]@{ CompletionText = '30'; ListItemText = '30'; ToolTip = 'Thirty-second countdown.' }
                    [pscustomobject]@{ CompletionText = '60'; ListItemText = '60'; ToolTip = 'One-minute countdown.' }
                    [pscustomobject]@{ CompletionText = '300'; ListItemText = '300'; ToolTip = 'Five-minute countdown.' }
                    [pscustomobject]@{ CompletionText = '1:00'; ListItemText = '1:00'; ToolTip = 'Shutdown time in 24-hour notation.' }
                    [pscustomobject]@{ CompletionText = '23:00'; ListItemText = '23:00'; ToolTip = 'Shutdown time in 24-hour notation.' }
                ))
        }
        '-v' {
            return @(Get-PsShutdownSampleValueResults -CurrentValue $CurrentWord -Placeholder '<seconds>' -PlaceholderToolTip 'Seconds to display the shutdown dialog.' -Samples @(
                    [pscustomobject]@{ CompletionText = '0'; ListItemText = '0'; ToolTip = 'Skip the shutdown notification dialog.' }
                    [pscustomobject]@{ CompletionText = '5'; ListItemText = '5'; ToolTip = 'Display the shutdown dialog for 5 seconds.' }
                    [pscustomobject]@{ CompletionText = '10'; ListItemText = '10'; ToolTip = 'Display the shutdown dialog for 10 seconds.' }
                    [pscustomobject]@{ CompletionText = '30'; ListItemText = '30'; ToolTip = 'Display the shutdown dialog for 30 seconds.' }
                ))
        }
        '-e' {
            return @(Get-PsShutdownSampleValueResults -CurrentValue $CurrentWord -Placeholder '[u|p]:xx:yy' -PlaceholderToolTip 'Shutdown reason code in planned/unplanned major:minor form.' -Samples @(
                    [pscustomobject]@{ CompletionText = 'u:0:0'; ListItemText = 'u:0:0'; ToolTip = 'Other (Unplanned).' }
                    [pscustomobject]@{ CompletionText = 'p:0:0'; ListItemText = 'p:0:0'; ToolTip = 'Other (Planned).' }
                    [pscustomobject]@{ CompletionText = 'u:2:18'; ListItemText = 'u:2:18'; ToolTip = 'Operating System: Security fix (Unplanned).' }
                    [pscustomobject]@{ CompletionText = 'p:2:17'; ListItemText = 'p:2:17'; ToolTip = 'Operating System: Hot fix (Planned).' }
                    [pscustomobject]@{ CompletionText = 'p:4:2'; ListItemText = 'p:4:2'; ToolTip = 'Application: Installation (Planned).' }
                ))
        }
        '-m' {
            return @(New-PsShutdownLiteralValueResults -CurrentValue $CurrentWord -Placeholder '"<message>"' -ToolTip 'Message text shown to logged on users.')
        }
        '-u' {
            return @(Get-PsShutdownSampleValueResults -CurrentValue $CurrentWord -Placeholder '<username>' -PlaceholderToolTip 'Remote credentials user name.' -Samples @(
                    [pscustomobject]@{ CompletionText = '<domain\user>'; ListItemText = '<domain\user>'; ToolTip = 'Domain-qualified user name.' }
                    [pscustomobject]@{ CompletionText = '.\Administrator'; ListItemText = '.\Administrator'; ToolTip = 'Local Administrator account example.' }
                    [pscustomobject]@{ CompletionText = 'Administrator'; ListItemText = 'Administrator'; ToolTip = 'Simple user name example.' }
                ))
        }
        '-p' {
            return @(New-PsShutdownLiteralValueResults -CurrentValue $CurrentWord -Placeholder '<password>' -ToolTip 'Remote credentials password.')
        }
        '-n' {
            return @(Get-PsShutdownSampleValueResults -CurrentValue $CurrentWord -Placeholder '<seconds>' -PlaceholderToolTip 'Connection timeout in seconds for remote computers.' -Samples @(
                    [pscustomobject]@{ CompletionText = '5'; ListItemText = '5'; ToolTip = 'Five-second connection timeout.' }
                    [pscustomobject]@{ CompletionText = '10'; ListItemText = '10'; ToolTip = 'Ten-second connection timeout.' }
                    [pscustomobject]@{ CompletionText = '30'; ListItemText = '30'; ToolTip = 'Thirty-second connection timeout.' }
                    [pscustomobject]@{ CompletionText = '60'; ListItemText = '60'; ToolTip = 'One-minute connection timeout.' }
                ))
        }
    }

    @()
}

function Complete-PsShutdown {
    param(
        [string]$wordToComplete,
        [System.Management.Automation.Language.CommandAst]$commandAst,
        [int]$cursorPosition
    )

    Initialize-PsShutdownCompletionCatalog

    $line = $commandAst.ToString()
    $currentWord = if ($null -eq $wordToComplete) {
        Get-PsShutdownCurrentToken -Line $line -CursorPosition ($cursorPosition - $commandAst.Extent.StartOffset) -Fallback ''
    } elseif ($wordToComplete.Length -eq 0) {
        ''
    } elseif ([string]::IsNullOrWhiteSpace($wordToComplete)) {
        Get-PsShutdownCurrentToken -Line $line -CursorPosition ($cursorPosition - $commandAst.Extent.StartOffset) -Fallback $wordToComplete
    } else {
        $wordToComplete
    }

    # Only elements that end before the cursor are consumed, so completing inside
    # or at the end of an earlier token sees the same context as typing it fresh.
    $tokensBeforeCurrent = @(
        $commandAst.CommandElements |
            Select-Object -Skip 1 |
            Where-Object { $_.Extent.EndOffset -lt $cursorPosition } |
            ForEach-Object { $_.Extent.Text }
    )

    $state = Get-PsShutdownCommandState -TokensBeforeCurrent $tokensBeforeCurrent

    if ($state.ValueContext) {
        return @(Get-PsShutdownValueCompletions -OptionToken $state.ValueContext -CurrentWord $currentWord)
    }

    if (-not [string]::IsNullOrWhiteSpace($currentWord)) {
        if ($currentWord.StartsWith('-')) {
            return @(Get-PsShutdownOptionCompletions -CurrentWord $currentWord -State $state)
        }

        if ($currentWord.StartsWith('\') -or $currentWord.StartsWith('@') -or $currentWord.StartsWith('"@')) {
            return @(Get-PsShutdownRemoteTargetCompletions -CurrentWord $currentWord)
        }

        if ($state.HasRemoteTarget) {
            return @()
        }

        return @(Get-PsShutdownRemoteTargetCompletions -CurrentWord $currentWord)
    }

    $results = New-Object System.Collections.Generic.List[object]
    foreach ($result in @(Get-PsShutdownOptionCompletions -CurrentWord $currentWord -State $state)) {
        [void]$results.Add($result)
    }

    # Switches stay valid after the target; only the target candidates must not repeat.
    if (-not $state.HasRemoteTarget) {
        foreach ($result in @(Get-PsShutdownRemoteTargetCompletions -CurrentWord $currentWord)) {
            [void]$results.Add($result)
        }
    }

    @($results.ToArray())
}

Register-ArgumentCompleter -Native -CommandName 'psshutdown', 'psshutdown.exe' -ScriptBlock {
    param($wordToComplete, $commandAst, $cursorPosition)
    Complete-PsShutdown -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition
}