ProfileFever.psm1



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


<#
    .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 = 'PSRemotingWithCredential')]
        [Parameter(Mandatory = $true, ParameterSetName = 'PSRemotingWithVault')]
        [System.String]
        $ComputerName,

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

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

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

        # To invoke an ssh session, the target hostname.
        [Parameter(Mandatory = $true, ParameterSetName = 'SSHRemoting')]
        [System.String]
        $Hostname,

        # To invoke an ssh session, the username to use.
        [Parameter(Mandatory = $true, ParameterSetName = 'SSHRemoting')]
        [System.String]
        $Username
    )

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

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

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

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

        'SSHRemoting'
        {
            $command.CommandType = 'SSH'
            $command.Hostname    = $Hostname
            $command.Username    = $Username
        }
    }

    $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
                                    {
                                        $Host.UI.RawUI.WindowTitle = "$Env:Username@$Env:ComputerName | $($executionContext.SessionState.Path.CurrentLocation)"
                                        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()
                    }
                }

                'SSH'
                {
                    $hostname = $command.Hostname
                    $username = $command.Username

                    Write-Verbose "ssh.exe $username@$hostname"

                    $CommandLookupEventArgs.StopSearch = $true
                    $CommandLookupEventArgs.CommandScriptBlock = {

                        ssh.exe "$username@$hostname"
                    }.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
        Format the text with RGB colors and weight.
 
    .DESCRIPTION
        Use the ANSI escape sequence to use the full RGB colors formatting the
        text. The foreground and background can be specified as RGB. The font
        can be specified as bold
 
    .PARAMETER Message
        The message to format.
 
    .PARAMETER ForegroundColor
        Set the foreground color as RGB.
 
    .PARAMETER BackgroundColor
        Set the background color as RGB.
 
    .PARAMETER Bold
        Show the text in bold font.
#>

function Format-HostText
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Message,

        [Parameter(Mandatory = $false)]
        [ValidateRange(0,255)]
        [ValidateCount(3,3)]
        [System.Int32[]]
        $ForegroundColor,

        [Parameter(Mandatory = $false)]
        [ValidateRange(0,255)]
        [ValidateCount(3,3)]
        [System.Int32[]]
        $BackgroundColor,

        [Parameter(Mandatory = $false)]
        [switch]
        $Bold
    )

    $ansiEscape = [System.Char] 27

    $stringBuilder = [System.Text.StringBuilder]::new()

    # Foreground Color Prefix
    if ($PSBoundParameters.ContainsKey('ForegroundColor'))
    {
        $stringBuilder.AppendFormat("$ansiEscape[38;2;{0};{1};{2}m", $ForegroundColor[0], $ForegroundColor[1], $ForegroundColor[2]) | Out-Null
    }

    # Background Color Prefix
    if ($PSBoundParameters.ContainsKey('BackgroundColor'))
    {
        $stringBuilder.AppendFormat("$ansiEscape[48;2;{0};{1};{2}m", $BackgroundColor[0], $BackgroundColor[1], $BackgroundColor[2]) | Out-Null
    }

    # Bold Prefix
    if ($Bold.IsPresent)
    {
        $stringBuilder.Append("$ansiEscape[1m") | Out-Null
    }

    $stringBuilder.Append($Message) | Out-Null

    # Bold Suffix
    if ($Bold.IsPresent)
    {
        $stringBuilder.Append("$ansiEscape[0m") | Out-Null
    }

    # Background Color Suffix
    if ($PSBoundParameters.ContainsKey('BackgroundColor'))
    {
        $stringBuilder.Append("$ansiEscape[0m") | Out-Null
    }

    # Foreground Color Suffix
    if ($PSBoundParameters.ContainsKey('ForegroundColor'))
    {
        $stringBuilder.Append("$ansiEscape[0m") | Out-Null
    }

    return $stringBuilder.ToString()
}

<#
    .SYNOPSIS
        Test if the current directory is a git repository.
 
    .DESCRIPTION
        Recursive test the current and all it's parents if the repository is
        part of a git repository. It will use the current location provieded by
        the Get-Location cmdlet.
#>

function Test-GitRepository
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param ()

    $pathInfo = Get-Location

    if (!$pathInfo -or ($pathInfo.Provider.Name -ne 'FileSystem'))
    {
        return $false
    }
    elseif ($Env:GIT_DIR)
    {
        return $true
    }
    else
    {
        $currentDir = Get-Item -LiteralPath $pathInfo -Force
        while ($currentDir)
        {
            $gitDirPath = Join-Path -Path $currentDir.FullName -ChildPath '.git'
            if (Test-Path -LiteralPath $gitDirPath -PathType Container)
            {
                return $true
            }
            if (Test-Path -LiteralPath $gitDirPath -PathType Leaf)
            {
                return $true
            }

            $headPath = Join-Path -Path $currentDir.FullName -ChildPath 'HEAD'
            if (Test-Path -LiteralPath $headPath -PathType Leaf)
            {
                $refsPath = Join-Path -Path $currentDir.FullName -ChildPath 'refs'
                $objsPath = Join-Path -Path $currentDir.FullName -ChildPath 'objects'
                if ((Test-Path -LiteralPath $refsPath -PathType Container) -and
                    (Test-Path -LiteralPath $objsPath -PathType Container))
                {
                    return $true
                }
            }

            $currentDir = $currentDir.Parent
        }
    }

    return $false
}

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

function Get-ProfileHeadline
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param ()

    $stringBuilder = [System.Text.StringBuilder]::new()

    # Get the PowerShell version depending on the edition
    if ($PSVersionTable.PSEdition -eq 'Core')
    {
        $stringBuilder.AppendFormat('PowerShell {0}', $PSVersionTable.PSVersion) | Out-Null
    }
    else
    {
        $stringBuilder.AppendFormat('Windows PowerShell {0}.{1}', $PSVersionTable.PSVersion.Major, $PSVersionTable.PSVersion.Minor) | Out-Null
    }

    $stringBuilder.AppendLine() | Out-Null

    # Get the operating system information, based on the operating system
    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' | Select-Object 'ProductName', 'ReleaseId', 'CurrentVersion'
        if ([System.String]::IsNullOrEmpty($osVersion.ReleaseId))
        {
            $osVersion.ReleaseId = $osVersion.CurrentVersion
        }

        $stringBuilder.AppendFormat('{0}, Version {1}', $osVersion.ProductName, $osVersion.ReleaseId) | Out-Null
    }
    if ([System.Environment]::OSVersion.Platform -eq 'Unix')
    {
        # Kernel name, Kenrel release, Kerner version
        $stringBuilder.AppendFormat('{0} {1} {2}', (uname -s), (uname -r), (uname -v)) | Out-Null
    }

    $stringBuilder.AppendLine() | Out-Null
    $stringBuilder.AppendLine() | Out-Null

    # Get the info about the current logged on user, system and uptime
    if ([System.Environment]::OSVersion.Platform -eq 'Win32NT')
    {
        $stringBuilder.AppendFormat('{0}\{1} on {2} ({3}), Uptime {4:%d} day(s) {4:hh\:mm\:ss}', $Env:UserDomain, $Env:Username, $Env:ComputerName.ToUpper(), $PID, [System.TimeSpan]::FromMilliseconds([System.Environment]::TickCount)) | Out-Null
    }
    if ([System.Environment]::OSVersion.Platform -eq 'Unix')
    {
        $stringBuilder.AppendFormat('{0} on {1} ({2}), {3}', $Env:Username, (hostname), $PID, (uptime).Split(',')[0].Trim()) | Out-Null
    }

    $stringBuilder.AppendLine() | Out-Null

    return $stringBuilder.ToString()
}

<#
    .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 profile scripts.
#>

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
        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
    ##

    Show-ProfileLoadStatus -Section 'Load 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


    ##
    ## LOCATION
    ##

    Show-ProfileLoadStatus -Section 'Set Location'

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


    ##
    ## WORKSPACE
    ##

    Show-ProfileLoadStatus -Section 'Create Workspace'



    if ((Test-Path -Path $config.Workspace) -and -not (Test-Path -Path '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', $config.Workspace, [System.EnvironmentVariableTarget]::Process)
    }


    ##
    ## PROMPT
    ##

    Show-ProfileLoadStatus -Section 'Enable Prompt'

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

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

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

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


    ##
    ## COMMAND NOT FOUND
    ##

    Show-ProfileLoadStatus -Section 'Enable Command Not Found'

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


    ##
    ## ALIASES
    ##

    Show-ProfileLoadStatus -Section 'Register 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
    ##

    Show-ProfileLoadStatus -Section 'Register 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
    ##

    Show-ProfileLoadStatus -Section 'Invoke Scripts'

    foreach ($script in $config.Scripts)
    {
        Show-ProfileLoadStatus -Section "Invoke Script $script"

        . $script
    }


    ##
    ## BINARIES
    ##

    Show-ProfileLoadStatus -Section 'Update Path for Binaries'

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


    ##
    ## PSREADLINE
    ##

    Show-ProfileLoadStatus -Section 'Update 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
    ##

    Show-ProfileLoadStatus -Section 'Enable Strict Mode'

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


    ##
    ## HEADLINE
    ##

    Show-ProfileLoadStatus -Section 'Show Headline'
    Write-Host "`r" -NoNewline

    # 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 ($config.Headline -and $Host.UI.RawUI.CursorPosition.Y -eq 0)
    {
        $Host.UI.WriteLine((Get-ProfileHeadline))
    }
}

<#
    .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.PromptType)
    {
        $config | Add-Member -MemberType 'NoteProperty' -Name 'PromptType' -Value 'Basic'
    }
    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
        Clear the static prompt title.
 
    .DESCRIPTION
        Clear the previously defined static title.
#>

function Clear-PromptTitle
{
    [CmdletBinding()]
    [Alias('ctitle')]
    param ()

    Remove-Variable -Scope 'Script' -Name 'PromptTitle' -ErrorAction 'SilentlyContinue' -Force
    New-Variable -Scope 'Script' -Name 'PromptTitle' -Option 'ReadOnly' -Value $null -Force
}

<#
    .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.
 
    .DESCRIPTION
        There are two prompts available. Be default, the Basic prompt is used.
        It will show all information without any fancy formatting. For a nice
        formiatting, the Advanced type can be used. It's recommended that the
        font Delugia Nerd Font is used. This is an extension of the new font
        Cascadia Code.
 
    .LINK
        https://github.com/microsoft/cascadia-code/
        https://github.com/adam7/delugia-code/
#>

function Enable-Prompt
{
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalFunctions', '')]
    param
    (
        [Parameter(Mandatory = $false)]
        [ValidateSet('Basic', 'Advanced')]
        [System.String]
        $Type = 'Basic'
    )

    if ($Type -eq 'Basic')
    {
        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)) "
        }
    }

    if ($Type -eq 'Advanced')
    {
        if ($PSVersionTable.PSVersion -lt '6.0')
        {
            Add-Type -AssemblyName 'PresentationFramework'
        }

        function Global:Prompt
        {
            # Definition of used colours
            # See cyan shades on https://www.color-hex.com/color/3a96dd
            $colorCyan1       = 0x17, 0x3C, 0x58
            $colorCyan2       = 0x28, 0x69, 0x9A
            $colorCyan3       = 0x3A, 0x96, 0xDD
            $colorCyan4       = 0x75, 0xB5, 0xE7
            $colorCyan5       = 0x85, 0xC5, 0xF7
            $colorWhite       = 0xF2, 0xF2, 0xF2
            $colorBlack       = 0x0C, 0x0C, 0x0C
            $colorRed         = 0xCC, 0x00, 0x00
            $colorDarkRed     = 0xC5, 0x0F, 0x1F
            $colorDarkYellow  = 0xC1, 0x9C, 0x00
            $colorDarkGreen   = 0x13, 0x90, 0x0E # Darker than console color
            $colorDarkMagenta = 0x88, 0x17, 0x98

            # Definition of special characters
            $separator   = [char] 57520
            $diagonal    = "$([char]57532)$([char]57530)"
            $iconBranch  = [char] 57504
            $iconIndex   = [char] 57354
            $iconWorking = [char] 57353
            $iconStash   = [char] 58915
            $iconAdmin   = [char] 9760
            $iconDebug   = [char] 9874

            # Get location and replace the user home directory
            $location = $ExecutionContext.SessionState.Path.CurrentLocation.Path
            $location = $location.Replace($Home, "~")

            # Set the window title with the current location
            if ($null -eq $Script:PromptTitle)
            {
                $Host.UI.RawUI.WindowTitle = "$Env:Username@$Env:ComputerName | $location"
            }
            else
            {
                $Host.UI.RawUI.WindowTitle = $Script:PromptTitle
            }

            $output = [System.Text.StringBuilder]::new()

            $showFullPrompt = $PSVersionTable.PSVersion -lt '6.0' -and ([System.Windows.Input.Keyboard]::IsKeyDown('RightShift'))

            # If the prompt history id chagned, e.g. a command was executed,
            # show the alias suggestion and last command duration, if enabled.
            if ($Script:PromptHistory -ne $MyInvocation.HistoryId -or $showFullPrompt)
            {
                # Update history information
                $Script:PromptHistory = $MyInvocation.HistoryId

                # Show promt alias and command duration
                if ($Script:PromptAlias) { Show-PromptAliasSuggestion }
                if ($Script:PromptTimeSpan) { Show-PromptLastCommandDuration }

                if ($Script:PromptIsAdmin)
                {
                    $output.Append((Format-HostText -Message " $iconAdmin " -BackgroundColor $colorRed)) | Out-Null
                }

                # Show an information about the debug prompt
                if ($NestedPromptLevel -gt 0)
                {
                    $output.Append((Format-HostText -Message " $iconDebug " -BackgroundColor $colorDarkMagenta)) | Out-Null
                }

                # Get the prompt info and current location
                $output.Append((Format-HostText -Message " $Script:PromptInfo " -ForegroundColor $colorWhite -BackgroundColor $colorCyan1)) | Out-Null
                $output.Append((Format-HostText -Message $separator -ForegroundColor $colorCyan1 -BackgroundColor $colorCyan2)) | Out-Null
                $output.Append((Format-HostText -Message " $location " -ForegroundColor $colorWhite -BackgroundColor $colorCyan2)) | Out-Null
                $output.Append((Format-HostText -Message $separator -ForegroundColor $colorCyan2)) | Out-Null

                # Check if the current directory is member of a git repo
                if ($NestedPromptLevel -eq 0 -and $Script:PromptGit -and (Test-GitRepository))
                {
                    try
                    {
                        if ($null -eq (Get-Module -Name 'posh-git'))
                        {
                            Import-Module -Name 'posh-git' -Global
                        }

                        $Global:GitPromptSettings.EnableStashStatus = $true
                        $Global:GitStatus = Get-GitStatus

                        $status  = $Global:GitStatus
                        $setting = $Global:GitPromptSettings

                        $branchText = '{0} {1}' -f $iconBranch, (Format-GitBranchName -BranchName $status.Branch)

                        if (!$status.Upstream)
                        {
                            # No upstream branch configured
                            $branchText += ' '
                            $branchColor = $colorCyan3
                        }
                        elseif ($status.UpstreamGone -eq $true)
                        {
                            # Upstream branch is gone
                            $branchText += ' {0} ' -f $setting.BranchGoneStatusSymbol.Text
                            $branchColor = $colorDarkRed
                        }
                        elseif (($status.BehindBy -eq 0) -and ($status.AheadBy -eq 0))
                        {
                            # We are aligned with remote
                            $branchText += ' {0} ' -f $setting.BranchIdenticalStatusSymbol.Text
                            $branchColor = $colorCyan3
                        }
                        elseif (($status.BehindBy -ge 1) -and ($status.AheadBy -ge 1))
                        {
                            # We are both behind and ahead of remote
                            $branchText += ' {0}{1} {2}{3} ' -f $setting.BranchBehindStatusSymbol.Text, $status.BehindBy, $setting.BranchAheadStatusSymbol.Text, $status.AheadBy
                            $branchColor = $colorDarkYellow
                        }
                        elseif ($status.BehindBy -ge 1)
                        {
                            # We are behind remote
                            $branchText += ' {0}{1} ' -f $setting.BranchBehindStatusSymbol.Text, $status.BehindBy
                            $branchColor = $colorDarkRed
                        }
                        elseif ($status.AheadBy -ge 1)
                        {
                            # We are ahead of remote
                            $branchText += ' {0}{1} ' -f $setting.BranchAheadStatusSymbol.Text, $status.AheadBy
                            $branchColor = $colorDarkGreen
                        }
                        else
                        {
                            $branchText += ' ? '
                            $branchColor = $colorCyan3
                        }

                        $output.Append((Format-HostText -Message "`b$separator " -ForegroundColor $colorCyan2 -BackgroundColor $branchColor)) | Out-Null
                        $output.Append((Format-HostText -Message $branchText -ForegroundColor $colorWhite -BackgroundColor $branchColor)) | Out-Null
                        $output.Append((Format-HostText -Message $separator -ForegroundColor $branchColor)) | Out-Null

                        if ($status.HasIndex -or $status.HasWorking -or $GitStatus.StashCount -gt 0)
                        {
                            $output.Append((Format-HostText -Message "`b$separator" -ForegroundColor $branchColor -BackgroundColor $colorCyan4)) | Out-Null

                            $outputPart  = @()
                            $outputSplit = Format-HostText -Message $diagonal -ForegroundColor $colorCyan4 -BackgroundColor $colorCyan5

                            if ($status.HasIndex)
                            {
                                $indexText = ' '
                                $indexText += '{0}{1} ' -f $setting.FileAddedText, $status.Index.Added.Count
                                $indexText += '{0}{1} ' -f $setting.FileModifiedText, $status.Index.Modified.Count
                                $indexText += '{0}{1} ' -f $setting.FileRemovedText, $status.Index.Deleted.Count
                                if ($status.Index.Unmerged)
                                {
                                    $indexText += '{0}{1} ' -f $setting.FileConflictedText, $status.Index.Unmerged.Count
                                }
                                $indexText += "$iconIndex "

                                $outputPart += Format-HostText -Message $indexText -ForegroundColor 0,96,0 -BackgroundColor $colorCyan4
                            }

                            if ($status.HasWorking)
                            {
                                $workingText = ' '
                                $workingText += '{0}{1} ' -f $setting.FileAddedText, $status.Working.Added.Count
                                $workingText += '{0}{1} ' -f $setting.FileModifiedText, $status.Working.Modified.Count
                                $workingText += '{0}{1} ' -f $setting.FileRemovedText, $status.Working.Deleted.Count
                                if ($status.Working.Unmerged)
                                {
                                    $workingText += '{0}{1} ' -f $setting.FileConflictedText, $status.Working.Unmerged.Count
                                }
                                $workingText += "$iconWorking "

                                $outputPart += Format-HostText -Message $workingText -ForegroundColor 96,0,0 -BackgroundColor $colorCyan4
                            }

                            if ($GitStatus.StashCount -gt 0)
                            {
                                $stashText = " +{0} $iconStash " -f $GitStatus.StashCount

                                $outputPart += Format-HostText -Message $stashText -ForegroundColor 0,0,96 -BackgroundColor $colorCyan4
                            }

                            $output.Append($outputPart[0]) | Out-Null
                            for ($i = 1; $i -lt $outputPart.Count; $i++)
                            {
                                $output.Append($outputSplit) | Out-Null
                                $output.Append($outputPart[$i]) | Out-Null
                            }

                            $output.Append((Format-HostText -Message $separator -ForegroundColor $colorCyan4)) | Out-Null
                        }
                    }
                    catch
                    {
                        $output.Append((Format-HostText -Message "`b$separator" -ForegroundColor $colorCyan2 -BackgroundColor $colorCyan3)) | Out-Null
                        $output.Append((Format-HostText -Message " ERROR: $_ " -ForegroundColor $colorWhite -BackgroundColor $colorCyan3)) | Out-Null
                        $output.Append((Format-HostText -Message $separator -ForegroundColor $colorCyan3)) | Out-Null
                    }
                }
            }

            # Define the command counter and imput line
            $input = "$($MyInvocation.HistoryId.ToString().PadLeft(3, '0'))$('>' * ($NestedPromptLevel + 1)) "

            # Finally, show the output about path, debug, git etc. and then on a
            # new line the command count and the prompt level indicator
            if ([System.String]::IsNullOrEmpty($output.ToString()))
            {
                return $input
            }
            else
            {
                Write-Host $output -NoNewline
                return "`n$input"
            }
        }
    }
}

<#
    .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 ()

    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
        Set a static prompt title.
 
    .DESCRIPTION
        Overwrite the dynamic prompt title with a static title.
#>

function Set-PromptTitle
{
    [CmdletBinding()]
    [Alias('title')]
    param
    (
        # Global title definition
        [Parameter(Mandatory = $true, Position = 0)]
        [System.String]
        $Title
    )

    Remove-Variable -Scope 'Script' -Name 'PromptTitle' -ErrorAction 'SilentlyContinue' -Force
    New-Variable -Scope 'Script' -Name 'PromptTitle' -Option 'ReadOnly' -Value $Title -Force
}

<#
    .SYNOPSIS
        Show the alias suggestion for the latest command.
 
    .DESCRIPTION
        Show a suggestion for the last prompt, all aliases for the used command
        are shown to the user.
#>

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

    if ($MyInvocation.HistoryId -gt 1)
    {
        $history = Get-History -Id ($MyInvocation.HistoryId - 1)
        $reports = @()
        foreach ($alias in (Get-Alias))
        {
            $match1 = ' {0} ' -f $alias.ResolvedCommandName
            $match2 = ' {0}$' -f $alias.ResolvedCommandName
            if ($history.CommandLine -match $match1 -or $history.CommandLine -match $match2)
            {
                $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.
 
    .DESCRIPTION
        Use the $MyInvocation variable and the Get-History to get the last
        executed command and calculate the execution duration.
#>

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

    if ($MyInvocation.HistoryId -gt 1 -and $Host.UI.RawUI.CursorPosition.Y -gt 0)
    {
        $clock    = [System.Char] 9201
        $history  = Get-History -Id ($MyInvocation.HistoryId - 1)
        $duration = " {0} {1:0.000}s`r" -f $clock, ($history.EndExecutionTime - $history.StartExecutionTime).TotalSeconds

        # Move cursor to the right to show the execution time
        $position = $Host.UI.RawUI.CursorPosition
        $position.X = $Host.UI.RawUI.WindowSize.Width - $duration.Length - 2
        $Host.UI.RawUI.CursorPosition = $position

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

        # Move cursor back
        $position = $Host.UI.RawUI.CursorPosition
        $position.X = 0
        $Host.UI.RawUI.CursorPosition = $position
    }
}

<#
    .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.
 
    .DESCRIPTION
        By default the path $HOME\Workspace is used to prepare the project list
        for the vscode-open-project extension. All *.code-workspace files in the
        root of the path are used for grouped Visual Studio Code workspaces.
 
    .PARAMETER Path
        Path to the workspace. $HOME\Workspace is used by default.
 
    .PARAMETER ProjectListPath
        Path to the JSON config file of the vscode-open-project extension.
 
    .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 = "$HOME\Workspace",

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

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

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

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

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

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

<#
    .SYNOPSIS
        Show the current profile load status.
#>

function Show-ProfileLoadStatus
{
    param
    (
        # Section which will be loaded now.
        [Parameter(Mandatory = $false)]
        [System.String]
        $Section
    )

    if ($VerbosePreference -eq 'Continue')
    {
        Write-Verbose "$(Get-Date -Format HH:mm:ss.fffff) $Section"
    }
    else
    {
        Write-Host "`r$(' ' * $Host.UI.RawUI.WindowSize.Width)`r$Section..." -NoNewline
    }
}



# 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'
$Script:PromptTitle    = $null
$Script:PromptIsAdmin  = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)

# 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  = @{}