mirror of
https://github.com/go-sylixos/elvish.git
synced 2024-12-13 09:57:51 +08:00
42c6c3b1aa
Styled text is not supposed to "inherit" the current SGR styling context when written to the terminal. This has always been the intention, but not correctly implemented. This commit fixes that for both styled segments and styled texts. Tests are amended to account for the difference in the output. With context insensitivity correctly implemented, there is now no need for a "default" color. It is functionally equivalent to a lack of color. The parsing of SGR still needs to be aware of the codes 39 (default foreground) and 49 (default background), but these codes are now translated into FgDefault and BgDefault, which resets the foreground and background color fields.
163 lines
3.6 KiB
Go
163 lines
3.6 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
|
|
case code == 39:
|
|
moreStyling = FgDefault
|
|
case code == 49:
|
|
moreStyling = BgDefault
|
|
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
|
|
}
|