I'm surprised this question isn't more popular.
The issue with Corben's answer is that, after pressing 'j', if the next key pressed is return or a modifier like ctrl, a literal is inserted instead of the key you would expect being used.
I've re-written the answer to fix these two problems, and also turned it into a function to make it easier to re-use (for example when binding two different letters, like jk).
Set-PSReadLineKeyHandler -vimode insert -Chord "k" -ScriptBlock { mapTwoLetterNormal 'k' 'j' }
Set-PSReadLineKeyHandler -vimode insert -Chord "j" -ScriptBlock { mapTwoLetterNormal 'j' 'k' }
function mapTwoLetterNormal($a, $b){
mapTwoLetterFunc $a $b -func $function:setViCommandMode
}
function setViCommandMode{
[Microsoft.PowerShell.PSConsoleReadLine]::ViCommandMode()
}
function mapTwoLetterFunc($a,$b,$func) {
if ([Microsoft.PowerShell.PSConsoleReadLine]::InViInsertMode()) {
$key = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
if ($key.Character -eq $b) {
&$func
} else {
[Microsoft.Powershell.PSConsoleReadLine]::Insert("$a")
# Representation of modifiers (like shift) when ReadKey uses IncludeKeyDown
if ($key.Character -eq 0x00) {
return
} else {
# Insert func above converts escape characters to their literals, e.g.
# converts return to ^M. This doesn't.
$wshell = New-Object -ComObject wscript.shell
$wshell.SendKeys("{$($key.Character)}")
}
}
}
}
# Bonus example
function replaceWithExit {
[Microsoft.PowerShell.PSConsoleReadLine]::BackwardKillLine()
[Microsoft.PowerShell.PSConsoleReadLine]::KillLine()
[Microsoft.PowerShell.PSConsoleReadLine]::Insert('exit')
}
Set-PSReadLineKeyHandler -Chord ";" -ScriptBlock { mapTwoLetterFunc ';' 'q' -func $function:replaceWithExit }