ProfileFever.psm1

<#
    .SYNOPSIS
        Root module file.
 
    .DESCRIPTION
        The root module file loads all classes, helpers and functions into the
        module context.
#>



## Module loader

<#
    .SYNOPSIS
        Import the profile configuration file.
#>

function Import-ProfileConfig
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Path
    )

    $configs = Get-Content -Path $Path | ConvertFrom-Json

    # Update configurations
    if ($configs.Name -contains "$Env:ComputerName\$Env:Username")
    {
        $config = $configs.Where({$_.Name -eq "$Env:ComputerName\$Env:Username"})[0]
    }
    else
    {
        $config = $configs.Where({$_.Name -eq 'Default'})[0]
    }

    Write-Output $config
}

<#
    .SYNOPSIS
        Show the headline with information about the local system and current
        user.
#>

function Show-HostHeadline
{
    # 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
    }
    if ($null -eq $osVersion.UBR)
    {
        $osVersion | Add-Member -MemberType NoteProperty -Name 'UBR' -Value '0'
    }

    # Rename the ConsoleHost string to a nice understandable string
    $profileHost = $Host.Name.Replace('ConsoleHost', 'Windows PowerShell Console Host')

    # Get the PowerShell version depending on the edition
    if ($PSVersionTable.PSEdition -eq 'Core')
    {
        $psVersion = 'Version {0}' -f $PSVersionTable.PSVersion
    }
    else
    {
        $psVersion = 'Version {0}.{1} (Build {2}.{3})' -f $PSVersionTable.PSVersion.Major, $PSVersionTable.PSVersion.Minor, $PSVersionTable.PSVersion.Build, $PSVersionTable.PSVersion.Revision
    }

    $Host.UI.WriteLine(('{0}, Version {1} (Build {2}.{3})' -f $osVersion.ProductName, $osVersion.ReleaseId, $osVersion.CurrentBuildNumber, $osVersion.UBR))
    $Host.UI.WriteLine(('{0}, {1}' -f $profileHost, $psVersion))
    $Host.UI.WriteLine()
    $Host.UI.WriteLine(('{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)))
    $Host.UI.WriteLine()
}

<#
    .SYNOPSIS
        Set the console host configuration.
#>

function Set-ConsoleConfig
{
    [CmdletBinding(SupportsShouldProcess = $true)]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalFunctions', '')]
    param
    (
        [Parameter(Mandatory = $false)]
        [System.Int32]
        $WindowWidth,

        [Parameter(Mandatory = $false)]
        [System.Int32]
        $WindowHeight,

        [Parameter(Mandatory = $false)]
        [System.ConsoleColor]
        $ForegroundColor,

        [Parameter(Mandatory = $false)]
        [System.ConsoleColor]
        $BackgroundColor
    )


    # Step 1: Window and buffer size
    if ($PSCmdlet.ShouldProcess('Window and Buffer', 'Change Size'))
    {
        $bufferSize = $Global:Host.UI.RawUI.BufferSize
        $windowSize = $Global:Host.UI.RawUI.WindowSize

        $bufferSize.Height = 9999

        if ($PSBoundParameters.ContainsKey('WindowWidth'))
        {
            $bufferSize.Width = $WindowWidth
            $windowSize.Width = $WindowWidth
        }

        if ($PSBoundParameters.ContainsKey('WindowHeight'))
        {
            $windowSize.Height = $WindowHeight
        }

        $Global:Host.UI.RawUI.BufferSize = $bufferSize
        $Global:Host.UI.RawUI.WindowSize = $windowSize
    }


    # Step 2: Window foreground and background color
    if ($PSCmdlet.ShouldProcess('Color', 'Change Color'))
    {
        if ($PSBoundParameters.ContainsKey('ForegroundColor'))
        {
            $Global:Host.UI.RawUI.ForegroundColor = $ForegroundColor
        }

        if ($PSBoundParameters.ContainsKey('BackgroundColor'))
        {
            $Global:Host.UI.RawUI.BackgroundColor = $BackgroundColor
        }
    }
}

<#
    .SYNOPSIS
        Connect to a SSH jump host.
 
    .DESCRIPTION
        This script will connect to the specified SSH jump host by using the
        credentials and the plink.exe tool. With the shared secret, it will
        generate a time-based one-time password as two factor authentication.
 
    .PARAMETER ComputerName
        DNS hostname of the jump host.
 
    .PARAMETER Credential
        Username and password of the jump host user.
 
    .PARAMETER SharedSecret
        Shared secret for the TOTP calculation.
#>

function Connect-JumpHost
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $ComputerName,

        [Parameter(Mandatory = $true)]
        [System.Management.Automation.PSCredential]
        $Credential,

        [Parameter(Mandatory = $true)]
        [System.Security.SecureString]
        $SharedSecret,

        [Parameter(Mandatory = $false)]
        [System.Int32]
        $InputOffset = 2
    )

    # Hide verbose preference
    $VerbosePreference = 'SilentlyContinue'

    # Connection credentials
    $username = $Credential.UserName
    $password = $Credential.Password | Unprotect-SecureString

    # TOTP shared secret
    $secret = $SharedSecret | Unprotect-SecureString

    # Get the current cursor position, to calculate the input
    $cursorTop = [System.Console]::CursorTop + $InputOffset

    # This script block is invoked asynchronously to enter the TOTP two factor
    # as soon as the desired line is reached.
    $scriptBlock = {
        param ($title, $line, $secret)
        while ([System.Console]::CursorTop -lt $line)
        {
            Start-Sleep -Milliseconds 5
        }
        $wShell = New-Object -ComObject 'WScript.Shell'
        $wShell.AppActivate($title)
        $totp = Get-TimeBasedOneTimePassword -SharedSecret $secret
        $totp.ToString().ToCharArray() | ForEach-Object { $wShell.SendKeys($_) }
        $wShell.SendKeys('~')
    }
    $runspace = [PowerShell]::Create()
    $runspace.AddScript($scriptBlock).AddArgument($Host.UI.RawUI.WindowTitle).AddArgument($cursorTop).AddArgument($secret) | Out-Null
    $runspace.BeginInvoke() | Out-Null

    plink.exe '-ssh' "$username@$ComputerName" '-pw' $password
}

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

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

        'RemotingWithVault'
        {
            $command['CommandType']     = 'Remoting'
            $command['ComputerName']    = $ComputerName
            $command['VaultTargetName'] = $VaultTargetName
        }

        'ScriptBlock'
        {
            $command['CommandType'] = 'ScriptBlock'
            $command['ScriptBlock'] = $ScriptBlock
        }
    }

    $Script:CommandNotFoundAction += [PSCustomObject] $command
}

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

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

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

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

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

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

        foreach ($command in $Script:CommandNotFoundAction)
        {
            if ($command.CommandName -eq $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.VaultTargetName)
                        {
                            $credential = Use-VaultCredential -TargetName $command.VaultTargetName
                            $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)) " } }
                                }
                                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
        Disable the custom prompt and restore the default prompt.
#>

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

    function Global:Prompt
    {
        "PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "
        # .Link
        # http://go.microsoft.com/fwlink/?LinkID=225750
        # .ExternalHelp System.Management.Automation.dll-help.xml
    }
}

<#
    .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, '[{0:dd MMM HH:mm}]' -f [DateTime]::Now)
        $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.00}" -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
        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)]
        [System.String]
        $Path = "$HOME\Workspace",

        [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
    }
}

# Get and dot source all external functions (public)
Split-Path -Path $PSCommandPath |
    Get-ChildItem -Filter 'Functions' -Directory |
        Get-ChildItem -Include '*.ps1' -File -Recurse |
            ForEach-Object { . $_.FullName }


## Module configuration

# Module path
New-Variable -Name 'ModulePath' -Value $PSScriptRoot

# Module profile configuration variables
$Script:PromptHistory  = 0
$Script:PromptAdmin    = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
$Script:PromptColor    = $(if($Script:PromptAdmin) { 'Red' } else { 'DarkCyan' })
$Script:PromptAlias    = $false
$Script:PromptTimeSpan = $false
$Script:PromptGit      = $false

# Module command not found action variables
$Script:CommandNotFoundAction = @()