blob: 698a1169ab7158f63fd3ed28f219fa10a843aeb9 [file] [log] [blame]
// 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 (
"bufio"
"bytes"
"errors"
"io"
"regexp"
"strconv"
"strings"
"sync"
"github.com/nsf/termbox-go"
)
// A View is a window. It maintains its own internal buffer and cursor
// position.
type View struct {
name string
x0, y0, x1, y1 int
ox, oy int
cx, cy int
lines [][]rune
readOffset int
readCache string
tainted bool // marks if the viewBuffer must be updated
viewLines []viewLine // internal representation of the view's buffer
// 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
linesMutex sync.Mutex
ParseASCIAttributes bool
fgAttributes [][]Attribute
// If Editable is true, keystrokes will be added to the view's internal
// buffer at the cursor position.
Editable bool
// 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
}
type viewLine struct {
linesX, linesY int // coordinates relative to v.lines
line []rune
}
// newView returns a new View object.
func newView(name string, x0, y0, x1, y1 int) *View {
v := &View{
name: name,
x0: x0,
y0: y0,
x1: x1,
y1: y1,
Frame: true,
tainted: true,
}
return v
}
// 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 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 {
maxX, maxY := v.Size()
if x < 0 || x >= maxX || y < 0 || y >= maxY {
return errors.New("invalid point")
}
var fgColor, bgColor Attribute
if v.Highlight && y == v.cy {
fgColor = v.SelFgColor
bgColor = v.SelBgColor
} else {
fgColor = v.FgColor
bgColor = v.BgColor
}
if !v.Highlight && v.fgAttributes != nil && len(v.fgAttributes) >= y+1 && v.fgAttributes[y] != nil && len(v.fgAttributes[y]) >= x+1 {
fgattr := v.fgAttributes[y][x]
termbox.SetCell(v.x0+x+1, v.y0+y+1, ch,
termbox.Attribute(fgattr), termbox.Attribute(bgColor))
} else {
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 errors.New("invalid point")
}
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
}
// NumberOfLines returns the Number of Lines of the view.
func (v *View) NumberOfLines() (nol int) {
return len(v.lines)
}
// 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 errors.New("invalid point")
}
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
}
// 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.linesMutex.Lock()
defer v.linesMutex.Unlock()
v.tainted = true
re := regexp.MustCompile(regexp.QuoteMeta("\033[") + "[0-9]+m")
r := bytes.NewReader(p)
s := bufio.NewScanner(r)
var currentForground Attribute
currentForground = v.FgColor
for s.Scan() {
//runesLine := bytes.Runes(s.Bytes())
line := s.Text()
var parsedLine string = ""
var offset int = 0
positions := re.FindAllStringIndex(line, -1)
attributes := re.FindAllString(line, -1)
var AttributeLine []Attribute
for index, element := range positions {
start := offset
laenge := element[0] - start
if laenge > 0 {
parsedLine += line[start : start+laenge]
}
offset = element[1]
for i := 0; i < laenge; i++ {
AttributeLine = append(AttributeLine, currentForground)
}
attributeString := attributes[index][2:]
switch {
case attributeString == "0m":
currentForground = v.FgColor
case attributeString == "31m":
currentForground = ColorRed
case attributeString == "32m":
currentForground = ColorGreen
case attributeString == "33m":
currentForground = ColorYellow
case attributeString == "34m":
currentForground = ColorBlue
case attributeString == "35m":
currentForground = ColorMagenta
case attributeString == "36m":
currentForground = ColorCyan
case attributeString == "37m":
currentForground = ColorWhite
}
}
if offset < len(line) {
start := offset
laenge := len(line) - start
for i := 0; i < laenge; i++ {
AttributeLine = append(AttributeLine, currentForground)
}
parsedLine += line[start : start+laenge]
}
runes := []rune(parsedLine)
//runesLine = append(nil, runes...)
v.lines = append(v.lines, runes)
v.fgAttributes = append(v.fgAttributes, AttributeLine)
}
if err := s.Err(); err != nil {
return 0, err
}
return len(p), nil
}
// 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.
func (v *View) Read(p []byte) (n int, err error) {
if v.readOffset == 0 {
v.readCache = v.Buffer()
}
if v.readOffset < len(v.readCache) {
n = copy(p, v.readCache[v.readOffset:])
v.readOffset += n
} else {
err = io.EOF
}
return
}
// Rewind sets the offset for the next Read to 0, which also refresh the
// read cache.
func (v *View) Rewind() {
v.readOffset = 0
}
// draw re-draws the view's contents.
func (v *View) draw() error {
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
for i, line := range v.lines {
if v.Wrap {
if len(line) <= maxX {
vline := viewLine{linesX: 0, linesY: i, line: line}
v.viewLines = append(v.viewLines, vline)
continue
} else {
vline := viewLine{linesX: 0, linesY: i, line: line[:maxX]}
v.viewLines = append(v.viewLines, vline)
}
// Append remaining lines
for n := maxX; n < len(line); n += maxX {
if len(line[n:]) <= maxX {
vline := viewLine{linesX: n, linesY: i, line: line[n:]}
v.viewLines = append(v.viewLines, vline)
} else {
vline := viewLine{linesX: n, linesY: i, line: line[n : n+maxX]}
v.viewLines = append(v.viewLines, vline)
}
}
} else {
vline := viewLine{linesX: 0, linesY: i, line: line}
v.viewLines = append(v.viewLines, vline)
}
}
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, ch := range vline.line {
if j < v.ox {
continue
}
if x >= maxX {
break
}
if err := v.setRune(x, y, ch); err != nil {
return err
}
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, errors.New("invalid point")
}
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.
func (v *View) Clear() {
v.linesMutex.Lock()
defer v.linesMutex.Unlock()
v.tainted = true
v.lines = nil
v.clearRunes()
v.fgAttributes = nil
}
// 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))
}
}
}
// writeRune writes a rune into the view's internal buffer, at the
// position corresponding to the point (x, y). The length of the internal
// buffer is increased if the point is out of bounds. Overwrite mode is
// governed by the value of View.overwrite.
func (v *View) writeRune(x, y int, ch rune) error {
v.tainted = true
x, y, err := v.realPosition(x, y)
if err != nil {
return err
}
if x < 0 || y < 0 {
return errors.New("invalid point")
}
if y >= len(v.lines) {
s := make([][]rune, 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)
v.lines[y] = append(v.lines[y], s...)
}
if !v.Overwrite && x < olen {
v.lines[y] = append(v.lines[y], '\x00')
copy(v.lines[y][x+1:], v.lines[y][x:])
}
v.lines[y][x] = ch
return nil
}
// deleteRune removes a rune from the view's internal buffer, at the
// position corresponding to the point (x, y).
func (v *View) deleteRune(x, y int) error {
v.tainted = true
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 errors.New("invalid point")
}
v.lines[y] = append(v.lines[y][:x], v.lines[y][x+1:]...)
return nil
}
// mergeLines merges the lines "y" and "y+1" if possible.
func (v *View) mergeLines(y int) error {
v.tainted = true
_, y, err := v.realPosition(0, y)
if err != nil {
return err
}
if y < 0 || y >= len(v.lines) {
return errors.New("invalid point")
}
if y < len(v.lines)-1 { // otherwise we don't need to merge anything
v.lines[y] = append(v.lines[y], v.lines[y+1]...)
v.lines = append(v.lines[:y+1], v.lines[y+2:]...)
}
return nil
}
// breakLine breaks a line of the internal buffer at the position corresponding
// to the point (x, y).
func (v *View) breakLine(x, y int) error {
v.linesMutex.Lock()
defer v.linesMutex.Unlock()
v.tainted = true
x, y, err := v.realPosition(x, y)
if err != nil {
return err
}
if y < 0 || y >= len(v.lines) {
return errors.New("invalid point")
}
var left, right []rune
if x < len(v.lines[y]) { // break line
left = make([]rune, len(v.lines[y][:x]))
copy(left, v.lines[y][:x])
right = make([]rune, 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[y] = left
lines[y+1] = right
copy(lines, v.lines[:y])
copy(lines[y+2:], v.lines[y+1:])
v.lines = lines
return nil
}
// Buffer returns a string with the contents of the view's internal
// buffer
func (v *View) Buffer() string {
str := ""
for _, l := range v.lines {
str += string(l) + "\n"
}
return strings.Replace(str, "\x00", " ", -1)
}
// removeLine removes a line into the view's internal buffer at the position
// corresponding to the point (x, y).
func (v *View) RemoveLine(y_org int) error {
v.linesMutex.Lock()
defer v.linesMutex.Unlock()
y := v.oy + y_org
if y < 0 || y >= len(v.lines) {
return errors.New("invalid point")
}
v.lines = v.lines[:y+copy(v.lines[y:], v.lines[y+1:])]
if v.fgAttributes != nil && len(v.fgAttributes) > y_org { //&& v.fgAttributes[y_org] != nil {
v.fgAttributes = v.fgAttributes[:y_org+copy(v.fgAttributes[y_org:], v.fgAttributes[y_org+1:])]
}
//v.lines = append(v.lines, nil)
//copy(v.lines[y+1:], v.lines[y:])
//v.lines[y] = nil
return nil
}
// 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 "", errors.New("invalid point " + strconv.Itoa(y) + " maxvalue < " + strconv.Itoa(len(v.lines)))
}
return string(v.lines[y]), 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 "", errors.New("invalid point")
}
l := string(v.lines[y])
nl := strings.LastIndexFunc(l[:x], indexFunc)
if nl == -1 {
nl = 0
} else {
nl = nl + 1
}
nr := strings.IndexFunc(l[x:], indexFunc)
if nr == -1 {
nr = len(l)
} else {
nr = nr + x
}
return string(l[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
}