mirror of
https://github.com/jroimartin/gocui.git
synced 2025-04-24 13:48:51 +08:00
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:
parent
8c49240a03
commit
40dec91023
53
_examples/colors.go
Normal file
53
_examples/colors.go
Normal 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
174
escape.go
Normal 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
133
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user