A colorscheme creation micro-framework.
polychrome.nvim is a colorscheme creation micro-framework for Neovim. The
main features of polychrome are:
hsl, Oklab, etc)Here's a fully functional example of a (very basic) colorscheme defined with
polychrome:
-- colors/your_theme.lua
local Colorscheme = require('polychrome').Colorscheme
Colorscheme.define('your_theme', function ()
Normal { fg = rgb(150, 150, 219), bg = rgb(20, 20, 20) }
Comment { fg = rgb(135, 135, 135) }
Conceal { fg = rgb(222, 222, 222), bg = Comment.fg }
Cursor { Reverse }
Directory { fg = '#00ffff' }
SpellBad { Undercurl }
Error { fg = 'red' }
Todo { Error, Standout, Underdouble, Italic }
Constant { fg = rgb(0, 255, 255) }
Boolean { Constant }
_'@string.escape' { Constant }
_'@number' { Constant }
end):apply()
If you want to dive into a complete example already using polychrome.nvim, check
out sunburn.nvim.
To start testing things out, open a new (empty) buffer and run :Polychrome template theme. This will fill the buffer with an empty colorscheme that
contains all the most common highlight groups, and looks something like this:
local Colorscheme = require('polychrome').Colorscheme
-- replace `your_theme` with the name of your colorscheme
Colorscheme.define('your_theme', function ()
-- Normal { }
-- Comment { }
-- ...many other highlight groups
end)
Then, run :Polychrome preview (or :Polychrome preview start) to enable the
live preview mode. You can then start setting highlights, and you'll instantly
see the changes in your current window. You can disable the live preview mode
with :Polychrome preview stop.
[!IMPORTANT] If you are planning on creating a full colorscheme plugin, you may want to skip to the Proper Project Structure section now to start with a flexible project structure.
Check :h polychrome-usage and :h polychrome-attributes for more in-depth
documentation.
Here are a few examples of some of the different syntaxes and helpers that are available:
-- most groups can be specified like this
Constant { fg = rgb(0, 255, 0), bg = '#ff0000' }
-- specify a GUI feature
Underline { underline = true }
-- link a group to another one (equivalent to `:hi link Boolean Constant`)
Boolean { Constant }
-- groups that use `@` or other special characters (ex. treesitter groups)
-- need a slightly different syntax: _'<name>' instead of <name>
_'@punctuation.delimiter' { fg = rgb(0, 255, 0), bg = oklch(0, 0, 0) }
That example shows off the rgb and oklch helper commands, but quite a few
more color systems are supported:
rgb helper)lrgb)p3)linear_p3)hsl)oklab)oklch)cmyk)ciexyz)lms)The helpers are automatically injected into the colorscheme definition context,
so you don't need to require them if you don't want to (although you can
anyway for clarity). Check out :h polychrome-colorspaces for more in-depth
info on all the available helpers.
Here's some quick context for people who aren't familiar with how colorschemes work in Vim/Neovim:
When you run colorscheme your_theme, Neovim looks through the runtimepath for
some file inside <some runtimepath entry>/colors, named your_theme.vim or
your_theme.lua, and if found, runs that file. Historically, colorschemes are
made by simply putting a bunch of hi commands in this file (if you want an
example of this, check out the runtime/colors directory of the neovim source).
Modern plugin managers like lazy.nvim, pckr.nvim, vim-plug, etc. will
handle adding plugins to the runtimepath, so setting up a plugin is simple if
you format your project properly.
The quick start section is great for testing things out, but if you're writing a full theme, it makes sense to set up a repo in a certain way, for maximum flexibility. Here's my recommendation:
.
└── your_theme/
├── lua/
│ └── your_theme/
│ ├── highlights.lua
│ ├── palette.lua
│ └── init.lua
└── colors/
└── your_theme.lua
highlights.lua is where you'll define the bulk of your colorscheme, and this
is largely the same as the file shown in the Quick Start section:
-- your_theme/lua/your_theme/highlights.lua
local Colorscheme = require('polychrome').Colorscheme
local theme = Colorscheme.define('your_theme', function ()
-- Normal { fg = rgb(150, 150, 219), bg = rgb(20, 20, 20) }
end)
return theme
palette.lua is where you'll define all your colors:
-- your_theme/lua/your_theme/palette.lua
local rgb = require('polychrome.color.rgb')
local palette = {
red = rgb(255, 0, 0),
green = rgb(0, 255, 0),
blue = rgb(0, 0, 255),
}
return palette
init.lua can look something like this:
-- your_theme/lua/your_theme/init.lua
local theme = require('your_theme.highlights')
return theme
colors/your_theme.lua should then import your theme object and run :apply()
on it:
-- your_theme/colors/your_theme.lua
local theme = require('your_theme')
-- or
-- local theme = require('your_theme.highlights')
theme:apply()
This way, other plugins can use your palettes (via
require('your_theme.palette')) or other logic.
When you go to write documentation for users of your colorscheme, assuming that
everything is set up correctly, all a user needs to do is install your plugin
and polychrome.nvim, and then run colorscheme your_theme somewhere in their
config. Here's a simple example of a spec for lazy.nvim:
{
'you/your_theme',
dependencies = {
'loganswartz/polychrome.nvim'
},
}
By default, a few groups are automatically defined before your definition function is run. These groups represent all the supported spcial GUI features supported by Neovim at this time. Currently, this means:
Strikethrough { strikethrough = true }
Underline { underline = true }
Underdouble { underdouble = true }
Undercurl { undercurl = true }
Underdotted { underdotted = true }
Underdashed { underdashed = true }
Reverse { reverse = true }
Standout { standout = true }
Bold { bold = true }
Italic { italic = true }
These groups are not special in any way, so use them as you would any other:
SpellBad { Underdotted }
SpellCap { underdashed = Underdashed.underdashed }
You can overwrite any of these by simply specifying them yourself in your
definition, or you can disable the injection entirely by passing { inject_gui_groups = false } as a third argument to Colorscheme.define:
Colorscheme.define('your_theme', function (_)
...
end, { inject_gui_groups = false })
You can turn on a dynamic preview of your colorscheme via the :Polychrome preview start command, or require('polychrome.preview').start() from a Lua
context.
When enabled, several things happen:
Comment highlight group)Additionally, diagnostics are created for any common issues or errors found in the colorscheme definition. This creates a very tight feedback loop as you add and tweak your highlights, and you can immediately see the effects of each change as you make them.
There isn't a single authoritative source for all the most common highlight groups used in Vim/Neovim, so here are some good places to get at least some of the most common groups:
# docs for the most basic recommended highlight groups
:h group-name
:h highlight-groups
# docs for treesitter highlight groups
# also check https://github.com/nvim-treesitter/nvim-treesitter/blob/master/CONTRIBUTING.md#highlights
:h treesitter-highlight-groups
# show all highlight groups in your current (neo)vim session
:so $VIMRUNTIME/syntax/hitest.vim
When converting from a larger gamut down into sRGB, chroma clipping is performed using code adapted from Björn Ottosson's methods described here. The clipping is performed in the Oklab colorspace, which should result in a very accurate translation.
The testing setup is based on this blog post.
Ensure you have luarocks and a version of lua@5.1 installed. On
Ubuntu/Debian/other distros, this should be as simple as installing luarocks
via your package manager.
Additionally, your shell needs to be configured to be able to use luarocks.
Something like this in your .profile/.bashrc/.zshrc/etc should be
sufficient:
eval $(luarocks path)
Then, run luarocks test --local in the root of the repo.
Some platforms (like homebrew on macOS) don't provide vanilla Lua 5.1 packages
anymore, so installing luarocks will use a later version of Lua that doesn't
work out of the box with this test setup. In those situations, you can tell
luarocks to use LuaJIT instead:
brew install luajit
brew install luarocks
Without further environment modification, you can run tests with the following commands:
eval $(luarocks --lua-dir=$(brew --prefix)/opt/luajit path)
luarocks --lua-dir=$(brew --prefix)/opt/luajit test --local
However, if you'll be running this or identical testing setups on other repos, it makes more sense to modify your luarocks environment to use these as defaults:
luarocks config lua_dir "$(brew --prefix)/opt/luajit"
luarocks config lua_version 5.1
After those config changes, you should be able to run the test suite normally
using luarocks test --local (and eval $(luarocks path) in your
.profile/etc.
color-space test dataThe color conversion tests use the color-space package to generate the
reference test data, so it need to be installed for those to work. It will
automatically detect the package if installed globally via NPM:
npm install -g color-space
If you want LSP hints while working on tests, install and configure these
plugins like so (example assumes you're using lazy.nvim):
{ "LuaCATS/luassert", name = "luassert-types", lazy = true },
{ "LuaCATS/busted", name = "busted-types", lazy = true },
{
"folke/lazydev.nvim",
opts = {
library = {
-- Load luvit types when the `vim.uv` word is found
{ path = "${3rd}/luv/library", words = { "vim%.uv" } },
-- busted types
{ path = "luassert-types/library", words = { "assert" } },
{ path = "busted-types/library", words = { "describe" } },
},
},
},