Synctex With Kitty

21 Jan 2023

I’m a big fan of the terminal. I do almost all of my writing inside kitty, a terminal program, running a text-editor called neovim. As a mathematician, I’m usually writing LaTeX. One of the nice features of LaTeX is called “SyncTeX”, which allows a PDF generated by LaTeX to remember a bit about the source files that compiled it, so that you can jump from the PDF to the source code. The purpose of this post is to describe two ways of doing this, one of which involves only neovim, and the other of which takes advantage of kitty’s scripting powers (and some Lua, my favorite programming language).

Getting Started

To take advantage of SyncTeX, you’ll need a PDF viewer that supports it. Another keyword that’s relevant here is “backward search” or “inverse search”. I’m usually running on either macOS or Linux. In the former, I use Skim, and in the latter, I use Zathura. I believe both of these are free and open source, which I always like.

You’ll also need to make sure your TeX compiler has SyncTeX enabled. I’ll probably talk about my TeX compiler in another post, so I’ll be brief about this: you probably want to set synctex=1 somewhere.

Both of the above PDF viewers require a little configuration to use SyncTeX. For Zathura, you want to create a configuration file: ~/.config/zathura/zathurarc and add the following lines to it.

set synctex true
set synctex-editor-command "COMMAND"

We’ll describe two ways of filling in COMMAND with an actual command.

For Skim, you want to open your Settings with either “Skim > Settings” in the menu or the keyboard shortcut “CMD-,”. With the Settings menu open, navigate to the “Sync” tab. There are two checkbox options, “Check for file changes” and “Reload automatically” which I have checked. The important part is the lower part of the tab, which is about PDF-TeX Sync support. I have Preset: “Custom”, which allows me to fill in the “Command” and “Arguments” pieces myself. Again, I’ll describe how to fill this in later.

A Neovim-Centric Solution

For a while, I was using neovim as my terminal. Let me explain: if you enter neovim and execute the “Ex command” :te, you are dropped into a terminal! This can be very useful; for example you can use neovim’s splits to have a terminal running some command open while you’re also working on a file. I created a convenience shell script called nvt that looks like this.

#!/bin/sh
nvim --listen /tmp/nvim.pipe -c ":te"

Then in ~/.config/kitty/kitty.conf, I set the following: shell /bin/zsh -c nvt. With this setting, whenever I open kitty, it will automatically open an instance of neovim and drop me into a terminal inside of it. This instance of neovim will be listening to the “socket” /tmp/nvim.pipe, which is how we will control it with Zathura or Skim.

Zathura

To control neovim with Zathura, we will replace COMMAND above with the following.

nvim --server /tmp/nvim.pipe --remote-send '<C-\><C-N>:e %{input}<CR>:%{line}<CR>'

Zathura will automagically expand %{input} with the relevant filename and %{line} with the line number.

Skim

The process for Skim is very similar. For the “Command” setting, put nvim and for the “Arguments”, we’ll put the following.

--server /tmp/nvim.pipe --remote-send '<C-\><C-N>:e %file<CR>:%line<CR>'

Like Zathura, Skim automagically expands %file with the filename and %line with the line number. Both of these solutions are using the socket /tmp/nvim.pipe to tell neovim to do something. In particular, they are sending keystrokes. <C-\><C-N> ensures that we are in “Normal mode”, :e, as you know, opens a file, and <CR> is interpreted as a press of the enter key, so that the command is run.

Lua and Kitty

The above works great! I used it for a couple years. Recently, though, I’ve realized that I’m often wanting to juggle multiple “projects” in one terminal. For example, maybe I’m not quite ready to put away my LaTeX document for the day, but I want to pivot to working on a personal coding project like TrianglePTR. It’s perfectly doable to do this in one neovim instance, but I found that I started to get sort of overwhelmed by the presence of two very different contexts within one process.

So, rather than simply force myself to close out of one context before opening another, I’ve decided to take advantage of kitty’s tabs. Each of these tabs is an instance of kitty, so I can use one (or I suppose more) tabs per context. The disadvantage here, though, is that you can only really have one neovim instance listening to a given socket. So to make the switch, I had to find a SyncTeX solution. I ended up settling on the following command (I use Skim’s automagic keywords; you can easily replace them with Zathura’s.)

lua ~/.config/kitty/synctex_kitty.lua "%file" %line

This uses a script in the wonderful little scripting language Lua to “preprocess” the SyncTeX information. To use it, though, you’ll need to have the following in your kitty.conf

listen_on unix:/tmp/kitty
allow_remote_control yes

This is like the /tmp/nvim.pipe stuff we had for neovim earlier, although it will let us drop that stuff entirely. Basically it has the effect of telling kitty to listen to a socket (the socket turns out to be /tmp/kitty-$KITTY_PID, a gotcha that frustrated me for about an hour until I actually read the manual) and allow scripting.

Here is the lua script:

-- ~/.config/kitty/synctex_kitty.lua
local path = arg[1]
path = string.gsub(path, '/%./', '/')
local _, _, folder = string.find(path, "/(%w+)/%w+.tex")
if not folder then return end
local file = io.popen("kitty @ --to unix:/tmp/kitty-$KITTY_PID ls")
if not file then return end
local json = file:read('*a')
file:close()
local flag, id
for line in string.gmatch(json, "[^\r\n]+") do
  if not flag then
    flag = string.find(line, '"title": "nvim ' .. folder)
  else
    _, _, id = string.find(line, '"id": (%d+)')
    if id then break end
  end
end
if not id then return end
local command = "kitty @ --to unix:/tmp/kitty-$KITTY_PID send_text --match-tab id:"
  .. id
  .. " '^[ :e "
  .. path
  .. "\r:"
  .. arg[2]
  .. "\r'"
os.execute(command)

I’m sure there are strictly speaking far more gymnastics here than are necessary, but it works! Let’s talk through it. I tend to have each .tex project that I work on in its own folder, which the first call to string.find is looking for. When you open neovim with a specific command, kitty will set the name of the tab you have open to that command for the duration of your neovim session. This script makes the assumption that you open neovim from outside that folder. So for example, most but not all of my papers are in ~/Research/Papers and one of the folders is named NewCAT0FreebyZ. The expectation is that I will begin by calling nvim NewCAT0FreebyZ/main.tex or similar.

For some reason, I wasn’t able to get kitty’s matching by tab title to work for me, so the next main chunk of the Lua script is using the output of the command kitty @ ls cleverly. This command outputs some JSON that basically tells you the state of the kitty process. I use the folder name (let’s run with our example) to find a tab whose title starts with nvim NewCAT0FreebyZ. Once we find it, we pay attention to the next time we find a line of JSON that says “id”; this will be the id of the relevant tab. Then, similar to how we used --remote-send with neovim, we’ll use send_text to talk to that tab. This makes the hopefully innocuous assumption that we are not in “Terminal mode” in that tab of neovim. The only difference is that we use ^[, which is the code for the escape key, and that kitty expects the newlines to be formatted like \r rather than <CR> as neovim does.