home

Copy Paste with Neovim and Kitty on MacOS

The kitty terminal emulator has great support for copy and paste using cmd+c and cmd+v on macOS. Neovim has a very robust clipboard system using y and p. Neovim users can configure y and p to integrate directly into the system clipboard. However, I wanted to keep neovim’s default clipboard register separate from the system clipboard. I wanted to use y and p inside vim only, then use cmd+c and cmd+v to copy and paste between neovim and the rest of the system. This turned out to be surprisingly complicated.

First, I separated the neovim and system clipboard by commenting out the following line in my ~/.config/nvim/init.lua file.

-- Don't use the system clipboard by default
-- vim.opt.clipboard = "unnamedplus"

Neovim and kitty have separate systems for selecting text. Therefore, Kitty doesn’t know which text you are trying to copy.

I was excited to learn that I could add cmd mappings in neovim. I added the following keymaps to tell neovim to copy/paste to the system clipboard using cmd+c and cmd+v.

vim.keymap.set({ 'n', 'v' }, '<D-c>', '"+y') -- cmd+c to copy to system clipboard
vim.keymap.set({ 'n', 'v' }, '<D-v>', '"+p') -- cmd+v to paste from system clipboard

However, these keymaps didn’t work at first. The shortcuts were being handled by kitty and never reached neovim at all. I was stuck on this problem for quite a while. I found the solution by looking at the vim-kitty-navigator plugin.

The fix is to map cmd+c and cmd+v to a custom kitten. A kitten is a kitty extension written in python. The code forwards the keys to neovim if it is running. Otherwise, it does the normal kitty clipboard operations. I lifted the code below from vim-kitty-navigator and modified it for my purposes.

I wrote the following program to ~/.config/kitty/pass_keys.py:

import re

from kittens.tui.handler import result_handler
from kitty.key_encoding import KeyEvent, parse_shortcut

def is_window_vim(window, vim_id):
    fp = window.child.foreground_processes
    return any(re.search(vim_id, p['cmdline'][0] if len(p['cmdline']) else '', re.I) for p in fp)

def encode_key_mapping(window, key_mapping):
    mods, key = parse_shortcut(key_mapping)
    event = KeyEvent(
        mods=mods,
        key=key,
        shift=bool(mods & 1),
        alt=bool(mods & 2),
        ctrl=bool(mods & 4),
        super=bool(mods & 8),
        hyper=bool(mods & 16),
        meta=bool(mods & 32),
    ).as_window_system_event()

    return window.encoded_key(event)

def main():
    pass

@result_handler(no_ui=True)
def handle_result(args, result, target_window_id, boss):
    cmd = args[1] # bottom, top, left, right, copy, paste
    key_mapping = args[2] # ctrl+j, ctrl+k, ctrl+h, ctrl+l, ctrl+c, ctrl+v
    vim_id = args[3] if len(args) > 3 else "n?vim"

    window = boss.window_id_map.get(target_window_id)

    if window is None:
        return
    if is_window_vim(window, vim_id):
        for keymap in key_mapping.split(">"):
            encoded = encode_key_mapping(window, keymap)
            window.write_to_child(encoded)
    elif cmd == "copy":  # this is new
        window.copy_and_clear_or_interrupt()
    elif cmd == "paste": # this is new
        window.paste_selection_or_clipboard()
    else:
        boss.active_tab.neighboring_window(cmd)

Lastly, I added the following like to my kitty config in ~/.config/kitty/kitty.conf

map cmd+c kitten pass_keys.py copy cmd+c
map cmd+b kitten pass_keys.py paste cmd+v

Now, when I hit cmd+c or cmd+v, kitty will detect if neovim is running. If neovim is in the foreground, it will forward the keypress so that nvim’s keymaps work using nvim’s selected text. Otherwise, kitty will copy and paste using it’s own methods of copy/paste and selection.

You can find my neovim config file on github and my kitty config file in this gist

With all of this in place, I can use cmd+c and cmd+v to copy/paste to the system clipboard in both the kitty terminal and in neovim. Was it worth it? Probably not. Was it fun? Sure.

2024-04-17