Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save sba923/5671d24e0ae533689490b504eb5d8401 to your computer and use it in GitHub Desktop.

Select an option

Save sba923/5671d24e0ae533689490b504eb5d8401 to your computer and use it in GitHub Desktop.
PowerShell utilities to deal with keybindings
# this is one of Stéphane BARIZIEN's public domain scripts
# the most recent version can be found at: https://gist.github.com/sba923/5671d24e0ae533689490b504eb5d8401#file-get-powershellrunninginwindowsterminalkeybinding-ps1
#requires -version 7
<#
.SYNOPSIS
Returns effective PowerShell key bindings when running inside Windows Terminal.
.DESCRIPTION
Calls the sibling Get-WindowsTerminalKeyBinding.ps1 script to load Windows Terminal
bindings, then combines them with PSReadLine bindings that do not conflict.
When a key sequence is already handled by Windows Terminal, the matching PSReadLine
binding is omitted so the output reflects what is effectively reachable in an
interactive session hosted by Windows Terminal.
.PARAMETER WindowsTerminalPackageName
Appx package name forwarded to Get-WindowsTerminalKeyBinding.ps1.
Common values are:
- Microsoft.WindowsTerminal
- Microsoft.WindowsTerminalPreview
When omitted, the sibling script auto-detects the package name.
.PARAMETER ExcludeUnresolved
Forwarded switch for Get-WindowsTerminalKeyBinding.ps1 to exclude unresolved Windows
Terminal commands before computing the effective merged list.
.OUTPUTS
System.Management.Automation.PSCustomObject
Objects with the properties:
- Command
- Parameters
- Keys
- Source
.NOTES
Script dependencies/calls:
- Calls sibling script Get-WindowsTerminalKeyBinding.ps1 to retrieve Windows
Terminal bindings before merging with PSReadLine key handlers.
- (Indirectly) calls Get-WindowsTerminalVersion.ps1 to auto-detect stable vs preview package
when -WindowsTerminalPackageName is not provided.
.EXAMPLE
Shows effective key bindings from Windows Terminal plus non-conflicting PSReadLine
bindings.
Get-PowerShellRunningInWindowsTerminalKeyBinding.ps1
.EXAMPLE
Shows effective key bindings while filtering unresolved Windows Terminal commands.
Get-PowerShellRunningInWindowsTerminalKeyBinding.ps1 -ExcludeUnresolved
#>
param(
[string] $WindowsTerminalPackageName,
[switch] $ExcludeUnresolved
)
# cSpell: ignore pgup pgdn Stéphane BARIZIEN
function Get-NormalizedChord {
param([string] $Chord)
if ([string]::IsNullOrWhiteSpace($Chord))
{
return $null
}
$alias = @{
'control' = 'ctrl'
'ctl' = 'ctrl'
'windows' = 'win'
'escape' = 'esc'
'return' = 'enter'
'pageup' = 'pgup'
'pagedown' = 'pgdn'
}
$modOrder = @('ctrl', 'alt', 'shift', 'win')
$mods = New-Object System.Collections.Generic.List[string]
$keys = New-Object System.Collections.Generic.List[string]
foreach ($part in ($Chord -split '\+'))
{
$token = $part.Trim().ToLowerInvariant()
if ([string]::IsNullOrWhiteSpace($token))
{
continue
}
if ($alias.ContainsKey($token))
{
$token = $alias[$token]
}
if ($token -in $modOrder)
{
if (-not $mods.Contains($token))
{
$mods.Add($token)
}
}
else
{
$keys.Add($token)
}
}
$orderedMods = foreach ($m in $modOrder)
{
if ($mods.Contains($m))
{
$m
}
}
@($orderedMods + $keys) -join '+'
}
function Get-NormalizedKeySequence {
param([string] $KeySequence)
if ([string]::IsNullOrWhiteSpace($KeySequence))
{
return $null
}
(($KeySequence -split ',') | ForEach-Object { Get-NormalizedChord -Chord $_ }) -join ','
}
function Get-CanonicalPSReadLineKeyDisplay {
param([string] $KeySequence)
if ([string]::IsNullOrWhiteSpace($KeySequence))
{
return $KeySequence
}
$modifierAlias = @{
'ctrl' = 'Ctrl'
'control' = 'Ctrl'
'ctl' = 'Ctrl'
'alt' = 'Alt'
'shift' = 'Shift'
'win' = 'Win'
'windows' = 'Win'
}
$modifierOrder = @('Ctrl', 'Alt', 'Shift', 'Win')
$canonicalChords = foreach ($chord in ($KeySequence -split '(?<!\+),'))
{
$tokens = @($chord -split '\+' | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
if ($tokens.Count -eq 0)
{
continue
}
$mods = @{}
$nonMods = New-Object System.Collections.Generic.List[string]
foreach ($token in $tokens)
{
$lower = $token.ToLowerInvariant()
if ($modifierAlias.ContainsKey($lower))
{
$mods[$modifierAlias[$lower]] = $true
continue
}
if ($token.Length -eq 1 -and $token -cmatch '[A-Z]')
{
# In PSReadLine notation, uppercase letters imply Shift.
$mods['Shift'] = $true
$nonMods.Add($token.ToLowerInvariant())
continue
}
if ($token.Length -eq 1 -and $token -match '[a-zA-Z]')
{
$nonMods.Add($token.ToLowerInvariant())
continue
}
$nonMods.Add($token)
}
$orderedMods = @(
foreach ($m in $modifierOrder)
{
if ($mods.ContainsKey($m))
{
$m
}
}
)
@($orderedMods + @($nonMods.ToArray())) -join '+'
}
$canonicalChords -join ','
}
$wtScript = Join-Path -Path $PSScriptRoot -ChildPath 'Get-WindowsTerminalKeyBinding.ps1'
if (-not (Test-Path -LiteralPath $wtScript))
{
throw "Cannot find sibling script: $wtScript"
}
$wtParams = @{}
if ($PSBoundParameters.ContainsKey('WindowsTerminalPackageName'))
{
$wtParams['PackageName'] = $WindowsTerminalPackageName
}
if ($ExcludeUnresolved)
{
$wtParams['ExcludeUnresolved'] = $true
}
$windowsTerminalBindings = & $wtScript @wtParams
$wtKeySet = @{}
$effectiveBindings = @()
foreach ($binding in $windowsTerminalBindings)
{
$normalized = Get-NormalizedKeySequence -KeySequence ([string]$binding.Keys)
if ($null -eq $normalized)
{
continue
}
$wtKeySet[$normalized] = $true
$effectiveBindings += [PSCustomObject][ordered]@{
Command = $binding.Command
Parameters = $binding.Parameters
Keys = $binding.Keys
Source = 'WindowsTerminal'
}
}
$psReadLineBindings = Get-PSReadLineKeyHandler
foreach ($binding in $psReadLineBindings)
{
$normalized = Get-NormalizedKeySequence -KeySequence ([string]$binding.Key)
if ($null -eq $normalized)
{
continue
}
if ($wtKeySet.ContainsKey($normalized))
{
continue
}
$effectiveBindings += [PSCustomObject][ordered]@{
Command = $binding.Function
Parameters = [PSCustomObject]@{
Description = $binding.Description
Group = $binding.Group
}
Keys = Get-CanonicalPSReadLineKeyDisplay -KeySequence ([string]$binding.Key)
Source = 'PSReadLine'
}
}
$effectiveBindings | Sort-Object -Property 'Keys', 'Source'
# this is one of Stéphane BARIZIEN's public domain scripts
# the most recent version can be found at: https://gist.github.com/sba923/5671d24e0ae533689490b504eb5d8401#file-get-windowsterminalkeybinding-ps1
#requires -version 7
<#
.SYNOPSIS
Returns effective Windows Terminal key bindings from defaults and user settings.
.DESCRIPTION
Loads the Windows Terminal defaults.json and user settings.json files, resolves key
bindings from both action syntaxes (actions/keybindings), merges user and default
bindings, and reports whether a user binding overrides a default binding.
If PackageName is not provided, the script auto-detects whether the current Windows
Terminal flavor is stable or preview and uses the corresponding package name.
.PARAMETER PackageName
Appx package name to inspect.
Common values are:
- Microsoft.WindowsTerminal
- Microsoft.WindowsTerminalPreview
When omitted, the package name is auto-detected.
.PARAMETER ExcludeUnresolved
Excludes key bindings whose command cannot be resolved (for example keybindings that
reference an action id not found in defaults/user actions).
.OUTPUTS
System.Management.Automation.PSCustomObject
Objects with the properties:
- Command
- Parameters
- Keys
- Source
- OverridesDefault
.NOTES
Script dependencies/calls:
- Calls Get-WindowsTerminalVersion.ps1 to auto-detect stable vs preview package
when -PackageName is not provided.
.EXAMPLE
Returns merged Windows Terminal key bindings for the detected running flavor.
Get-WindowsTerminalKeyBinding.ps1
.EXAMPLE
Returns bindings for Windows Terminal Preview while filtering unresolved commands.
Get-WindowsTerminalKeyBinding.ps1 -PackageName Microsoft.WindowsTerminalPreview -ExcludeUnresolved
#>
param(
[string] $PackageName,
[switch] $ExcludeUnresolved
)
# cSpell: ignore wekyb bbwe Scancode numpad pgdn pgup Stéphane BARIZIEN
if (-not $PSBoundParameters.ContainsKey('PackageName'))
{
$wtVersion = Get-WindowsTerminalVersion.ps1
if ($null -eq $wtVersion)
{
throw("Cannot determine the version / flavor of the current Windows Terminal")
}
if ($wtVersion.IsPreview)
{
$PackageName = 'Microsoft.WindowsTerminalPreview'
}
else
{
$PackageName = 'Microsoft.WindowsTerminal'
}
}
try
{
$version = (Get-AppxPackage $PackageName).Version
}
catch
{
# retry after explicitly importing the Appx module
if ($PSVersionTable.PSVersion.Major -eq 5)
{
Import-Module Appx
}
else
{
Import-Module Appx -UseWindowsPowerShell
}
$version = (Get-AppxPackage $PackageName).Version
}
$defaultJSON = Join-Path -Path $env:ProgramFiles -ChildPath ('WindowsApps\{0}_{1}_x64__8wekyb3d8bbwe\defaults.json' -f $PackageName, $version)
$userJSON = Join-Path -Path $env:LOCALAPPDATA -ChildPath ('Packages\{0}_8wekyb3d8bbwe\LocalState\settings.json' -f $PackageName)
$defaultSettings = Get-Content -Path $defaultJSON -Raw | ConvertFrom-Json
$userSettings = Get-Content -Path $userJSON -Raw | ConvertFrom-Json
function Get-NormalizedWTKey {
param([string] $Key)
if ([string]::IsNullOrWhiteSpace($Key))
{
return $null
}
# Split key sequences on commas that are not part of a key token like Ctrl+,
(($Key -split '(?<!\+),') | ForEach-Object { $_.Trim().ToLowerInvariant() }) -join ','
}
function Get-CanonicalWTKeyDisplay {
param([string] $Key)
if ([string]::IsNullOrWhiteSpace($Key))
{
return $Key
}
$modifierMap = @{
'ctrl' = 'Ctrl'
'control' = 'Ctrl'
'alt' = 'Alt'
'shift' = 'Shift'
'win' = 'Win'
'windows' = 'Win'
}
$modifierOrder = @('Ctrl', 'Alt', 'Shift', 'Win')
function Convert-WTNonModifierToken {
param([string] $Token)
if ([string]::IsNullOrWhiteSpace($Token))
{
return $Token
}
$lower = $Token.ToLowerInvariant()
if ($lower -match '^f(\d{1,2})$')
{
return ('F' + $matches[1])
}
if ($lower -match '^sc\((\d+)\)$')
{
return ('Scancode(' + $matches[1] + ')')
}
if ($lower -match '^numpad_(.+)$')
{
$suffix = $matches[1]
if ($suffix -eq 'plus')
{
$suffix = 'Plus'
}
elseif ($suffix -eq 'minus')
{
$suffix = 'Minus'
}
return ('NumPad_' + $suffix)
}
switch ($lower)
{
'enter' { return 'Enter' }
'plus' { return 'Plus' }
'minus' { return 'Minus' }
'insert' { return 'Insert' }
'delete' { return 'Delete' }
'del' { return 'Del' }
'numpad_plus' { return 'NumPad_Plus' }
'numpad_minus' { return 'NumPad_Minus' }
'period' { return '.' }
'menu' { return 'Menu' }
'tab' { return 'Tab' }
'pgdn' { return 'PgDn' }
'pgup' { return 'PgUp' }
'down' { return 'DownArrow' }
'up' { return 'UpArrow' }
'home' { return 'Home' }
'end' { return 'End' }
'left' { return 'LeftArrow' }
'right' { return 'RightArrow' }
'space' { return 'Space' }
default { return $lower }
}
}
# Split key sequences on commas that are not part of a key token like Ctrl+,
$normalizedChords = foreach ($chord in ($Key -split '(?<!\+),'))
{
$tokens = @($chord -split '\+' | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
if ($tokens.Count -eq 0)
{
continue
}
$mods = @{}
$nonMods = New-Object System.Collections.Generic.List[string]
foreach ($token in $tokens)
{
$lowerToken = $token.ToLowerInvariant()
if ($modifierMap.ContainsKey($lowerToken))
{
$mods[$modifierMap[$lowerToken]] = $true
}
else
{
$nonMods.Add((Convert-WTNonModifierToken -Token $token))
}
}
$orderedMods = @(
foreach ($m in $modifierOrder)
{
if ($mods.ContainsKey($m))
{
$m
}
}
)
$nonModifierTokens = @($nonMods.ToArray())
@($orderedMods + $nonModifierTokens) -join '+'
}
($normalizedChords -join ',')
}
function Convert-WTActionToObject {
param(
[PSCustomObject] $Action,
[ValidateSet('Default', 'User')]
[string] $Source
)
if ($null -eq $Action)
{
return @()
}
$keys = @()
if ($Action.PSObject.Properties.Name -contains 'keys')
{
if ($Action.keys -is [array])
{
$keys = @($Action.keys)
}
elseif (-not [string]::IsNullOrWhiteSpace([string]$Action.keys))
{
$keys = @([string]$Action.keys)
}
}
if ($keys.Count -eq 0)
{
return @()
}
$commandName = $null
$parameters = $null
if ($Action.PSObject.Properties.Name -contains 'command')
{
if ($Action.command -is [string])
{
$commandName = $Action.command
}
elseif ($null -ne $Action.command)
{
$commandObj = $Action.command
if ($commandObj.PSObject.Properties.Name -contains 'action')
{
$commandName = $commandObj.action
}
$parameterMap = [ordered]@{}
foreach ($prop in $commandObj.PSObject.Properties)
{
if ($prop.Name -ne 'action')
{
$parameterMap[$prop.Name] = $prop.Value
}
}
if ($parameterMap.Count -gt 0)
{
$parameters = [PSCustomObject]$parameterMap
}
}
}
elseif ($Action.PSObject.Properties.Name -contains 'action')
{
# Backward-compatible fallback for older objects.
$commandName = $Action.action
}
$result = foreach ($key in $keys)
{
$normalizedKey = Get-NormalizedWTKey -Key ([string]$key)
if ($null -ne $normalizedKey)
{
[PSCustomObject][ordered]@{
Command = $commandName
Parameters = $parameters
Keys = Get-CanonicalWTKeyDisplay -Key ([string]$key)
Source = $Source
OverridesDefault = $false
_NormalizedKey = $normalizedKey
}
}
}
@($result)
}
function Get-WTCommandInfo {
param([PSCustomObject] $Node)
$commandName = $null
$parameters = $null
if ($null -eq $Node)
{
return [PSCustomObject]@{ Command = $null; Parameters = $null }
}
if ($Node.PSObject.Properties.Name -contains 'command')
{
if ($Node.command -is [string])
{
$commandName = $Node.command
}
elseif ($null -ne $Node.command)
{
$commandObj = $Node.command
if ($commandObj.PSObject.Properties.Name -contains 'action')
{
$commandName = $commandObj.action
}
$parameterMap = [ordered]@{}
foreach ($prop in $commandObj.PSObject.Properties)
{
if ($prop.Name -ne 'action')
{
$parameterMap[$prop.Name] = $prop.Value
}
}
if ($parameterMap.Count -gt 0)
{
$parameters = [PSCustomObject]$parameterMap
}
}
}
elseif ($Node.PSObject.Properties.Name -contains 'action')
{
$commandName = $Node.action
}
[PSCustomObject]@{ Command = $commandName; Parameters = $parameters }
}
function Get-WTActionMapById {
param([PSCustomObject] $Settings)
$actionById = @{}
if ($null -eq $Settings)
{
return $actionById
}
$actionsArray = @()
if ($Settings.PSObject.Properties.Name -contains 'actions')
{
$actionsArray = @($Settings.actions)
}
foreach ($action in $actionsArray)
{
if (($action.PSObject.Properties.Name -contains 'id') -and -not [string]::IsNullOrWhiteSpace([string]$action.id))
{
$actionById[[string]$action.id] = Get-WTCommandInfo -Node $action
}
}
$actionById
}
function Convert-WTSettingsToBindingObjects {
param(
[PSCustomObject] $Settings,
[ValidateSet('Default', 'User')]
[string] $Source,
[hashtable] $FallbackActionById = @{}
)
$result = @()
$actionById = @{}
$actionsArray = @()
if ($Settings.PSObject.Properties.Name -contains 'actions')
{
$actionsArray = @($Settings.actions)
}
foreach ($action in $actionsArray)
{
$cmdInfo = Get-WTCommandInfo -Node $action
if (($action.PSObject.Properties.Name -contains 'id') -and -not [string]::IsNullOrWhiteSpace([string]$action.id))
{
$actionById[[string]$action.id] = $cmdInfo
}
if ($action.PSObject.Properties.Name -contains 'keys')
{
foreach ($binding in (Convert-WTActionToObject -Action $action -Source $Source))
{
$result += $binding
}
}
}
$keyBindingsArray = @()
if ($Settings.PSObject.Properties.Name -contains 'keybindings')
{
$keyBindingsArray = @($Settings.keybindings)
}
foreach ($keyBinding in $keyBindingsArray)
{
$keys = @()
if ($keyBinding.keys -is [array])
{
$keys = @($keyBinding.keys)
}
elseif (-not [string]::IsNullOrWhiteSpace([string]$keyBinding.keys))
{
$keys = @([string]$keyBinding.keys)
}
if ($keys.Count -eq 0)
{
continue
}
$cmdInfo = $null
if ($keyBinding.PSObject.Properties.Name -contains 'command')
{
$cmdInfo = Get-WTCommandInfo -Node $keyBinding
}
elseif (($keyBinding.PSObject.Properties.Name -contains 'id') -and $actionById.ContainsKey([string]$keyBinding.id))
{
$cmdInfo = $actionById[[string]$keyBinding.id]
}
elseif (($keyBinding.PSObject.Properties.Name -contains 'id') -and $FallbackActionById.ContainsKey([string]$keyBinding.id))
{
$cmdInfo = $FallbackActionById[[string]$keyBinding.id]
}
else
{
$unresolvedId = $null
if ($keyBinding.PSObject.Properties.Name -contains 'id')
{
$unresolvedId = [string]$keyBinding.id
}
$unresolvedParameters = $null
if (-not [string]::IsNullOrWhiteSpace($unresolvedId))
{
$unresolvedParameters = [PSCustomObject]@{ Id = $unresolvedId }
}
$cmdInfo = [PSCustomObject]@{
Command = '(!!! Unresolved !!!)'
Parameters = $unresolvedParameters
}
}
foreach ($key in $keys)
{
$normalizedKey = Get-NormalizedWTKey -Key ([string]$key)
if ($null -eq $normalizedKey)
{
continue
}
$result += [PSCustomObject][ordered]@{
Command = $cmdInfo.Command
Parameters = $cmdInfo.Parameters
Keys = Get-CanonicalWTKeyDisplay -Key ([string]$key)
Source = $Source
OverridesDefault = $false
_NormalizedKey = $normalizedKey
}
}
}
@($result)
}
$defaultActionById = Get-WTActionMapById -Settings $defaultSettings
$defaultActions = Convert-WTSettingsToBindingObjects -Settings $defaultSettings -Source 'Default'
$userActions = Convert-WTSettingsToBindingObjects -Settings $userSettings -Source 'User' -FallbackActionById $defaultActionById
$userKeys = @{}
foreach ($ua in $userActions)
{
$userKeys[$ua._NormalizedKey] = $true
}
foreach ($ua in $userActions)
{
if ($ua._NormalizedKey -in @($defaultActions._NormalizedKey))
{
$ua.OverridesDefault = $true
}
}
$actions = @($userActions)
foreach ($da in $defaultActions)
{
if (-not $userKeys.ContainsKey($da._NormalizedKey))
{
$actions += $da
}
}
$outputActions = $actions
if ($ExcludeUnresolved)
{
$outputActions = $outputActions | Where-Object { $_.Command -ne '(!!! Unresolved !!!)' }
}
$outputActions |
Sort-Object -Property 'Keys' |
Select-Object Command, Parameters, Keys, Source, OverridesDefault
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment