escape.go

  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}