// 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" "errors" "io" "strings" "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 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 // If Editable is true, keystrokes will be added to the view's internal // buffer at the cursor position. Editable 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 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 } // 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, } 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 } 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 } // 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) { 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 } // 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() // 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 } y := 0 for i, line := range buf { if i < v.oy { continue } if y >= maxY { break } for x, ch := range line { if x >= maxX { break } if err := v.setRune(x, y, ch); err != nil { return err } } y++ } return nil } // Clear empties the view's internal buffer. func (v *View) Clear() { v.lines = nil v.clearRunes() } // 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 { x = v.ox + x y = v.oy + y if x < 0 || y < 0 { return errors.New("invalid point") } if y >= len(v.lines) { 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 { v.lines[y] = make([]rune, x+1, (x+1)*2) } else if x >= len(v.lines[y]) { 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] } } 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 } // 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 { 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") } copy(v.lines[y][x:], v.lines[y][x+1:]) v.lines[y][len(v.lines[y])-1] = ' ' return nil } // 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 { y = v.oy + y if y < 0 || y >= len(v.lines) { return errors.New("invalid point") } v.lines = append(v.lines, nil) copy(v.lines[y+1:], v.lines[y:]) v.lines[y] = nil 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) } // 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 } // 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]) 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 }