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 MesloLGS NF is used. #> 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') { 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 $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 # 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) { $Script:PromptHistory = $MyInvocation.HistoryId if ($Script:PromptAlias) { Show-PromptAliasSuggestion } if ($Script:PromptTimeSpan) { Show-PromptLastCommandDuration } } # 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() # Show an information about the debug prompt if ($NestedPromptLevel -gt 0) { $output.Append((Format-HostText -Message ' DBG ' -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: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 } } # Write the prompt Write-Host $output -NoNewline # Finally, show the command count and the prompt level indicator on # the a seperate line 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 () 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)) { if ($history.CommandLine.IndexOf($alias.ResolvedCommandName, [System.StringComparison]::CurrentCultureIgnoreCase) -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. .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) { $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. .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 # 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 = @{} |