Ideas For a New Templating Language
Designing a non-destructive templating system by use of an informed compiler
November 12, 2020
ALL INFO IS BRAINSTORMING AND SUBJECT TO CHANGE
Like many self-proclaimed techies, especially those with an affinity for Linux, I enjoy tinkering. Particularly, I like tinkering with my development config and modifying my dotfiles and in the process have wanted to make my configuration configurable as well (themability!).
I’ve added a rudimentary sed-based templating system to make my dotfiles configurable and while it works, it just feels… messy somehow. The files themselves are no longer usable without running through my install script as the template pieces aren’t valid syntax in any of the various files.
Requirements for a Solution
As I require the dotfile to be a standalone config file, the new templating system (henceforth referred to as Cactus
for reasons which will be explained later) should consist of markup which
is combined with the dotfile itself to generate a template.
As a second requirement, I want to be able to develop this sytem somewhat quickly and only want to create a barebones DSL for markup and have the majority of the logic take place within the piece that acts on the template which was generated by the informed config file (config data + markup). For this reason, Cactus should have a robust API for interacting with the compiled file. This is CaCtus - Code as Configuration.
Cactus config should act as a tree. There should be a root config file, containing things like language extensions, global variables, and files to process, and each file to process is recursively
its own config file. The root config should live at ~/.config/cactus/cactus.json
(probably JSON since this will most likely use Node.js for reasons given in Other Bells and Whistles below).
Cactus needs a cache. Since Cactus files are compiled into a template, this compilation should only occur as needed so the compiled version should be cached. Cache invalidation will either occur by checking the last modified date on the source file and compiled file and recompiling if necessary or by manual compilation as desired through use of the driver program.
Cactus will have a driver program. This program will allow control over compilation and the daemon (ooh, spooky). More on daemon throughts below.
Bonus
An ambitous bonus would be an event simpler markup system using heuristics to generate a compiled Cactus file (e.g. using spacing and comments to determine blocks, etc). While less powerful than Cactus markup, the host language API would be able to pick up the slack to achieve the same functionality. While this is probably technically feasible, it’s not without its downsides.
-
Pros:
- Cleaner config files
-
Cons:
- Imperfect heuristics will require more frequent updates
- Configuration processing logic is moved from compile time to runtime
- Configuration processing lives spatially further from the configuration it acts on leading to a greater penchant for Cactus to get out of sync with its config file
Rundown
After installing Cactus and adding the necessary config, the cactus
binary will be used to compile a template from the config:
cactus compile # reads config in ~/.cactus/cactus.json and runs compilation based on config
If there were no compilation errors, template files will have been added to the Cactus cache. The cactus
binary can now act on the templates to write a config. This step runs the Javascript files that
use the Cactus API to interact with the template files.
In this example, the Javascript files specified a global variable called theme
. The user has tied the value of this variable to a config object which specifies downstream configuration values for
various programs which are themable.
cactus set theme nord
Afer this step, the new dotfiles have been written. Note that running cactus compile
at this point would compile the newly created dotfiles into a new template (though if this turns out to not be the
desired behavior we could cache SHAs of the template files to check whether they were modified by a user since being written).
Markdown Considerations
In order to avoid polluting the config file it lives in it should be placed within comments and use unambiguous start tags as to avoid having to deal with escape sequences. It should be simple but powerful; it needs to be able to instruct the compiler how to generate tokens from a config file of any syntax.
But how do I know ahead of time how powerful this system needs to be? Well, I don’t know and because of that I need a simple yet powerful SYNTAX (for sure I’m thinking Scheme!). I should aim to make available a good set of functions for use within the cactus template for interacting with the config file. For those things I don’t think of that others may need, I’ll create a Javascript extension API to allow others to use Javascript to export functions for use within Cactus templates.
As stated earlier, a Javascript API will act on the compiled template. To inform the API of available actions on a Cactus node, Cactus nodes will be typed. I foresee there being at least three types and more likely four or more:
-
Variables
- Allow replacing a value
-
Sections
- Allow portions of the configuration to be turned on/off (for instance, if you want to keep separate profiles for Mac and Linux in the same file and only enable one at a time)
-
Switches
- Allow a single option to be selected from a list of many (for instance, if you had multiple Plug themes listed in your config file but only wanted one installed at a time you could list them all but comment all but one)
Example
" @cactus variable colorscheme {(extract-from 'nord')}
colorscheme nord
" @cactus section lisp begin
Plug 'guns/vim-sexp'
Plug 'tpope/vim-sexp-mappings-for-regular-people'
Plug 'Olical/conjure'
Plug 'clojure-vim/vim-jack-in'
" @cactus end
import cactus from '@cactus/core'
const config = cactus('./init.vim')
config.find('colorscheme').set('gruvbox')
config.find('lisp').remove()
Supported Use Cases
Keeping Static Config Files
If you keep a repo of your dotfiles, the most common use case is to maintain the static version of your config and use Cactus to modify portions of it, such as generating a theme.
Using Generated Files as Input Files
If you like to live on the edge, you may just have your single set of dotfiles not stored in a repo. If this is the case, you can still use Cactus. Rather than using static files from a repo as input, Cactus can act on the dotfiles, compile a template, and then run Cactus actions on that template to replace the initial input file. If you decide to go this route be sure not to reference data which may be altered by Cactus (such as extracting a value to a variable which may change based on dynamic configuration)!
cactus compile
Proposed Configuration Structures
There are three main files involved in a Cactus configuration: a main Cactus config file, a config file with cactus templates, and a javascript file which acts on the template.
I’ll consider two different structures for organizing a Cactus-federated configuration suite: Centralized and Distributed organization
Centralized
In a centralized configuration, Cactus-specific files reside in a single place (everything aside from Cactus templates which still go in the corresponding files they act on).
Distributed
In a distributed configuration, Cactus-specific files (i.e. the Node files which utilize the Cactus API) live alongside the config files for the various programs being configured.
Other Bells and Whistles
Since this may already be the most overengineered configuration solution ever conceived, let’s go one step further and add a “killer feature”.
With other config systems you usually have a single point of control. If you want to change a theme, you need to go through the right place to apply that theme everywhere (e.g. changing your alacritty theme will not also change your vim theme). What if updating any of your configs also applied changes elsewhere?
Enter the Cactus Daemon. This will watch target files for changes and propogate related changes to other configurations in the Cactus federation. I haven’t thought through this much yet so additional ideas will be forthcoming (or more likely, abandoned) as the project progresses.