Color Renderers - Matt3o12/termui GitHub Wiki

How to use a color-renderer

All supported Bufferers (e.g. Lists) should have an exported filed called RendererFactory. This field takes a TextRendererFactory object.
Currently, the following factories are available: PlainRendererFactory, MarkdownTextRendererFactory, and EscapeCodeRendererFactory (you can see what each one does and how it works below). PlainRendererFactory is used by default.
For instance, if you want to have a Markdown-like rendering in a List, just do:

    list := termui.NewList()
    list.RendererFactory = termui.MarkdownTextRendererFactory{}

And you're ready to go.
You can find more examples here.

All color-renderers (aka. TextRenderers)

Currently, these color-renderers are supported:

  • PlainRenderer (factory: PlainRendererFactory)
  • MarkdownTextRenderer (factory: MarkdownTextRendererFactory)
  • EscapeCodeRenderer (factory: EscapeCodeRendererFactory)

PlainRenderer

PlainRenderer renders the text as it is. I doesn't do any formatting or other fancy stuff. This is the default renderer.

MarkdownTextRenderer

MarkdownTextRenderer renders the text using a markdown-like syntax:
Text [Colored Text](Color, and, attributes) other text. The brackets []) will be removed and only Colored Text will be rendered accordingly.

The following colors are supported: Black, Red, Green, Yellow, Blue, Magenta, Cyan, White, and Default. The following attributes are supported: Bold, Underline, Reverse. It does not matter if you capitalize the colors or attributes. You can combine as many attributes with a color as you want, just separate them with a comma ,.

Cavets:

  • Brackets within brackets are not supported (e.g. [[Text]]).

EscapeCodeRenderer

EscapeCodeRenderer renders the text using ANSI escape codes. They work just like the ones you'd use in bash and they also work on windows.
Currently, the following CSI ranges are supported: 0-1, 4, and 30-37.


How to implement any TextRender

Here is how to implement any existing TextRenderers into your Bufferer (or any other method that returns a Point slice).

First you need to add a RendererFactory (type: TextRendererFactory) field into your Bufferer struct. Although the name is of no importance, you should be consistent and use it.
Every render factory is able to produce a TextRenderer using any int text. The TextRenderer can return a RenderedSequence and normalize the text. Normalized means that all formatting symbols are stripped out. That means if the MarkdownTextRenderer is used, [Hello world](RED, BOLD) will become "Hello world".
You should use the normalized text to determine if you need to trim the text. Render and RenderSequence will return a RenderedSequence, which helps you to colorize the output. You should only use RenderSequence if you know how much of the text you'll have to display. Use -1 if you want the text to render until the end is reached.
Remember end-start is not the width of the text. Some characters are wider then others. For instance '一' (EM DASH; U+2014) takes the width of 2 ASCII characters.

After you have decided which method to use (normally you use Render), you need to call either Buffer or PointAt. Buffer renders the whole string (sequence) and PointAt only the character at position n. Both need to know where to put the point on the screen (that's what the X and Y parameters are for).
Render will return all points that need to be appended to the buffer and the last color that was used (and thus the next color to continue). It may overflow the boarder.
PointAt returns a single point at the given position and the width it will take. Please check if the point overflows the boarder before appending it to the buffer.


How to write any TextRender

Writing a text render is a little bit more complicated then using and implementing it. You will need to structs. The Renderer, and the RendererFactory. The RendererFactory is used by the buffer to create a new renderer using a given text. If you're unsure why this is necessary, you should learn more about the Factory Design Pattern.

In this example, I'm going to write a random color renderer. As the name says, it will just generate random colors.
Let's start with the factory. Since we want to customize the min and max length of a colorized sequence, your factory needs 2 fields: Max and Min:

	type RandomRendererFactory struct {
		Max int
		Min int
	}

	func NewRandomRendererFactory(min, max int) RandomRendererFactory {
		return RandomRendererFactory{Min: min, Max: max}
	}

	func (f RandomRendererFactory) TextRenderer(text string) TextRenderer {
		return RandomRenderer{Text: text, Min: f.Min, Max: f.Max}
	}

The RandomRenderer will have 3 Fields, Min, Max and the Text and all methods specified in the TextRenderer interface.

The Render and NormalizedText methods are pretty simple:

	type RandomRenderer struct {
		Text string
		Min  int
		Max  int
	}
   
	func (r RandomRenderer) NormalizedText() string {
		return r.Text
	}

	func (r RandomRenderer) Render(lastColor, background Attribute) RenderedSequence {
		return r.RenderSequence(0, -1, lastColor, background)
	}

Since we don't need to manipulate the string, NormalizedText() will just return the text as it is. If you need to remove characters that help format the text properly, you will have to remove these and return the correct form in NormalziedText(). Render() will just call RenderSequence() with 0, and -1 as its length parameter (-1 means "to the end").

RenderSequence() (and its helper method nextSequence()) are a little bit more complex:

	func (r RandomRenderer) nextSequence(text []rune) (Attribute, int) {
		var nextGuess int

		// if RandomRender has not been properly initialzied
		// (i.e. without min/max), let's just assume: 1
		if r.Max == 0 && r.Min == 0 {
			nextGuess = 1
		} else {
			nextGuess = r.Min + rand.Intn(r.Max-r.Min)
		}

		if nextGuess > len(text) {
			nextGuess = len(text)
		}

		color := Attribute(rand.Intn(9))
		return color, nextGuess
	}

	func (r RandomRenderer) RenderSequence(start, end int, lastColor, background Attribute) RenderedSequence {
		runes := []rune(r.Text)
		if end < 0 {
			end = len(runes)
		}

		runes = runes[start:end]
		offset := 0

		var colorSeq []ColorSubsequence
		for offset < len(runes) {
			color, length := r.nextSequence(runes[offset:])
			seq := ColorSubsequence{Color: color, Start: offset, End: offset + length}
			colorSeq = append(colorSeq, seq)
			offset += length
		}

		seq := RenderedSequence{NormalizedText: string(runes)}
		seq.LastColor = lastColor
		seq.BackgroundColor = background
		seq.Sequences = colorSeq

		return seq
	}

The algorithm first strips the text to the desired length and casts it to a rune slice. It is important that you use a rune slice so we can deal with unicode characters correctly.
Then, it selects a random length and random color and creates a new ColorSubsequence from it and appends it. It repeats that until the whole text is colorized. Then it creates a RenderedSequence instance and returns it.
It is important that next input interprets -1 as the length as: go until done.
If you want to more complicated algorithms, take a look at textRender.go:37.
You can find the full source of this example here.

If you write a such TextRenderer, you should also write unit tests (this is going to help you a lot, I promise!).

⚠️ **GitHub.com Fallback** ⚠️