Theming With Neovim

For as long as I’ve been a Neovim user, I’ve relied on themes created by other people. For the most part, this has been a wonderful experience as there are plenty of great looking themes out there. That said, every now and then, I do run into issues. One example of this is when I want to make a specific style change in my editor, but it conflicts with my current theme. A few weeks ago I posted a retrospective in which I shared I have been using Tmux to organize my projects. Another pain point I come across is getting my theme to play nice with some of my Tmux defined styles.

While these issues rarely happen, they are pretty annoying when they do. So this week I decided to get to the bottom of this by creating my own Neovim theme and learning what is what so that I can deal with these issues directly next time. The rest of this post will cover what I learned, and how you can create a basic Neovim theme yourself.

Highlight All the Things

To start, let’s cover the command that is the bread and butter of creating our theme, highlight. The highlight command accepts a bunch of optional arguments, but we will only use the following:

  • cterm – a comma separated list of attributes (bold,underline,…)
  • ctermfg – a terminal foreground color
  • ctermbg – a terminal background color
  • gui – same as cterm above
  • guifg – a gui foreground color
  • guibg – a gui background color

You’ll notice some repetition in the arguments above. This is because Neovim allows you to target two different UIs when defining highlights:

  1. cterm refers to the terminal UI. The number of available cterm colors depends on your system, but this will likely be 256.
  2. gui refers to Neovim GUIs. This can be enabled for terminal UIs as well depending on your system. This option accepts a wider range of colors, including hex values which we will be using in this post.

With that background out of the way, let’s see how we can use this command to apply a color change:

hi Normal ctermfg=255 ctermbg='NONE' guifg='#eeeeee' guibg='NONE'

Let’s break this down. hi here is shorthand for the full highlight command. Normal is the name of a highlight group, targeting normal text (more on this later). Then we have four of the previously listed arguments. ctermfg and guifg in this case correspond to the color of the text, while ctermbg and guibg refer to the background color of this text. You’ll notice the background values have been set to NONE, which equates to transparent.

Call it a Theme

And that’s it! At least, this is in essence all we need to create a theme. But where do we put it? You could go with your init.vim config file. This works fine for small style changes here and there, but if your goal is to fully customize your editor, then you’ll likely not want to clutter up your config file with too many theme specific configuration. This is what the colors directory is for. Neovim actually expects any color schemes (themes) to exist in this directory. So you’ll want to create a new theme file here. In my case, this is ~/.config/nvim/colors/custom.vim. We can then tell Neovim to load this color scheme on startup by adding the following line in your main config file:

colorscheme custom

With that, we’ve set the foundation for our basic theme. We’ve created a home for our changes to live, and made sure to load them in Neovim. Now all that is left is to start highlighting! In order to do that, we need to identify highlight groups, categories of elements that make up the front-end of Neovim, and apply our desired colors or attributes. There are tons of great resources where you can learn about these groups, but the best is from Neovim itself. Entering the following command in the editor will not only list the available groups, but also show them in their current color scheme:

 :so $VIMRUNTIME/syntax/hitest.vim

Alternatively, if you enter highlight with no arguments, you will see a list of all available groups and the values set for the various cterm and gui arguments:

:hi

Colors, Colors, Colors

So we have all of the tools we need to create our theme, but we’re missing one vital piece. A color palette! In my case, I wanted to stick with the 256 xterm colors for compatibility, and ended up with the following:

Screenshot from colorpeek

Color palette decided, we can now add this to our custom.vim theme file:

let s:white={ "xterm": 255, "hex": "#eeeeee" }
let s:lgray={ "xterm": 248, "hex": "#a8a8a8" }
let s:gray={ "xterm": 238, "hex": "#444444" }
let s:dgray={ "xterm": 234, "hex": "#1c1c1c" }
let s:black={ "xterm": 232, "hex": "#080808" }
let s:blue={ "xterm": 81, "hex": "#5fd7ff" }
let s:dblue={ "xterm": 32, "hex": "#0087d7" }
let s:green={ "xterm": 157, "hex": "#afffaf" }
let s:yellow={ "xterm": 228, "hex": "#ffff87" }
let s:orange={ "xterm": 215, "hex": "ffaf5f" }
let s:red={ "xterm": 203, "hex": "#ff5f5f" }
let s:purple={ "xterm": 140, "hex": "#af87d7" }

You’ll notice, I’ve assigned a dictionary containing both the colors xterm and hex values to each color variable. This is so we can programmatically execute highlight without having to provide separate values for both each time. This also allows us to update these colors in a single place if we ever want to make changes in the future. Here is the function we’ll use to execute the highlighting:

function! s:hl( group, fg, bg, attr )
	let l:ctermfg = type( a:fg ) == type({}) ? get( a:fg, "xterm" ) : 'none'
	let l:ctermbg = type( a:bg ) == type({}) ? get( a:bg, "xterm" ) : 'none'
	let l:guifg = type( a:fg ) == type({}) ? get( a:fg, "hex" ) : 'none'
	let l:guibg = type( a:bg ) == type({}) ? get( a:bg, "hex" ) : 'none'
	let l:attr = exists( a:attr ) ? a:attr : 'none'
	exec 'hi! ' . a:group . ' cterm=' . l:attr . ' gui=' . l:attr .
		\ ' ctermfg=' . l:ctermfg . ' ctermbg=' . l:ctermbg .
		\ ' guifg=' . l:guifg . ' guibg=' . l:guibg
endfunction

This can be added right under our color definitions. The function accepts a group name, fg and bg dictionaries, and a string of attributes. It then checks for fg and bg types. If an object is not provided, it assumes we want to set the respective value to NONE. Finally it executes the highlight command with the corresponding arguments correctly formatted. We can use this function to highlight a group like so:

call s:hl( 'Normal', s:white, '', '' )

Note the empty strings where we want NONE to be provided. This results in our function executing the following:

hi! Normal cterm='none' gui='none' ctermfg=255 ctermbg='none' guifg='#eeeeee' guibg='none'

The Finished Result

From here, the rest is fairly straightforward. You just have to apply your palette to any relevant groups. I won’t go into which groups represent what in the editor, but will say that :help highlight provides some great information to get you started. You can also find my completed theme in my dot files in Github. Here are some screenshots of my theme in action:

I’ll end this post with a short disclaimer. I’ve not covered everything that is possible with creating Neovim themes, only the basics. For example, it’s possible to apply language specific styling. I encourage you to explore the Neovim documentation for more on themes. It’s also a great idea to check out one of the many amazing themes available for Neovim.

As always, thanks for reading 🙇‍♂️ I hope this has been helpful!

%d bloggers like this: