| // 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 |
| } |