rktjmp / lush.nvim

Create Neovim themes with real-time feedback, export anywhere.
MIT License
1.44k stars 47 forks source link

RFC: LushBuild (export lush anywhere, without lush) #78

Closed rktjmp closed 2 years ago

rktjmp commented 2 years ago

Goals

The main goal of this is to allow theme developers to use Lush solely as a theme creation tool with it's live updates, extension system and DSL syntax, while not forcing end users to install Lush. Part of this includes exporting your theme to VimL, but it also includes a Lua exporter with hooks to provide enduser configuration.

As a side effect, it also aims to let you export your Lush data easily to other formats.

WIP PR #77 (sort of misnamed compiler-plugins branch).

RFC:

Preamble

The Lush build system is designed to take a lush spec (i.e. the color and group data from your theme) and apply any number of transforms to that data. These transforms can include conversion to a vim theme, terminal emulator theme, writing to different files, etc.

Some prerequisite knowledge

-- lush_build.lua
local theme = require("zenbones")
run(theme,
  viml,
  {overwrite, "colors/zenbones.vim"})

The builder simply pushes data through a function pipeline, these functions are termed "transforms".

There are two kinds of transforms: "head" and "tail".

Head transforms must accept a parsed lush spec (returned by require("theme")). It must return a table of any content. You will place a head transform at the start of your pipeline.

Tail transforms must accept a table and must return a table. The contents of these tables is not enforced, they could be strings (a table of lines), functions (such as closures around data), other tables, etc.

Transforms can take any additional number of arguments after the table.

Lush ships with some default transforms:

These transforms are automatically injected into the build environment along with lush and run.

You can provide any of your own transforms just by writing a function, either in the build file or in another module.

We will discuss the simplest example, where you have a theme with no variations or configuration options and simply want to let non-lush users use your theme.

A basic theme, exported to VimL

To ship our theme as a viml file, we simply must load our theme, convert it to viml and save the output to a file.

We will use the viml and overwrite transforms.

Our build file would look something like this:

-- lush_build.lua

-- we start by calling run and giving it our theme as the first argument.
-- any other arguments form the pipeline.
local theme = require("my.lush.theme")
run(theme,
  -- now we will convert that theme to a list of viml highlight commands
  viml, 
  -- the viml commands alone are generally not enough for a colorscheme, we
  -- will need to append a few housekeeping lines first.
  -- note how we are passing arguments to append by wrapping the transform
  -- in a table.
  -- {transform 1 2 3} will result in transform(last_pipe_value, 1, 2, 3)
  -- append accepts a table, so this call ends up being:
  -- append(last_pipe_value, {"set...",  "let..."})
  {append {"set background=dark", "let g:colors_name=\"my_theme\""}},
  -- now we are ready to write our colors file. note: there is no reason this has
  -- to be written to the relative "colors" dir, you could write the file to an
  -- entirely different vim plugin.
  {overwrite, "colors/my_theme.vim"})
-- and that is the whole build file

You can run :LushBuild <build_file> which will load and execute the given build file, or if no buildfile is specified, lush_build.lua is used. You probably want to do this from your lush themes root dir but you can run it anywhere.

Take Aways and Notes

It's important to remember:

As a further example, we will write our own transform next.

Converting a Lush theme into an Alacritty theme

As an example, we will convert a theme into a (truncated) Alacritty theme.

To do this we will need to:

```lua -- As an example, we will imagine we are developing a lush transform -- for release into the community. -- -- We will say this transform expects to get a table shaped as: -- -- { -- primary = { -- bg = color -- fg = color -- } -- } -- -- along with a name. local function hash_to_0x(color) return string.gsub(color, "^#", "0x") end local function alacritty(colors, name) return { "# Colors: " .. name .. " theme", "colors:", " primary:" " background: " .. hash_to_0x(colors.primary.bg), " foreground: " .. hash_to_0x(colors.primary.fg), } end return alacritty ``` ```lua -- lush_build.lua local theme = require("my_theme") local alacritty = require("lush_community.transform.alacritty") run(theme, -- we must adjust our theme to conform to the alacritty transforms format. -- we can do this with an inline transform. function (groups) return { primary = { bg = groups.Normal.bg, fg = groups.Normal.fg } } end, -- now we can pass to alacritty, note that it needs a name {alacritty, "my_theme"}, -- and now we can write, either to share or to our local config {overwrite, "~/.config/alacritty/theme.yaml"} -- note, as overwrite is a transform, it *must* return a table, and infact -- overwrite returns the same lines it was given. we can pass these lines -- another transform. {overwrite, "extra/terms/alacritty.yaml"}) ```

Exporting as configurable lua

The lua transform generates code you can call to load and apply a lush theme without lush. It will require you to provide a support context around it.

By using the patchwrite transform, we can instruct the lush build system to only update its own code, leaving our support code intact.

```lua -- lush_build.lua run(require("theme"), -- generate lua code lua, -- write the lua code into our destination. -- you must specify open and close markers yourself to account -- for differing comment styles, patchwrite isn't limited to lua files. {patchwrite "colors/theme.lua", "-- PATCH_OPEN", "-- PATCH_CLOSE"}) ```

Before running this build file, we should prepare the destination for patchwrite.

```lua -- colors/theme.lua -- content here will not be touched -- PATCH_OPEN -- PATCH_CLOSE -- content here will not be touched ```

After running :LushBuild, we will have a lush_apply function.

By default, lush_apply will convert your theme (now compiled as a table) into viml highlight commands and apply them, but you can provide optional function hooks to lush_apply to alter data along the way.

The following hooks are provided:

For complete details, see the documentation in the generated code (or lua/lush/transformer/lua.lua in the branch)

Now that our theme has been exported, we can adjust our theme.lua file to use the generated loader.

```lua -- colors/theme.lua -- PATCH_OPEN -- Generated by lush builder on Mon Nov 1 22:20:06 2021 -- -- You can configure how this build function operates by passing in optional -- function handlers via the options table. -- -- See each default handler below for guidance on writing your own. -- -- { -- apply_fn = function(rules) ... end, -- before_apply_fn = function(rules) ... end, -- generate_group_fn = function(group) .. end, -- configure_group_fn = function(group) ... end, -- } -- local lush_groups = { ... } local lush_apply = function(opts) ... end -- PATCH_CLOSE -- imagine we want to provide some optional adjustments to groups local overrides = { Comment = {italic = false} } local setup = function(config) if config.italic_comments then overrides["Comment"]["italic"] = true end local my_configure_group = function(group) if overrides[group.name] then if overrides[group.name]["italic"] then -- apply configured override group.gui = "italic" end end -- return maybe adjusted group return group end -- run lush loader with our custom configure function to -- adjust the groups per user config. lush_apply(lush_groups, { configure_group_fn = my_configure_group }) end return { setup = setup } ```

Note, you don't have to run this exported lua directly, you could still have your "core theme file" that takes a config and requires which ever theme is appropriate.

```lua return { setup = function(config) if config.light then require("theme.lush_export.light").apply() else -- ... etc etc end end } ```

Pipelines are composable

Since run itself is a transform, you can pipe any table value into it, along with a list of transforms to run in that context.

run(zenbones,
  viml,
  {run, {
    {prepend, [["see http://... for more details]]},
    {patchwrite, "../dist/...", [[" M_OPEN]], [[" M_CLOSE]]}}}
  {run, {
    {patchwrite, "colors/", [[" M_OPEN]], [[" M_CLOSE]]}}})

-- or
run(zenbones,
  extract_term_colors, -- generic map of colors to use in terminals
  {run, {
    term_colors_to_kitty_map, -- translate generic map to kitty shaped map
    contrib.kitty,
    {overwrite, "extra/kitty.conf"}}},
  {run, {
    term_colors_to_alacritty_map, -- translate generic map to alacritty shaped map
    contrib.alacritty,
    {overwrite, "extra/alacritty.yaml"}}})

Other Transform Ideas or LushBuild concepts

Issues

mcchrish commented 2 years ago

I'm really excited for this. I was in the middle of restructuring my build system but glad to see that lush is having something that is quite extensible and more powerful. I'll be moving to lush's build system when it's ready.

I'd like to share an idea about a vim compatible structure that doesn't need a separate colors file. Basically we don't need to have a separate colors/zenbones.vim and colors/zenbones_compat.vim. The structure of the colorscheme file would look like a simple branch:

...
let g:colors_name = 'zenbones'

if has('nvim')
    lua require("zenbones.util").apply_colorscheme() " wrapper to just apply specs, term colors etc
else
    call {g:colors_name . '_' . &background}#load() " use autoload files to store vim compat hl groups
endif

There will be two autoload files that will be generated by lush builder: autoload/zenbones_light.vim autoload/zenbones_dark.vim. Just each one containing light and dark hl groups respectively. With this way, I can separate generated stuff from actual written code. I can even publish it in a separate repo as an optional dependency (mcchrish/zenbones_artifacts.vim) if one wants the compatible option.

This is at least what I plan to do.