Initial support for colored text

- View contents are stored as cells (rune + colors) instead of runes.
- Uses the escape interpreter coded by @deweerdt in #39.
This commit is contained in:
Roi Martin 2016-04-27 22:34:55 +02:00
parent 8c49240a03
commit 40dec91023
3 changed files with 325 additions and 35 deletions

53
_examples/colors.go Normal file
View File

@ -0,0 +1,53 @@
// Copyright 2014 The gocui Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"fmt"
"log"
"github.com/jroimartin/gocui"
)
func main() {
g := gocui.NewGui()
if err := g.Init(); err != nil {
log.Panicln(err)
}
defer g.Close()
g.SetLayout(layout)
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
log.Panicln(err)
}
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
log.Panicln(err)
}
}
func layout(g *gocui.Gui) error {
maxX, maxY := g.Size()
if v, err := g.SetView("hello", maxX/2-6, maxY/2-5, maxX/2+6, maxY/2+5); err != nil {
if err != gocui.ErrUnknownView {
return err
}
fmt.Fprintln(v, "\x1b[0;30;47mHello world")
fmt.Fprintln(v, "\x1b[0;31mHello world")
fmt.Fprintln(v, "\x1b[0;32mHello world")
fmt.Fprintln(v, "\x1b[0;33mHello world")
fmt.Fprintln(v, "\x1b[0;34mHello world")
fmt.Fprintln(v, "\x1b[0;35mHello world")
fmt.Fprintln(v, "\x1b[0;36mHello world")
fmt.Fprintln(v, "\x1b[0;37mHello world")
fmt.Fprintln(v, "\x1b[0;30;41mHello world")
}
return nil
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}

174
escape.go Normal file
View File

@ -0,0 +1,174 @@
// Copyright 2014 The gocui Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gocui
import (
"errors"
"strconv"
)
type (
escapeState int
interpreterReturn int
)
const (
stateNone escapeState = iota
stateEscape
stateCSI
stateParams
)
type escapeInterpreter struct {
state escapeState
curch rune
csiParam []string
curFgColor, curBgColor Attribute
defaultFgColor, defaultBgColor Attribute
}
var (
errNotCSI = errors.New("Not a CSI escape sequence")
errCSINotANumber = errors.New("CSI escape sequence was expecting a number or a ;")
errCSIParseError = errors.New("CSI escape sequence parsing error")
errCSITooLong = errors.New("CSI escape sequence is too long")
)
// runes in case of error will output the non-parsed runes as a string.
func (ei *escapeInterpreter) runes() []rune {
switch ei.state {
case stateNone:
return []rune{0x1b}
case stateEscape:
return []rune{0x1b, ei.curch}
case stateCSI:
return []rune{0x1b, '[', ei.curch}
case stateParams:
ret := []rune{0x1b, '['}
for _, s := range ei.csiParam {
ret = append(ret, []rune(s)...)
ret = append(ret, ';')
}
return append(ret, ei.curch)
}
return nil
}
// newEscapeInterpreter returns an escapeInterpreter that will be able to parse
// terminal escape sequences.
func newEscapeInterpreter() *escapeInterpreter {
ei := &escapeInterpreter{
defaultFgColor: ColorWhite,
defaultBgColor: ColorBlack,
state: stateNone,
curFgColor: ColorWhite,
curBgColor: ColorBlack,
}
return ei
}
// reset sets the escapeInterpreter in inital state.
func (ei *escapeInterpreter) reset() {
ei.state = stateNone
ei.curFgColor = ei.defaultFgColor
ei.curBgColor = ei.defaultBgColor
ei.csiParam = nil
}
// paramToColor returns an attribute given a terminfo coloring.
func paramToColor(p int) Attribute {
switch p {
case 0:
return ColorBlack
case 1:
return ColorRed
case 2:
return ColorGreen
case 3:
return ColorYellow
case 4:
return ColorBlue
case 5:
return ColorMagenta
case 6:
return ColorCyan
case 7:
return ColorWhite
}
return ColorDefault
}
// parseOne parses a rune. If isEscape is true, it means that the rune is part
// of an scape sequence, and as such should not be printed verbatim. Otherwise,
// it's not an escape sequence.
func (ei *escapeInterpreter) parseOne(ch rune) (isEscape bool, err error) {
// Sanity checks to make sure we're not parsing something totally bogus.
if len(ei.csiParam) > 20 {
return false, errCSITooLong
}
if len(ei.csiParam) > 0 && len(ei.csiParam[len(ei.csiParam)-1]) > 255 {
return false, errCSITooLong
}
ei.curch = ch
switch ei.state {
case stateNone:
if ch == 0x1b {
ei.state = stateEscape
return true, nil
}
return false, nil
case stateEscape:
if ch == '[' {
ei.state = stateCSI
return true, nil
}
return false, errNotCSI
case stateCSI:
if ch >= '0' && ch <= '9' {
ei.state = stateParams
ei.csiParam = append(ei.csiParam, string(ch))
return true, nil
}
return false, errCSINotANumber
case stateParams:
switch {
case ch >= '0' && ch <= '9':
ei.csiParam[len(ei.csiParam)-1] += string(ch)
return true, nil
case ch == ';':
ei.csiParam = append(ei.csiParam, "")
return true, nil
case ch == 'm':
if len(ei.csiParam) < 1 {
return false, errCSIParseError
}
for _, param := range ei.csiParam {
p, err := strconv.Atoi(param)
if err != nil {
return false, errCSIParseError
}
switch {
case p >= 30 && p <= 37:
ei.curFgColor = paramToColor(p - 30)
case p >= 40 && p <= 47:
ei.curBgColor = paramToColor(p - 40)
case p == 1:
ei.curFgColor |= AttrBold
case p == 4:
ei.curFgColor |= AttrUnderline
case p == 7:
ei.curFgColor |= AttrReverse
case p == 0 || p == 39:
ei.curFgColor = ei.defaultFgColor
ei.curBgColor = ei.defaultBgColor
}
}
ei.state = stateNone
ei.csiParam = nil
return true, nil
}
}
return false, nil
}

133
view.go
View File

@ -20,13 +20,15 @@ type View struct {
x0, y0, x1, y1 int
ox, oy int
cx, cy int
lines [][]rune
lines [][]cell
readOffset int
readCache string
tainted bool // marks if the viewBuffer must be updated
viewLines []viewLine // internal representation of the view's buffer
ei *escapeInterpreter // used to decode ESC sequences on Write
// BgColor and FgColor allow to configure the background and foreground
// colors of the View.
BgColor, FgColor Attribute
@ -68,7 +70,23 @@ type View struct {
type viewLine struct {
linesX, linesY int // coordinates relative to v.lines
line []rune
line []cell
}
type cell struct {
chr rune
bgColor, fgColor Attribute
}
type lineType []cell
// String returns a string from a given cell slice.
func (l lineType) String() string {
str := ""
for _, c := range l {
str += string(c.chr)
}
return str
}
// newView returns a new View object.
@ -81,6 +99,7 @@ func newView(name string, x0, y0, x1, y1 int) *View {
y1: y1,
Frame: true,
tainted: true,
ei: newEscapeInterpreter(),
}
return v
}
@ -95,10 +114,10 @@ func (v *View) Name() string {
return v.name
}
// setRune writes a rune at the given point, relative to the view. It
// checks if the position is valid and applies the view's colors, taking
// into account if the cell must be highlighted.
func (v *View) setRune(x, y int, ch rune) error {
// setRune sets a rune at the given point relative to the view. It applies the
// specified colors, taking into account if the cell must be highlighted. Also,
// it checks if the position is valid.
func (v *View) setRune(x, y int, ch rune, fgColor, bgColor Attribute) error {
maxX, maxY := v.Size()
if x < 0 || x >= maxX || y < 0 || y >= maxY {
return errors.New("invalid point")
@ -119,20 +138,18 @@ func (v *View) setRune(x, y int, ch rune) error {
}
}
var fgColor, bgColor Attribute
if v.Highlight && ry == rcy {
fgColor = v.SelFgColor
bgColor = v.SelBgColor
} else {
if v.Mask != 0 {
fgColor = v.FgColor
bgColor = v.BgColor
ch = v.Mask
} else if v.Highlight && ry == rcy {
fgColor = v.SelFgColor
bgColor = v.SelBgColor
}
if v.Mask != 0 {
ch = v.Mask
}
termbox.SetCell(v.x0+x+1, v.y0+y+1, ch,
termbox.Attribute(fgColor), termbox.Attribute(bgColor))
return nil
}
@ -188,20 +205,63 @@ func (v *View) Write(p []byte) (n int, err error) {
if nl > 0 {
v.lines[nl-1] = nil
} else {
v.lines = make([][]rune, 1)
v.lines = make([][]cell, 1)
}
default:
cells := v.parseInput(ch)
if cells == nil {
continue
}
nl := len(v.lines)
if nl > 0 {
v.lines[nl-1] = append(v.lines[nl-1], ch)
v.lines[nl-1] = append(v.lines[nl-1], cells...)
} else {
v.lines = append(v.lines, []rune{ch})
v.lines = append(v.lines, cells)
}
}
}
return len(p), nil
}
// parseInput parses char by char the input written to the View. It returns nil
// while processing ESC sequences. Otherwise, it returns a cell slice that
// contains the processed data.
func (v *View) parseInput(ch rune) []cell {
cells := []cell{}
// ei's default colors must be updated, because they could have been
// changed
v.ei.defaultFgColor = v.FgColor
v.ei.defaultBgColor = v.BgColor
isEscape, err := v.ei.parseOne(ch)
if err != nil {
for _, r := range v.ei.runes() {
c := cell{
fgColor: v.FgColor,
bgColor: v.BgColor,
chr: r,
}
cells = append(cells, c)
}
v.ei.reset()
} else {
if isEscape {
return nil
} else {
c := cell{
fgColor: v.ei.curFgColor,
bgColor: v.ei.curBgColor,
chr: ch,
}
cells = append(cells, c)
}
}
return cells
}
// Read reads data into p. It returns the number of bytes read into p.
// At EOF, err will be io.EOF. Calling Read() after Rewind() makes the
// cache to be refreshed with the contents of the view.
@ -276,14 +336,14 @@ func (v *View) draw() error {
break
}
x := 0
for j, ch := range vline.line {
for j, c := range vline.line {
if j < v.ox {
continue
}
if x >= maxX {
break
}
if err := v.setRune(x, y, ch); err != nil {
if err := v.setRune(x, y, c.chr, c.fgColor, c.bgColor); err != nil {
return err
}
x++
@ -356,21 +416,21 @@ func (v *View) writeRune(x, y int, ch rune) error {
}
if y >= len(v.lines) {
s := make([][]rune, y-len(v.lines)+1)
s := make([][]cell, y-len(v.lines)+1)
v.lines = append(v.lines, s...)
}
olen := len(v.lines[y])
if x >= len(v.lines[y]) {
s := make([]rune, x-len(v.lines[y])+1)
s := make([]cell, x-len(v.lines[y])+1)
v.lines[y] = append(v.lines[y], s...)
}
if !v.Overwrite && x < olen {
v.lines[y] = append(v.lines[y], '\x00')
v.lines[y] = append(v.lines[y], cell{chr: '\x00'})
copy(v.lines[y][x+1:], v.lines[y][x:])
}
v.lines[y][x] = ch
v.lines[y][x] = cell{chr: ch}
return nil
}
@ -425,17 +485,17 @@ func (v *View) breakLine(x, y int) error {
return errors.New("invalid point")
}
var left, right []rune
var left, right []cell
if x < len(v.lines[y]) { // break line
left = make([]rune, len(v.lines[y][:x]))
left = make([]cell, len(v.lines[y][:x]))
copy(left, v.lines[y][:x])
right = make([]rune, len(v.lines[y][x:]))
right = make([]cell, len(v.lines[y][x:]))
copy(right, v.lines[y][x:])
} else { // new empty line
left = v.lines[y]
}
lines := make([][]rune, len(v.lines)+1)
lines := make([][]cell, len(v.lines)+1)
lines[y] = left
lines[y+1] = right
copy(lines, v.lines[:y])
@ -449,7 +509,7 @@ func (v *View) breakLine(x, y int) error {
func (v *View) Buffer() string {
str := ""
for _, l := range v.lines {
str += string(l) + "\n"
str += lineType(l).String() + "\n"
}
return strings.Replace(str, "\x00", " ", -1)
}
@ -459,7 +519,7 @@ func (v *View) Buffer() string {
func (v *View) ViewBuffer() string {
str := ""
for _, l := range v.viewLines {
str += string(l.line) + "\n"
str += lineType(l.line).String() + "\n"
}
return strings.Replace(str, "\x00", " ", -1)
}
@ -475,7 +535,8 @@ func (v *View) Line(y int) (string, error) {
if y < 0 || y >= len(v.lines) {
return "", errors.New("invalid point")
}
return string(v.lines[y]), nil
return lineType(v.lines[y]).String(), nil
}
// Word returns a string with the word of the view's internal buffer
@ -489,20 +550,22 @@ func (v *View) Word(x, y int) (string, error) {
if x < 0 || y < 0 || y >= len(v.lines) || x >= len(v.lines[y]) {
return "", errors.New("invalid point")
}
l := string(v.lines[y])
nl := strings.LastIndexFunc(l[:x], indexFunc)
str := lineType(v.lines[y]).String()
nl := strings.LastIndexFunc(str[:x], indexFunc)
if nl == -1 {
nl = 0
} else {
nl = nl + 1
}
nr := strings.IndexFunc(l[x:], indexFunc)
nr := strings.IndexFunc(str[x:], indexFunc)
if nr == -1 {
nr = len(l)
nr = len(str)
} else {
nr = nr + x
}
return string(l[nl:nr]), nil
return string(str[nl:nr]), nil
}
// indexFunc allows to split lines by words taking into account spaces