Pull of the Undercurl

February 18, 2023

Golang program in Neovim showing undercurls

One day I popped open Alacritty to edit something quickly in Neovim. And it was going to be such a quick edit that I didn't bother to start a tmux session first. When I got into Neovim I saw it... a little red squiggly line underneath a variable with a typo in the name. It was so beautiful. A single tear rolled down my cheek at the sight of this thing that I later learned was called an undercurl.

The undercurl entered my life on that fateful day for a few reasons:

  1. The LSP server sends diagnostic errors, which Neovim as the LSP client can mark with some sort of underline.
  2. Neovim added undercurl support.
  3. Alacritty added undercurl support.

The very next day I started a tmux session inside of Alacritty, opened Neovim, and was aghast to see that the diagnostics were once again a pedestrian regular underline and in the wrong color to boot.

It worked in Alacritty but not when combined with tmux... but why?

The Solution

Right up front without reading the rest, here's what you need.

Paste this into your .tmux.conf. As noted in the comments, you'll need at least tmux 3.0 for colors, and you may need to update your terminfo if you're on MacOS or just generally have an older terminfo. Older terminfo won't recognize tmux-256color, which Neovim prefers to work with.

# ==================
# {n}vim compability
# MacOS ships with a very old ncurses and terminfo. May need to update terminfo to recognize tmux-256color. 
# https://gist.github.com/bbqtd/a4ac060d6f6b9ea6fe3aabe735aa9d95
set -g default-terminal "tmux-256color"

set-option -gas terminal-overrides "*:Tc" # true color support
set-option -gas terminal-overrides "*:RGB" # true color support
set -as terminal-overrides ',*:Smulx=\E[4::%p1%dm'  # undercurl support
set -as terminal-overrides ',*:Setulc=\E[58::2::%p1%{65536}%/%d::%p1%{256}%/%{255}%&%d::%p1%{255}%&%d%;m'  # underscore colours - needs tmux-3.0

Explanation

The undercurl was originally popularized by kitty. The creator decided to use the CSI code 58 to set underline colors and supported a style extension to 4 to get the undercurl. You can try the escape sequences out in your terminal like this: printf '\e[4:3m\e[58:2:206:134:51mUnderlined\n\e[0m'.

If your terminal supports it, that will print out the word "Underlined" with an orange squiggly underneath it. The \e[4:3m is the escape sequence to add a curly underline. There are other options that you can try yourself.

<ESC>[4:0m  # no underline
<ESC>[4:1m  # straight underline
<ESC>[4:2m  # double underline
<ESC>[4:3m  # curly underline
<ESC>[4:4m  # dotted underline
<ESC>[4:5m  # dashed underline
<ESC>[4m    # straight underline (for backwards compat)
<ESC>[24m   # no underline (for backwards compat)

The \e[58:2:206:134:51m is the escape sequence to color the underline. The 58 says this will be about underline colors, the 2 says the following values will be RGB, and then 206:134:51 are said RGB values. You can change those numbers to get different colors.

Now that we know a bit about escape sequences, the two lines from above start to make a bit more sense:

set -as terminal-overrides ',*:Smulx=\E[4::%p1%dm'  # undercurl support
set -as terminal-overrides ',*:Setulc=\E[58::2::%p1%{65536}%/%d::%p1%{256}%/%{255}%&%d::%p1%{255}%&%d%;m'  # underscore colours - needs tmux-3.0

You can see the \E[4 and \E[58 in there followed by sequences that allow tmux to interpolate incoming values to handle the escape sequences we just saw above.

The last parts to understand here are terminal-overrides and what the heck are Smulx and Setulc? The terminal-overrides allow terminal descriptions read using terminfo to be overriden. And Smulx and Setulc are terminfo capability names. You can read more about what terminfo is here, but my basic understanding is that it lets calling applications ask for the capability without needing to know exactly how each terminal has it implemented. So you as the application writer could say, hey I need an undercurl so I'm asking for Smulx and any terminal that knows how to Smulx will then match on that and provide the relevant escape codes. And as the application author I didn't need to write it to know the sequence that every terminal needs to see to make an undercurl. Yay abstraction.

So in the end, when our underlying terminal is * (anything, but you could be more specific and just narrow it down to alacritty, for example) and tmux receives a Smulx from Neovim, it will spit out something like E[4:2m, which the host terminal Alacritty understands and renders into an actual wobbly red (or orange, or blue, or whatever) line.