Add widget files from cjbassi/termui
This commit is contained in:
parent
7b77956fee
commit
b73fe56e94
128
src/termui/linegraph.go
Normal file
128
src/termui/linegraph.go
Normal 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
99
src/termui/sparkline.go
Normal 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
195
src/termui/table.go
Normal 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()
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user