gocui/view.go

392 lines
9.4 KiB
Go
Raw Normal View History

// Copyright 2014 The gocui Authors. All rights reserved.
2014-01-14 20:11:12 +01:00
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
2013-12-27 21:36:26 +01:00
package gocui
import (
2014-01-09 20:20:14 +01:00
"bytes"
2014-01-06 18:36:38 +01:00
"errors"
"io"
"strings"
2014-01-09 20:20:14 +01:00
2013-12-31 21:23:28 +01:00
"github.com/nsf/termbox-go"
2013-12-27 21:36:26 +01:00
)
2014-01-19 17:03:52 +01:00
// A View is a window. It maintains its own internal buffer and cursor
// position.
2013-12-27 21:36:26 +01:00
type View struct {
2014-05-03 15:20:46 +02:00
name string
x0, y0, x1, y1 int
ox, oy int
cx, cy int
lines [][]rune
overwrite bool // overwrite in edit mode
readOffset int
readCache string
// 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
2014-01-16 23:01:53 +01:00
// If Editable is true, keystrokes will be added to the view's internal
// buffer at the cursor position.
Editable bool
2014-01-19 17:03:52 +01:00
// If Highlight is true, Sel{Bg,Fg}Colors will be used
// for the line under the cursor position.
2014-01-16 23:01:53 +01:00
Highlight bool
2014-10-17 17:22:28 -04:00
2014-11-15 13:50:56 +01:00
// If Frame is true, a border will be drawn around the view.
2014-10-17 17:22:28 -04:00
Frame bool
2014-11-11 17:10:55 +01:00
// 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.
2014-11-11 17:10:55 +01:00
Wrap bool
// If Wrap is true, each wrapping line is prefixed with this prefix.
WrapPrefix string
// 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
2013-12-27 21:36:26 +01:00
}
2014-01-19 17:03:52 +01:00
// newView returns a new View object.
func newView(name string, x0, y0, x1, y1 int) *View {
v := &View{
2014-10-17 17:22:28 -04:00
name: name,
x0: x0,
y0: y0,
x1: x1,
y1: y1,
Frame: true,
}
return v
2013-12-27 21:36:26 +01:00
}
2014-01-06 18:36:38 +01:00
2014-01-19 17:03:52 +01:00
// Size returns the number of visible columns and rows in the View.
2014-01-09 20:20:14 +01:00
func (v *View) Size() (x, y int) {
2014-01-16 23:01:53 +01:00
return v.x1 - v.x0 - 1, v.y1 - v.y0 - 1
}
2014-01-19 17:03:52 +01:00
// Name returns the name of the view.
2014-01-16 23:01:53 +01:00
func (v *View) Name() string {
return v.name
2014-01-09 20:20:14 +01:00
}
2014-01-19 17:03:52 +01:00
// 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 {
2014-01-09 20:20:14 +01:00
maxX, maxY := v.Size()
if x < 0 || x >= maxX || y < 0 || y >= maxY {
return errors.New("invalid point")
}
2014-01-09 21:55:23 +01:00
var fgColor, bgColor Attribute
2014-01-16 23:01:53 +01:00
if v.Highlight && y == v.cy {
2014-05-03 15:20:46 +02:00
fgColor = v.SelFgColor
bgColor = v.SelBgColor
2014-01-09 21:55:23 +01:00
} else {
2014-05-03 15:20:46 +02:00
fgColor = v.FgColor
bgColor = v.BgColor
2014-01-09 21:55:23 +01:00
}
2014-01-16 23:01:53 +01:00
termbox.SetCell(v.x0+x+1, v.y0+y+1, ch,
2014-01-09 21:55:23 +01:00
termbox.Attribute(fgColor), termbox.Attribute(bgColor))
2014-01-09 20:20:14 +01:00
return nil
}
2014-01-19 17:03:52 +01:00
// 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 {
2014-01-09 20:20:14 +01:00
maxX, maxY := v.Size()
if x < 0 || x >= maxX || y < 0 || y >= maxY {
2014-01-06 18:36:38 +01:00
return errors.New("invalid point")
}
2014-01-16 23:01:53 +01:00
v.cx = x
v.cy = y
2014-01-06 18:36:38 +01:00
return nil
}
2014-01-19 17:03:52 +01:00
// Cursor returns the cursor position of the view.
2014-01-16 23:01:53 +01:00
func (v *View) Cursor() (x, y int) {
return v.cx, v.cy
}
2014-01-19 17:03:52 +01:00
// 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")
}
2014-01-16 23:01:53 +01:00
v.ox = x
v.oy = y
return nil
2014-01-16 23:01:53 +01:00
}
2014-01-19 17:03:52 +01:00
// Origin returns the origin position of the view.
2014-01-16 23:01:53 +01:00
func (v *View) Origin() (x, y int) {
return v.ox, v.oy
2014-01-13 20:15:39 +01:00
}
2014-01-19 17:03:52 +01:00
// Write appends a byte slice into the view's internal buffer. Because
// View implements the io.Writer interface, it can be passed as parameter
2014-01-19 17:44:36 +01:00
// of functions like fmt.Fprintf, fmt.Fprintln, io.Copy, etc. Clear must
2014-01-19 17:03:52 +01:00
// be called to clear the view's buffer.
2014-01-06 18:36:38 +01:00
func (v *View) Write(p []byte) (n int, err error) {
for _, ch := range bytes.Runes(p) {
switch ch {
case '\n':
v.lines = append(v.lines, nil)
case '\r':
nl := len(v.lines)
if nl > 0 {
v.lines[nl-1] = nil
} else {
v.lines = make([][]rune, 1)
}
default:
nl := len(v.lines)
if nl > 0 {
v.lines[nl-1] = append(v.lines[nl-1], ch)
} else {
v.lines = make([][]rune, 1)
v.lines[0] = append(v.lines[0], ch)
}
}
}
return len(p), nil
2014-01-09 20:20:14 +01:00
}
// Read reads data into p. It returns the number of bytes read into p.
2014-05-03 15:20:46 +02:00
// 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
}
2014-01-19 17:03:52 +01:00
// draw re-draws the view's contents.
func (v *View) draw() error {
2014-01-09 20:20:14 +01:00
maxX, maxY := v.Size()
// This buffering takes care of v.ox
if v.Wrap {
if len(v.WrapPrefix) >= maxX {
return errors.New("WrapPrefix bigger or equal to X size")
}
v.ox = 0
}
buf := make([][]rune, 0)
for _, line := range v.lines {
if v.Wrap {
// Copy first line
bufLine := make([]rune, maxX)
// if v.ox >= len(line), then the line will be empty
if v.ox < len(line) {
copy(bufLine, line[v.ox:])
}
buf = append(buf, bufLine)
// Append wrapped lines with WrapPrefix
for n := maxX; n < len(line); n += maxX - len(v.WrapPrefix) {
prefixLine := make([]rune, maxX)
if v.ox < len(line) {
copy(prefixLine, []rune(v.WrapPrefix))
copy(prefixLine[len([]rune(v.WrapPrefix)):], line[v.ox+n:])
}
buf = append(buf, prefixLine)
}
} else {
bufLine := make([]rune, maxX)
if v.ox < len(line) {
copy(bufLine, line[v.ox:])
}
buf = append(buf, bufLine)
}
}
// The actual drawing takes into account v.oy
if v.Autoscroll && len(buf) > maxY {
v.oy = len(buf) - maxY
}
2014-01-13 20:21:49 +01:00
y := 0
for i, line := range buf {
2014-01-16 23:01:53 +01:00
if i < v.oy {
2014-01-13 20:15:39 +01:00
continue
}
if y >= maxY {
break
}
for x, ch := range line {
if x >= maxX {
break
2014-01-13 20:15:39 +01:00
}
if err := v.setRune(x, y, ch); err != nil {
return err
2014-01-09 20:20:14 +01:00
}
}
2014-01-13 20:21:49 +01:00
y++
2014-01-09 20:20:14 +01:00
}
return nil
2014-01-06 18:36:38 +01:00
}
2014-01-10 12:38:08 +01:00
2014-01-19 17:03:52 +01:00
// Clear empties the view's internal buffer.
2014-01-10 12:38:08 +01:00
func (v *View) Clear() {
v.lines = nil
2014-01-13 20:15:39 +01:00
v.clearRunes()
}
2014-01-19 17:03:52 +01:00
// clearRunes erases all the cells in the view.
2014-01-13 20:15:39 +01:00
func (v *View) clearRunes() {
2014-01-10 12:38:08 +01:00
maxX, maxY := v.Size()
for x := 0; x < maxX; x++ {
for y := 0; y < maxY; y++ {
2014-01-20 23:28:08 +01:00
termbox.SetCell(v.x0+x+1, v.y0+y+1, ' ',
2014-05-03 15:20:46 +02:00
termbox.Attribute(v.FgColor), termbox.Attribute(v.BgColor))
2014-01-10 12:38:08 +01:00
}
}
}
2014-01-22 22:37:53 +01:00
// 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 {
2014-01-22 22:37:53 +01:00
x = v.ox + x
y = v.oy + y
if x < 0 || y < 0 {
return errors.New("invalid point")
2014-01-22 22:37:53 +01:00
}
if y >= len(v.lines) {
2014-01-22 20:42:00 +01:00
if y >= cap(v.lines) {
s := make([][]rune, y+1, (y+1)*2)
copy(s, v.lines)
v.lines = s
} else {
v.lines = v.lines[:y+1]
}
}
if v.lines[y] == nil {
2014-01-22 20:42:00 +01:00
v.lines[y] = make([]rune, x+1, (x+1)*2)
} else if x >= len(v.lines[y]) {
2014-01-22 20:42:00 +01:00
if x >= cap(v.lines[y]) {
s := make([]rune, x+1, (x+1)*2)
copy(s, v.lines[y])
v.lines[y] = s
} else {
v.lines[y] = v.lines[y][:x+1]
}
}
2014-01-22 22:37:53 +01:00
if !v.overwrite {
v.lines[y] = append(v.lines[y], ' ')
copy(v.lines[y][x+1:], v.lines[y][x:])
}
v.lines[y][x] = ch
return nil
2014-01-22 22:37:53 +01:00
}
// 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 {
2014-01-22 22:37:53 +01:00
x = v.ox + x
y = v.oy + y
if x < 0 || y < 0 || y >= len(v.lines) || v.lines[y] == nil || x >= len(v.lines[y]) {
return errors.New("invalid point")
2014-01-22 22:37:53 +01:00
}
2014-01-23 00:46:15 +01:00
copy(v.lines[y][x:], v.lines[y][x+1:])
v.lines[y][len(v.lines[y])-1] = ' '
return nil
2014-01-22 22:37:53 +01:00
}
// addLine adds a line into the view's internal buffer at the position
// corresponding to the point (x, y).
func (v *View) addLine(y int) error {
2014-01-22 22:37:53 +01:00
y = v.oy + y
if y < 0 || y >= len(v.lines) {
return errors.New("invalid point")
2014-01-22 22:37:53 +01:00
}
v.lines = append(v.lines, nil)
copy(v.lines[y+1:], v.lines[y:])
v.lines[y] = nil
return nil
}
2014-02-03 02:04:30 +01:00
// 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)
}
2014-01-23 23:14:11 +01:00
// 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 = v.oy + y
if y < 0 || y >= len(v.lines) {
return "", errors.New("invalid point")
}
return string(v.lines[y]), nil
}
2014-01-23 23:14:11 +01:00
// 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 = v.ox + x
y = v.oy + y
if y < 0 || y >= len(v.lines) || x >= len(v.lines[y]) {
return "", errors.New("invalid point")
}
l := string(v.lines[y])
2014-01-23 23:42:52 +01:00
nl := strings.LastIndexFunc(l[:x], indexFunc)
if nl == -1 {
nl = 0
} else {
nl = nl + 1
}
2014-01-23 23:42:52 +01:00
nr := strings.IndexFunc(l[x:], indexFunc)
if nr == -1 {
nr = len(l)
} else {
nr = nr + x
}
return string(l[nl:nr]), nil
}
2014-01-23 23:42:52 +01:00
// indexFunc allows to split lines by words taking into account spaces
// and 0
func indexFunc(r rune) bool {
return r == ' ' || r == 0
}