Skip to content

Commit

Permalink
Merge pull request #656 from noborus/es-compatibility
Browse files Browse the repository at this point in the history
Improve escape sequence compatibility
  • Loading branch information
noborus authored Nov 14, 2024
2 parents a0ca6de + 5bb3b15 commit b1b251b
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 79 deletions.
203 changes: 135 additions & 68 deletions oviewer/convert_es.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package oviewer

import (
"errors"
"fmt"
"log"
"strconv"
Expand Down Expand Up @@ -77,7 +78,12 @@ func (es *escapeSequence) convert(st *parseState) bool {
case ansiControlSequence:
switch {
case mainc == 'm':
st.style = csToStyle(st.style, es.parameter.String())
style, err := csToStyle(st.style, es.parameter.String())
if err != nil {
es.state = ansiText
return false
}
st.style = style
case mainc == 'K':
// CSI 0 K or CSI K maintains the style after the newline
// (can change the background color of the line).
Expand All @@ -90,6 +96,9 @@ func (es *escapeSequence) convert(st *parseState) bool {
case mainc >= '0' && mainc <= 'f':
es.parameter.WriteRune(mainc)
return true
case mainc < 0x40:
es.parameter.WriteRune(mainc)
return true
}
es.state = ansiText
return true
Expand Down Expand Up @@ -161,128 +170,186 @@ func (es *escapeSequence) convert(st *parseState) bool {
}

// csToStyle returns tcell.Style from the control sequence.
func csToStyle(style tcell.Style, params string) tcell.Style {
func csToStyle(style tcell.Style, params string) (tcell.Style, error) {
if params == "0" || params == "" || params == ";" {
return tcell.StyleDefault.Normal()
return tcell.StyleDefault.Normal(), nil
}

if s, ok := csiCache.Load(params); ok {
style = applyStyle(style, s.(OVStyle))
return style
return style, nil
}

s := parseCSI(params)
s, err := parseCSI(params)
if err != nil {
return style, err
}
csiCache.Store(params, s)
style = applyStyle(style, s)
return style
return applyStyle(style, s), nil
}

// parseCSI actually parses the style and returns ovStyle.
func parseCSI(params string) OVStyle {
func parseCSI(params string) (OVStyle, error) {
s := OVStyle{}
fields := strings.Split(params, ";")
for index := 0; index < len(fields); index++ {
field := fields[index]
switch field {
case "1", "01":
num, err := toESCode(field)
if err != nil {
if errors.Is(err, ErrNotSuuport) {
return s, nil
}
return s, err
}
switch num {
case 0:
s = OVStyle{}
case 1:
s.Bold = true
case "2", "02":
case 2:
s.Dim = true
case "3", "03":
case 3:
s.Italic = true
case "4", "04":
case 4:
s.Underline = true
case "5", "05":
case 5:
s.Blink = true
case "6", "06":
case 6:
s.Blink = true
case "7", "07":
case 7:
s.Reverse = true
case "8", "08":
case 8:
// Invisible On (not implemented)
case "9", "09":
case 9:
s.StrikeThrough = true
case "22":
case 22:
s.UnBold = true
case "23":
case 23:
s.UnItalic = true
case "24":
case 24:
s.UnUnderline = true
case "25":
case 25:
s.UnBlink = true
case "27":
case 27:
s.UnReverse = true
case "28":
case 28:
// Invisible Off (not implemented)
case "29":
case 29:
s.UnStrikeThrough = true
case "30", "31", "32", "33", "34", "35", "36", "37":
colorNumber, _ := strconv.Atoi(field)
s.Foreground = colorName(colorNumber - 30)
case "38":
case 30, 31, 32, 33, 34, 35, 36, 37:
s.Foreground = colorName(num - 30)
case 38:
i, color := csColor(fields[index:])
if i == 0 {
return s, nil
}
index += i
s.Foreground = color
case "39":
case 39:
s.Foreground = "default"
case "40", "41", "42", "43", "44", "45", "46", "47":
colorNumber, _ := strconv.Atoi(field)
s.Background = colorName(colorNumber - 40)
case "48":
case 40, 41, 42, 43, 44, 45, 46, 47:
s.Background = colorName(num - 40)
case 48:
i, color := csColor(fields[index:])
if i == 0 {
return s, nil
}
index += i
s.Background = color
case "49":
case 49:
s.Background = "default"
case "53":
case 53:
s.OverLine = true
case "55":
case 55:
s.UnOverLine = true
case "90", "91", "92", "93", "94", "95", "96", "97":
colorNumber, _ := strconv.Atoi(field)
s.Foreground = colorName(colorNumber - 82)
case "100", "101", "102", "103", "104", "105", "106", "107":
colorNumber, _ := strconv.Atoi(field)
s.Background = colorName(colorNumber - 92)
case 90, 91, 92, 93, 94, 95, 96, 97:
s.Foreground = colorName(num - 82)
case 100, 101, 102, 103, 104, 105, 106, 107:
s.Background = colorName(num - 92)
}
}
return s
return s, nil
}

// toESCode converts a string to an integer.
// If the code is smaller than 0x40 for compatibility,
// return -1 instead of an error.
func toESCode(str string) (int, error) {
num, err := strconv.Atoi(str)
if err != nil {
for _, char := range str {
if char >= 0x40 {
return 0, ErrInvalidCSI
}
}
return 0, ErrNotSuuport
}
return num, nil
}

// csColor parses 8-bit color and 24-bit color.
func csColor(fields []string) (int, string) {
if len(fields) < 2 {
return 0, ""
}
if fields[1] == "" {
return 1, ""
}
ex, err := strconv.Atoi(fields[1])
if err != nil {
return 1, ""
}
switch ex {
case 5: // 8-bit colors.
if len(fields) < 3 {
return 1, ""
}
if fields[2] == "" {
return len(fields), ""
}

index := 1
color := "default"
switch fields[1] {
case "5": // 8-bit colors.
if len(fields) > index+1 {
eColor := fields[2]
if eColor == "" {
return index, color
}
c, err := strconv.Atoi(eColor)
if err != nil {
return index, color
color, err := parse8BitColor(fields[2])
if err != nil {
return 0, color
}
return 2, color
case 2: // 24-bit colors.
if len(fields) < 5 {
return len(fields), ""
}
for i := 2; i < 5; i++ {
if fields[i] == "" {
return i, ""
}
index++
color = colorName(c)
}
case "2": // 24-bit colors.
if len(fields) <= index+3 {
index += len(fields) // Skip the Invalid fields.
return index, color

color, err := parseRGBColor(fields[2:5])
if err != nil {
return 0, color
}
red, _ := strconv.Atoi(fields[2])
green, _ := strconv.Atoi(fields[3])
blue, _ := strconv.Atoi(fields[4])
index += 3
color = fmt.Sprintf("#%02x%02x%02x", red, green, blue)
return 4, color
}
return 1, ""
}

func parse8BitColor(field string) (string, error) {
c, err := strconv.Atoi(field)
if err != nil {
return "", fmt.Errorf("invalid 8-bit color value: %v", field)
}
color := colorName(c)
return color, nil
}

func parseRGBColor(fields []string) (string, error) {
red, err1 := strconv.Atoi(fields[0])
green, err2 := strconv.Atoi(fields[1])
blue, err3 := strconv.Atoi(fields[2])
if err1 != nil || err2 != nil || err3 != nil {
return "", fmt.Errorf("invalid RGB color values: %v, %v, %v", fields[0], fields[1], fields[2])
}
return index, color
color := fmt.Sprintf("#%02x%02x%02x", red, green, blue)
return color, nil
}

// colorName returns a string that can be used to specify the color of tcell.
Expand Down
Loading

0 comments on commit b1b251b

Please sign in to comment.