Seamless Panes and Splits With Vim

Over the past few months, I’ve been on a bit of a journey through different OSes1, desktop environments, apps, and even editors. Ultimately I settled on Arch Linux + KDE Plasma + Neovim, but I ended up with some different ideas along the way.

One such idea was to ditch tmux and simply use a modern, more featureful terminal emulator. I primarily used tmux for splitting “windows” and its scrollback buffer, but it contributed another layer of abstraction to debug when things like performance, colour support, or displaying images became a problem. Meanwhile, almost any modern terminal emulator can do splits and scrollback buffers.

So I settled on Konsole, which supports all that I need and some other niceties (like ligatures). I set up my key bindings to create splits similarly to those I used in Tmux, and… immediately missed a feature of my tmux setup. This was the ability to switch between tmux panes and vim splits using the same key combination, as though they were in fact the same thing.

I searched around to see if anyone had solved this problem — as they had for tmux, which I previously adapted — but came up with no results. So I set about the task. Turns out it requires several components to get right, and is very specific to one’s overall environment. Since the ideas might be useful to others trying to do similarly, I’ve documented this as a bit of a deep dive.

Do note that I’m running Plasma under Xorg and not Wayland; an important distinction as several tools won’t work under the latter.

The requirements

We need three things to get this all working:

  1. Scripting the terminal emulator to contextually switch panes or delegate to vim
  2. Mappings in vim which contextually switch splits or delegate to the terminal emulator
  3. Mapping a key combination to run our script inside the terminal emulator

1: Scripting the Terminal Emulator

So there are two things we need to do here:

  1. Determine the program currently running in active terminal (tab/split)
  2. If it’s vim, tell vim to do something. Otherwise, move to a split.

Side-note for those considering D-Bus for this: Konsole does support D-Bus, but I wasn’t able to use it because this script will not necessarily be run in the context of the terminal2, meaning it’s very difficult to determine the current Konsole window.

We can determine the running program by looking at the window title. This is configurable, but using default settings it’ll always be <current_path>: <program>; e.g. ~: zsh or Documents: vim. Under Plasma, the full window title ends up being <current_path: <program> — Konsole.

I chose to use xdotool to retrieve the window title, but I’m sure there are other tools:

xdotool getwindowfocus getwindowname

Given the window title, we now need a way to switch on the current command. We can use bash regex matching for this:

[[ "$(xdotool getwindowfocus getwindowname)" =~ n?vim\ \ Konsole$ ]] && echo "vim" || "echo not vim"

Finally, what do we need to tell vim and what do we need to tell Konsole? This question is partly answered later, but for now we need to pass vim our final key combination (e.g. Ctrl+H to move to the left pane), whilst we pass a generic mapping to Konsole — one I’m not necessarily going to use, but which switches the pane. So let’s throw this all into a script:

#!/usr/bin/env bash
# Navigates between panes in vim and konsole
# Depends on xdotool.
command=$1

function is_vim {
  local windowName=$(xdotool getwindowfocus getwindowname)

  [[ "$windowName" =~ n?vim\ \ Konsole$ ]] && echo 1
}

function left {
  if [[ $(is_vim) == 1 ]]; then
    xdotool key "ctrl+h"
  else
    xdotool key "ctrl+shift+Left"
  fi
}

function right {
  if [[ $(is_vim) == 1 ]]; then
    xdotool key "ctrl+l"
  else
    xdotool key "ctrl+shift+Right"
  fi
}

function down {
  if [[ $(is_vim) == 1 ]]; then
    xdotool key "ctrl+j"
  else
    xdotool key "ctrl+shift+Down"
  fi
}

function up {
  if [[ $(is_vim) == 1 ]]; then
    xdotool key "ctrl+k"
  else
    xdotool key "ctrl+shift+Up"
  fi
}

function command_not_found {
  echo 'Usage: konsole-vim-navigate [up|down|left|right]'
  exit 1
}

case "${command}" in
  left) left;;
  right) right;;
  up) up;;
  down) down;;
  *)
    command_not_found
esac

Testing the script for Konsole pane switching, it works great:

./konsole-vim-navigate left

So I placed that somewhere in the $PATH and moved on.

2: The Vim Part

In vim we need to specify our preferred mappings for split panes and call a function on each:

nnoremap <silent> <c-h> :call KonsolePane('h', 'Left')<CR>
nnoremap <silent> <c-j> :call KonsolePane('j', 'Down')<CR>
nnoremap <silent> <c-k> :call KonsolePane('k', 'Up')<CR>
nnoremap <silent> <c-l> :call KonsolePane('l', 'Right')<CR>

The function simply needs to check if there is a pane in that direction and switch to it if so, else tell the terminal emulator to move in that direction instead. We re-use our generic pane mappings here:

function! KonsolePane(direction, key)
  let wnr = winnr()
  silent! execute 'wincmd ' . a:direction

  " If the winnr is still the same after we moved, it is the last pane
  if wnr == winnr()
    call system('xdotool key "ctrl+shift+' . a:key . '"')
  end
endfunction

3: Mapping

This was probably the hardest part. Plasma does support mapping a key combination to an arbitrary command via custom shortcuts, but only at a global level3. I also explored xbindkeys and sxhkd but they both had the same shortcoming. And there’s AutoKey which comes with a GUI and seems like it would be able to handle my use case, but sadly I couldn’t get it working with Konsole4.

Eventually I stumbled upon xkeysnail and this finally did it.

Here’s the config:

import re
from xkeysnail.transform import *

define_keymap(re.compile("konsole"), {
    K("LC-J"): launch(["konsole-vim-navigate", "down"]),
    K("LC-K"): launch(["konsole-vim-navigate", "up"]),
    K("LC-H"): launch(["konsole-vim-navigate", "left"]),
    K("LC-L"): launch(["konsole-vim-navigate", "right"]),
}, "Konsole")

All it does is filter windows for the class konsole and launch a script on keypress. You might remember that we’re explicitly sending the same key-strokes to vim with our script, but xkeysnail doesn’t listen to virtual keystrokes so we don’t end up in an infinite loop.

Do note that xkeysnail needs to be run as your user for this to work, which may mean you have to add your user to the input group. Otherwise, see this issue.

Summary

Okay, that’s a hell of a lot of work for something that seems so trivial, but muscle memory built up over years can be difficult to re-train, and now I don’t have to consider my context every time I switch pane. Besides, I learnt a couple of things and have have a very nice framework for building more custom shortcuts in other applications.


  1. There’s a particularly unpleasant story behind this that I intend to write up in the future. ↩︎

  2. With the method I use to map the key, it won’t. ↩︎

  3. It is possible to filter by window class, but because hotkeys can only be mapped to one shortcut at a time, we can’t re-use that shortcut in other applications. Hopefully we’ll see an improvement in the future. ↩︎

  4. I ran into some bug reports which seem to be related to KDE / terminal functionality. ↩︎