238 lines
6.7 KiB
Go
238 lines
6.7 KiB
Go
|
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()
|
||
|
}
|