github.com/elves/elvish@v0.15.0/pkg/ui/parse_sgr.go (about)

     1  package ui
     2  
     3  import (
     4  	"strconv"
     5  	"strings"
     6  )
     7  
     8  type sgrTokenizer struct {
     9  	text string
    10  
    11  	styling Styling
    12  	content string
    13  }
    14  
    15  const sgrPrefix = "\033["
    16  
    17  func (st *sgrTokenizer) Next() bool {
    18  	for strings.HasPrefix(st.text, sgrPrefix) {
    19  		trimmed := strings.TrimPrefix(st.text, sgrPrefix)
    20  		// Find the terminator of this sequence.
    21  		termIndex := strings.IndexFunc(trimmed, func(r rune) bool {
    22  			return r != ';' && (r < '0' || r > '9')
    23  		})
    24  		if termIndex == -1 {
    25  			// The string ends with an unterminated escape sequence; ignore
    26  			// it.
    27  			st.text = ""
    28  			return false
    29  		}
    30  		term := trimmed[termIndex]
    31  		sgr := trimmed[:termIndex]
    32  		st.text = trimmed[termIndex+1:]
    33  		if term == 'm' {
    34  			st.styling = StylingFromSGR(sgr)
    35  			st.content = ""
    36  			return true
    37  		}
    38  		// If the terminator is not 'm'; we have seen a non-SGR escape sequence;
    39  		// ignore it and continue.
    40  	}
    41  	if st.text == "" {
    42  		return false
    43  	}
    44  	// Parse a content segment until the next SGR prefix.
    45  	content := ""
    46  	nextSGR := strings.Index(st.text, sgrPrefix)
    47  	if nextSGR == -1 {
    48  		content = st.text
    49  	} else {
    50  		content = st.text[:nextSGR]
    51  	}
    52  	st.text = st.text[len(content):]
    53  	st.styling = nil
    54  	st.content = content
    55  	return true
    56  }
    57  
    58  func (st *sgrTokenizer) Token() (Styling, string) {
    59  	return st.styling, st.content
    60  }
    61  
    62  // ParseSGREscapedText parses SGR-escaped text into a Text. It also removes
    63  // non-SGR CSI sequences sequences in the text.
    64  func ParseSGREscapedText(s string) Text {
    65  	var text Text
    66  	var style Style
    67  
    68  	tokenizer := sgrTokenizer{text: s}
    69  	for tokenizer.Next() {
    70  		styling, content := tokenizer.Token()
    71  		if styling != nil {
    72  			styling.transform(&style)
    73  		}
    74  		if content != "" {
    75  			text = append(text, &Segment{style, content})
    76  		}
    77  	}
    78  	return text
    79  }
    80  
    81  var sgrStyling = map[int]Styling{
    82  	0: Reset,
    83  	1: Bold,
    84  	2: Dim,
    85  	4: Underlined,
    86  	5: Blink,
    87  	7: Inverse,
    88  }
    89  
    90  // StyleFromSGR builds a Style from an SGR sequence.
    91  func StyleFromSGR(s string) Style {
    92  	var ret Style
    93  	StylingFromSGR(s).transform(&ret)
    94  	return ret
    95  }
    96  
    97  // StylingFromSGR builds a Style from an SGR sequence.
    98  func StylingFromSGR(s string) Styling {
    99  	styling := jointStyling{}
   100  	codes := getSGRCodes(s)
   101  	if len(codes) == 0 {
   102  		return Reset
   103  	}
   104  	for len(codes) > 0 {
   105  		code := codes[0]
   106  		consume := 1
   107  		var moreStyling Styling
   108  
   109  		switch {
   110  		case sgrStyling[code] != nil:
   111  			moreStyling = sgrStyling[code]
   112  		case 30 <= code && code <= 37:
   113  			moreStyling = Fg(ansiColor(code - 30))
   114  		case 40 <= code && code <= 47:
   115  			moreStyling = Bg(ansiColor(code - 40))
   116  		case 90 <= code && code <= 97:
   117  			moreStyling = Fg(ansiBrightColor(code - 90))
   118  		case 100 <= code && code <= 107:
   119  			moreStyling = Bg(ansiBrightColor(code - 100))
   120  		case code == 38 && len(codes) >= 3 && codes[1] == 5:
   121  			moreStyling = Fg(xterm256Color(codes[2]))
   122  			consume = 3
   123  		case code == 48 && len(codes) >= 3 && codes[1] == 5:
   124  			moreStyling = Bg(xterm256Color(codes[2]))
   125  			consume = 3
   126  		case code == 38 && len(codes) >= 5 && codes[1] == 2:
   127  			moreStyling = Fg(trueColor{
   128  				uint8(codes[2]), uint8(codes[3]), uint8(codes[4])})
   129  			consume = 5
   130  		case code == 48 && len(codes) >= 5 && codes[1] == 2:
   131  			moreStyling = Bg(trueColor{
   132  				uint8(codes[2]), uint8(codes[3]), uint8(codes[4])})
   133  			consume = 5
   134  		default:
   135  			// Do nothing; skip this code
   136  		}
   137  		codes = codes[consume:]
   138  		if moreStyling != nil {
   139  			styling = append(styling, moreStyling)
   140  		}
   141  	}
   142  	return styling
   143  }
   144  
   145  func getSGRCodes(s string) []int {
   146  	var codes []int
   147  	for _, part := range strings.Split(s, ";") {
   148  		if part == "" {
   149  			codes = append(codes, 0)
   150  		} else {
   151  			code, err := strconv.Atoi(part)
   152  			if err == nil {
   153  				codes = append(codes, code)
   154  			}
   155  		}
   156  	}
   157  	return codes
   158  }