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	"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}