Add widget files from cjbassi/termui

This commit is contained in:
Caleb Bassi 2018-12-31 15:01:13 -08:00
parent 7b77956fee
commit b73fe56e94
3 changed files with 422 additions and 0 deletions

128
src/termui/linegraph.go Normal file
View File

@ -0,0 +1,128 @@
package termui
import (
"sort"
drawille "github.com/cjbassi/drawille-go"
)
// LineGraph implements a line graph of data points.
type LineGraph struct {
*Block
Data map[string][]float64
LineColor map[string]Color
Zoom int
Labels map[string]string
DefaultLineColor Color
}
// NewLineGraph returns a new LineGraph with current theme.
func NewLineGraph() *LineGraph {
return &LineGraph{
Block: NewBlock(),
Data: make(map[string][]float64),
LineColor: make(map[string]Color),
Labels: make(map[string]string),
Zoom: 5,
DefaultLineColor: Theme.LineGraph,
}
}
// Buffer implements Bufferer interface.
func (self *LineGraph) Buffer() *Buffer {
buf := self.Block.Buffer()
// we render each data point on to the canvas then copy over the braille to the buffer at the end
// fyi braille characters have 2x4 dots for each character
c := drawille.NewCanvas()
// used to keep track of the braille colors until the end when we render the braille to the buffer
colors := make([][]Color, self.X+2)
for i := range colors {
colors[i] = make([]Color, self.Y+2)
}
// sort the series so that overlapping data will overlap the same way each time
seriesList := make([]string, len(self.Data))
i := 0
for seriesName := range self.Data {
seriesList[i] = seriesName
i++
}
sort.Strings(seriesList)
// draw lines in reverse order so that the first color defined in the colorscheme is on top
for i := len(seriesList) - 1; i >= 0; i-- {
seriesName := seriesList[i]
seriesData := self.Data[seriesName]
seriesLineColor, ok := self.LineColor[seriesName]
if !ok {
seriesLineColor = self.DefaultLineColor
}
// coordinates of last point
lastY, lastX := -1, -1
// assign colors to `colors` and lines/points to the canvas
for i := len(seriesData) - 1; i >= 0; i-- {
x := ((self.X + 1) * 2) - 1 - (((len(seriesData) - 1) - i) * self.Zoom)
y := ((self.Y + 1) * 4) - 1 - int((float64((self.Y)*4)-1)*(seriesData[i]/100))
if x < 0 {
// render the line to the last point up to the wall
if x > 0-self.Zoom {
for _, p := range drawille.Line(lastX, lastY, x, y) {
if p.X > 0 {
c.Set(p.X, p.Y)
colors[p.X/2][p.Y/4] = seriesLineColor
}
}
}
break
}
if lastY == -1 { // if this is the first point
c.Set(x, y)
colors[x/2][y/4] = seriesLineColor
} else {
c.DrawLine(lastX, lastY, x, y)
for _, p := range drawille.Line(lastX, lastY, x, y) {
colors[p.X/2][p.Y/4] = seriesLineColor
}
}
lastX, lastY = x, y
}
// copy braille and colors to buffer
for y, line := range c.Rows(c.MinX(), c.MinY(), c.MaxX(), c.MaxY()) {
for x, char := range line {
x /= 3 // idk why but it works
if x == 0 {
continue
}
if char != 10240 { // empty braille character
buf.SetCell(x, y, Cell{char, colors[x][y], self.Bg})
}
}
}
}
// renders key/label ontop
for i, seriesName := range seriesList {
if i+2 > self.Y {
continue
}
seriesLineColor, ok := self.LineColor[seriesName]
if !ok {
seriesLineColor = self.DefaultLineColor
}
// render key ontop, but let braille be drawn over space characters
str := seriesName + " " + self.Labels[seriesName]
for k, char := range str {
if char != ' ' {
buf.SetCell(3+k, i+2, Cell{char, seriesLineColor, self.Bg})
}
}
}
return buf
}

99
src/termui/sparkline.go Normal file
View File

@ -0,0 +1,99 @@
package termui
import (
"fmt"
)
var SPARKS = [8]rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
// Sparkline is like: ▅▆▂▂▅▇▂▂▃▆▆▆▅▃. The data points should be non-negative integers.
type Sparkline struct {
Data []int
Title1 string
Title2 string
TitleColor Color
LineColor Color
}
// Sparklines is a renderable widget which groups together the given sparklines.
type Sparklines struct {
*Block
Lines []*Sparkline
}
// Add appends a given Sparkline to the *Sparklines.
func (self *Sparklines) Add(sl Sparkline) {
self.Lines = append(self.Lines, &sl)
}
// NewSparkline returns an unrenderable single sparkline that intended to be added into a Sparklines.
func NewSparkline() *Sparkline {
return &Sparkline{
TitleColor: Theme.Fg,
LineColor: Theme.Sparkline,
}
}
// NewSparklines return a new *Sparklines with given Sparklines, you can always add a new Sparkline later.
func NewSparklines(ss ...*Sparkline) *Sparklines {
return &Sparklines{
Block: NewBlock(),
Lines: ss,
}
}
// Buffer implements Bufferer interface.
func (self *Sparklines) Buffer() *Buffer {
buf := self.Block.Buffer()
lc := len(self.Lines) // lineCount
// renders each sparkline and its titles
for i, line := range self.Lines {
// prints titles
title1Y := 2 + (self.Y/lc)*i
title2Y := (2 + (self.Y/lc)*i) + 1
title1 := MaxString(line.Title1, self.X)
title2 := MaxString(line.Title2, self.X)
buf.SetString(1, title1Y, title1, line.TitleColor|AttrBold, self.Bg)
buf.SetString(1, title2Y, title2, line.TitleColor|AttrBold, self.Bg)
sparkY := (self.Y / lc) * (i + 1)
// finds max data in current view used for relative heights
max := 1
for i := len(line.Data) - 1; i >= 0 && self.X-((len(line.Data)-1)-i) >= 1; i-- {
if line.Data[i] > max {
max = line.Data[i]
}
}
// prints sparkline
for x := self.X; x >= 1; x-- {
char := SPARKS[0]
if (self.X - x) < len(line.Data) {
offset := self.X - x
cur_item := line.Data[(len(line.Data)-1)-offset]
percent := float64(cur_item) / float64(max)
index := int(percent * 7)
if index < 0 || index >= len(SPARKS) {
Error("sparkline",
fmt.Sprint(
"len(line.Data): ", len(line.Data), "\n",
"max: ", max, "\n",
"x: ", x, "\n",
"self.X: ", self.X, "\n",
"offset: ", offset, "\n",
"cur_item: ", cur_item, "\n",
"percent: ", percent, "\n",
"index: ", index, "\n",
"len(SPARKS): ", len(SPARKS),
))
}
char = SPARKS[index]
}
buf.SetCell(x, sparkY, Cell{char, line.LineColor, self.Bg})
}
}
return buf
}

195
src/termui/table.go Normal file
View File

@ -0,0 +1,195 @@
package termui
import (
"fmt"
"strings"
)
// Table tracks all the attributes of a Table instance
type Table struct {
*Block
Header []string
Rows [][]string
ColWidths []int
CellXPos []int // column position
ColResizer func() // for widgets that inherit a Table and want to overload the ColResize method
Gap int // gap between columns
PadLeft int
Cursor bool
CursorColor Color
UniqueCol int // the column used to identify the selected item
SelectedItem string // used to keep the cursor on the correct item if the data changes
SelectedRow int
TopRow int // used to indicate where in the table we are scrolled at
}
// NewTable returns a new Table instance
func NewTable() *Table {
self := &Table{
Block: NewBlock(),
CursorColor: Theme.TableCursor,
SelectedRow: 0,
TopRow: 0,
UniqueCol: 0,
}
self.ColResizer = self.ColResize
return self
}
// ColResize is the default column resizer, but can be overriden.
// ColResize calculates the width of each column.
func (self *Table) ColResize() {
}
// Buffer implements the Bufferer interface.
func (self *Table) Buffer() *Buffer {
buf := self.Block.Buffer()
self.ColResizer()
// finds exact column starting position
self.CellXPos = []int{}
cur := 1 + self.PadLeft
for _, w := range self.ColWidths {
self.CellXPos = append(self.CellXPos, cur)
cur += w
cur += self.Gap
}
// prints header
for i, h := range self.Header {
width := self.ColWidths[i]
if width == 0 {
continue
}
// don't render column if it doesn't fit in widget
if width > (self.X-self.CellXPos[i])+1 {
continue
}
buf.SetString(self.CellXPos[i], 1, h, self.Fg|AttrBold, self.Bg)
}
// prints each row
for rowNum := self.TopRow; rowNum < self.TopRow+self.Y-1 && rowNum < len(self.Rows); rowNum++ {
if rowNum < 0 || rowNum >= len(self.Rows) {
Error("table rows",
fmt.Sprint(
"rowNum: ", rowNum, "\n",
"self.TopRow: ", self.TopRow, "\n",
"len(self.Rows): ", len(self.Rows), "\n",
"self.Y: ", self.Y,
))
}
row := self.Rows[rowNum]
y := (rowNum + 2) - self.TopRow
// prints cursor
fg := self.Fg
if self.Cursor {
if (self.SelectedItem == "" && rowNum == self.SelectedRow) || (self.SelectedItem != "" && self.SelectedItem == row[self.UniqueCol]) {
fg = self.CursorColor | AttrReverse
for _, width := range self.ColWidths {
if width == 0 {
continue
}
buf.SetString(1, y, strings.Repeat(" ", self.X), fg, self.Bg)
}
self.SelectedItem = row[self.UniqueCol]
self.SelectedRow = rowNum
}
}
// prints each col of the row
for i, width := range self.ColWidths {
if width == 0 {
continue
}
// don't render column if width is greater than distance to end of widget
if width > (self.X-self.CellXPos[i])+1 {
continue
}
r := MaxString(row[i], width)
buf.SetString(self.CellXPos[i], y, r, fg, self.Bg)
}
}
return buf
}
/////////////////////////////////////////////////////////////////////////////////
// Cursor Movement //
/////////////////////////////////////////////////////////////////////////////////
// calcPos is used to calculate the cursor position and the current view.
func (self *Table) calcPos() {
self.SelectedItem = ""
if self.SelectedRow < 0 {
self.SelectedRow = 0
}
if self.SelectedRow < self.TopRow {
self.TopRow = self.SelectedRow
}
if self.SelectedRow > len(self.Rows)-1 {
self.SelectedRow = len(self.Rows) - 1
}
if self.SelectedRow > self.TopRow+(self.Y-2) {
self.TopRow = self.SelectedRow - (self.Y - 2)
}
}
func (self *Table) Up() {
self.SelectedRow -= 1
self.calcPos()
}
func (self *Table) Down() {
self.SelectedRow += 1
self.calcPos()
}
func (self *Table) Top() {
self.SelectedRow = 0
self.calcPos()
}
func (self *Table) Bottom() {
self.SelectedRow = len(self.Rows) - 1
self.calcPos()
}
// The number of lines in a page is equal to the height of the widgeself.
func (self *Table) HalfPageUp() {
self.SelectedRow = self.SelectedRow - (self.Y-2)/2
self.calcPos()
}
func (self *Table) HalfPageDown() {
self.SelectedRow = self.SelectedRow + (self.Y-2)/2
self.calcPos()
}
func (self *Table) PageUp() {
self.SelectedRow -= (self.Y - 2)
self.calcPos()
}
func (self *Table) PageDown() {
self.SelectedRow += (self.Y - 2)
self.calcPos()
}
func (self *Table) Click(x, y int) {
x = x - self.XOffset
y = y - self.YOffset
if (x > 0 && x <= self.X) && (y > 0 && y <= self.Y) {
self.SelectedRow = (self.TopRow + y) - 2
self.calcPos()
}
}