forked from tslocum/cview

5 changed files with 361 additions and 89 deletions
@ -0,0 +1,237 @@
@@ -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() |
||||
} |
Loading…
Reference in new issue