1// Copyright 2014 The gocui Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package gocui
6
7import (
8 "strconv"
9
10 "github.com/go-errors/errors"
11)
12
13type escapeInterpreter struct {
14 state escapeState
15 curch rune
16 csiParam []string
17 curFgColor, curBgColor Attribute
18 mode OutputMode
19}
20
21type (
22 escapeState int
23 fontEffect int
24)
25
26const (
27 stateNone escapeState = iota
28 stateEscape
29 stateCSI
30 stateParams
31
32 bold fontEffect = 1
33 underline fontEffect = 4
34 reverse fontEffect = 7
35 setForegroundColor fontEffect = 38
36 setBackgroundColor fontEffect = 48
37)
38
39var (
40 errNotCSI = errors.New("Not a CSI escape sequence")
41 errCSIParseError = errors.New("CSI escape sequence parsing error")
42 errCSITooLong = errors.New("CSI escape sequence is too long")
43)
44
45// runes in case of error will output the non-parsed runes as a string.
46func (ei *escapeInterpreter) runes() []rune {
47 switch ei.state {
48 case stateNone:
49 return []rune{0x1b}
50 case stateEscape:
51 return []rune{0x1b, ei.curch}
52 case stateCSI:
53 return []rune{0x1b, '[', ei.curch}
54 case stateParams:
55 ret := []rune{0x1b, '['}
56 for _, s := range ei.csiParam {
57 ret = append(ret, []rune(s)...)
58 ret = append(ret, ';')
59 }
60 return append(ret, ei.curch)
61 }
62 return nil
63}
64
65// newEscapeInterpreter returns an escapeInterpreter that will be able to parse
66// terminal escape sequences.
67func newEscapeInterpreter(mode OutputMode) *escapeInterpreter {
68 ei := &escapeInterpreter{
69 state: stateNone,
70 curFgColor: ColorDefault,
71 curBgColor: ColorDefault,
72 mode: mode,
73 }
74 return ei
75}
76
77// reset sets the escapeInterpreter in initial state.
78func (ei *escapeInterpreter) reset() {
79 ei.state = stateNone
80 ei.curFgColor = ColorDefault
81 ei.curBgColor = ColorDefault
82 ei.csiParam = nil
83}
84
85// parseOne parses a rune. If isEscape is true, it means that the rune is part
86// of an escape sequence, and as such should not be printed verbatim. Otherwise,
87// it's not an escape sequence.
88func (ei *escapeInterpreter) parseOne(ch rune) (isEscape bool, err error) {
89 // Sanity checks
90 if len(ei.csiParam) > 20 {
91 return false, errCSITooLong
92 }
93 if len(ei.csiParam) > 0 && len(ei.csiParam[len(ei.csiParam)-1]) > 255 {
94 return false, errCSITooLong
95 }
96
97 ei.curch = ch
98
99 switch ei.state {
100 case stateNone:
101 if ch == 0x1b {
102 ei.state = stateEscape
103 return true, nil
104 }
105 return false, nil
106 case stateEscape:
107 if ch == '[' {
108 ei.state = stateCSI
109 return true, nil
110 }
111 return false, errNotCSI
112 case stateCSI:
113 switch {
114 case ch >= '0' && ch <= '9':
115 ei.csiParam = append(ei.csiParam, "")
116 case ch == 'm':
117 ei.csiParam = append(ei.csiParam, "0")
118 default:
119 return false, errCSIParseError
120 }
121 ei.state = stateParams
122 fallthrough
123 case stateParams:
124 switch {
125 case ch >= '0' && ch <= '9':
126 ei.csiParam[len(ei.csiParam)-1] += string(ch)
127 return true, nil
128 case ch == ';':
129 ei.csiParam = append(ei.csiParam, "")
130 return true, nil
131 case ch == 'm':
132 var err error
133 switch ei.mode {
134 case OutputNormal:
135 err = ei.outputNormal()
136 case Output256:
137 err = ei.output256()
138 }
139 if err != nil {
140 return false, errCSIParseError
141 }
142
143 ei.state = stateNone
144 ei.csiParam = nil
145 return true, nil
146 default:
147 return false, errCSIParseError
148 }
149 }
150 return false, nil
151}
152
153// outputNormal provides 8 different colors:
154// black, red, green, yellow, blue, magenta, cyan, white
155func (ei *escapeInterpreter) outputNormal() error {
156 for _, param := range ei.csiParam {
157 p, err := strconv.Atoi(param)
158 if err != nil {
159 return errCSIParseError
160 }
161
162 switch {
163 case p >= 30 && p <= 37:
164 ei.curFgColor = Attribute(p - 30 + 1)
165 case p == 39:
166 ei.curFgColor = ColorDefault
167 case p >= 40 && p <= 47:
168 ei.curBgColor = Attribute(p - 40 + 1)
169 case p == 49:
170 ei.curBgColor = ColorDefault
171 case p == 1:
172 ei.curFgColor |= AttrBold
173 case p == 4:
174 ei.curFgColor |= AttrUnderline
175 case p == 7:
176 ei.curFgColor |= AttrReverse
177 case p == 0:
178 ei.curFgColor = ColorDefault
179 ei.curBgColor = ColorDefault
180 }
181 }
182
183 return nil
184}
185
186// output256 allows you to leverage the 256-colors terminal mode:
187// 0x01 - 0x08: the 8 colors as in OutputNormal
188// 0x09 - 0x10: Color* | AttrBold
189// 0x11 - 0xe8: 216 different colors
190// 0xe9 - 0x1ff: 24 different shades of grey
191func (ei *escapeInterpreter) output256() error {
192 if len(ei.csiParam) < 3 {
193 return ei.outputNormal()
194 }
195
196 mode, err := strconv.Atoi(ei.csiParam[1])
197 if err != nil {
198 return errCSIParseError
199 }
200 if mode != 5 {
201 return ei.outputNormal()
202 }
203
204 for _, param := range splitFgBg(ei.csiParam) {
205 fgbg, err := strconv.Atoi(param[0])
206 if err != nil {
207 return errCSIParseError
208 }
209 color, err := strconv.Atoi(param[2])
210 if err != nil {
211 return errCSIParseError
212 }
213
214 switch fontEffect(fgbg) {
215 case setForegroundColor:
216 ei.curFgColor = Attribute(color + 1)
217
218 for _, s := range param[3:] {
219 p, err := strconv.Atoi(s)
220 if err != nil {
221 return errCSIParseError
222 }
223
224 switch fontEffect(p) {
225 case bold:
226 ei.curFgColor |= AttrBold
227 case underline:
228 ei.curFgColor |= AttrUnderline
229 case reverse:
230 ei.curFgColor |= AttrReverse
231
232 }
233 }
234 case setBackgroundColor:
235 ei.curBgColor = Attribute(color + 1)
236 default:
237 return errCSIParseError
238 }
239 }
240 return nil
241}
242
243func splitFgBg(params []string) [][]string {
244 var out [][]string
245 var current []string
246 for _, p := range params {
247 if len(current) == 3 && (p == "48" || p == "38") {
248 out = append(out, current)
249 current = []string{}
250 }
251 current = append(current, p)
252 }
253
254 if len(current) > 0 {
255 out = append(out, current)
256 }
257
258 return out
259}