6

I have installed the PSReadline module for PowerShell to get Keybindings from Bash in PowerShell. I enabled Vi-Mode and it works good.

The problem is: In Vim I always use j, k to exit insert-mode. That means: First I type j and then k very fast. If I really want to type j and k then I just wait for the timeout after typing j.

How can I do the same in Vi-Mode in PSReadline? I already tried: Set-PSReadlineKeyHandler -Chord 'j', 'k' ViCommandMode, but then I couldn't type j or k anymore. Any ideas?

JasonMArcher
  • 14,195
  • 22
  • 56
  • 52
itmuckel
  • 1,300
  • 15
  • 23
  • 1
    I added this as an issue enhancement on the PSReadline github https://github.com/PowerShell/PSReadLine/issues/1701 – David Hatch Jul 27 '20 at 04:49

2 Answers2

1

To achieve this, put the following in your $Profile:

Set-PSReadLineKeyHandler -Chord 'j' -ScriptBlock {
  if ([Microsoft.PowerShell.PSConsoleReadLine]::InViInsertMode()) {
    $key = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
    if ($key.Character -eq 'k') {
      [Microsoft.PowerShell.PSConsoleReadLine]::ViCommandMode()
    }
    else {
      [Microsoft.Powershell.PSConsoleReadLine]::Insert('j')
      [Microsoft.Powershell.PSConsoleReadLine]::Insert($key.Character)
    }
  }
}

This may cause problems with pasting 'j', however.

1

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 }

silico-biomancer
  • 281
  • 1
  • 2
  • 13
  • 1
    This does not work on pwsh (Powershell Core) -ComObject is not known. – quadroid Jun 24 '20 at 06:18
  • Useful caveat to know. The line `$wshell = New-Object -ComObject wscript.shell` is needed to insert a return key. I don't know what Posh extension provides that. – silico-biomancer Jun 24 '20 at 06:25