diff --git a/_examples/colors.go b/_examples/colors.go new file mode 100644 index 0000000..50f0f8f --- /dev/null +++ b/_examples/colors.go @@ -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 +} diff --git a/escape.go b/escape.go new file mode 100644 index 0000000..b3d6e2e --- /dev/null +++ b/escape.go @@ -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 +} diff --git a/view.go b/view.go index 176ab9e..d5fa654 100644 --- a/view.go +++ b/view.go @@ -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