elvish/pkg/ui/parse_sgr.go
Qi Xiao 89d95c17a6 pkg/ui: Do not strip unprintable characters from the prompt.
This will preserve private use characters that are sometimes used by icon fonts.
Control characters are already escaped by cli/term.Writer.
2020-08-28 22:00:40 +01:00

159 lines
3.5 KiB
Go

package ui
import (
"strconv"
"strings"
)
type sgrTokenizer struct {
text string
styling Styling
content string
}
const sgrPrefix = "\033["
func (st *sgrTokenizer) Next() bool {
for strings.HasPrefix(st.text, sgrPrefix) {
trimmed := strings.TrimPrefix(st.text, sgrPrefix)
// Find the terminator of this sequence.
termIndex := strings.IndexFunc(trimmed, func(r rune) bool {
return r != ';' && (r < '0' || r > '9')
})
if termIndex == -1 {
// The string ends with an unterminated escape sequence; ignore
// it.
st.text = ""
return false
}
term := trimmed[termIndex]
sgr := trimmed[:termIndex]
st.text = trimmed[termIndex+1:]
if term == 'm' {
st.styling = StylingFromSGR(sgr)
st.content = ""
return true
}
// If the terminator is not 'm'; we have seen a non-SGR escape sequence;
// ignore it and continue.
}
if st.text == "" {
return false
}
// Parse a content segment until the next SGR prefix.
content := ""
nextSGR := strings.Index(st.text, sgrPrefix)
if nextSGR == -1 {
content = st.text
} else {
content = st.text[:nextSGR]
}
st.text = st.text[len(content):]
st.styling = nil
st.content = content
return true
}
func (st *sgrTokenizer) Token() (Styling, string) {
return st.styling, st.content
}
// ParseSGREscapedText parses SGR-escaped text into a Text. It also removes
// non-SGR CSI sequences sequences in the text.
func ParseSGREscapedText(s string) Text {
var text Text
var style Style
tokenizer := sgrTokenizer{text: s}
for tokenizer.Next() {
styling, content := tokenizer.Token()
if styling != nil {
styling.transform(&style)
}
if content != "" {
text = append(text, &Segment{style, content})
}
}
return text
}
var sgrStyling = map[int]Styling{
0: Reset,
1: Bold,
2: Dim,
4: Underlined,
5: Blink,
7: Inverse,
}
// StyleFromSGR builds a Style from an SGR sequence.
func StyleFromSGR(s string) Style {
var ret Style
StylingFromSGR(s).transform(&ret)
return ret
}
// StylingFromSGR builds a Style from an SGR sequence.
func StylingFromSGR(s string) Styling {
styling := jointStyling{}
codes := getSGRCodes(s)
if len(codes) == 0 {
return Reset
}
for len(codes) > 0 {
code := codes[0]
consume := 1
var moreStyling Styling
switch {
case sgrStyling[code] != nil:
moreStyling = sgrStyling[code]
case 30 <= code && code <= 37:
moreStyling = Fg(ansiColor(code - 30))
case 40 <= code && code <= 47:
moreStyling = Bg(ansiColor(code - 40))
case 90 <= code && code <= 97:
moreStyling = Fg(ansiBrightColor(code - 90))
case 100 <= code && code <= 107:
moreStyling = Bg(ansiBrightColor(code - 100))
case code == 38 && len(codes) >= 3 && codes[1] == 5:
moreStyling = Fg(xterm256Color(codes[2]))
consume = 3
case code == 48 && len(codes) >= 3 && codes[1] == 5:
moreStyling = Bg(xterm256Color(codes[2]))
consume = 3
case code == 38 && len(codes) >= 5 && codes[1] == 2:
moreStyling = Fg(trueColor{
uint8(codes[2]), uint8(codes[3]), uint8(codes[4])})
consume = 5
case code == 48 && len(codes) >= 5 && codes[1] == 2:
moreStyling = Bg(trueColor{
uint8(codes[2]), uint8(codes[3]), uint8(codes[4])})
consume = 5
default:
// Do nothing; skip this code
}
codes = codes[consume:]
if moreStyling != nil {
styling = append(styling, moreStyling)
}
}
return styling
}
func getSGRCodes(s string) []int {
var codes []int
for _, part := range strings.Split(s, ";") {
if part == "" {
codes = append(codes, 0)
} else {
code, err := strconv.Atoi(part)
if err == nil {
codes = append(codes, code)
}
}
}
return codes
}