Merge from Matt3o12/colored-list

This commit is contained in:
gizak 2015-04-16 18:44:26 -04:00
commit 00192cd7b0
9 changed files with 1179 additions and 89 deletions

View File

@ -112,6 +112,11 @@ The `helloworld` color scheme drops in some colors!
<img src="./example/list.png" alt="list" type="image/png" width="200">
#### Colored List
[demo code](https://github.com/gizak/termui/blob/master/example/coloredList.go)
TODO: Image (let's wait until the implementation is finished).
#### Gauge
[demo code](https://github.com/gizak/termui/blob/master/example/gauge.go)

90
example/coloredList.go Normal file
View File

@ -0,0 +1,90 @@
// +build ignore
package main
import "github.com/gizak/termui"
import "github.com/nsf/termbox-go"
func markdownList() *termui.List {
strs := []string{
"[0] github.com/gizak/termui",
"[1] 笀耔 [澉 灊灅甗](RED) 郔镺 笀耔 澉 [灊灅甗](yellow) 郔镺",
"[2] こんにちは世界",
"[3] keyboard.go",
"[4] [output](RED).go",
"[5] random_out.go",
"[6] [dashboard](BOLD).go",
"[7] nsf/termbox-go",
"[8] OVERFLOW!!!!!!![!!!!!!!!!!!!](red,bold)!!!"}
list := termui.NewList()
list.Items = strs
list.Height = 15
list.Width = 26
list.RendererFactory = termui.MarkdownTextRendererFactory{}
return list
}
func hideList(list *termui.List) *termui.List {
list.Border.Label = "List - Hidden"
list.Overflow = "hidden"
return list
}
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
}
func main() {
err := termui.Init()
if err != nil {
panic(err)
}
defer termui.Close()
hiddenMarkdownList := hideList(markdownList())
wrappedMarkdownList := wrapList(markdownList())
hiddenEscapeList := hideList(escapeList())
wrappedEscapeList := wrapList(escapeList())
lists := []termui.Bufferer{
hiddenEscapeList,
hiddenMarkdownList,
wrappedMarkdownList,
wrappedEscapeList,
}
termui.UseTheme("helloworld")
termui.Render(lists...)
termbox.PollEvent()
}

View File

@ -29,7 +29,8 @@ func main() {
par1.X = 20
par1.Border.Label = "标签"
par2 := termui.NewPar("Simple text\nwith label. It can be multilined with \\n or break automatically")
par2 := termui.NewPar("Simple colored text\nwith label. It [can be](RED) multilined with \\n or [break automatically](GREEN, BOLD)")
par2.RendererFactory = termui.MarkdownTextRendererFactory{}
par2.Height = 5
par2.Width = 37
par2.Y = 4

View File

@ -4,7 +4,12 @@
package termui
import tm "github.com/nsf/termbox-go"
import (
"regexp"
"strings"
tm "github.com/nsf/termbox-go"
)
import rw "github.com/mattn/go-runewidth"
/* ---------------Port from termbox-go --------------------- */
@ -45,15 +50,39 @@ func str2runes(s string) []rune {
return []rune(s)
}
// Here for backwards-compatibility.
func trimStr2Runes(s string, w int) []rune {
return TrimStr2Runes(s, w)
}
// TrimStr2Runes trims string to w[-1 rune], appends …, and returns the runes
// of that string if string is grather then n. If string is small then w,
// return the runes.
func TrimStr2Runes(s string, w int) []rune {
if w <= 0 {
return []rune{}
}
sw := rw.StringWidth(s)
if sw > w {
return []rune(rw.Truncate(s, w, dot))
}
return str2runes(s) //[]rune(rw.Truncate(s, w, ""))
return str2runes(s)
}
// TrimStrIfAppropriate trim string to "s[:-1] + …"
// if string > width otherwise return string
func TrimStrIfAppropriate(s string, w int) string {
if w <= 0 {
return ""
}
sw := rw.StringWidth(s)
if sw > w {
return rw.Truncate(s, w, dot)
}
return s
}
func strWidth(s string) int {
@ -63,3 +92,59 @@ func strWidth(s string) int {
func charWidth(ch rune) int {
return rw.RuneWidth(ch)
}
var whiteSpaceRegex = regexp.MustCompile(`\s`)
// StringToAttribute converts text to a termui attribute. You may specifiy more
// then one attribute like that: "BLACK, BOLD, ...". All whitespaces
// are ignored.
func StringToAttribute(text string) Attribute {
text = whiteSpaceRegex.ReplaceAllString(strings.ToLower(text), "")
attributes := strings.Split(text, ",")
result := Attribute(0)
for _, theAttribute := range attributes {
var match Attribute
switch theAttribute {
case "reset", "default":
match = ColorDefault
case "black":
match = ColorBlack
case "red":
match = ColorRed
case "green":
match = ColorGreen
case "yellow":
match = ColorYellow
case "blue":
match = ColorBlue
case "magenta":
match = ColorMagenta
case "cyan":
match = ColorCyan
case "white":
match = ColorWhite
case "bold":
match = AttrBold
case "underline":
match = AttrUnderline
case "reverse":
match = AttrReverse
}
result |= match
}
return result
}

View File

@ -7,22 +7,20 @@ package termui
import (
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/stretchr/testify/assert"
)
func TestStr2Rune(t *testing.T) {
s := "你好,世界."
rs := str2runes(s)
if len(rs) != 6 {
t.Error()
t.Error(t)
}
}
func TestWidth(t *testing.T) {
s0 := "つのだ☆HIRO"
s1 := "11111111111"
spew.Dump(s0)
spew.Dump(s1)
// above not align for setting East Asian Ambiguous to wide!!
if strWidth(s0) != strWidth(s1) {
@ -56,3 +54,17 @@ func TestTrim(t *testing.T) {
t.Error("avoid trim failed")
}
}
func TestTrimStrIfAppropriate_NoTrim(t *testing.T) {
assert.Equal(t, "hello", TrimStrIfAppropriate("hello", 5))
}
func TestTrimStrIfAppropriate(t *testing.T) {
assert.Equal(t, "hel…", TrimStrIfAppropriate("hello", 4))
assert.Equal(t, "h…", TrimStrIfAppropriate("hello", 2))
}
func TestStringToAttribute(t *testing.T) {
assert.Equal(t, ColorRed, StringToAttribute("ReD"))
assert.Equal(t, ColorRed|AttrBold, StringToAttribute("RED, bold"))
}

97
list.go
View File

@ -4,8 +4,6 @@
package termui
import "strings"
// List displays []string as its items,
// it has a Overflow option (default is "hidden"), when set to "hidden",
// the item exceeding List's width is truncated, but when set to "wrap",
@ -31,10 +29,11 @@ import "strings"
*/
type List struct {
Block
Items []string
Overflow string
ItemFgColor Attribute
ItemBgColor Attribute
Items []string
Overflow string
ItemFgColor Attribute
ItemBgColor Attribute
RendererFactory TextRendererFactory
}
// NewList returns a new *List with current theme.
@ -43,62 +42,54 @@ func NewList() *List {
l.Overflow = "hidden"
l.ItemFgColor = theme.ListItemFg
l.ItemBgColor = theme.ListItemBg
l.RendererFactory = PlainRendererFactory{}
return l
}
// Buffer implements Bufferer interface.
func (l *List) Buffer() []Point {
ps := l.Block.Buffer()
switch l.Overflow {
case "wrap":
rs := str2runes(strings.Join(l.Items, "\n"))
i, j, k := 0, 0, 0
for i < l.innerHeight && k < len(rs) {
w := charWidth(rs[k])
if rs[k] == '\n' || j+w > l.innerWidth {
i++
j = 0
if rs[k] == '\n' {
k++
buffer := l.Block.Buffer()
breakLoop := func(y int) bool {
return y+1 > l.innerHeight
}
y := 0
MainLoop:
for _, item := range l.Items {
x := 0
bg, fg := l.ItemFgColor, l.ItemBgColor
renderer := l.RendererFactory.TextRenderer(item)
sequence := renderer.Render(bg, fg)
for n := range []rune(sequence.NormalizedText) {
point, width := sequence.PointAt(n, x+l.innerX, y+l.innerY)
if width+x <= l.innerWidth {
buffer = append(buffer, point)
x += width
} else {
if l.Overflow == "wrap" {
y++
if breakLoop(y) {
break MainLoop
}
x = 0
} else {
dotR := []rune(dot)[0]
dotX := l.innerWidth + l.innerX - charWidth(dotR)
p := newPointWithAttrs(dotR, dotX, y+l.innerY, bg, fg)
buffer = append(buffer, p)
break
}
continue
}
pi := Point{}
pi.X = l.innerX + j
pi.Y = l.innerY + i
pi.Ch = rs[k]
pi.Bg = l.ItemBgColor
pi.Fg = l.ItemFgColor
ps = append(ps, pi)
k++
j++
}
case "hidden":
trimItems := l.Items
if len(trimItems) > l.innerHeight {
trimItems = trimItems[:l.innerHeight]
}
for i, v := range trimItems {
rs := trimStr2Runes(v, l.innerWidth)
j := 0
for _, vv := range rs {
w := charWidth(vv)
p := Point{}
p.X = l.innerX + j
p.Y = l.innerY + i
p.Ch = vv
p.Bg = l.ItemBgColor
p.Fg = l.ItemFgColor
ps = append(ps, p)
j += w
}
y++
if breakLoop(y) {
break MainLoop
}
}
return l.Block.chopOverflow(ps)
return l.Block.chopOverflow(buffer)
}

56
p.go
View File

@ -13,38 +13,43 @@ package termui
*/
type Par struct {
Block
Text string
TextFgColor Attribute
TextBgColor Attribute
Text string
TextFgColor Attribute
TextBgColor Attribute
RendererFactory TextRendererFactory
}
// NewPar returns a new *Par with given text as its content.
func NewPar(s string) *Par {
return &Par{
Block: *NewBlock(),
Text: s,
TextFgColor: theme.ParTextFg,
TextBgColor: theme.ParTextBg}
Block: *NewBlock(),
Text: s,
TextFgColor: theme.ParTextFg,
TextBgColor: theme.ParTextBg,
RendererFactory: PlainRendererFactory{},
}
}
// Buffer implements Bufferer interface.
func (p *Par) Buffer() []Point {
ps := p.Block.Buffer()
rs := str2runes(p.Text)
i, j, k := 0, 0, 0
for i < p.innerHeight && k < len(rs) {
// the width of char is about to print
w := charWidth(rs[k])
fg, bg := p.TextFgColor, p.TextBgColor
sequence := p.RendererFactory.TextRenderer(p.Text).Render(fg, bg)
runes := []rune(sequence.NormalizedText)
if rs[k] == '\n' || j+w > p.innerWidth {
i++
j = 0 // set x = 0
if rs[k] == '\n' {
k++
y, x, n := 0, 0, 0
for y < p.innerHeight && n < len(runes) {
point, width := sequence.PointAt(n, x+p.innerX, y+p.innerY)
if runes[n] == '\n' || x+width > p.innerWidth {
y++
x = 0 // set x = 0
if runes[n] == '\n' {
n++
}
if i >= p.innerHeight {
if y >= p.innerHeight {
ps = append(ps, newPointWithAttrs('…',
p.innerX+p.innerWidth-1,
p.innerY+p.innerHeight-1,
@ -54,18 +59,11 @@ func (p *Par) Buffer() []Point {
continue
}
pi := Point{}
pi.X = p.innerX + j
pi.Y = p.innerY + i
pi.Ch = rs[k]
pi.Bg = p.TextBgColor
pi.Fg = p.TextFgColor
ps = append(ps, pi)
k++
j += w
ps = append(ps, point)
n++
x += width
}
return p.Block.chopOverflow(ps)
}

454
textRender.go Normal file
View File

@ -0,0 +1,454 @@
package termui
import (
"fmt"
"regexp"
"strconv"
"strings"
)
// TextRenderer adds common methods for rendering a text on screeen.
type TextRenderer interface {
NormalizedText() string
Render(lastColor, background Attribute) RenderedSequence
RenderSequence(start, end int, lastColor, background Attribute) RenderedSequence
}
// TextRendererFactory is factory for creating text renderers.
type TextRendererFactory interface {
TextRenderer(text string) TextRenderer
}
// MarkdownRegex is used by MarkdownTextRenderer to determine how to format the
// text.
const MarkdownRegex = `(?:\[([^]]+)\])\(([a-z\s,]+)\)`
// unexported because a pattern can't be a constant and we don't want anyone
// messing with the regex.
var markdownPattern = regexp.MustCompile(MarkdownRegex)
// MarkdownTextRenderer is used for rendering the text with colors using
// markdown-like syntax.
// See: https://github.com/gizak/termui/issues/4#issuecomment-87270635
type MarkdownTextRenderer struct {
Text string
}
// NormalizedText returns the text the user will see (without colors).
// It strips out all formatting option and only preserves plain text.
func (r MarkdownTextRenderer) NormalizedText() string {
return r.normalizeText(r.Text)
}
func (r MarkdownTextRenderer) normalizeText(text string) string {
lText := strings.ToLower(text)
indexes := markdownPattern.FindAllStringSubmatchIndex(lText, -1)
// Interate through indexes in reverse order.
for i := len(indexes) - 1; i >= 0; i-- {
theIndex := indexes[i]
start, end := theIndex[0], theIndex[1]
contentStart, contentEnd := theIndex[2], theIndex[3]
text = text[:start] + text[contentStart:contentEnd] + text[end:]
}
return text
}
// Returns the position considering unicode characters.
func posUnicode(text string, pos int) int {
return len([]rune(text[:pos]))
}
/*
Render renders the sequence `text` using a markdown-like syntax:
`[hello](red) world` will become: `hello world` where hello is red.
You may also specify other attributes such as bold text:
`[foo](YELLOW, BOLD)` will become `foo` in yellow, bold text.
For all available combinations, colors, and attribute, see: `StringToAttribute`.
This method returns a RenderedSequence
*/
func (r MarkdownTextRenderer) Render(lastColor, background Attribute) RenderedSequence {
return r.RenderSequence(0, -1, lastColor, background)
}
// RenderSequence renders the text just like Render but the start and end can
// be specified.
func (r MarkdownTextRenderer) RenderSequence(start, end int, lastColor, background Attribute) RenderedSequence {
text := r.Text
if end == -1 {
end = len([]rune(r.NormalizedText()))
}
getMatch := func(s string) []int {
return markdownPattern.FindStringSubmatchIndex(strings.ToLower(s))
}
var sequences []ColorSubsequence
for match := getMatch(text); match != nil; match = getMatch(text) {
// Check if match is in the start/end range.
matchStart, matchEnd := match[0], match[1]
colorStart, colorEnd := match[4], match[5]
contentStart, contentEnd := match[2], match[3]
color := StringToAttribute(text[colorStart:colorEnd])
content := text[contentStart:contentEnd]
theSequence := ColorSubsequence{color, contentStart - 1, contentEnd - 1}
theSequence.Start = posUnicode(text, contentStart) - 1
theSequence.End = posUnicode(text, contentEnd) - 1
if start < theSequence.End && end > theSequence.Start {
// Make the sequence relative and append.
theSequence.Start -= start
if theSequence.Start < 0 {
theSequence.Start = 0
}
theSequence.End -= start
if theSequence.End < 0 {
theSequence.End = 0
} else if theSequence.End > end-start {
theSequence.End = end - start
}
sequences = append(sequences, theSequence)
}
text = text[:matchStart] + content + text[matchEnd:]
}
if end == -1 {
end = len(text)
}
runes := []rune(text)[start:end]
return RenderedSequence{string(runes), lastColor, background, sequences, nil}
}
// MarkdownTextRendererFactory is a TextRendererFactory for
// the MarkdownTextRenderer.
type MarkdownTextRendererFactory struct{}
// TextRenderer returns a MarkdownTextRenderer instance.
func (f MarkdownTextRendererFactory) TextRenderer(text string) TextRenderer {
return MarkdownTextRenderer{text}
}
// RenderedSequence is a string sequence that is capable of returning the
// Buffer used by termui for displaying the colorful string.
type RenderedSequence struct {
NormalizedText string
LastColor Attribute
BackgroundColor Attribute
Sequences []ColorSubsequence
// Use the color() method for getting the correct value.
mapSequences map[int]Attribute
}
// A ColorSubsequence represents a color for the given text span.
type ColorSubsequence struct {
Color Attribute
Start int
End int
}
// ColorSubsequencesToMap creates a map with all colors that from the
// subsequences.
func ColorSubsequencesToMap(sequences []ColorSubsequence) map[int]Attribute {
result := make(map[int]Attribute)
for _, theSequence := range sequences {
for i := theSequence.Start; i < theSequence.End; i++ {
result[i] = theSequence.Color
}
}
return result
}
func (s *RenderedSequence) colors() map[int]Attribute {
if s.mapSequences == nil {
s.mapSequences = ColorSubsequencesToMap(s.Sequences)
}
return s.mapSequences
}
// Buffer returns the colorful formatted buffer and the last color that was
// used.
func (s *RenderedSequence) Buffer(x, y int) ([]Point, Attribute) {
buffer := make([]Point, 0, len(s.NormalizedText)) // This is just an assumtion
for i := range []rune(s.NormalizedText) {
p, width := s.PointAt(i, x, y)
buffer = append(buffer, p)
x += width
}
return buffer, s.LastColor
}
// PointAt returns the point at the position of n. The x, and y coordinates
// are used for placing the point at the right position.
// Since some charaters are wider (like `一`), this method also returns the
// width the point actually took.
// This is important for increasing the x property properly.
func (s *RenderedSequence) PointAt(n, x, y int) (Point, int) {
color, ok := s.colors()[n]
if !ok {
color = s.LastColor
}
char := []rune(s.NormalizedText)[n]
return Point{char, s.BackgroundColor, color, x, y}, charWidth(char)
}
// A PlainRenderer does not render the text at all.
type PlainRenderer struct {
Text string
}
// NormalizedText returns the text given in
func (r PlainRenderer) NormalizedText() string {
return r.Text
}
// RenderSequence returns a RenderedSequence that does not have any color
// sequences.
func (r PlainRenderer) RenderSequence(start, end int, lastColor, background Attribute) RenderedSequence {
runes := []rune(r.Text)
if end < 0 {
end = len(runes)
}
runes = runes[start:end]
var s []ColorSubsequence
return RenderedSequence{string(runes), lastColor, background, s, nil}
}
// Render just like RenderSequence
func (r PlainRenderer) Render(lastColor, background Attribute) RenderedSequence {
return r.RenderSequence(0, -1, lastColor, background)
}
// PlainRendererFactory is a TextRendererFactory for
// the PlainRenderer.
type PlainRendererFactory struct{}
// TextRenderer returns a PlainRenderer instance.
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}
}

454
textRender_test.go Normal file
View File

@ -0,0 +1,454 @@
package termui
import (
"fmt"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
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))
}
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))
}
func TestMarkdownTextRenderer_normalizeText(t *testing.T) {
renderer := MarkdownTextRenderer{}
got := renderer.normalizeText("[ERROR](red,bold) Something went wrong")
assert.Equal(t, got, "ERROR Something went wrong")
got = renderer.normalizeText("[foo](red) hello [bar](green) world")
assert.Equal(t, got, "foo hello bar world")
got = renderer.normalizeText("[foo](g) hello [bar]green (world)")
assert.Equal(t, got, "foo hello [bar]green (world)")
got = "笀耔 [澉 灊灅甗](RED) 郔镺 笀耔 澉 [灊灅甗](yellow) 郔镺"
expected := "笀耔 澉 灊灅甗 郔镺 笀耔 澉 灊灅甗 郔镺"
assert.Equal(t, renderer.normalizeText(got), expected)
got = renderer.normalizeText("[(foo)](red,white) bar")
assert.Equal(t, renderer.normalizeText(got), "(foo) bar")
// TODO: make this regex work correctly:
// got = renderer.normalizeText("[[foo]](red,white) bar")
// assert.Equal(t, renderer.normalizeText(got), "[foo] bar")
// I had to comment it out because the unit tests keep failing and
// I don't know how to fix it. See more:
// https://github.com/gizak/termui/pull/22
}
func TestMarkdownTextRenderer_NormalizedText(t *testing.T) {
renderer := MarkdownTextRenderer{"[ERROR](red,bold) Something went wrong"}
assert.Equal(t, renderer.NormalizedText(), "ERROR Something went wrong")
}
func assertRenderSequence(t *testing.T, sequence RenderedSequence, last, background Attribute, text string, lenSequences int) bool {
msg := fmt.Sprintf("seq: %v", spew.Sdump(sequence))
assert.Equal(t, last, sequence.LastColor, msg)
assert.Equal(t, background, sequence.BackgroundColor, msg)
assert.Equal(t, text, sequence.NormalizedText, msg)
return assert.Equal(t, lenSequences, len(sequence.Sequences), msg)
}
func assertColorSubsequence(t *testing.T, s ColorSubsequence, color string, start, end int) {
assert.Equal(t, ColorSubsequence{StringToAttribute(color), start, end}, s)
}
func TestMarkdownTextRenderer_RenderSequence(t *testing.T) {
// Simple test.
renderer := MarkdownTextRenderer{"[ERROR](red,bold) something went wrong"}
got := renderer.RenderSequence(0, -1, 3, 5)
if assertRenderSequence(t, got, 3, 5, "ERROR something went wrong", 1) {
assertColorSubsequence(t, got.Sequences[0], "RED,BOLD", 0, 5)
}
got = renderer.RenderSequence(3, 8, 3, 5)
if assertRenderSequence(t, got, 3, 5, "OR so", 1) {
assertColorSubsequence(t, got.Sequences[0], "RED,BOLD", 0, 2)
}
// Test for mutiple colors.
renderer = MarkdownTextRenderer{"[foo](red) hello [bar](blue) world"}
got = renderer.RenderSequence(0, -1, 7, 2)
if assertRenderSequence(t, got, 7, 2, "foo hello bar world", 2) {
assertColorSubsequence(t, got.Sequences[0], "RED", 0, 3)
assertColorSubsequence(t, got.Sequences[1], "BLUE", 10, 13)
}
// Test that out-of-bound color sequences are not added.
got = renderer.RenderSequence(4, 6, 8, 1)
assertRenderSequence(t, got, 8, 1, "he", 0)
// Test Half-rendered text
got = renderer.RenderSequence(1, 12, 0, 0)
if assertRenderSequence(t, got, 0, 0, "oo hello ba", 2) {
assertColorSubsequence(t, got.Sequences[0], "RED", 0, 2)
assertColorSubsequence(t, got.Sequences[1], "BLUE", 9, 11)
}
// Test Half-rendered text (edges)
got = renderer.RenderSequence(2, 11, 0, 0)
if assertRenderSequence(t, got, 0, 0, "o hello b", 2) {
assertColorSubsequence(t, got.Sequences[0], "RED", 0, 1)
assertColorSubsequence(t, got.Sequences[1], "BLUE", 8, 9)
}
// TODO: test barkets
// Test with unicodes
text := "笀耔 [澉 灊灅甗](RED) 郔镺 笀耔 澉 [灊灅甗](yellow) 郔镺"
normalized := "笀耔 澉 灊灅甗 郔镺 笀耔 澉 灊灅甗 郔镺"
renderer = MarkdownTextRenderer{text}
got = renderer.RenderSequence(0, -1, 4, 7)
if assertRenderSequence(t, got, 4, 7, normalized, 2) {
assertColorSubsequence(t, got.Sequences[0], "RED", 3, 8)
assertColorSubsequence(t, got.Sequences[1], "YELLOW", 17, 20)
}
got = renderer.RenderSequence(6, 7, 0, 0)
if assertRenderSequence(t, got, 0, 0, "灅", 1) {
assertColorSubsequence(t, got.Sequences[0], "RED", 0, 1)
}
got = renderer.RenderSequence(7, 19, 0, 0)
if assertRenderSequence(t, got, 0, 0, "甗 郔镺 笀耔 澉 灊灅", 2) {
assertColorSubsequence(t, got.Sequences[0], "RED", 0, 1)
assertColorSubsequence(t, got.Sequences[1], "YELLOW", 10, 12)
}
// Test inside
renderer = MarkdownTextRenderer{"foo [foobar](red) bar"}
got = renderer.RenderSequence(4, 10, 0, 0)
if assertRenderSequence(t, got, 0, 0, "foobar", 1) {
assertColorSubsequence(t, got.Sequences[0], "RED", 0, 6)
}
}
func TestMarkdownTextRenderer_Render(t *testing.T) {
renderer := MarkdownTextRenderer{"[foo](red,bold) [bar](blue)"}
got := renderer.Render(6, 8)
if assertRenderSequence(t, got, 6, 8, "foo bar", 2) {
assertColorSubsequence(t, got.Sequences[0], "RED,BOLD", 0, 3)
assertColorSubsequence(t, got.Sequences[1], "blue", 4, 7)
}
}
func TestMarkdownTextRendererFactory(t *testing.T) {
factory := MarkdownTextRendererFactory{}
expected := MarkdownTextRenderer{"Hello world"}
assert.Equal(t, factory.TextRenderer("Hello world"), expected)
}
func TestColorSubsequencesToMap(t *testing.T) {
colorSubsequences := []ColorSubsequence{
{ColorRed, 1, 4},
{ColorBlue | AttrBold, 9, 10},
}
expected := make(map[int]Attribute)
expected[1] = ColorRed
expected[2] = ColorRed
expected[3] = ColorRed
expected[9] = ColorBlue | AttrBold
assert.Equal(t, expected, ColorSubsequencesToMap(colorSubsequences))
}
func getTestRenderedSequence() RenderedSequence {
cs := []ColorSubsequence{
{ColorRed, 3, 5},
{ColorBlue | AttrBold, 9, 10},
}
return RenderedSequence{"Hello world", ColorWhite, ColorBlack, cs, nil}
}
func newTestPoint(char rune, x, y int, colorA ...Attribute) Point {
var color Attribute
if colorA != nil && len(colorA) == 1 {
color = colorA[0]
} else {
color = ColorWhite
}
return Point{char, ColorBlack, color, x, y}
}
func TestRenderedSequence_Buffer(t *testing.T) {
sequence := getTestRenderedSequence()
expected := []Point{
newTestPoint('H', 5, 7),
newTestPoint('e', 6, 7),
newTestPoint('l', 7, 7),
newTestPoint('l', 7, 7, ColorRed),
newTestPoint('o', 8, 7, ColorRed),
newTestPoint(' ', 9, 7),
newTestPoint('w', 10, 7),
newTestPoint('o', 11, 7),
newTestPoint('r', 12, 7),
newTestPoint('l', 13, 7, ColorBlue|AttrBold),
newTestPoint('d', 14, 7),
}
buffer, lastColor := sequence.Buffer(5, 7)
assert.Equal(t, expected[:3], buffer[:3])
assert.Equal(t, ColorWhite, lastColor)
}
func AssertPoint(t *testing.T, got Point, char rune, x, y int, colorA ...Attribute) {
expected := newTestPoint(char, x, y, colorA...)
assert.Equal(t, expected, got)
}
func TestRenderedSequence_PointAt(t *testing.T) {
sequence := getTestRenderedSequence()
pointAt := func(n, x, y int) Point {
p, w := sequence.PointAt(n, x, y)
assert.Equal(t, w, 1)
return p
}
AssertPoint(t, pointAt(0, 3, 4), 'H', 3, 4)
AssertPoint(t, pointAt(1, 2, 1), 'e', 2, 1)
AssertPoint(t, pointAt(2, 6, 3), 'l', 6, 3)
AssertPoint(t, pointAt(3, 8, 8), 'l', 8, 8, ColorRed)
AssertPoint(t, pointAt(4, 1, 4), 'o', 1, 4, ColorRed)
AssertPoint(t, pointAt(5, 3, 6), ' ', 3, 6)
AssertPoint(t, pointAt(6, 4, 3), 'w', 4, 3)
AssertPoint(t, pointAt(7, 5, 2), 'o', 5, 2)
AssertPoint(t, pointAt(8, 0, 5), 'r', 0, 5)
AssertPoint(t, pointAt(9, 9, 0), 'l', 9, 0, ColorBlue|AttrBold)
AssertPoint(t, pointAt(10, 7, 1), 'd', 7, 1)
}
func getTestPlainRenderer() PlainRenderer {
return PlainRenderer{"[Hello](red) \x1b[31mworld"}
}
func TestPlainRenderer_NormalizedText(t *testing.T) {
r := getTestPlainRenderer()
assert.Equal(t, "[Hello](red) \x1b[31mworld", r.NormalizedText())
assert.Equal(t, "[Hello](red) \x1b[31mworld", r.Text)
}
func TestPlainRenderer_Render(t *testing.T) {
renderer := getTestPlainRenderer()
got := renderer.Render(5, 7)
assertRenderSequence(t, got, 5, 7, "[Hello](red) \x1b[31mworld", 0)
}
func TestPlainRenderer_RenderSequence(t *testing.T) {
renderer := getTestPlainRenderer()
got := renderer.RenderSequence(3, 5, 9, 1)
assertRenderSequence(t, got, 9, 1, "ll", 0)
}
func TestPlainRendererFactory(t *testing.T) {
factory := PlainRendererFactory{}
expected := PlainRenderer{"Hello world"}
assert.Equal(t, factory.TextRenderer("Hello world"), expected)
}
func TestPosUnicode(t *testing.T) {
// Every characters takes 3 bytes
text := "你好世界"
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"))
}