Added EscapeCodeRenderer

- Added `EscapeCode`-type
 - Implemented EscapeCode.String()
 - Implemented EscapeCode.Raw()
 - Implemented EscapeCode.MakeSafe()
 - Implemented EscapeCode.IsValid()

 - Added `EscapeCodeRenderer`
 - Implemented EscapeCodeRenderer.RenderSequence()
 - Implemented EscapeCodeRenderer.Render()
 - Implemented `EscapeCodeRenderer.NormalizedText`.

 - Added EscapeCodeRendererFactory
 - Implemented EscapeCodeRendererFactory.TextRenderer()
 - Added escape code examples to examples/coloredList.go
This commit is contained in:
Matteo Kloiber 2015-04-10 23:12:28 +02:00
parent ac747cb49f
commit a3f1384a3b
3 changed files with 431 additions and 9 deletions

View File

@ -5,7 +5,7 @@ package main
import "github.com/gizak/termui"
import "github.com/nsf/termbox-go"
func commonList() *termui.List {
func markdownList() *termui.List {
strs := []string{
"[0] github.com/gizak/termui",
"[1] 笀耔 [澉 灊灅甗](RED) 郔镺 笀耔 澉 [灊灅甗](yellow) 郔镺",
@ -26,18 +26,40 @@ func commonList() *termui.List {
return list
}
func listHidden() *termui.List {
list := commonList()
func hideList(list *termui.List) *termui.List {
list.Border.Label = "List - Hidden"
list.Overflow = "hidden"
return list
}
func listWrap() *termui.List {
list := commonList()
func wrapList(list *termui.List) *termui.List {
list.Border.Label = "List - Wrapped"
list.Overflow = "wrap"
list.X = 30
return list
}
func escapeList() *termui.List {
strs := []string{
"[0] github.com/gizak/termui",
"[1] 笀耔 \033[31m澉 灊灅甗 \033[0m郔镺 笀耔 澉 \033[33m灊灅甗 郔镺",
"[2] こんにちは世界",
"[3] keyboard.go",
"[4] \033[31moutput\033[0m.go",
"[5] random_out.go",
"[6] \033[1mdashboard\033[0m.go",
"[7] nsf/termbox-go",
"[8] OVERFLOW!!!!!!!\033[31;1m!!!!!!!!!!!!\033[0m!!!",
}
list := termui.NewList()
list.RendererFactory = termui.EscapeCodeRendererFactory{}
list.Items = strs
list.Height = 15
list.Width = 26
list.Y = 15
return list
}
@ -49,11 +71,20 @@ func main() {
}
defer termui.Close()
hiddenList := listHidden()
wrappedList := listWrap()
wrappedList.X = 30
hiddenMarkdownList := hideList(markdownList())
wrappedMarkdownList := wrapList(markdownList())
hiddenEscapeList := hideList(escapeList())
wrappedEscapeList := wrapList(escapeList())
lists := []termui.Bufferer{
hiddenEscapeList,
hiddenMarkdownList,
wrappedMarkdownList,
wrappedEscapeList,
}
termui.UseTheme("helloworld")
termui.Render(hiddenList, wrappedList)
termui.Render(lists...)
termbox.PollEvent()
}

View File

@ -1,7 +1,9 @@
package termui
import (
"fmt"
"regexp"
"strconv"
"strings"
)
@ -243,3 +245,210 @@ type PlainRendererFactory struct{}
func (f PlainRendererFactory) TextRenderer(text string) TextRenderer {
return PlainRenderer{text}
}
// We can't use a raw string here because \033 must not be escaped.
// I'd like to append (?<=m; i.e. lookbehind), but unfortunately,
// it is not supported. So we will need to do that manually.
var escapeRegex = "\033\\[(([0-9]{1,2}[;m])+)"
var colorEscapeCodeRegex = regexp.MustCompile(escapeRegex)
var colorEscapeCodeRegexMatchAll = regexp.MustCompile("^" + escapeRegex + "$")
// An EscapeCode is a unix ASCII Escape code.
type EscapeCode string
func (e EscapeCode) escapeNumberToColor(colorID int) (Attribute, error) {
var color Attribute
switch colorID {
case 0:
color = ColorDefault
case 1:
color = AttrBold
case 4:
color = AttrUnderline
case 30:
color = ColorBlack
case 31:
color = ColorRed
case 32:
color = ColorGreen
case 33:
color = ColorYellow
case 34:
color = ColorBlue
case 35:
color = ColorMagenta
case 36:
color = ColorCyan
case 37:
color = ColorWhite
default:
safeCode := e.MakeSafe()
return 0, fmt.Errorf("Unkown/unsupported escape code: '%v'", safeCode)
}
return color, nil
}
// Color converts the escape code to an `Attribute` (color).
// The EscapeCode must be formatted like this:
// - ASCII-Escape chacter (\033) + [ + Number + (;Number...) + m
// The second number is optimal. The semicolon (;) is used
// to seperate the colors.
// For example: `\033[1;31m` means: the following text is red and bold.
func (e EscapeCode) Color() (Attribute, error) {
escapeCode := string(e)
matches := colorEscapeCodeRegexMatchAll.FindStringSubmatch(escapeCode)
invalidEscapeCode := func() error {
safeCode := e.MakeSafe()
return fmt.Errorf("%v is not a valid ASCII escape code", safeCode)
}
if matches == nil || escapeCode[len(escapeCode)-1] != 'm' {
return 0, invalidEscapeCode()
}
color := Attribute(0)
for _, id := range strings.Split(matches[1][:len(matches[1])-1], ";") {
colorID, err := strconv.Atoi(id)
if err != nil {
return 0, invalidEscapeCode()
}
newColor, err := e.escapeNumberToColor(colorID)
if err != nil {
return 0, err
}
color |= newColor
}
return color, nil
}
// MakeSafe replace the invisible escape code chacacter (\0333)
// with \\0333 so that it will not mess up the terminal when an error
// is shown.
func (e EscapeCode) MakeSafe() string {
return strings.Replace(string(e), "\033", "\\033", -1)
}
// Alias to `EscapeCode.MakeSafe()`
func (e EscapeCode) String() string {
return e.MakeSafe()
}
// Raw returns the raw value of the escape code.
// Alias to string(EscapeCode)
func (e EscapeCode) Raw() string {
return string(e)
}
// IsValid returns whether or not the syntax of the escape code is
// valid and the code is supported.
func (e EscapeCode) IsValid() bool {
_, err := e.Color()
return err == nil
}
// A EscapeCodeRenderer does not render the text at all.
type EscapeCodeRenderer struct {
Text string
}
// NormalizedText strips all escape code outs (even the unkown/unsupported)
// ones.
func (r EscapeCodeRenderer) NormalizedText() string {
matches := colorEscapeCodeRegex.FindAllStringIndex(r.Text, -1)
text := []byte(r.Text)
// Iterate through matches in reverse order
for i := len(matches) - 1; i >= 0; i-- {
start, end := matches[i][0], matches[i][1]
if EscapeCode(text[start:end]).IsValid() {
text = append(text[:start], text[end:]...)
}
}
return string(text)
}
// RenderSequence renders the text just like Render but the start and end may
// be set. If end is -1, the end of the string will be used.
func (r EscapeCodeRenderer) RenderSequence(start, end int, lastColor, background Attribute) RenderedSequence {
normalizedRunes := []rune(r.NormalizedText())
if end < 0 {
end = len(normalizedRunes)
}
text := []byte(r.Text)
matches := colorEscapeCodeRegex.FindAllSubmatchIndex(text, -1)
removed := 0
var sequences []ColorSubsequence
runeLength := func(length int) int {
return len([]rune(string(text[:length])))
}
runes := []rune(r.Text)
for _, theMatch := range matches {
// Escapde code start, escape code end
eStart := runeLength(theMatch[0]) - removed
eEnd := runeLength(theMatch[1]) - removed
escapeCode := EscapeCode(runes[eStart:eEnd])
// If an error occurs (e.g. unkown escape code), we will just ignore it :)
color, err := escapeCode.Color()
if err != nil {
continue
}
// Patch old color sequence
if len(sequences) > 0 {
last := &sequences[len(sequences)-1]
last.End = eStart - start
}
// eEnd < 0 means the the sequence is withing the range.
if eEnd-start >= 0 {
// The sequence starts when the escape code ends and ends when the text
// end. If there is another escape code, this will be patched in the
// previous line.
colorSeq := ColorSubsequence{color, eStart - start, end - start}
if colorSeq.Start < 0 {
colorSeq.Start = 0
}
sequences = append(sequences, colorSeq)
}
runes = append(runes[:eStart], runes[eEnd:]...)
removed += eEnd - eStart
}
runes = runes[start:end]
return RenderedSequence{string(runes), lastColor, background, sequences, nil}
}
// Render just like RenderSequence
func (r EscapeCodeRenderer) Render(lastColor, background Attribute) RenderedSequence {
return r.RenderSequence(0, -1, lastColor, background)
}
// EscapeCodeRendererFactory is a TextRendererFactory for
// the EscapeCodeRenderer.
type EscapeCodeRendererFactory struct{}
// TextRenderer returns a EscapeCodeRenderer instance.
func (f EscapeCodeRendererFactory) TextRenderer(text string) TextRenderer {
return EscapeCodeRenderer{text}
}

View File

@ -13,6 +13,7 @@ func TestTextRender_TestInterface(t *testing.T) {
var inter *TextRenderer
assert.Implements(t, inter, new(MarkdownTextRenderer))
assert.Implements(t, inter, new(EscapeCodeRenderer))
assert.Implements(t, inter, new(PlainRenderer))
}
@ -20,6 +21,7 @@ func TestTextRendererFactory_TestInterface(t *testing.T) {
var inter *TextRendererFactory
assert.Implements(t, inter, new(MarkdownTextRendererFactory))
assert.Implements(t, inter, new(EscapeCodeRendererFactory))
assert.Implements(t, inter, new(PlainRendererFactory))
}
@ -270,3 +272,183 @@ func TestPosUnicode(t *testing.T) {
require.Equal(t, "你好", text[:6])
assert.Equal(t, 2, posUnicode(text, 6))
}
// Make `escapeCode` safe (i.e. replace \033 by \\033) so that it is not
// formatted.
// func makeEscapeCodeSafe(escapeCode string) string {
// return strings.Replace(escapeCode, "\033", "\\033", -1)
// }
func TestEscapeCode_Color(t *testing.T) {
codes := map[EscapeCode]Attribute{
"\033[30m": ColorBlack,
"\033[31m": ColorRed,
"\033[32m": ColorGreen,
"\033[33m": ColorYellow,
"\033[34m": ColorBlue,
"\033[35m": ColorMagenta,
"\033[36m": ColorCyan,
"\033[37m": ColorWhite,
"\033[1;31m": ColorRed | AttrBold,
"\033[1;4;31m": ColorRed | AttrBold | AttrUnderline,
"\033[0m": ColorDefault,
}
for code, color := range codes {
got, err := code.Color()
msg := fmt.Sprintf("Escape code: '%v'", code.MakeSafe())
if assert.NoError(t, err, msg) {
assert.Equal(t, color, got, msg)
}
}
invalidEscapeCodes := []EscapeCode{
"\03354m",
"[54m",
"\033[34",
"\033[34;m",
"\033[34m;",
"\033[34;",
"\033[5432m",
"t\033[30m",
"t\033[30ms",
"\033[30ms",
}
errMsg := "%v is not a valid ASCII escape code"
for _, invalidEscapeCode := range invalidEscapeCodes {
color, err := invalidEscapeCode.Color()
safeEscapeCode := invalidEscapeCode.MakeSafe()
expectedErr := fmt.Sprintf(errMsg, safeEscapeCode)
if assert.EqualError(t, err, expectedErr, "Expected: "+expectedErr) {
assert.Equal(t, color, Attribute(0))
}
}
outOfRangeCodes := []EscapeCode{
"\033[2m",
"\033[3m",
"\033[3m",
"\033[5m",
"\033[6m",
"\033[7m",
"\033[8m",
"\033[38m",
"\033[39m",
"\033[40m",
"\033[41m",
"\033[43m",
"\033[45m",
"\033[46m",
"\033[48m",
"\033[49m",
"\033[50m",
}
for _, code := range outOfRangeCodes {
color, err := code.Color()
safeCode := code.MakeSafe()
errMsg := fmt.Sprintf("Unkown/unsupported escape code: '%v'", safeCode)
if assert.EqualError(t, err, errMsg) {
assert.Equal(t, color, Attribute(0), "Escape Code: "+safeCode)
}
}
// Special case: check for out of slice panic on empty string
_, err := EscapeCode("").Color()
assert.EqualError(t, err, " is not a valid ASCII escape code")
}
func TestEscapeCode_String(t *testing.T) {
e := EscapeCode("\033[32m")
assert.Equal(t, "\\033[32m", e.String())
}
func TestEscapeCode_Raw(t *testing.T) {
e := EscapeCode("\033[32m")
assert.Equal(t, "\033[32m", e.Raw())
}
func TestEscapeCodeRenderer_NormalizedText(t *testing.T) {
renderer := EscapeCodeRenderer{"\033[33mtest \033[35mfoo \033[33;1mbar"}
assert.Equal(t, "test foo bar", renderer.NormalizedText())
renderer = EscapeCodeRenderer{"hello \033[38mtest"}
assert.Equal(t, "hello \033[38mtest", renderer.NormalizedText())
}
func TestEscapeCodeRenderer_RenderSequence(t *testing.T) {
black, white := ColorWhite, ColorBlack
renderer := EscapeCodeRenderer{"test \033[33mfoo \033[31mbar"}
sequence := renderer.RenderSequence(0, -1, black, white)
if assertRenderSequence(t, sequence, black, white, "test foo bar", 2) {
assertColorSubsequence(t, sequence.Sequences[0], "YELLOW", 5, 9)
assertColorSubsequence(t, sequence.Sequences[1], "RED", 9, 12)
getPoint := func(n int) Point {
point, width := sequence.PointAt(n, 10+n, 30)
assert.Equal(t, 1, width)
return point
}
// Also test the points at to make sure that
// I didn't make a counting mistake...
AssertPoint(t, getPoint(0), 't', 10, 30)
AssertPoint(t, getPoint(1), 'e', 11, 30)
AssertPoint(t, getPoint(2), 's', 12, 30)
AssertPoint(t, getPoint(3), 't', 13, 30)
AssertPoint(t, getPoint(4), ' ', 14, 30)
AssertPoint(t, getPoint(5), 'f', 15, 30, ColorYellow)
AssertPoint(t, getPoint(6), 'o', 16, 30, ColorYellow)
AssertPoint(t, getPoint(7), 'o', 17, 30, ColorYellow)
AssertPoint(t, getPoint(8), ' ', 18, 30, ColorYellow)
AssertPoint(t, getPoint(9), 'b', 19, 30, ColorRed)
AssertPoint(t, getPoint(10), 'a', 20, 30, ColorRed)
AssertPoint(t, getPoint(11), 'r', 21, 30, ColorRed)
}
renderer = EscapeCodeRenderer{"甗 郔\033[33m镺 笀耔 澉 灊\033[31m灅甗"}
sequence = renderer.RenderSequence(2, -1, black, white)
if assertRenderSequence(t, sequence, black, white, "郔镺 笀耔 澉 灊灅甗", 2) {
assertColorSubsequence(t, sequence.Sequences[0], "YELLOW", 1, 9)
assertColorSubsequence(t, sequence.Sequences[1], "RED", 9, 11)
}
renderer = EscapeCodeRenderer{"\033[33mHell\033[31mo world"}
sequence = renderer.RenderSequence(2, -1, black, white)
if assertRenderSequence(t, sequence, black, white, "llo world", 2) {
assertColorSubsequence(t, sequence.Sequences[0], "YELLOW", 0, 2)
assertColorSubsequence(t, sequence.Sequences[1], "RED", 2, 9)
}
sequence = renderer.RenderSequence(1, 7, black, white)
if assertRenderSequence(t, sequence, black, white, "ello w", 2) {
assertColorSubsequence(t, sequence.Sequences[0], "YELLOW", 0, 3)
assertColorSubsequence(t, sequence.Sequences[1], "RED", 3, 6)
}
sequence = renderer.RenderSequence(6, 10, black, white)
if assertRenderSequence(t, sequence, black, white, "worl", 1) {
assertColorSubsequence(t, sequence.Sequences[0], "RED", 0, 4)
}
// Test with out-of-range escape code
renderer = EscapeCodeRenderer{"hello \033[38mtest"}
sequence = renderer.RenderSequence(0, -1, black, white)
assertRenderSequence(t, sequence, black, white, "hello \033[38mtest", 0)
}
func TestEscapeCodeRenderer_Render(t *testing.T) {
renderer := EscapeCodeRenderer{"test \033[33mfoo \033[31mbar"}
sequence := renderer.Render(4, 6)
if assertRenderSequence(t, sequence, 4, 6, "test foo bar", 2) {
assertColorSubsequence(t, sequence.Sequences[0], "YELLOW", 5, 9)
assertColorSubsequence(t, sequence.Sequences[1], "RED", 9, 12)
}
}
func TestEscapeCodeRendererFactory_TextRenderer(t *testing.T) {
factory := EscapeCodeRendererFactory{}
assert.Equal(t, EscapeCodeRenderer{"foo"}, factory.TextRenderer("foo"))
assert.Equal(t, EscapeCodeRenderer{"bar"}, factory.TextRenderer("bar"))
}