escapes.go

 1package text
 2
 3import (
 4	"strings"
 5	"unicode/utf8"
 6)
 7
 8// EscapeItem hold the description of terminal escapes in a line.
 9// 'item' is the actual escape command
10// 'pos' is the index in the rune array where the 'item' shall be inserted back.
11// For example, the escape item in "F\x1b33mox" is {"\x1b33m", 1}.
12type EscapeItem struct {
13	Item string
14	Pos  int
15}
16
17// ExtractTermEscapes extract terminal escapes out of a line and returns a new
18// line without terminal escapes and a slice of escape items. The terminal escapes
19// can be inserted back into the new line at rune index 'item.pos' to recover the
20// original line.
21//
22// Required: The line shall not contain "\n"
23func ExtractTermEscapes(line string) (string, []EscapeItem) {
24	var termEscapes []EscapeItem
25	var line1 strings.Builder
26
27	pos := 0
28	item := ""
29	occupiedRuneCount := 0
30	inEscape := false
31	for i, r := range []rune(line) {
32		if r == '\x1b' {
33			pos = i
34			item = string(r)
35			inEscape = true
36			continue
37		}
38		if inEscape {
39			item += string(r)
40			if r == 'm' {
41				termEscapes = append(termEscapes, EscapeItem{item, pos - occupiedRuneCount})
42				occupiedRuneCount += utf8.RuneCountInString(item)
43				inEscape = false
44			}
45			continue
46		}
47		line1.WriteRune(r)
48	}
49
50	return line1.String(), termEscapes
51}
52
53// ApplyTermEscapes apply the extracted terminal escapes to the edited line.
54// Escape sequences need to be ordered by their position.
55// If the position is < 0, the escape is applied at the beginning of the line.
56// If the position is > len(line), the escape is applied at the end of the line.
57func ApplyTermEscapes(line string, escapes []EscapeItem) string {
58	if len(escapes) == 0 {
59		return line
60	}
61
62	var out strings.Builder
63
64	currPos := 0
65	currItem := 0
66	for _, r := range line {
67		for currItem < len(escapes) && currPos >= escapes[currItem].Pos {
68			out.WriteString(escapes[currItem].Item)
69			currItem++
70		}
71		out.WriteRune(r)
72		currPos++
73	}
74
75	// Don't forget the trailing escapes, if any.
76	for currItem < len(escapes) {
77		out.WriteString(escapes[currItem].Item)
78		currItem++
79	}
80
81	return out.String()
82}
83
84// OffsetEscapes is a utility function to offset the position of a
85// collection of EscapeItem.
86func OffsetEscapes(escapes []EscapeItem, offset int) []EscapeItem {
87	result := make([]EscapeItem, len(escapes))
88	for i, e := range escapes {
89		result[i] = EscapeItem{
90			Item: e.Item,
91			Pos:  e.Pos + offset,
92		}
93	}
94	return result
95}