From cb8236c9c22007eb622b7219c58d3342f1f53d50 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 3 Nov 2019 20:47:29 +0100 Subject: termui: migrate to awesome-gocui instead of the old fork I had --- vendor/github.com/awesome-gocui/gocui/view.go | 800 ++++++++++++++++++++++++++ 1 file changed, 800 insertions(+) create mode 100644 vendor/github.com/awesome-gocui/gocui/view.go (limited to 'vendor/github.com/awesome-gocui/gocui/view.go') diff --git a/vendor/github.com/awesome-gocui/gocui/view.go b/vendor/github.com/awesome-gocui/gocui/view.go new file mode 100644 index 00000000..81f90603 --- /dev/null +++ b/vendor/github.com/awesome-gocui/gocui/view.go @@ -0,0 +1,800 @@ +// 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") +} -- cgit