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