Taking back control of hjkl with modifiers keys

The arrow cluster

One of the most famous lore about vim and its derivative like kakoune is the use of h j k l as arrow keys.
This keys placement is often lauded by the users of these editors as a power-feature because the fingers barely have to move from the home-row of the keyboard.

Strangely, this kind of spatial considerations is almost unique to the h j k l cluster. Other keys tend to be placed on the keyboard according to some mnemonics related to their name.
Non exhaustive examples using the “first letter”:

  • w for Word or Window
  • f for Find
  • b for Backward or Before
  • n for Next
  • p for Previous or Paste

More extreme examples, coming from regexp notation:

  • ^ for Goto first non-blank char of line
  • $ for Goto end of the line

So as we can see the rules to affect a command to a key are quite arbitrary.
But let’s focus back on the arrow cluster and on kakoune.

  • h, j, k, l move the selection by 1 char
  • shift + h, j, k, l extend the selection by 1 char (or line)

These 2 sets of keys above are coherent.
The discrepancy appears when the alt modifier is involved.

Good:

  • alt + h, alt + l select to line begin / end

Less good:

  • alt + j Join lines (legacy from vim)
  • alt + k Keep matching selections (in vim K opens the keywordprg)

Suddenly the “spatial trait” of j and k are exchanged for the “mnemonic trait”.
Arguably, joining lines is a pretty common operation, but filtering selections on a regex is less common (at least in my usage of kakoune, your mileage may vary).

This got me thinking:

Should these 2 commands move somewhere else? If so, which commands should take their place?

The ctrl modifier

But before diving into these questions, we should address the elephant in the room: what about the ctrl modifier?
Because of well-known historical reasons dating back to the prehistory of computers, terminal generate ambiguous keys for the following scenarios:

  • ctrl + itab
  • ctrl + mret

More painfully related to our present discussion:

  • ctrl + hbackspace
  • ctrl + jret

Which means that in a regular setup, our mighty arrow cluster is amputated of the h and j keys when used with the ctrl modifier.

One could argue that vim and kakoune try to minimize the use of actions involving modifiers. (a great counter example in vim world is the u / ctrl + r combo).
I agree that commands involving 2 modifiers like shift + alt + k are indeed cumbersome to type. But on the contrary, commands where the left ctrl (pinky because remapped on caps-lock) and left alt (under the thumb) is pressed
in combination with a right-handed letter (like our h, j, k, l candidates here) are quite comfortable (for my hands at least).

Therefore, what would happen if we were able to disambiguate the ctrl + h and ctrl + j, to claim back the arrow cluster? Along the years, attempts have been made to fix this problem, most notably libtermkey. Kitty, also offers an interesting fullkbd mode but kakoune CSI parser needs to be adapted to take this protocol into account (in the future maybe?). But right now, we can nevertheless use kitty to build our own “hacky” solution.

~/.config/kitty/kitty.conf

map ctrl+h send_text all \u24D7
map ctrl+i send_text all \u24D8
map ctrl+j send_text all \u24D9
map ctrl+m send_text all \u24DC

These unicode code-points have been chosen because they are rare in the wild and their representation is a circled letter like this: ⓘ (\u24D8).
Thanks to this “hack” (which can be adapted to other terminal emulators having remapping capabilities), we can now use these chars in kakoune mapping, like I will show in the rest of this post.

Adapting the layout

Without further ado, let’s discover my current experiment resuming all of the above observations. (I stress out the word experiment as it’s by no means a definitive stance)

Let’s free the alt + k (and alt + K) key, by moving it to the D (and alt + D) key. The mnemonic kind of work. Regular d “delete the content of selections”, whereas this new D “delete the selections themselves”.
Let’s free the alt + j (and alt + J) key, by moving it in place of C (and alt + C). This time the mnemonic can be “Combine lines”.

Now that they are free, let’s affect them a duo of commands that are often requested in the GitHub issues. Kakoune offers the X key to extend down, but has no equivalent to extend up.
Hopefully, many users provided dedicated commands like these ones:

define-command -hidden -params 1 extend-line-down %{
  execute-keys "<a-:>%arg{1}X"
}

define-command -hidden  -params 1 extend-line-up %{
  execute-keys "<a-:><a-;>%arg{1}K<a-;>"
  try %{
    execute-keys -draft ';<a-K>\n<ret>'
    execute-keys X
  }
  execute-keys '<a-;><a-X>'
}

So we can map alt + k to extend-line-up and alt + j to extend-line-down.

But “what about the C and alt + C key you sacrificed earlier?” you may asked.

Here’s where hacking the ctrl key becomes very handy. Because we can now map C (copy selection to next line) to ctrl + j and alt + C to ctrl + k!

map global normal ⓙ 'C'         -docstring 'copy selection on next line'
map global normal <c-k> '<a-C>'  -docstring 'copy selection on previous line'

I’m still thinking about what would be a great choice for ctrl + h and ctrl + l to complement this arrow. It could be to generate arbitrary selections on the left/right of the same line. (WIP)

Conclusion

Overall it feels very natural to gain back the “spatial attributes” of h j k l with the shift, alt or ctrl modifiers.

I also try to apply the same philosophy when designing user-modes, privileging this arrow for commands having a before / after or next / previous behaviors.

11 Likes

I love my ctrl+j, thank you so much :100:

I think it’s also beneficial to remap y to at least for qwerty users. That way yank and select register are on the same key. And here is win win, map <y> to <a-k> and
<Y> to <a-K>. Letter Y reminds a funnel which I think corresponds to keep matching operations thus easy to mnemonic.

2 Likes

Here is the “hack” / config for alacritty:

# ~/.config/alacritty/alacritty.yml
key_bindings:
  - { key: H,         mods: Control,       chars: "\u24D7" }
  - { key: I,         mods: Control,       chars: "\u24D8" }
  - { key: J,         mods: Control,       chars: "\u24D9" }
  - { key: M,         mods: Control,       chars: "\u24DC" }
  - { key: Return,    mods: Control,       chars: "\u240D" }

@Delapouite I also remapped <c-ret> to “” - otherwise it just maps to <ret> (you might incorporate it into your reconquest of today) - see: <c-ret>

2 Likes

What would be freed <a-J> and <a-K>? I think we could use them, too … Since Shift signals extension maybe use them for extending up and down rather than <a-j> & <a-k> — which then can be used for something else. — maybe a better version for combining into next or previous line?


<a-k> / <a-j>: move half page up / down. Eureka! That default binding is also way too “awkward” (in the spirit of this analysis) — I can’t even tell which it is.

However, to be really analogous to j & k, not the buffer should jump, but the cursor should jump half a buffer but in a way that the buffer moves “underneath the cursor”. How would I do that?

How to page move while keeping cursor centered?set global scrolloff 30,30


@delapouite buffer’s:
<c-h>: → q
<c-H>: → Q
<c-a-h>: → <a-q>
<c-a-H>: → <a-Q> - maybe this should instead cumulatively extend — the punctuation expansion is probably mostly useful on single word selects rather than on (Shift-)expansion selects.
— I think it’s normally b, B, <a-b> & <a-B>
<c-l>: → e
<c-L>: → E - this needs a similar hack
<c-a-l>: → <a-e>
<c-a-L>: → <a-E> - this needs a similar hack / see “cumulative comment” above.
w & derivations would stick at their place but b & e are the symmetic pair.


Maybe use private area instead: https://unicode.org/charts/PDF/UE000.pdf

Range: E000-F8FF


The “hack”, then looks like

# ~/.config/alacritty/alacritty.yml
key_bindings:
  - { key: H,         mods: Control,             chars: "\uE100" }
  - { key: H,         mods: Control|Shift,       chars: "\uE101" }
  - { key: H,         mods: Control|Shift|Alt,   chars: "\uE102" }
  - { key: H,         mods: Control|Alt,         chars: "\uE103" }
  - { key: I,         mods: Control,             chars: "\uE110" }
  - { key: I,         mods: Control|Shift,       chars: "\uE111" }
  - { key: I,         mods: Control|Shift|Alt,   chars: "\uE112" }
  - { key: I,         mods: Control|Alt,         chars: "\uE113" }
  - { key: J,         mods: Control,             chars: "\uE120" }
  - { key: J,         mods: Control|Shift,       chars: "\uE121" }
  - { key: J,         mods: Control|Shift|Alt,   chars: "\uE122" }
  - { key: J,         mods: Control|Alt,         chars: "\uE123" }
  - { key: M,         mods: Control,             chars: "\uE130" }
  - { key: M,         mods: Control|Shift,       chars: "\uE131" }
  - { key: M,         mods: Control|Shift|Alt,   chars: "\uE132" }
  - { key: M,         mods: Control|Alt,         chars: "\uE133" }

# Ctrl+Shift are problematic on those too:
  - { key: L,         mods: Control|Shift,       chars: "\uE141" }
  - { key: L,         mods: Control|Shift|Alt,   chars: "\uE142" }
  - { key: K,         mods: Control|Shift,       chars: "\uE151" }
  - { key: K,         mods: Control|Shift|Alt,   chars: "\uE152" }

  - { key: Return,    mods: Control,             chars: "\u240D" }


since <c-J> & <c-K> are still unused, I’d propose:

  • change <c-j> & <c-k> to not cumulatively extend the selection, but behave vertically what B and E would do horizontally if they would be pressed in sequence.
  • map <c-J> & <c-K> to extend one line up / down (non-cumulatively)
  • combined with <alt>: select cumulatively

`EBEB` cumulative? `C<a-C>C<a-C>` non-cumulative?
'<c-H> <a-:><a-semicolon>B'
<c-L> '<a-:>E'
→ but no quick solution (yet) for C<a-C>C<a-C>


Here is my current config: — I’m already pretty happy!

define-command -hidden -params 1 extend-line-down %{
  execute-keys "<a-:>%arg{1}X"
}

define-command -hidden  -params 1 extend-line-up %{
  execute-keys "<a-:><a-;>%arg{1}K<a-;>"
  try %{
    execute-keys -draft ';<a-K>\n<ret>'
    execute-keys X
  }
  execute-keys '<a-;><a-X>'
}
map global normal <c-k> '<a-C>'                             -docstring 'copy selection on previous line' # <c-k>
map global normal      ': extend-line-up 1<ret>'           -docstring 'extend selection one line up'    # <c-K>
map global normal         'C'                              -docstring 'copy selection on next line'     # <c-j>
map global normal      ': extend-line-down 1<ret>'         -docstring 'extend selection one line down'  # <c-J>
map global normal D     '<a-k>'    # keep matching
map global normal <a-D> '<a-K>'    # keep not matching
map global normal C     '<a-j>'    # join lines
map global normal <a-C> '<a-J>'    # alt join lines
map global normal <a-k> '<c-u>'    # half page up
map global normal <a-j> '<c-d>'    # half page down

map global normal                         'b'    # <c-h>
map global normal       '<a-:><a-semicolon>B'    # <c-H>
map global normal                      '<a-b>'   # <c-a-h>
map global normal                         'B'    # <c-a-H>

map global normal <c-l>         'e'  # <c-l>
map global normal         '<a-:>E'  # <c-L>
map global normal <c-a-l>    '<a-e>' # <c-a-l>
map global normal              'E'  # <c-a-L>

# Keep cursor relatively centered when scrolling (<a-k> / <a-j>)
hook global WinResize .* %{
    set-option window scrolloff %sh{
        echo $(( (kak_window_height - 15) / 2 )),30
    }
}
1 Like