Skip to content

Latest commit

 

History

History
21 lines (11 loc) · 3.91 KB

architecture.md

File metadata and controls

21 lines (11 loc) · 3.91 KB

Architecture

Chalk

GChalk's architecture is heavily inspired by Chalk's architecture.

First of all, in Chalk when you dereference chalk.blue, what's actually happening here is that you're calling into a getter for blue. There are default getters defined through prototype inheritance which don't actually produce a color - instead they redefine .blue on the instance you're calling into to create a new "styler" and a new "builder".

The styler for .blue has an open, close, openAll, and closeAll. The styler for .blue.bgGreen has the styler for .blue as a parent, and has an .open, .close for bgGreen and a .openAll, .closeAll for both bgGreen and blue. Chalk is quite clever; since the default getters redefine .blue and .blue.bgGreen as you call them, these computed stylers are saved for future invocations, so they don't need to be rebuilt.

When we run a styler (in applyStyle()), if the string has no escape codes inside, we just append the openAll and closeAll of the leaf styler, and we're done.

If there are escape codes, then we need to worry about reapplying our styles - if we're coloring something green, and there's a blue bit in the middle, we still need to add the openAll and closeAll to the start and end, but then we also need to replace every blue "close tag" with a green "open tag". There's a different close tag for each kind of open tag (fg, bg, and one for each kind of modifier). To do this, we start at the leaf and replace all the closeTag instances with openTag, then we walk to the parent and do the same, all the way up the chain.

GChalk

GChalk operates on more or less the same principal. When you call WithBlue(), it will return a Builder instance. Calling WithBlue().BgGreen(...) creates a Blue builder (and associated stylerData), and then a BgGreen builder. Just like in Chalk, the stylerData in the BgGreen builder will have Blue's stylerData as .parent. We of course can't overwrite functions on a receiver in Golang, but instead we store a pointer to each generated child builder in the parent builder; when you call .WithBlue() on the root builder, it will create a new builder and store it in .blue on the root builder. This mimics Chalk's behavior, with excellent performance. (At one point I tried returning a Builder interface from WithBlue() and the other With...() functions, but surprisingly this made coloring a string about four times slower.)

This does mean that calling .WithBlue() will immediately allocate a structure with 41 pointers in it. If you call every permutation of bgcolor.color.modifier we end up allocating 16 * 16 * 9 * 41 = 94464 pointers, or about 750K of RAM (although... no one is going to do this). Calling the RGB() and other color model functions don't do this (we don't allocate 16.7m pointers) but they do still create an object which will need to be garbage collected eventually. If you're doing something like color gradients, you're better off calling directly into pkg/ansistyles to color things.

One possible change here would be; instead of storing 41 pointers, store child colors in a childBuilders map[string]*Builder. So then when you call WithBlue() we'd go find the builder in builder.childBuilders["blue"] instead of in builder.blue. This is very slightly slower at runtime (around 70ns/op vs 60ns/op on my MacBook) but would save some RAM in excessive use cases. One other thing here is that we could use a trick like this to cache RGB() and other color model functions; we'd obviously want to set a cap on how many we cache (we, again, do not want to cache 16.7m objects). This could improve performance quite a bit for cases where someone is using a small number of hex/RGB colors, though.