// 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 (
"bytes"
"io"
"strings"
"sync"
"unicode/utf8"
"github.com/go-errors/errors"
"github.com/awesome-gocui/termbox-go"
"github.com/mattn/go-runewidth"
)
// Constants for overlapping edges
const (
TOP = 1 // view is overlapping at top edge
BOTTOM = 2 // view is overlapping at bottom edge
LEFT = 4 // view is overlapping at left edge
RIGHT = 8 // view is overlapping at right edge
)
var (
// ErrInvalidPoint is returned when client passed invalid coordinates of a cell.
// Most likely client has passed negative coordinates of a cell.
ErrInvalidPoint = errors.New("invalid point")
)
// A View is a window. It maintains its own internal buffer and cursor
// position.
type View struct {
name string
x0, y0, x1, y1 int // left top right bottom
ox, oy int // view offsets
cx, cy int // cursor position
rx, ry int // Read() offsets
wx, wy int // Write() offsets
lines [][]cell // All the data
// readBuffer is used for storing unread bytes
readBuffer []byte
// tained is true if the viewLines must be updated
tainted bool
// internal representation of the view's buffer
viewLines []viewLine
// writeMutex protects locks the write process
writeMutex sync.Mutex
// ei is used to decode ESC sequences on Write
ei *escapeInterpreter
// Visible specifies whether the view is visible.
Visible bool
// BgColor and FgColor allow to configure the background and foreground
// colors of the View.
BgColor, FgColor Attribute
// SelBgColor and SelFgColor are used to configure the background and
// foreground colors of the selected line, when it is highlighted.
SelBgColor, SelFgColor Attribute
// If Editable is true, keystrokes will be added to the view's internal
// buffer at the cursor position.
Editable bool
// Editor allows to define the editor that manages the editing mode,
// including keybindings or cursor behaviour. DefaultEditor is used by
// default.
Editor Editor
// Overwrite enables or disables the overwrite mode of the view.
Overwrite bool
// If Highlight is true, Sel{Bg,Fg}Colors will be used
// for the line under the cursor position.
Highlight bool
// If Frame is true, a border will be drawn around the view.
Frame bool
// If Wrap is true, the content that is written to this View is
// automatically wrapped when it is longer than its width. If true the
// view's x-origin will be ignored.
Wrap bool
// If Autoscroll is true, the View will automatically scroll down when the
// text overflows. If true the view's y-origin will be ignored.
Autoscroll bool
// If Frame is true, Title allows to configure a title for the view.
Title string
// If Frame is true, Subtitle allows to configure a subtitle for the view.
Subtitle string
// If Mask is true, the View will display the mask instead of the real
// content
Mask rune
// Overlaps describes which edges are overlapping with another view's edges
Overlaps byte
// If HasLoader is true, the message will be appended with a spinning loader animation
HasLoader bool
}
type viewLine struct {
linesX, linesY int // coordinates relative to v.lines
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.
func newView(name string, x0, y0, x1, y1 int, mode OutputMode) *View {
v := &View{
name: name,
x0: x0,
y0: y0,
x1: x1,
y1: y1,
Visible: true,
Frame: true,
Editor: DefaultEditor,
tainted: true,
ei: newEscapeInterpreter(mode),
}
return v
}
// Dimensions returns the dimensions of the View
func (v *View) Dimensions() (int, int, int, int) {
return v.x0, v.y0, v.x1, v.y1
}
// Size returns the number of visible columns and rows in the View.
func (v *View) Size() (x, y int) {
return v.x1 - v.x0 - 1, v.y1 - v.y0 - 1
}
// Name returns the name of the view.
func (v *View) Name() string {
return v.name
}
// 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 ErrInvalidPoint
}
var (
ry, rcy int
err error
)
if v.Highlight {
_, ry, err = v.realPosition(x, y)
if err != nil {
return err
}
_, rcy, err = v.realPosition(v.cx, v.cy)
if err != nil {
return err
}
}
if v.Mask != 0 {
fgColor = v.FgColor
bgColor = v.BgColor
ch = v.Mask
} else if v.Highlight && ry == rcy {
fgColor = fgColor | AttrBold
}
// Don't display NUL characters
if ch == 0 {
ch = ' '
}
termbox.SetCell(v.x0+x+1, v.y0+y+1, ch,
termbox.Attribute(fgColor), termbox.Attribute(bgColor))
return nil
}
// SetCursor sets the cursor position of the view at the given point,
// relative to the view. It checks if the position is valid.
func (v *View) SetCursor(x, y int) error {
maxX, maxY := v.Size()
if x < 0 || x >= maxX || y < 0 || y >= maxY {
return ErrInvalidPoint
}
v.cx = x
v.cy = y
return nil
}
// Cursor returns the cursor position of the view.
func (v *View) Cursor() (x, y int) {
return v.cx, v.cy
}
// SetOrigin sets the origin position of the view's internal buffer,
// so the buffer starts to be printed from this point, which means that
// it is linked with the origin point of view. It can be used to
// implement Horizontal and Vertical scrolling with just incrementing
// or decrementing ox and oy.
func (v *View) SetOrigin(x, y int) error {
if x < 0 || y < 0 {
return ErrInvalidPoint
}
v.ox = x
v.oy = y
return nil
}
// Origin returns the origin position of the view.
func (v *View) Origin() (x, y int) {
return v.ox, v.oy
}
// SetWritePos sets the write position of the view's internal buffer.
// So the next Write call would write directly to the specified position.
func (v *View) SetWritePos(x, y int) error {
if x < 0 || y < 0 {
return ErrInvalidPoint
}
v.wx = x
v.wy = y
return nil
}
// WritePos returns the current write position of the view's internal buffer.
func (v *View) WritePos() (x, y int) {
return v.wx, v.wy
}
// SetReadPos sets the read position of the view's internal buffer.
// So the next Read call would read from the specified position.
func (v *View) SetReadPos(x, y int) error {
if x < 0 || y < 0 {
return ErrInvalidPoint
}
v.readBuffer = nil
v.rx = x
v.ry = y
return nil
}
// ReadPos returns the current read position of the view's internal buffer.
func (v *View) ReadPos() (x, y int) {
return v.rx, v.ry
}
// makeWriteable creates empty cells if required to make position (x, y) writeable.
func (v *View) makeWriteable(x, y int) {
// TODO: make this more efficient
// line `y` must be index-able (that's why `<=`)
for len(v.lines) <= y {
if cap(v.lines) > len(v.lines) {
newLen := cap(v.lines)
if newLen > y {
newLen = y + 1
}
v.lines = v.lines[:newLen]
} else {
v.lines = append(v.lines, nil)
}
}
// cell `x` must not be index-able (that's why `<`)
// append should be used by `lines[y]` user if he wants to write beyond `x`
for len(v.lines[y]) < x {
if cap(v.lines[y]) > len(v.lines[y]) {
newLen := cap(v.lines[y])
if newLen > x {
newLen = x
}
v.lines[y] = v.lines[y][:newLen]
} else {
v.lines[y] = append(v.lines[y], cell{})
}
}
}
// writeCells copies []cell to specified location (x, y)
// !!! caller MUST ensure that specified location (x, y) is writeable by calling makeWriteable
func (v *View) writeCells(x, y int, cells []cell) {
var newLen int
// use maximum len available
line := v.lines[y][:cap(v.lines[y])]
maxCopy := len(line) - x
if maxCopy < len(cells) {
copy(line[x:], cells[:maxCopy])
line = append(line, cells[maxCopy:]...)
newLen = len(line)
} else { // maxCopy >= len(cells)
copy(line[x:], cells)
newLen = x + len(cells)
if newLen < len(v.lines[y]) {
newLen = len(v.lines[y])
}
}
v.lines[y] = line[:newLen]
}
// Write appends a byte slice into the view's internal buffer. Because
// View implements the io.Writer interface, it can be passed as parameter
// of functions like fmt.Fprintf, fmt.Fprintln, io.Copy, etc. Clear must
// be called to clear the view's buffer.
func (v *View) Write(p []byte) (n int, err error) {
v.tainted = true
v.writeMutex.Lock()
v.makeWriteable(v.wx, v.wy)
v.writeRunes(bytes.Runes(p))
v.writeMutex.Unlock()
return len(p), nil
}
func (v *View) WriteRunes(p []rune) {
v.tainted = true
// Fill with empty cells, if writing outside current view buffer
v.makeWriteable(v.wx, v.wy)
v.writeRunes(p)
}
func (v *View) WriteString(s string) {
v.WriteRunes([]rune(s))
}
// writeRunes copies slice of runes into internal lines buffer.
// caller must make sure that writing position is accessable.
func (v *View) writeRunes(p []rune) {
for _, r := range p {
switch r {
case '\n':
v.wy++
if v.wy >= len(v.lines) {
v.lines = append(v.lines, nil)
}
fallthrough
// not valid in every OS, but making runtime OS checks in cycle is bad.
case '\r':
v.wx = 0
default:
cells := v.parseInput(r)
if cells == nil {
continue
}
v.writeCells(v.wx, v.wy, cells)
v.wx += len(cells)
}
}
}
// 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{}
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
}
repeatCount := 1
if ch == '\t' {
ch = ' '
repeatCount = 4
}
for i := 0; i < repeatCount; i++ {
c := cell{
fgColor: v.ei.curFgColor,
bgColor: v.ei.curBgColor,
chr: ch,
}
cells = append(cells, c)
}
}
return cells
}
// Read reads data into p from the current reading position set by SetReadPos.
// It returns the number of bytes read into p.
// At EOF, err will be io.EOF.
func (v *View) Read(p []byte) (n int, err error) {
buffer := make([]byte, utf8.UTFMax)
offset := 0
if v.readBuffer != nil {
copy(p, v.readBuffer)
if len(v.readBuffer) >= len(p) {
if len(v.readBuffer) > len(p) {
v.readBuffer = v.readBuffer[len(p):]
}
return len(p), nil
}
v.readBuffer = nil
}
for v.ry < len(v.lines) {
for v.rx < len(v.lines[v.ry]) {
count := utf8.EncodeRune(buffer, v.lines[v.ry][v.rx].chr)
copy(p[offset:], buffer[:count])
v.rx++
newOffset := offset + count
if newOffset >= len(p) {
if newOffset > len(p) {
v.readBuffer = buffer[newOffset-len(p):]
}
return len(p), nil
}
offset += count
}
v.rx = 0
v.ry++
}
return offset, io.EOF
}
// Rewind sets read and write pos to (0, 0).
func (v *View) Rewind() {
if err := v.SetReadPos(0, 0); err != nil {
// SetReadPos returns error only if x and y are negative
// we are passing 0, 0, thus no error should occur.
panic(err)
}
if err := v.SetWritePos(0, 0); err != nil {
// SetWritePos returns error only if x and y are negative
// we are passing 0, 0, thus no error should occur.
panic(err)
}
}
// IsTainted tells us if the view is tainted
func (v *View) IsTainted() bool {
return v.tainted
}
// draw re-draws the view's contents.
func (v *View) draw() error {
if !v.Visible {
return nil
}
maxX, maxY := v.Size()
if v.Wrap {
if maxX == 0 {
return errors.New("X size of the view cannot be 0")
}
v.ox = 0
}
if v.tainted {
v.viewLines = nil
lines := v.lines
if v.HasLoader {
lines = v.loaderLines()
}
for i, line := range lines {
wrap := 0
if v.Wrap {
wrap = maxX
}
ls := lineWrap(line, wrap)
for j := range ls {
vline := viewLine{linesX: j, linesY: i, line: ls[j]}
v.viewLines = append(v.viewLines, vline)
}
}
if !v.HasLoader {
v.tainted = false
}
}
if v.Autoscroll && len(v.viewLines) > maxY {
v.oy = len(v.viewLines) - maxY
}
y := 0
for i, vline := range v.viewLines {
if i < v.oy {
continue
}
if y >= maxY {
break
}
x := 0
for j, c := range vline.line {
if j < v.ox {
continue
}
if x >= maxX {
break
}
fgColor := c.fgColor
if fgColor == ColorDefault {
fgColor = v.FgColor
}
bgColor := c.bgColor
if bgColor == ColorDefault {
bgColor = v.BgColor
}
if err := v.setRune(x, y, c.chr, fgColor, bgColor); err != nil {
return err
}
if c.chr != 0 {
// If it is a rune, add rune width
x += runewidth.RuneWidth(c.chr)
} else {
// If it is NULL rune, add 1 to be able to use SetWritePos
// (runewidth.RuneWidth of space is 1)
x++
}
}
y++
}
return nil
}
// realPosition returns the position in the internal buffer corresponding to the
// point (x, y) of the view.
func (v *View) realPosition(vx, vy int) (x, y int, err error) {
vx = v.ox + vx
vy = v.oy + vy
if vx < 0 || vy < 0 {
return 0, 0, ErrInvalidPoint
}
if len(v.viewLines) == 0 {
return vx, vy, nil
}
if vy < len(v.viewLines) {
vline := v.viewLines[vy]
x = vline.linesX + vx
y = vline.linesY
} else {
vline := v.viewLines[len(v.viewLines)-1]
x = vx
y = vline.linesY + vy - len(v.viewLines) + 1
}
return x, y, nil
}
// Clear empties the view's internal buffer.
// And resets reading and writing offsets.
func (v *View) Clear() {
v.writeMutex.Lock()
v.Rewind()
v.tainted = true
v.ei.reset()
v.lines = nil
v.viewLines = nil
v.clearRunes()
v.writeMutex.Unlock()
}
// clearRunes erases all the cells in the view.
func (v *View) clearRunes() {
maxX, maxY := v.Size()
for x := 0; x < maxX; x++ {
for y := 0; y < maxY; y++ {
termbox.SetCell(v.x0+x+1, v.y0+y+1, ' ',
termbox.Attribute(v.FgColor), termbox.Attribute(v.BgColor))
}
}
}
// BufferLines returns the lines in the view's internal
// buffer.
func (v *View) BufferLines() []string {
lines := make([]string, len(v.lines))
for i, l := range v.lines {
str := lineType(l).String()
str = strings.Replace(str, "\x00", " ", -1)
lines[i] = str
}
return lines
}
// Buffer returns a string with the contents of the view's internal
// buffer.
func (v *View) Buffer() string {
return linesToString(v.lines)
}
// ViewBufferLines returns the lines in the view's internal
// buffer that is shown to the user.
func (v *View) ViewBufferLines() []string {
lines := make([]string, len(v.viewLines))
for i, l := range v.viewLines {
str := lineType(l.line).String()
str = strings.Replace(str, "\x00", " ", -1)
lines[i] = str
}
return lines
}
// LinesHeight is the count of view lines (i.e. lines excluding wrapping)
func (v *View) LinesHeight() int {
return len(v.lines)
}
// ViewLinesHeight is the count of view lines (i.e. lines including wrapping)
func (v *View) ViewLinesHeight() int {
return len(v.viewLines)
}
// ViewBuffer returns a string with the contents of the view's buffer that is
// shown to the user.
func (v *View) ViewBuffer() string {
lines := make([][]cell, len(v.viewLines))
for i := range v.viewLines {
lines[i] = v.viewLines[i].line
}
return linesToString(lines)
}
// Line returns a string with the line of the view's internal buffer
// at the position corresponding to the point (x, y).
func (v *View) Line(y int) (string, error) {
_, y, err := v.realPosition(0, y)
if err != nil {
return "", err
}
if y < 0 || y >= len(v.lines) {
return "", ErrInvalidPoint
}
return lineType(v.lines[y]).String(), nil
}
// Word returns a string with the word of the view's internal buffer
// at the position corresponding to the point (x, y).
func (v *View) Word(x, y int) (string, error) {
x, y, err := v.realPosition(x, y)
if err != nil {
return "", err
}
if x < 0 || y < 0 || y >= len(v.lines) || x >= len(v.lines[y]) {
return "", ErrInvalidPoint
}
str := lineType(v.lines[y]).String()
nl := strings.LastIndexFunc(str[:x], indexFunc)
if nl == -1 {
nl = 0
} else {
nl = nl + 1
}
nr := strings.IndexFunc(str[x:], indexFunc)
if nr == -1 {
nr = len(str)
} else {
nr = nr + x
}
return string(str[nl:nr]), nil
}
// indexFunc allows to split lines by words taking into account spaces
// and 0.
func indexFunc(r rune) bool {
return r == ' ' || r == 0
}
// SetLine changes the contents of an existing line.
func (v *View) SetLine(y int, text string) error {
if y < 0 || y >= len(v.lines) {
err := ErrInvalidPoint
return err
}
v.tainted = true
line := make([]cell, 0)
for _, r := range text {
c := v.parseInput(r)
line = append(line, c...)
}
v.lines[y] = line
return nil
}
// SetHighlight toggles highlighting of separate lines, for custom lists
// or multiple selection in views.
func (v *View) SetHighlight(y int, on bool) error {
if y < 0 || y >= len(v.lines) {
err := ErrInvalidPoint
return err
}
line := v.lines[y]
cells := make([]cell, 0)
for _, c := range line {
if on {
c.bgColor = v.SelBgColor
c.fgColor = v.SelFgColor
} else {
c.bgColor = v.BgColor
c.fgColor = v.FgColor
}
cells = append(cells, c)
}
v.tainted = true
v.lines[y] = cells
return nil
}
func lineWidth(line []cell) (n int) {
for i := range line {
n += runewidth.RuneWidth(line[i].chr)
}
return
}
func lineWrap(line []cell, columns int) [][]cell {
if columns == 0 {
return [][]cell{line}
}
var n int
var offset int
lines := make([][]cell, 0, 1)
for i := range line {
rw := runewidth.RuneWidth(line[i].chr)
n += rw
if n > columns {
n = rw
lines = append(lines, line[offset:i])
offset = i
}
}
lines = append(lines, line[offset:])
return lines
}
func linesToString(lines [][]cell) string {
str := make([]string, len(lines))
for i := range lines {
rns := make([]rune, 0, len(lines[i]))
line := lineType(lines[i]).String()
for _, c := range line {
if c != '\x00' {
rns = append(rns, c)
}
}
str[i] = string(rns)
}
return strings.Join(str, "\n")
}