ProfileFever.psm1



# Use namespaces for PSReadLine extension
using namespace System.Management.Automation
using namespace System.Management.Automation.Language


<#
    .SYNOPSIS
        Install all dependencies for the profile.
 
    .DESCRIPTION
        Insted of adding theses module dependencies into the module manifest,
        they are separated in this command. This is by design, to speed up the
        module load duration of ProfileFever. The module load time is essential
        for a fast profie script.
#>

function Install-Profile
{
    [CmdletBinding()]
    param ()


    ##
    ## MODULE DEPENDENCY
    ##

    $moduleNames = 'SecurityFever', 'Pester', 'posh-git', 'psake'

    if ($PSVersionTable.PSEdition -ne 'Core')
    {
        Install-PackageProvider -Name 'NuGet' -Scope 'CurrentUser' -MinimumVersion '2.8.5.201' -Force -ForceBootstrap -Verbose | Out-Null
    }

    # Only for Pester, update the built-in module with version 3.4.0
    if ((Get-Module -Name 'Pester' -ListAvailable | Sort-Object -Property 'Version' -Descending | Select-Object -First 1).Version -eq '3.4.0')
    {
        Install-Module -Name 'Pester' -Repository 'PSGallery' -Scope 'CurrentUser' -Force -AllowClobber -SkipPublisherCheck -Verbose
    }

    foreach ($moduleName in $moduleNames)
    {
        if ($null -eq (Get-Module -Name $moduleName -ListAvailable))
        {
            Install-Module -Name $moduleName -Repository 'PSGallery' -Scope 'CurrentUser' -Force -AllowClobber -SkipPublisherCheck -Verbose
        }
        else
        {
            Update-Module -Name $moduleName -Force -Verbose
        }
    }


    ##
    ## PROFILE SCRIPT
    ##

    $profilePaths = @()
    if ([System.Environment]::OSVersion.Platform -eq 'Win32NT')
    {
        $profilePaths += "$HOME\Documents\PowerShell"
        $profilePaths += "$HOME\Documents\WindowsPowerShell"
    }
    if ([System.Environment]::OSVersion.Platform -eq 'Unix')
    {
        $profilePaths += "$HOME/.config/powershell"
    }

    foreach ($profilePath in $profilePaths)
    {
        if (-not (Test-Path -Path $profilePath))
        {
            New-Item -Path $profilePath -ItemType 'Directory' -Force | Out-Null
        }

        if (-not (Test-Path -Path "$profilePath\profile.ps1"))
        {
            Set-Content -Path "$profilePath\profile.ps1" -Value 'Start-Profile'
        }
    }
}

<#
    .SYNOPSIS
        Show the headline with information about the local system and current
        user.
 
    .DESCRIPTION
        Show the current PowerShell version, Operationg System details an the
        user session as profile headline.
#>

function Show-HostHeadline
{
    # Only show the headline, if PowerShell was started with -NoLogo switch. The
    # test is more a workaround as checking the start parameter. Not found an
    # efficient way to test that without WMI.
    if ($Host.UI.RawUI.CursorPosition.Y -eq 0)
    {
        # Get the PowerShell version depending on the edition
        if ($PSVersionTable.PSEdition -eq 'Core')
        {
            $psInfo = 'PowerShell {0}' -f $PSVersionTable.PSVersion
        }
        else
        {
            $psInfo = 'Windows PowerShell {0}.{1}' -f $PSVersionTable.PSVersion.Major, $PSVersionTable.PSVersion.Minor
        }

        # Get the operating system information, based on the operating system
        $osInfo = ''
        if ([System.Environment]::OSVersion.Platform -eq 'Win32NT')
        {
            # Get Windows version from registry. Update the object for non Windows 10 or
            # Windows Server 2016 systems to match the same keys.
            $osVersion = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
            if ($null -eq $osVersion.ReleaseId)
            {
                $osVersion | Add-Member -MemberType NoteProperty -Name 'ReleaseId' -Value $osVersion.CurrentVersion
            }
            $osInfo = '{0}, Version {1}' -f $osVersion.ProductName, $osVersion.ReleaseId
        }
        if ([System.Environment]::OSVersion.Platform -eq 'Unix')
        {
            $osInfo = uname -a
        }

        # Get the info about the current logged on user, system and uptime
        $usrInfo = ''
        if ([System.Environment]::OSVersion.Platform -eq 'Win32NT')
        {
            $usrInfo = '{0}\{1} on {2}, Uptime {3:%d} day(s) {3:hh\:mm\:ss}' -f $Env:USERDOMAIN, $Env:USERNAME, $Env:COMPUTERNAME.ToUpper(), [System.TimeSpan]::FromMilliseconds([System.Environment]::TickCount)
        }
        if ([System.Environment]::OSVersion.Platform -eq 'Unix')
        {

        }

        # Show headline
        $Host.UI.WriteLine($psInfo)
        $Host.UI.WriteLine($osInfo)
        $Host.UI.WriteLine()
        $Host.UI.WriteLine($usrInfo)
        $Host.UI.WriteLine()
    }
}

<#
    .SYNOPSIS
        Initialize the PowerShell console profile.
 
    .DESCRIPTION
        This is the personal profile of Claudio Spizzi holding all commands to
        initialize the profile in the console. It's intended to be used on any
        PowerShell version and plattform.
#>

function Start-Profile
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalAliases', '')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    param
    (
        # Optionally specify a path to the config file.
        [Parameter(Mandatory = $false)]
        [System.String]
        $ConfigPath
    )


    ##
    ## PROFILE CONFIG
    ##

    # Guess the real config path for the all hosts current user
    if (-not $PSBoundParameters.ContainsKey('ConfigPath'))
    {
        $ConfigPath = $PROFILE.CurrentUserAllHosts -replace '\.ps1', '.json'
    }

    $config = Update-ProfileConfig -Path $ConfigPath -PassThru


    ##
    ## HEADLINE
    ##

    if ($config.Headline)
    {
        Show-HostHeadline
    }


    ##
    ## LOCATION
    ##

    if (Test-Path -Path $config.Location)
    {
        Set-Location -Path $config.Location
    }


    ##
    ## WORKSPACE
    ##

    if (Test-Path -Path $config.Workspace)
    {
        New-PSDrive -PSProvider 'FileSystem' -Scope 'Global' -Name 'Workspace' -Root $config.Workspace | Out-Null

        # Aliases to jump into the workspace named Workspace: and WS:
        Set-Item -Path 'Function:Global:Workspace:' -Value 'Set-Location -Path "Workspace:"'
        Set-Item -Path 'Function:Global:WS:' -Value 'Set-Location -Path "Workspace:"'

        # Specify the path to the workspace as environment variable
        [System.Environment]::SetEnvironmentVariable('Workspace', $Workspace, [System.EnvironmentVariableTarget]::Process)
    }


    ##
    ## PROMPT
    ##

    if ($config.Prompt)
    {
        Enable-Prompt
    }

    if ($config.PromptAlias)
    {
        Enable-PromptAlias
    }

    if ($config.PromptGit)
    {
        Enable-PromptGit
    }

    if ($config.PromptTimeSpan)
    {
        Enable-PromptTimeSpan
    }


    ##
    ## COMMAND NOT FOUND
    ##

    if ($config.CommandNotFound)
    {
        Enable-CommandNotFound
    }


    ##
    ## ALIASES
    ##

    $aliasKeys = $config.Aliases | Get-Member -MemberType 'NoteProperty' | Select-Object -ExpandProperty 'Name'
    foreach ($aliasKey in $aliasKeys)
    {
        New-Alias -Scope 'Global' -Name $aliasKey -Value $config.Aliases.$aliasKey
    }


    ##
    ## FUNCTIONS
    ##

    $functionKeys = $config.Functions | Get-Member -MemberType 'NoteProperty' | Select-Object -ExpandProperty 'Name'
    foreach ($functionKey in $functionKeys)
    {
        Set-Item -Path "Function:Global:$functionKey" -Value $config.Functions.$functionKey
    }


    ##
    ## SCRIPTS
    ##

    foreach ($script in $config.Scripts)
    {
        . $script
    }


    ##
    ## BINARIES
    ##

    foreach ($binary in $config.Binaries)
    {
        $Env:Path += ';' + $binary
    }


    ##
    ## PSREADLINE
    ##

    # History browser, history search and history save
    if ($config.ReadLineHistoryHelper)
    {
        Enable-PSReadLineHistoryHelper
    }

    # Enable smart insert/delete for ', ", [, ), {
    if ($config.ReadLineSmartInsertDelete)
    {
        Enable-PSReadLineSmartInsertDelete
    }

    # Enable F1 to show help
    if ($config.ReadLineCommandHelp)
    {
        Enable-PSReadLineCommandHelp
    }

    # Jump around in the file system
    if ($config.ReadLineLocationMark)
    {
        Enable-PSReadLineLocationMark
    }

    if ($config.ReadLinePSakeBuild)
    {
        # This will invoke the PSake build in the current directory
        Set-PSReadLineKeyHandler -Key 'Ctrl+B', 'Ctrl+b' -BriefDescription 'BuildCurrentDirectory' -LongDescription "Build the current directory" -ScriptBlock {
            [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()
            [Microsoft.PowerShell.PSConsoleReadLine]::Insert('Invoke-psake -buildFile ".\build.psake.ps1"')
            [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
        }
    }

    if ($config.ReadLinePesterTest)
    {
        # This will invoke all Pester tests in the current directory
        Set-PSReadLineKeyHandler -Key 'Ctrl+T', 'Ctrl+t' -BriefDescription 'TestCurrentDirectory' -LongDescription "Test the current directory" -ScriptBlock {
            [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()
            [Microsoft.PowerShell.PSConsoleReadLine]::Insert('Invoke-Pester')
            [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
        }
    }


    ##
    ## STRICT MODE
    ##

    if ($config.StrictMode)
    {
        Set-StrictMode -Version 'latest'
    }
}

<#
    .SYNOPSIS
        Create and update the profile configuration.
 
    .DESCRIPTION
        The profile configuration will be created if it does not exist. Every
        property will be initialized with a default value.
#>

function Update-ProfileConfig
{
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    param
    (
        # Path to the config file.
        [Parameter(Mandatory = $true)]
        [System.String]
        $Path,

        # Return the updated object
        [Parameter(Mandatory = $false)]
        [switch]
        $PassThru
    )

    # Load the config file or specify an empty config object
    if (Test-Path -Path $Path)
    {
        $config = Get-Content -Path $Path -Encoding 'UTF8' | ConvertFrom-Json
    }
    else
    {
        $config = [PSCustomObject] @{}
    }

    # Initialize the configuration if not specified with default values
    if ($null -eq $config.Location)
    {
        $config | Add-Member -MemberType 'NoteProperty' -Name 'Location' -Value $(if ($IsLinux -or $IsMacOs) { '~' } else { "$Home\Desktop" })
    }
    if ($null -eq $config.Workspace)
    {
        $config | Add-Member -MemberType 'NoteProperty' -Name 'Workspace' -Value $(if ($IsLinux -or $IsMacOs) { '~/workspace' } else { "$Home\Workspace" })
    }
    if ($null -eq $config.Prompt)
    {
        $config | Add-Member -MemberType 'NoteProperty' -Name 'Prompt' -Value $true
    }
    if ($null -eq $config.PromptAlias)
    {
        $config | Add-Member -MemberType 'NoteProperty' -Name 'PromptAlias' -Value $true
    }
    if ($null -eq $config.PromptGit)
    {
        $config | Add-Member -MemberType 'NoteProperty' -Name 'PromptGit' -Value $false   # Git client is not installed by default
    }
    if ($null -eq $config.PromptTimeSpan)
    {
        $config | Add-Member -MemberType 'NoteProperty' -Name 'PromptTimeSpan' -Value $true
    }
    if ($null -eq $config.ReadLineHistoryHelper)
    {
        $config | Add-Member -MemberType 'NoteProperty' -Name 'ReadLineHistoryHelper' -Value $true
    }
    if ($null -eq $config.ReadLineSmartInsertDelete)
    {
        $config | Add-Member -MemberType 'NoteProperty' -Name 'ReadLineSmartInsertDelete' -Value $true
    }
    if ($null -eq $config.ReadLineCommandHelp)
    {
        $config | Add-Member -MemberType 'NoteProperty' -Name 'ReadLineCommandHelp' -Value $true
    }
    if ($null -eq $config.ReadLineLocationMark)
    {
        $config | Add-Member -MemberType 'NoteProperty' -Name 'ReadLineLocationMark' -Value $true
    }
    if ($null -eq $config.ReadLinePSakeBuild)
    {
        $config | Add-Member -MemberType 'NoteProperty' -Name 'ReadLinePSakeBuild' -Value $true
    }
    if ($null -eq $config.ReadLinePesterTest)
    {
        $config | Add-Member -MemberType 'NoteProperty' -Name 'ReadLinePesterTest' -Value $true
    }
    if ($null -eq $config.StrictMode)
    {
        $config | Add-Member -MemberType 'NoteProperty' -Name 'StrictMode' -Value $false
    }
    if ($null -eq $config.CommandNotFound)
    {
        $config | Add-Member -MemberType 'NoteProperty' -Name 'CommandNotFound' -Value $false
    }
    if ($null -eq $config.Headline)
    {
        $config | Add-Member -MemberType 'NoteProperty' -Name 'Headline' -Value $true
    }
    if ($null -eq $config.Aliases)
    {
        $config | Add-Member -MemberType 'NoteProperty' -Name 'Aliases' -Value @{
            # Baseline
            'grep' = 'Select-String'
            # SecurityFever
            'cred' = 'Use-VaultCredential'
        }
    }
    if ($null -eq $config.Functions)
    {
        $config | Add-Member -MemberType 'NoteProperty' -Name 'Functions' -Value @{
            # Internet Search
            'google'        = 'Start-Process "https://www.google.com/search?q=$args"'
            'dict'          = 'Start-Process "https://www.dict.cc/?s=$args"'
            'wiki'          = 'Start-Process "https://en.wikipedia.org/wiki/Special:Search/$args"'
            'stackoverflow' = 'Start-Process "https://stackoverflow.com/search?q=$args"'
            # PSake Build Module
            'psake'         = 'Invoke-psake -buildFile ".\build.psake.ps1"'
            'psakedeploy'   = 'Invoke-psake -buildFile ".\build.psake.ps1" -taskList "Deploy"'
        }
    }
    if ($null -eq $config.Scripts)
    {
        $config | Add-Member -MemberType 'NoteProperty' -Name 'Scripts' -Value @()
    }
    if ($null -eq $config.Binaries)
    {
        $config | Add-Member -MemberType 'NoteProperty' -Name 'Binaries' -Value @()
    }

    # Finally, store the config file on the disk
    $config | ConvertTo-Json | Set-Content -Path $Path -Encoding 'UTF8'

    if ($PassThru.IsPresent)
    {
        Write-Output $config
    }
}

<#
    .SYNOPSIS
        Add a command not found action to the list of actions.
#>

function Add-CommandNotFoundAction
{
    [CmdletBinding()]
    param
    (
        # Name of the command.
        [Parameter(Mandatory = $true)]
        [System.String]
        $CommandName,

        # For the remoting command, set the computer name of the target system.
        [Parameter(Mandatory = $true, ParameterSetName = 'RemotingWithCredential')]
        [Parameter(Mandatory = $true, ParameterSetName = 'RemotingWithVault')]
        [System.String]
        $ComputerName,

        # For the remoting command, set the credentials.
        [Parameter(Mandatory = $false, ParameterSetName = 'RemotingWithCredential')]
        [System.Management.Automation.PSCredential]
        $Credential,

        # For the remoting command, but only a pointer to the credential vault.
        [Parameter(Mandatory = $true, ParameterSetName = 'RemotingWithVault')]
        [System.String]
        $VaultTargetName,

        # Define a script block to execute for the command.
        [Parameter(Mandatory = $true, ParameterSetName = 'ScriptBlock')]
        [System.Management.Automation.ScriptBlock]
        $ScriptBlock
    )

    $command = [PSCustomObject] @{
        PSTypeName      = 'ProfileFever.CommandNotFoundAction'
        CommandName     = $CommandName
        CommandType     = $null
        ComputerName    = $null
        Credential      = $null
        CredentialVault = $null
        ScriptBlock     = $null
    }

    switch ($PSCmdlet.ParameterSetName)
    {
        'RemotingWithCredential'
        {
            $command.CommandType  = 'Remoting'
            $command.ComputerName = $ComputerName
            $command.Credential   = $Credential
        }

        'RemotingWithVault'
        {
            $command.CommandType     = 'Remoting'
            $command.ComputerName    = $ComputerName
            $command.CredentialVault = $VaultTargetName
        }

        'ScriptBlock'
        {
            $command.CommandType = 'ScriptBlock'
            $command.ScriptBlock = $ScriptBlock
        }
    }

    $Script:CommandNotFoundAction[$CommandName] = $command
}

<#
    .SYNOPSIS
        Disable the command not found actions.
#>

function Disable-CommandNotFound
{
    [CmdletBinding()]
    param ()

    $Script:CommandNotFoundEnabled = $false
}

<#
    .SYNOPSIS
        Enable the command not found actions.
#>

function Enable-CommandNotFound
{
    [CmdletBinding()]
    param ()

    Register-CommandNotFound

    $Script:CommandNotFoundEnabled = $true
}

<#
    .SYNOPSIS
        Get the registered command not found actions.
#>

function Get-CommandNotFoundAction
{
    [CmdletBinding()]
    param ()

    $Script:CommandNotFoundAction.Values
}

<#
    .SYNOPSIS
        Register the command not found action callback.
#>

function Register-CommandNotFound
{
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalFunctions', '')]
    param ()

    $Global:ExecutionContext.InvokeCommand.CommandNotFoundAction = {
        param ($CommandName, $CommandLookupEventArgs)

        if ($Script:CommandNotFoundEnabled -and $Script:CommandNotFoundAction.ContainsKey($CommandName))
        {
            $command     = $Script:CommandNotFoundAction[$CommandName]
            $commandLine = (Get-PSCallStack)[1].Position.Text.Trim()

            switch ($command.CommandType)
            {
                'Remoting'
                {
                    $credentialSplat = @{}
                    if ($command.Credential)
                    {
                        $credentialSplat['Credential'] = $command.Credential
                        $credentialVerbose = " -Credential '{0}'" -f $command.Credential.UserName
                    }
                    if ($command.CredentialVault)
                    {
                        $credential = Use-VaultCredential -TargetName $command.CredentialVault
                        $credentialSplat['Credential'] = $credential
                        $credentialVerbose = " -Credential '{0}'" -f $credential.UserName
                    }

                    # Option 1: Enter Session
                    # If no parameters were specified, just enter into a
                    # remote session to the target system.
                    if ($CommandName -eq $commandLine)
                    {
                        Write-Verbose ("Enter-PSSession -ComputerName '{0}'{1}" -f $command.ComputerName, $credentialVerbose)

                        $CommandLookupEventArgs.StopSearch = $true
                        $CommandLookupEventArgs.CommandScriptBlock = {
                            $session = New-PSSession -ComputerName $command.ComputerName @credentialSplat -ErrorAction Stop
                            if ($Host.Name -eq 'ConsoleHost')
                            {
                                Invoke-Command -Session $session -ErrorAction Stop -ScriptBlock {
                                    Set-Location -Path "$Env:SystemDrive\"
                                    $PromptLabel = $Env:ComputerName.ToUpper()
                                    $PromptIndent = $using:session.ComputerName.Length + 4
                                    function Global:prompt
                                    {
                                        Write-Host "[$PromptLabel]" -NoNewline -ForegroundColor Cyan; "$("`b `b" * $PromptIndent) $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "
                                    }
                                    function profile
                                    {
                                        Install-Module -Name 'profile'
                                        Install-Profile
                                        Start-Profile
                                    }
                                }
                            }
                            Enter-PSSession -Session $session -ErrorAction Stop
                        }.GetNewClosure()
                    }

                    # Option 2: Open Session
                    # If a variable is specified as output of the command,
                    # a new remoting session will be opened and returned.
                    $openSessionRegex = '^\$\S+ = {0}$' -f ([System.Text.RegularExpressions.Regex]::Escape($CommandName))
                    if ($commandLine -match $openSessionRegex)
                    {
                        Write-Verbose ("New-PSSession -ComputerName '{0}'{1}" -f $command.ComputerName, $credentialVerbose)

                        $CommandLookupEventArgs.StopSearch = $true
                        $CommandLookupEventArgs.CommandScriptBlock = {
                            New-PSSession -ComputerName $command.ComputerName @credentialSplat -ErrorAction Stop
                        }.GetNewClosure()
                    }

                    # Option 3: Invoke Command
                    # If a script is appended to the command, execute that
                    # script on the remote system.
                    if ($commandline.StartsWith($CommandName) -and $commandLine.Length -gt $CommandName.Length)
                    {
                        $scriptBlock = [System.Management.Automation.ScriptBlock]::Create($commandLine.Substring($CommandName.Length).Trim())

                        Write-Verbose ("Invoke-Command -ComputerName '{0}'{1} -ScriptBlock {{ {2} }}" -f $command.ComputerName, $credentialVerbose, $scriptBlock.ToString())

                        $CommandLookupEventArgs.StopSearch = $true
                        $CommandLookupEventArgs.CommandScriptBlock = {
                            Invoke-Command -ComputerName $command.ComputerName @credentialSplat -ScriptBlock $scriptBlock -ErrorAction Stop
                        }.GetNewClosure()
                    }
                }

                'ScriptBlock'
                {
                    Write-Verbose ("& {{ {0} }}" -f $command.ScriptBlock)

                    $CommandLookupEventArgs.StopSearch = $true
                    $CommandLookupEventArgs.CommandScriptBlock = $command.ScriptBlock
                }
            }
        }
    }
}

<#
    .SYNOPSIS
        Unregister the command not found action callback.
#>

function Unregister-CommandNotFound
{
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalFunctions', '')]
    param ()

    $Global:ExecutionContext.InvokeCommand.CommandNotFoundAction = $null
}

<#
    .SYNOPSIS
        Disable the custom prompt and restore the default prompt.
#>

function Disable-Prompt
{
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalFunctions', '')]
    param ()

    Set-Item -Path 'Function:Global:prompt' -Value $Script:PromptDefault
}

<#
    .SYNOPSIS
        Disable the prompt alias recommendation output after each command.
#>

function Disable-PromptAlias
{
    [CmdletBinding()]
    [Alias('dalias')]
    param ()

    Remove-Variable -Scope Script -Name PromptAlias -ErrorAction SilentlyContinue -Force
    New-Variable -Scope Script -Option ReadOnly -Name PromptAlias -Value $false -Force
}

<#
    .SYNOPSIS
        Disable the git repository status in the prompt.
#>

function Disable-PromptGit
{
    [CmdletBinding()]
    [Alias('dgit')]
    param ()

    Remove-Variable -Scope Script -Name PromptGit -ErrorAction SilentlyContinue -Force
    New-Variable -Scope Script -Option ReadOnly -Name PromptGit -Value $false -Force
}

<#
    .SYNOPSIS
        Disable the prompt timestamp output.
#>

function Disable-PromptTimeSpan
{
    [CmdletBinding()]
    [Alias('dtimespan')]
    param ()

    Remove-Variable -Scope Script -Name PromptTimeSpan -ErrorAction SilentlyContinue -Force
    New-Variable -Scope Script -Option ReadOnly -Name PromptTimeSpan -Value $false -Force
}

<#
    .SYNOPSIS
        Enable the custom prompt by replacing the default prompt.
#>

function Enable-Prompt
{
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalFunctions', '')]
    param ()

    function Global:Prompt
    {
        if ($Script:PromptHistory -ne $MyInvocation.HistoryId)
        {
            $Script:PromptHistory = $MyInvocation.HistoryId

            if ($Script:PromptAlias) { Show-PromptAliasSuggestion }
            if ($Script:PromptTimeSpan) { Show-PromptLastCommandDuration }
        }

        $Host.UI.Write($Script:PromptColor, $Host.UI.RawUI.BackgroundColor, $Script:PromptInfo)
        $Host.UI.Write(" $($ExecutionContext.SessionState.Path.CurrentLocation)")
        if ($Script:PromptGit) { Write-VcsStatus }
        return "`n$($MyInvocation.HistoryId.ToString().PadLeft(3, '0'))$('>' * ($NestedPromptLevel + 1)) "
    }
}

<#
    .SYNOPSIS
        Enable the prompt alias recommendation output after each command.
#>

function Enable-PromptAlias
{
    [CmdletBinding()]
    [Alias('ealias')]
    param ()

    Remove-Variable -Scope Script -Name PromptAlias -ErrorAction SilentlyContinue -Force
    New-Variable -Scope Script -Option ReadOnly -Name PromptAlias -Value $true -Force
}

<#
    .SYNOPSIS
        Enable the git repository status in the prompt.
#>

function Enable-PromptGit
{
    [CmdletBinding()]
    [Alias('egit')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')]
    param ()

    if ($null -eq (Get-Module -Name posh-git))
    {
        Import-Module -Name posh-git -Force
        $Global:GitPromptSettings.EnableWindowTitle = '{0} ~ ' -f $Host.UI.RawUI.WindowTitle
    }

    Remove-Variable -Scope Script -Name PromptGit -ErrorAction SilentlyContinue -Force
    New-Variable -Scope Script -Option ReadOnly -Name PromptGit -Value $true -Force
}

<#
    .SYNOPSIS
        Enable the prompt timestamp output.
#>

function Enable-PromptTimeSpan
{
    [CmdletBinding()]
    [Alias('etimespan')]
    param ()

    Remove-Variable -Scope Script -Name PromptTimeSpan -ErrorAction SilentlyContinue -Force
    New-Variable -Scope Script -Option ReadOnly -Name PromptTimeSpan -Value $true -Force
}

<#
    .SYNOPSIS
        Show the alias suggestion for the latest command.
#>

function Show-PromptAliasSuggestion
{
    [CmdletBinding()]
    param ()

    if ($MyInvocation.HistoryId -gt 1)
    {
        $history = Get-History -Id ($MyInvocation.HistoryId - 1)
        $reports = @()
        foreach ($alias in (Get-Alias))
        {
            if ($history.CommandLine.IndexOf($alias.ResolvedCommandName) -ne -1)
            {
                $reports += $alias
            }
        }
        if ($reports.Count -gt 0)
        {
            $report = $reports | Group-Object -Property 'ResolvedCommandName' | ForEach-Object { ' ' + $_.Name + ' => ' + ($_.Group -join ', ') }
            $Host.UI.WriteLine('Magenta', $Host.UI.RawUI.BackgroundColor, "Alias suggestions:`n" + ($report -join "`n"))
        }
    }
}

<#
    .SYNOPSIS
        Show the during of the last executed command.
#>

function Show-PromptLastCommandDuration
{
    [CmdletBinding()]
    param ()

    if ($MyInvocation.HistoryId -gt 1 -and $Host.UI.RawUI.CursorPosition.Y -gt 0)
    {
        $history  = Get-History -Id ($MyInvocation.HistoryId - 1)
        $duration = "{0:0.000}s" -f ($history.EndExecutionTime - $history.StartExecutionTime).TotalSeconds

        # Move cursor one up and to the right to show the execution time
        $position = $Host.UI.RawUI.CursorPosition
        $position.Y = $position.Y - 1
        $position.X = $Host.UI.RawUI.WindowSize.Width - $duration.Length - 1
        $Host.UI.RawUI.CursorPosition = $position

        $Host.UI.WriteLine('Gray', $Host.UI.RawUI.BackgroundColor, $duration)
    }
}

<#
    .SYNOPSIS
        Enable command help.
 
    .DESCRIPTION
        Type F1 for help off the current command line.
 
    .LINK
        https://github.com/PowerShell/PSReadLine/blob/master/PSReadLine/SamplePSReadLineProfile.ps1
#>

function Enable-PSReadLineCommandHelp
{
    # Show a grid view output
    if ($PSVersionTable.PSEdition -ne 'Core')
    {
        $commandHelpSplat = @{
            Key              = 'F1'
            BriefDescription = 'CommandHelp'
            LongDescription  = 'Open the help window for the current command'
            ScriptBlock      = {

                param($key, $arg)

                $ast = $null
                $tokens = $null
                $errors = $null
                $cursor = $null
                [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$ast, [ref]$tokens, [ref]$errors, [ref]$cursor)

                $commandAst = $ast.FindAll( {
                    $node = $args[0]
                    $node -is [CommandAst] -and
                        $node.Extent.StartOffset -le $cursor -and
                        $node.Extent.EndOffset -ge $cursor
                    }, $true) | Select-Object -Last 1

                if ($null -ne $commandAst)
                {
                    $commandName = $commandAst.GetCommandName()
                    if ($null -ne $commandName)
                    {
                        $command = $ExecutionContext.InvokeCommand.GetCommand($commandName, 'All')
                        if ($command -is [AliasInfo])
                        {
                            $commandName = $command.ResolvedCommandName
                        }

                        if ($null -ne $commandName)
                        {
                            Get-Help $commandName -ShowWindow
                        }
                    }
                }
            }
        }
        Set-PSReadLineKeyHandler @commandHelpSplat
    }
}

<#
    .SYNOPSIS
        Enable the history browser, basic history search and history save.
 
    .DESCRIPTION
        On Windows PowerShell, use the F7 key to show a grid view with the last
        commands. A command can be selected and inserted to the current cmdline
        position.
        With the up and down arrows, search the history by the currently typed
        characters on the command line.
        Sometimes you enter a command but realize you forgot to do something
        else first. This binding will let you save that command in the history
        so you can recall it, but it doesn't actually execute. It also clears
        the line with RevertLine so the undo stack is reset - though redo will
        still reconstruct the command line.
 
    .LINK
        https://github.com/PowerShell/PSReadLine/blob/master/PSReadLine/SamplePSReadLineProfile.ps1
#>

function Enable-PSReadLineHistoryHelper
{
    [CmdletBinding()]
    param ()

    # Basic history searching
    Set-PSReadLineOption -HistorySearchCursorMovesToEnd
    Set-PSReadLineKeyHandler -Key 'UpArrow' -Function 'HistorySearchBackward'
    Set-PSReadLineKeyHandler -Key 'DownArrow' -Function 'HistorySearchForward'

    # Save current command line to history
    $saveInHistorySplat = @{
        Key              = 'Alt+w'
        BriefDescription = 'SaveInHistory'
        LongDescription  = 'Save current line in history but do not execute'
        ScriptBlock      = {

            param($key, $arg)

            $line = $null
            [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$null)
            [Microsoft.PowerShell.PSConsoleReadLine]::AddToHistory($line)
            [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()
        }
    }
    Set-PSReadLineKeyHandler @saveInHistorySplat

    # Show a grid view output
    if ($PSVersionTable.PSEdition -ne 'Core')
    {
        $historySplat = @{
            Key              = 'F7'
            BriefDescription = 'History'
            LongDescription  = 'Show command history'
            ScriptBlock      = {

                $pattern = $null
                [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$pattern, [ref]$null)
                if ($pattern)
                {
                    $pattern = [regex]::Escape($pattern)
                }

                $history = [System.Collections.ArrayList] @(
                    $last = ''
                    $lines = ''
                    foreach ($line in [System.IO.File]::ReadLines((Get-PSReadLineOption).HistorySavePath))
                    {
                        if ($line.EndsWith('`'))
                        {
                            $line = $line.Substring(0, $line.Length - 1)
                            $lines = if ($lines)
                            {
                                "$lines`n$line"
                            }
                            else
                            {
                                $line
                            }
                            continue
                        }

                        if ($lines)
                        {
                            $line = "$lines`n$line"
                            $lines = ''
                        }

                        if (($line -cne $last) -and (!$pattern -or ($line -match $pattern)))
                        {
                            $last = $line
                            $line
                        }
                    }
                )
                $history.Reverse()

                $command = $history | Out-GridView -Title 'History' -PassThru
                if ($command)
                {
                    [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()
                    [Microsoft.PowerShell.PSConsoleReadLine]::Insert(($command -join "`n"))
                }
            }
        }
        Set-PSReadLineKeyHandler @historySplat
    }
}

<#
    .SYNOPSIS
        Use this helper function to easy jump around in the shell.
 
    .DESCRIPTION
        Use Ctrl+Shift+J with a marker key to save the current directory in the
        marker list. Afterwards, with Ctrl+J, jump to the saved directory. To
        show all saved markers, use Alt+J.
 
    .LINK
        https://github.com/PowerShell/PSReadLine/blob/master/PSReadLine/SamplePSReadLineProfile.ps1
#>

function Enable-PSReadLineLocationMark
{
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')]
    param ()

    $Global:PSReadLineMarks = @{}

    $markDirectorySplat = @{
        Key              = 'Ctrl+Shift+j'
        BriefDescription = 'MarkDirectory'
        LongDescription  = 'Mark the current directory'
        ScriptBlock      = {
            param($key, $arg)

            $key = [Console]::ReadKey($true)
            $Global:PSReadLineMarks[$key.KeyChar] = $pwd
        }
    }
    Set-PSReadLineKeyHandler @markDirectorySplat

    $jumpDirectorySplat = @{
        Key              = 'Ctrl+j'
        BriefDescription = 'JumpDirectory'
        LongDescription  = 'Goto the marked directory'
        ScriptBlock      = {
            param($key, $arg)

            $key = [Console]::ReadKey()
            $dir = $Global:PSReadLineMarks[$key.KeyChar]
            if ($dir)
            {
                Set-Location $dir
                [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt()
            }
        }
    }
    Set-PSReadLineKeyHandler @jumpDirectorySplat

    $showDirectoryMarks = @{
        Key              = 'Alt+j'
        BriefDescription = 'ShowDirectoryMarks'
        LongDescription  = 'Show the currently marked directories'
        ScriptBlock      = {
            param($key, $arg)

            $Global:PSReadLineMarks.GetEnumerator() | ForEach-Object {
                [PSCustomObject]@{Key = $_.Key; Dir = $_.Value}
            } | Format-Table -AutoSize | Out-Host

            [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt()
        }
    }
    Set-PSReadLineKeyHandler @showDirectoryMarks
}

<#
    .SYNOPSIS
        Enable the smart insert/delete.
 
    .DESCRIPTION
        The next four key handlers are designed to make entering matched quotes
        parens, and braces a nicer experience. I'd like to include functions in
        the module that do this, but this implementation still isn't as smart as
        ReSharper, so I'm just providing it as a sample.
 
    .LINK
        https://github.com/PowerShell/PSReadLine/blob/master/PSReadLine/SamplePSReadLineProfile.ps1
#>

function Enable-PSReadLineSmartInsertDelete
{
    [CmdletBinding()]
    param ()

    $smartInsertQuoteSplat = @{
        Key              = '"',"'"
        BriefDescription = 'SmartInsertQuote'
        LongDescription  = 'Insert paired quotes if not already on a quote'
        ScriptBlock      = {

            param($key, $arg)

            $quote = $key.KeyChar

            $selectionStart = $null
            $selectionLength = $null
            [Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$selectionStart, [ref]$selectionLength)

            $line = $null
            $cursor = $null
            [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)

            # If text is selected, just quote it without any smarts
            if ($selectionStart -ne -1)
            {
                [Microsoft.PowerShell.PSConsoleReadLine]::Replace($selectionStart, $selectionLength, $quote + $line.SubString($selectionStart, $selectionLength) + $quote)
                [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($selectionStart + $selectionLength + 2)
                return
            }

            $ast = $null
            $tokens = $null
            $parseErrors = $null
            [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$ast, [ref]$tokens, [ref]$parseErrors, [ref]$null)

            function FindToken
            {
                param($tokens, $cursor)

                foreach ($token in $tokens)
                {
                    if ($cursor -lt $token.Extent.StartOffset) { continue }
                    if ($cursor -lt $token.Extent.EndOffset) {
                        $result = $token
                        $token = $token -as [StringExpandableToken]
                        if ($token) {
                            $nested = FindToken $token.NestedTokens $cursor
                            if ($nested) { $result = $nested }
                        }

                        return $result
                    }
                }
                return $null
            }

            $token = FindToken $tokens $cursor

            # If we're on or inside a **quoted** string token (so not generic), we need to be smarter
            if ($token -is [StringToken] -and $token.Kind -ne [TokenKind]::Generic) {
                # If we're at the start of the string, assume we're inserting a new string
                if ($token.Extent.StartOffset -eq $cursor) {
                    [Microsoft.PowerShell.PSConsoleReadLine]::Insert("$quote$quote ")
                    [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
                    return
                }

                # If we're at the end of the string, move over the closing quote if present.
                if ($token.Extent.EndOffset -eq ($cursor + 1) -and $line[$cursor] -eq $quote) {
                    [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
                    return
                }
            }

            if ($null -eq $token) {
                if ($line[0..$cursor].Where{$_ -eq $quote}.Count % 2 -eq 1) {
                    # Odd number of quotes before the cursor, insert a single quote
                    [Microsoft.PowerShell.PSConsoleReadLine]::Insert($quote)
                }
                else {
                    # Insert matching quotes, move cursor to be in between the quotes
                    [Microsoft.PowerShell.PSConsoleReadLine]::Insert("$quote$quote")
                    [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
                }
                return
            }

            if ($token.Extent.StartOffset -eq $cursor) {
                if ($token.Kind -eq [TokenKind]::Generic -or $token.Kind -eq [TokenKind]::Identifier) {
                    $end = $token.Extent.EndOffset
                    $len = $end - $cursor
                    [Microsoft.PowerShell.PSConsoleReadLine]::Replace($cursor, $len, $quote + $line.SubString($cursor, $len) + $quote)
                    [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($end + 2)
                }
                return
            }

            # We failed to be smart, so just insert a single quote
            [Microsoft.PowerShell.PSConsoleReadLine]::Insert($quote)
        }
    }
    Set-PSReadLineKeyHandler @smartInsertQuoteSplat

    $insertPairedBracesSplat = @{
        Key              = '(','{','['
        BriefDescription = 'InsertPairedBraces'
        LongDescription  = 'Insert matching braces'
        ScriptBlock      = {

            param($key, $arg)

            $closeChar = switch ($key.KeyChar)
            {
            <#case#> '(' { [char]')'; break }
            <#case#> '{' { [char]'}'; break }
            <#case#> '[' { [char]']'; break }
            }

            [Microsoft.PowerShell.PSConsoleReadLine]::Insert("$($key.KeyChar)$closeChar")
            $line = $null
            $cursor = $null
            [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
            [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor - 1)
        }
    }
    Set-PSReadLineKeyHandler @insertPairedBracesSplat

    $smartCloseBracesSplat = @{
        Key              = ')',']','}'
        BriefDescription = 'SmartCloseBraces'
        LongDescription  = 'Insert closing brace or skip'
        ScriptBlock      = {

            param($key, $arg)

            $line = $null
            $cursor = $null
            [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)

            if ($line[$cursor] -eq $key.KeyChar)
            {
                [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
            }
            else
            {
                [Microsoft.PowerShell.PSConsoleReadLine]::Insert("$($key.KeyChar)")
            }
        }
    }
    Set-PSReadLineKeyHandler @smartCloseBracesSplat

    $backspaceSplat = @{
        Key              = 'Backspace'
        BriefDescription = 'SmartBackspace'
        LongDescription  = 'Delete previous character or matching quotes/parens/braces'
        ScriptBlock      = {

            param($key, $arg)

            $line = $null
            $cursor = $null
            [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)

            if ($cursor -gt 0)
            {
                $toMatch = $null
                if ($cursor -lt $line.Length)
                {
                    switch ($line[$cursor])
                    {
                        <#case#> '"' { $toMatch = '"'; break }
                        <#case#> "'" { $toMatch = "'"; break }
                        <#case#> ')' { $toMatch = '('; break }
                        <#case#> ']' { $toMatch = '['; break }
                        <#case#> '}' { $toMatch = '{'; break }
                    }
                }

                if ($null -ne $toMatch -and $line[$cursor-1] -eq $toMatch)
                {
                    [Microsoft.PowerShell.PSConsoleReadLine]::Delete($cursor - 1, 2)
                }
                else
                {
                    [Microsoft.PowerShell.PSConsoleReadLine]::BackwardDeleteChar($key, $arg)
                }
            }
        }
    }
    Set-PSReadLineKeyHandler @backspaceSplat
}

<#
    .SYNOPSIS
        Disable the information output stream for the global shell.
#>

function Disable-Information
{
    [CmdletBinding()]
    [Alias('di')]
    param ()

    Set-Variable -Scope Global -Name InformationPreference -Value 'SilentlyContinue'
}

<#
    .SYNOPSIS
        Disable the verbose output stream for the global shell.
#>

function Disable-Verbose
{
    [CmdletBinding()]
    [Alias('dv')]
    param ()

    Set-Variable -Scope Global -Name VerbosePreference -Value 'SilentlyContinue'
}
<#
    .SYNOPSIS
        Enable the information output stream for the global shell.
#>

function Enable-Information
{
    [CmdletBinding()]
    [Alias('ei')]
    param ()

    Set-Variable -Scope Global -Name InformationPreference -Value 'Continue'
}

<#
    .SYNOPSIS
        Enable the verbose output stream for the global shell.
#>

function Enable-Verbose
{
    [CmdletBinding()]
    [Alias('ev')]
    param ()

    Set-Variable -Scope Global -Name VerbosePreference -Value 'Continue'
}

<#
    .SYNOPSIS
        Update the workspace configuration for Visual Studio Code which is used
        by the extension vscode-open-project.
 
    .LINK
        https://marketplace.visualstudio.com/items?itemName=svetlozarangelov.vscode-open-project
#>

function Update-Workspace
{
    [CmdletBinding(SupportsShouldProcess = $true)]
    param
    (
        [Parameter(Mandatory = $false)]
        [ValidateScript({Test-Path -Path $_})]
        [System.String]
        $Path = $Env:Worksapce,

        [Parameter(Mandatory = $false)]
        [System.String]
        $ProjectListPath = "$Env:AppData\Code\User\projectlist.json"
    )

    $projectList = @{
        projects = [Ordered] @{}
    }

    foreach ($workspace in (Get-ChildItem -Path $Path -Filter '*.code-workspace' -File))
    {
        $projectList.projects.Add(('Workspace {0}' -f $workspace.BaseName), $workspace.FullName)
    }

    foreach ($group in (Get-ChildItem -Path $Path -Directory))
    {
        foreach ($repo in (Get-ChildItem -Path $group.FullName -Directory))
        {
            $key = '{0} \ {1}' -f $group.Name, $repo.Name

            $projectList.projects.Add($key, $repo.FullName)
        }
    }

    if ($PSCmdlet.ShouldProcess($ProjectListPath, 'Update Project List'))
    {
        $projectList | ConvertTo-Json | Set-Content -Path $ProjectListPath
    }
}



# Prompt configuration and variables
$Script:PromptHistory  = 0
$Script:PromptColor    = 'Yellow'
$Script:PromptInfo     = '[PS {0}.{1}]' -f $PSVersionTable.PSVersion.Major, $PSVersionTable.PSVersion.Minor
$Script:PromptAlias    = $false
$Script:PromptTimeSpan = $false
$Script:PromptGit      = $false
$Script:PromptDefault  = Get-Command -Name 'prompt' | Select-Object -ExpandProperty 'Definition'

# Enumerate the prompt color based on the operating system
if ([System.Environment]::OSVersion.Platform -eq 'Win32NT')
{
    $Script:PromptColor = $(if(([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { 'Red' } else { 'DarkCyan' })
}
if ([System.Environment]::OSVersion.Platform -eq 'Unix')
{
    $Script:PromptColor = $(if((whoami) -eq 'root') { 'Red' } else { 'DarkCyan' })
}

# Module command not found action variables
$Script:CommandNotFoundEnabled = $false
$Script:CommandNotFoundAction  = @{}