Added Escape(), ANSIIWriter(), and TranslateANSII(). Resolves #84, resolves #24

This commit is contained in:
Oliver 2018-04-14 00:05:25 +02:00
parent 96473a04c6
commit 911fb9543e
5 changed files with 365 additions and 93 deletions

View File

@ -64,6 +64,9 @@ Add your issue here on GitHub. Feel free to get in touch if you have any questio
(There are no corresponding tags in the project. I only keep such a history in this README.)
- v0.14 (2018-04-13)
- Added an `Escape()` function which keep strings like color or region tags from being recognized as such.
- Added `ANSIIWriter()` and `TranslateANSII()` which convert ANSII escape sequences to `tview` color tags.
- v0.13 (2018-04-01)
- Added background colors and text attributes to color tags.
- v0.12 (2018-03-13)

237
ansii.go Normal file
View File

@ -0,0 +1,237 @@
package tview
import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
)
// The states of the ANSII escape code parser.
const (
ansiiText = iota
ansiiEscape
ansiiSubstring
ansiiControlSequence
)
// ansii is a io.Writer which translates ANSII escape codes into tview color
// tags.
type ansii struct {
io.Writer
// Reusable buffers.
buffer *bytes.Buffer // The entire output text of one Write().
csiParameter, csiIntermediate *bytes.Buffer // Partial CSI strings.
// The current state of the parser. One of the ansii constants.
state int
}
// ANSIIWriter returns an io.Writer which translates any ANSII escape codes
// written to it into tview color tags. Other escape codes don't have an effect
// and are simply removed. The translated text is written to the provided
// writer.
func ANSIIWriter(writer io.Writer) io.Writer {
return &ansii{
Writer: writer,
buffer: new(bytes.Buffer),
csiParameter: new(bytes.Buffer),
csiIntermediate: new(bytes.Buffer),
state: ansiiText,
}
}
// Write parses the given text as a string of runes, translates ANSII escape
// codes to color tags and writes them to the output writer.
func (a *ansii) Write(text []byte) (int, error) {
defer func() {
a.buffer.Reset()
}()
for _, r := range string(text) {
switch a.state {
// We just entered an escape sequence.
case ansiiEscape:
switch r {
case '[': // Control Sequence Introducer.
a.csiParameter.Reset()
a.csiIntermediate.Reset()
a.state = ansiiControlSequence
case 'c': // Reset.
fmt.Fprint(a.buffer, "[-:-:-]")
a.state = ansiiText
case 'P', ']', 'X', '^', '_': // Substrings and commands.
a.state = ansiiSubstring
default: // Ignore.
a.state = ansiiText
}
// CSI Sequences.
case ansiiControlSequence:
switch {
case r >= 0x30 && r <= 0x3f: // Parameter bytes.
if _, err := a.csiParameter.WriteRune(r); err != nil {
return 0, err
}
case r >= 0x20 && r <= 0x2f: // Intermediate bytes.
if _, err := a.csiIntermediate.WriteRune(r); err != nil {
return 0, err
}
case r >= 0x40 && r <= 0x7e: // Final byte.
switch r {
case 'E': // Next line.
count, _ := strconv.Atoi(a.csiParameter.String())
if count == 0 {
count = 1
}
fmt.Fprint(a.buffer, strings.Repeat("\n", count))
case 'm': // Select Graphic Rendition.
var (
background, foreground, attributes string
clearAttributes bool
)
fields := strings.Split(a.csiParameter.String(), ";")
if len(fields) == 0 || len(fields) == 1 && fields[0] == "0" {
// Reset.
if _, err := a.buffer.WriteString("[-:-:-]"); err != nil {
return 0, err
}
break
}
lookupColor := func(colorNumber int, bright bool) string {
if colorNumber < 0 || colorNumber > 7 {
return "black"
}
if bright {
colorNumber += 8
}
return [...]string{
"black",
"red",
"green",
"yellow",
"blue",
"darkmagenta",
"darkcyan",
"white",
"#7f7f7f",
"#ff0000",
"#00ff00",
"#ffff00",
"#5c5cff",
"#ff00ff",
"#00ffff",
"#ffffff",
}[colorNumber]
}
for index, field := range fields {
switch field {
case "1", "01":
attributes += "b"
case "2", "02":
attributes += "d"
case "4", "04":
attributes += "u"
case "5", "05":
attributes += "l"
case "7", "07":
attributes += "7"
case "22", "24", "25", "27":
clearAttributes = true
case "30", "31", "32", "33", "34", "35", "36", "37":
colorNumber, _ := strconv.Atoi(field)
foreground = lookupColor(colorNumber-30, false)
case "40", "41", "42", "43", "44", "45", "46", "47":
colorNumber, _ := strconv.Atoi(field)
background = lookupColor(colorNumber-40, false)
case "90", "91", "92", "93", "94", "95", "96", "97":
colorNumber, _ := strconv.Atoi(field)
foreground = lookupColor(colorNumber-90, true)
case "100", "101", "102", "103", "104", "105", "106", "107":
colorNumber, _ := strconv.Atoi(field)
background = lookupColor(colorNumber-100, true)
case "38", "48":
var color string
if len(fields) > index+1 {
if fields[index+1] == "5" && len(fields) > index+2 { // 8-bit colors.
colorNumber, _ := strconv.Atoi(fields[index+2])
if colorNumber <= 7 {
color = lookupColor(colorNumber, false)
} else if colorNumber <= 15 {
color = lookupColor(colorNumber, true)
} else if colorNumber <= 231 {
red := (colorNumber - 16) / 36
green := ((colorNumber - 16) / 6) % 6
blue := (colorNumber - 16) % 6
color = fmt.Sprintf("%02x%02x%02x", 255*red/5, 255*green/5, 255*blue/5)
} else if colorNumber <= 255 {
grey := 255 * (colorNumber - 232) / 23
color = fmt.Sprintf("%02x%02x%02x", grey, grey, grey)
}
} else if fields[index+1] == "2" && len(fields) > index+4 { // 24-bit colors.
red, _ := strconv.Atoi(fields[index+2])
green, _ := strconv.Atoi(fields[index+3])
blue, _ := strconv.Atoi(fields[index+4])
color = fmt.Sprintf("%02x%02x%02x", red, green, blue)
}
}
if len(color) > 0 {
if field == "38" {
foreground = color
} else {
background = color
}
}
}
}
if len(attributes) > 0 || clearAttributes {
attributes = ":" + attributes
}
if len(foreground) > 0 || len(background) > 0 || len(attributes) > 0 {
fmt.Fprintf(a.buffer, "[%s:%s%s]", foreground, background, attributes)
}
}
a.state = ansiiText
default: // Undefined byte.
a.state = ansiiText // Abort CSI.
}
// We just entered a substring/command sequence.
case ansiiSubstring:
if r == 27 { // Most likely the end of the substring.
a.state = ansiiEscape
} // Ignore all other characters.
// "ansiiText" and all others.
default:
if r == 27 {
// This is the start of an escape sequence.
a.state = ansiiEscape
} else {
// Just a regular rune. Send to buffer.
if _, err := a.buffer.WriteRune(r); err != nil {
return 0, err
}
}
}
}
// Write buffer to target writer.
n, err := a.buffer.WriteTo(a.Writer)
if err != nil {
return int(n), err
}
return len(text), nil
}
// TranslateANSII replaces ANSII escape sequences found in the provided string
// with tview's color tags and returns the resulting string.
func TranslateANSII(text string) string {
var buffer bytes.Buffer
writer := ANSIIWriter(&buffer)
writer.Write([]byte(text))
return buffer.String()
}

8
doc.go
View File

@ -85,8 +85,8 @@ tag is as follows:
Each of the three fields can be left blank and trailing fields can be ommitted.
(Empty square brackets "[]", however, are not considered color tags.) Colors
that are not specified will be left unchanged. (If the flags field is indicated
by a colon but left empty, it will reset any flags.)
that are not specified will be left unchanged. A field with just a dash ("-")
means "reset to default".
You can specify the following flags (some flags may not be supported by your
terminal):
@ -104,7 +104,9 @@ Examples:
[:red]Red background, text color unchanged
[yellow::u]Yellow text underlined
[::bl]Bold, blinking text
[::]Colors unchanged, flags reset
[::-]Colors unchanged, flags reset
[-]Reset foreground color
[-:-:-]Reset everything
[:]No effect
[]Not a valid color tag, will print square brackets as they are

View File

@ -18,13 +18,14 @@ var TabSize = 4
// textViewIndex contains information about each line displayed in the text
// view.
type textViewIndex struct {
Line int // The index into the "buffer" variable.
Pos int // The index into the "buffer" string (byte position).
NextPos int // The (byte) index of the next character in this buffer line.
Width int // The screen width of this line.
Style tcell.Style // The starting style.
OverwriteAttr bool // The starting flag indicating if style attributes should be overwritten.
Region string // The starting region ID.
Line int // The index into the "buffer" variable.
Pos int // The index into the "buffer" string (byte position).
NextPos int // The (byte) index of the next character in this buffer line.
Width int // The screen width of this line.
ForegroundColor string // The starting foreground color ("" = don't change, "-" = reset).
BackgroundColor string // The starting background color ("" = don't change, "-" = reset).
Attributes string // The starting attributes ("" = don't change, "-" = reset).
Region string // The starting region ID.
}
// TextView is a box which displays text. It implements the io.Writer interface
@ -500,8 +501,7 @@ func (t *TextView) reindexBuffer(width int) {
// Initial states.
regionID := ""
var highlighted, overwriteAttr bool
style := tcell.StyleDefault.Foreground(t.textColor)
var highlighted bool
// Go through each line in the buffer.
for bufferIndex, str := range t.buffer {
@ -558,14 +558,18 @@ func (t *TextView) reindexBuffer(width int) {
}
// Create index from split lines.
var originalPos, colorPos, regionPos, escapePos int
var (
originalPos, colorPos, regionPos, escapePos int
foregroundColor, backgroundColor, attributes string
)
for _, splitLine := range splitLines {
line := &textViewIndex{
Line: bufferIndex,
Pos: originalPos,
Style: style,
OverwriteAttr: overwriteAttr,
Region: regionID,
Line: bufferIndex,
Pos: originalPos,
ForegroundColor: foregroundColor,
BackgroundColor: backgroundColor,
Attributes: attributes,
Region: regionID,
}
// Shift original position with tags.
@ -574,7 +578,7 @@ func (t *TextView) reindexBuffer(width int) {
if colorPos < len(colorTagIndices) && colorTagIndices[colorPos][0] <= originalPos+lineLength {
// Process color tags.
originalPos += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
style, overwriteAttr = styleFromTag(style, overwriteAttr, colorTags[colorPos])
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos])
colorPos++
} else if regionPos < len(regionIndices) && regionIndices[regionPos][0] <= originalPos+lineLength {
// Process region tags.
@ -712,6 +716,7 @@ func (t *TextView) Draw(screen tcell.Screen) {
}
// Draw the buffer.
defaultStyle := tcell.StyleDefault.Foreground(t.textColor)
for line := t.lineOffset; line < len(t.index); line++ {
// Are we done?
if line-t.lineOffset >= height {
@ -721,8 +726,9 @@ func (t *TextView) Draw(screen tcell.Screen) {
// Get the text for this line.
index := t.index[line]
text := t.buffer[index.Line][index.Pos:index.NextPos]
style := index.Style
overwriteAttr := index.OverwriteAttr
foregroundColor := index.ForegroundColor
backgroundColor := index.BackgroundColor
attributes := index.Attributes
regionID := index.Region
// Get color tags.
@ -768,7 +774,7 @@ func (t *TextView) Draw(screen tcell.Screen) {
// Get the color.
if currentTag < len(colorTags) && pos >= colorTagIndices[currentTag][0] && pos < colorTagIndices[currentTag][1] {
if pos == colorTagIndices[currentTag][1]-1 {
style, overwriteAttr = styleFromTag(style, overwriteAttr, colorTags[currentTag])
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[currentTag])
currentTag++
}
continue
@ -809,8 +815,12 @@ func (t *TextView) Draw(screen tcell.Screen) {
break
}
// Mix the existing style with the new style.
_, _, existingStyle, _ := screen.GetContent(x+posX, y+line-t.lineOffset)
_, background, _ := existingStyle.Decompose()
style := overlayStyle(background, defaultStyle, foregroundColor, backgroundColor, attributes)
// Do we highlight this character?
finalStyle := style
var highlighted bool
if len(regionID) > 0 {
if _, ok := t.highlights[regionID]; ok {
@ -818,7 +828,7 @@ func (t *TextView) Draw(screen tcell.Screen) {
}
}
if highlighted {
fg, bg, _ := finalStyle.Decompose()
fg, bg, _ := style.Decompose()
if bg == tcell.ColorDefault {
r, g, b := fg.RGB()
c := colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255}
@ -829,15 +839,12 @@ func (t *TextView) Draw(screen tcell.Screen) {
bg = tcell.ColorBlack
}
}
finalStyle = style.Background(fg).Foreground(bg)
} else {
_, _, existingStyle, _ := screen.GetContent(x+posX, y+line-t.lineOffset)
finalStyle = overlayStyle(existingStyle, style, overwriteAttr)
style = style.Background(fg).Foreground(bg)
}
// Draw the character.
for offset := 0; offset < chWidth; offset++ {
screen.SetContent(x+posX+offset, y+line-t.lineOffset, ch, nil, finalStyle)
screen.SetContent(x+posX+offset, y+line-t.lineOffset, ch, nil, style)
}
// Advance.

151
util.go
View File

@ -1,6 +1,7 @@
package tview
import (
"fmt"
"math"
"regexp"
"strconv"
@ -104,7 +105,7 @@ var joints = map[string]rune{
// Common regular expressions.
var (
colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6})?(:([a-zA-Z]+|#[0-9a-zA-Z]{6})?(:([lbdru]+))?)?\]`)
colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([lbdru]+|\-)?)?)?\]`)
regionPattern = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`)
escapePattern = regexp.MustCompile(`\[([a-zA-Z0-9_,;: \-\."#]+)\[(\[*)\]`)
nonEscapePattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`)
@ -158,35 +159,72 @@ func init() {
}
}
// styleFromTag takes the given style and modifies it based on the substrings
// extracted by the regular expression for color tags. The new style is returned
// as well as the flag indicating if any style attributes were explicitly
// specified (whose original value is also returned).
func styleFromTag(style tcell.Style, overwriteAttr bool, tagSubstrings []string) (tcell.Style, bool) {
// Colors.
// styleFromTag takes the given style, defined by a foreground color (fgColor),
// a background color (bgColor), and style attributes, and modifies it based on
// the substrings (tagSubstrings) extracted by the regular expression for color
// tags. The new colors and attributes are returned where empty strings mean
// "don't modify" and a dash ("-") means "reset to default".
func styleFromTag(fgColor, bgColor, attributes string, tagSubstrings []string) (newFgColor, newBgColor, newAttributes string) {
if tagSubstrings[colorForegroundPos] != "" {
color := tagSubstrings[colorForegroundPos]
if color == "" {
style = style.Foreground(tcell.ColorDefault)
} else {
style = style.Foreground(tcell.GetColor(color))
}
}
if tagSubstrings[colorBackgroundPos-1] != "" {
color := tagSubstrings[colorBackgroundPos]
if color == "" {
style = style.Background(tcell.ColorDefault)
} else {
style = style.Background(tcell.GetColor(color))
if color == "-" {
fgColor = "-"
} else if color != "" {
fgColor = color
}
}
// Flags.
specified := tagSubstrings[colorFlagPos-1] != ""
if specified {
overwriteAttr = true
if tagSubstrings[colorBackgroundPos-1] != "" {
color := tagSubstrings[colorBackgroundPos]
if color == "-" {
bgColor = "-"
} else if color != "" {
bgColor = color
}
}
if tagSubstrings[colorFlagPos-1] != "" {
flags := tagSubstrings[colorFlagPos]
if flags == "-" {
attributes = "-"
} else if flags != "" {
attributes = flags
}
}
return fgColor, bgColor, attributes
}
// overlayStyle mixes a background color with a foreground color (fgColor),
// a (possibly new) background color (bgColor), and style attributes, and
// returns the resulting style. For a definition of the colors and attributes,
// see styleFromTag(). Reset instructions cause the corresponding part of the
// default style to be used.
func overlayStyle(background tcell.Color, defaultStyle tcell.Style, fgColor, bgColor, attributes string) tcell.Style {
defFg, defBg, defAttr := defaultStyle.Decompose()
style := defaultStyle.Background(background)
if fgColor == "-" {
style = style.Foreground(defFg)
} else if fgColor != "" {
style = style.Foreground(tcell.GetColor(fgColor))
}
if bgColor == "-" {
style = style.Background(defBg)
} else if bgColor != "" {
style = style.Background(tcell.GetColor(bgColor))
}
if attributes == "-" {
style = style.Bold(defAttr&tcell.AttrBold > 0)
style = style.Blink(defAttr&tcell.AttrBlink > 0)
style = style.Reverse(defAttr&tcell.AttrReverse > 0)
style = style.Underline(defAttr&tcell.AttrUnderline > 0)
style = style.Dim(defAttr&tcell.AttrDim > 0)
} else if attributes != "" {
style = style.Normal()
for _, flag := range tagSubstrings[colorFlagPos] {
for _, flag := range attributes {
switch flag {
case 'l':
style = style.Blink(true)
@ -201,26 +239,7 @@ func styleFromTag(style tcell.Style, overwriteAttr bool, tagSubstrings []string)
}
}
}
return style, overwriteAttr
}
// overlayStyle mixes a bottom and a top style and returns the result. Top
// colors (other than tcell.ColorDefault) overwrite bottom colors. Top
// style attributes overwrite bottom style attributes only if overwriteAttr is
// true.
func overlayStyle(bottom, top tcell.Style, overwriteAttr bool) tcell.Style {
style := bottom
fg, bg, attr := top.Decompose()
if bg != tcell.ColorDefault {
style = style.Background(bg)
}
if fg != tcell.ColorDefault {
style = style.Foreground(fg)
}
if overwriteAttr {
style = style.Normal()
style |= tcell.Style(attr)
}
return style
}
@ -272,13 +291,12 @@ func decomposeString(text string) (colorIndices [][]int, colors [][]string, esca
// Returns the number of actual runes printed (not including color tags) and the
// actual width used for the printed runes.
func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tcell.Color) (int, int) {
return printWithStyle(screen, text, x, y, maxWidth, align, tcell.StyleDefault.Foreground(color), false)
return printWithStyle(screen, text, x, y, maxWidth, align, tcell.StyleDefault.Foreground(color))
}
// printWithStyle works like Print() but it takes a style instead of just a
// foreground color. The overwriteAttr indicates whether or not a style's
// additional attributes (see tcell.AttrMask) should be overwritten.
func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, style tcell.Style, overwriteAttr bool) (int, int) {
// foreground color.
func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, style tcell.Style) (int, int) {
if maxWidth < 0 {
return 0, 0
}
@ -289,17 +307,20 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
// We deal with runes, not with bytes.
runes := []rune(strippedText)
// This helper function takes positions for a substring of "runes" and a start
// style and returns the substring with the original tags and the new start
// style.
substring := func(from, to int, style tcell.Style, overwriteAttr bool) (string, tcell.Style, bool) {
var colorPos, escapePos, runePos, startPos int
// This helper function takes positions for a substring of "runes" and returns
// a new string corresponding to this substring, making sure printing that
// substring will observe color tags.
substring := func(from, to int) string {
var (
colorPos, escapePos, runePos, startPos int
foregroundColor, backgroundColor, attributes string
)
for pos := range text {
// Handle color tags.
if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] {
if pos == colorIndices[colorPos][1]-1 {
if runePos <= from {
style, overwriteAttr = styleFromTag(style, overwriteAttr, colors[colorPos])
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
}
colorPos++
}
@ -319,13 +340,13 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
if runePos == from {
startPos = pos
} else if runePos >= to {
return text[startPos:pos], style, overwriteAttr
return fmt.Sprintf(`[%s:%s:%s]%s`, foregroundColor, backgroundColor, attributes, text[startPos:pos])
}
runePos++
}
return text[startPos:], style, overwriteAttr
return fmt.Sprintf(`[%s:%s:%s]%s`, foregroundColor, backgroundColor, attributes, text[startPos:])
}
// We want to reduce everything to AlignLeft.
@ -340,17 +361,16 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
width += w
start = index
}
text, style, overwriteAttr = substring(start, len(runes), style, overwriteAttr)
return printWithStyle(screen, text, x+maxWidth-width, y, width, AlignLeft, style, overwriteAttr)
return printWithStyle(screen, substring(start, len(runes)), x+maxWidth-width, y, width, AlignLeft, style)
} else if align == AlignCenter {
width := runewidth.StringWidth(strippedText)
if width == maxWidth {
// Use the exact space.
return printWithStyle(screen, text, x, y, maxWidth, AlignLeft, style, overwriteAttr)
return printWithStyle(screen, text, x, y, maxWidth, AlignLeft, style)
} else if width < maxWidth {
// We have more space than we need.
half := (maxWidth - width) / 2
return printWithStyle(screen, text, x+half, y, maxWidth-half, AlignLeft, style, overwriteAttr)
return printWithStyle(screen, text, x+half, y, maxWidth-half, AlignLeft, style)
} else {
// Chop off runes until we have a perfect fit.
var choppedLeft, choppedRight, leftIndex, rightIndex int
@ -366,20 +386,22 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
rightIndex--
}
}
text, style, overwriteAttr = substring(leftIndex, rightIndex, style, overwriteAttr)
return printWithStyle(screen, text, x, y, maxWidth, AlignLeft, style, overwriteAttr)
return printWithStyle(screen, substring(leftIndex, rightIndex), x, y, maxWidth, AlignLeft, style)
}
}
// Draw text.
drawn := 0
drawnWidth := 0
var colorPos, escapePos int
var (
colorPos, escapePos int
foregroundColor, backgroundColor, attributes string
)
for pos, ch := range text {
// Handle color tags.
if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] {
if pos == colorIndices[colorPos][1]-1 {
style, overwriteAttr = styleFromTag(style, overwriteAttr, colors[colorPos])
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
colorPos++
}
continue
@ -403,7 +425,8 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
// Print the rune.
_, _, finalStyle, _ := screen.GetContent(finalX, y)
finalStyle = overlayStyle(finalStyle, style, overwriteAttr)
_, background, _ := finalStyle.Decompose()
finalStyle = overlayStyle(background, style, foregroundColor, backgroundColor, attributes)
for offset := 0; offset < chWidth; offset++ {
// To avoid undesired effects, we place the same character in all cells.
screen.SetContent(finalX+offset, y, ch, nil, finalStyle)