summaryrefslogtreecommitdiffstats
path: root/termui
diff options
context:
space:
mode:
authorCaleb Bassi <calebjbassi@gmail.com>2018-02-18 23:25:02 -0800
committerCaleb Bassi <calebjbassi@gmail.com>2018-02-19 02:00:21 -0800
commit40775db60b90cd290a206108cc4d22b236be9ba5 (patch)
tree28b5e5d7921a399cdcc8fb9559df0e69a31354be /termui
Initial commit
Diffstat (limited to 'termui')
-rw-r--r--termui/block.go98
-rw-r--r--termui/block_common.go18
-rw-r--r--termui/block_windows.go18
-rw-r--r--termui/buffer.go122
-rw-r--r--termui/colors.go28
-rw-r--r--termui/events.go177
-rw-r--r--termui/gauge.go54
-rw-r--r--termui/grid.go60
-rw-r--r--termui/init.go28
-rw-r--r--termui/linegraph.go100
-rw-r--r--termui/list.go40
-rw-r--r--termui/render.go47
-rw-r--r--termui/sparkline.go114
-rw-r--r--termui/table.go182
-rw-r--r--termui/theme.go48
-rw-r--r--termui/utils.go22
16 files changed, 1156 insertions, 0 deletions
diff --git a/termui/block.go b/termui/block.go
new file mode 100644
index 0000000..2367ecf
--- /dev/null
+++ b/termui/block.go
@@ -0,0 +1,98 @@
+package termui
+
+import "image"
+
+// Block is a base struct for all other upper level widgets,
+// consider it as css: display:block.
+// Normally you do not need to create it manually.
+type Block struct {
+ Grid image.Rectangle
+ X int
+ Y int
+ XOffset int
+ YOffset int
+ Label string
+ BorderFg Attribute
+ BorderBg Attribute
+ LabelFg Attribute
+ LabelBg Attribute
+ Bg Attribute
+ Fg Attribute
+}
+
+// NewBlock returns a *Block which inherits styles from current theme.
+func NewBlock() *Block {
+ return &Block{
+ Fg: Theme.Fg,
+ Bg: Theme.Bg,
+ BorderFg: Theme.BorderFg,
+ BorderBg: Theme.BorderBg,
+ LabelFg: Theme.LabelFg,
+ LabelBg: Theme.LabelBg,
+ }
+}
+
+// Buffer draws a box border.
+func (b *Block) drawBorder(buf *Buffer) {
+ x := b.X + 1
+ y := b.Y + 1
+
+ // draw lines
+ buf.Merge(NewFilledBuffer(0, 0, x, 1, Cell{HORIZONTAL_LINE, b.BorderFg, b.BorderBg}))
+ buf.Merge(NewFilledBuffer(0, y, x, y+1, Cell{HORIZONTAL_LINE, b.BorderFg, b.BorderBg}))
+ buf.Merge(NewFilledBuffer(0, 0, 1, y+1, Cell{VERTICAL_LINE, b.BorderFg, b.BorderBg}))
+ buf.Merge(NewFilledBuffer(x, 0, x+1, y+1, Cell{VERTICAL_LINE, b.BorderFg, b.BorderBg}))
+
+ // draw corners
+ buf.SetCell(0, 0, Cell{TOP_LEFT, b.BorderFg, b.BorderBg})
+ buf.SetCell(x, 0, Cell{TOP_RIGHT, b.BorderFg, b.BorderBg})
+ buf.SetCell(0, y, Cell{BOTTOM_LEFT, b.BorderFg, b.BorderBg})
+ buf.SetCell(x, y, Cell{BOTTOM_RIGHT, b.BorderFg, b.BorderBg})
+}
+
+func (b *Block) drawLabel(buf *Buffer) {
+ r := MaxString(b.Label, (b.X-3)-1)
+ buf.SetString(3, 0, r, b.LabelFg, b.LabelBg)
+ if b.Label == "" {
+ return
+ }
+ c := Cell{' ', b.Fg, b.Bg}
+ buf.SetCell(2, 0, c)
+ if len(b.Label)+3 < b.X {
+ buf.SetCell(len(b.Label)+3, 0, c)
+ } else {
+ buf.SetCell(b.X-1, 0, c)
+ }
+}
+
+// Resize computes Height, Width, XOffset, and YOffset
+func (b *Block) Resize(termWidth, termHeight, termCols, termRows int) {
+ b.X = int((float64(b.Grid.Dx())/float64(termCols))*float64(termWidth)) - 2
+ b.Y = int((float64(b.Grid.Dy())/float64(termRows))*float64(termHeight)) - 2
+ b.XOffset = int(((float64(b.Grid.Min.X) / float64(termCols)) * float64(termWidth)))
+ b.YOffset = int(((float64(b.Grid.Min.Y) / float64(termRows)) * float64(termHeight)))
+}
+
+func (b *Block) SetGrid(c0, r0, c1, r1 int) {
+ b.Grid = image.Rect(c0, r0, c1, r1)
+}
+
+func (b *Block) GetXOffset() int {
+ return b.XOffset
+}
+
+func (b *Block) GetYOffset() int {
+ return b.YOffset
+}
+
+// Buffer implements Bufferer interface and draws background and border
+func (b *Block) Buffer() *Buffer {
+ buf := NewBuffer()
+ buf.SetAreaXY(b.X+2, b.Y+2)
+ buf.Fill(Cell{' ', ColorDefault, b.Bg})
+
+ b.drawBorder(buf)
+ b.drawLabel(buf)
+
+ return buf
+}
diff --git a/termui/block_common.go b/termui/block_common.go
new file mode 100644
index 0000000..aeef1c8
--- /dev/null
+++ b/termui/block_common.go
@@ -0,0 +1,18 @@
+// +build !windows
+
+package termui
+
+const (
+ TOP_RIGHT = '┐'
+ VERTICAL_LINE = '│'
+ HORIZONTAL_LINE = '─'
+ TOP_LEFT = '┌'
+ BOTTOM_RIGHT = '┘'
+ BOTTOM_LEFT = '└'
+ VERTICAL_LEFT = '┤'
+ VERTICAL_RIGHT = '├'
+ HORIZONTAL_DOWN = '┬'
+ HORIZONTAL_UP = '┴'
+ QUOTA_LEFT = '«'
+ QUOTA_RIGHT = '»'
+)
diff --git a/termui/block_windows.go b/termui/block_windows.go
new file mode 100644
index 0000000..af0307f
--- /dev/null
+++ b/termui/block_windows.go
@@ -0,0 +1,18 @@
+// +build windows
+
+package termui
+
+const (
+ TOP_RIGHT = '+'
+ VERTICAL_LINE = '|'
+ HORIZONTAL_LINE = '-'
+ TOP_LEFT = '+'
+ BOTTOM_RIGHT = '+'
+ BOTTOM_LEFT = '+'
+ VERTICAL_LEFT = '+'
+ VERTICAL_RIGHT = '+'
+ HORIZONTAL_DOWN = '+'
+ HORIZONTAL_UP = '+'
+ QUOTA_LEFT = '<'
+ QUOTA_RIGHT = '>'
+)
diff --git a/termui/buffer.go b/termui/buffer.go
new file mode 100644
index 0000000..c17c989
--- /dev/null
+++ b/termui/buffer.go
@@ -0,0 +1,122 @@
+package termui
+
+import "image"
+
+// Cell is a rune with assigned Fg and Bg
+type Cell struct {
+ Ch rune
+ Fg Attribute
+ Bg Attribute
+}
+
+// Buffer is a renderable rectangle cell data container.
+type Buffer struct {
+ Area image.Rectangle // selected drawing area
+ CellMap map[image.Point]Cell
+}
+
+func NewCell(ch rune, Fg, Bg Attribute) Cell {
+ return Cell{ch, Fg, Bg}
+}
+
+// NewBuffer returns a new Buffer
+func NewBuffer() *Buffer {
+ return &Buffer{
+ CellMap: make(map[image.Point]Cell),
+ Area: image.Rectangle{}}
+}
+
+// NewFilledBuffer returns a new Buffer filled with ch, fb and bg.
+func NewFilledBuffer(x0, y0, x1, y1 int, c Cell) *Buffer {
+ buf := NewBuffer()
+ buf.Area.Min = image.Pt(x0, y0)
+ buf.Area.Max = image.Pt(x1, y1)
+
+ for x := buf.Area.Min.X; x < buf.Area.Max.X; x++ {
+ for y := buf.Area.Min.Y; y < buf.Area.Max.Y; y++ {
+ buf.SetCell(x, y, c)
+ }
+ }
+ return buf
+}
+
+// Set assigns a char to (x,y)
+func (b *Buffer) SetCell(x, y int, c Cell) {
+ b.CellMap[image.Pt(x, y)] = c
+}
+
+func (b *Buffer) SetString(x, y int, s string, fg, bg Attribute) {
+ for i, char := range s {
+ b.SetCell(x+i, y, Cell{char, fg, bg})
+ }
+}
+
+// At returns the cell at (x,y).
+func (b *Buffer) At(x, y int) Cell {
+ return b.CellMap[image.Pt(x, y)]
+}
+
+// Bounds returns the domain for which At can return non-zero color.
+func (b *Buffer) Bounds() image.Rectangle {
+ x0, y0, x1, y1 := 0, 0, 0, 0
+ for p := range b.CellMap {
+ if p.X > x1 {
+ x1 = p.X
+ }
+ if p.X < x0 {
+ x0 = p.X
+ }
+ if p.Y > y1 {
+ y1 = p.Y
+ }
+ if p.Y < y0 {
+ y0 = p.Y
+ }
+ }
+ return image.Rect(x0, y0, x1+1, y1+1)
+}
+
+// SetArea assigns a new rect area to Buffer b.
+func (b *Buffer) SetArea(r image.Rectangle) {
+ b.Area.Max = r.Max
+ b.Area.Min = r.Min
+}
+
+func (b *Buffer) SetAreaXY(x, y int) {
+ b.Area.Min.Y = 0
+ b.Area.Min.X = 0
+ b.Area.Max.Y = y
+ b.Area.Max.X = x
+}
+
+// Sync sets drawing area to the buffer's bound
+func (b *Buffer) Sync() {
+ b.SetArea(b.Bounds())
+}
+
+// Merge merges bs Buffers onto b
+func (b *Buffer) Merge(bs ...*Buffer) {
+ for _, buf := range bs {
+ for p, c := range buf.CellMap {
+ b.SetCell(p.X, p.Y, c)
+ }
+ b.SetArea(b.Area.Union(buf.Area))
+ }
+}
+
+func (b *Buffer) MergeWithOffset(buf *Buffer, xOffset, yOffset int) {
+ for p, c := range buf.CellMap {
+ b.SetCell(p.X+xOffset, p.Y+yOffset, c)
+ }
+ rect := image.Rect(xOffset, yOffset, buf.Area.Max.X+xOffset, buf.Area.Max.Y+yOffset)
+ b.SetArea(b.Area.Union(rect))
+}
+
+// Fill fills the Buffer b with ch,fg and bg.
+func (b *Buffer) Fill(c Cell) {
+ for x := b.Area.Min.X; x < b.Area.Max.X; x++ {
+ for y := b.Area.Min.Y; y < b.Area.Max.Y; y++ {
+ b.SetCell(x, y, c)
+ }
+ }
+}
diff --git a/termui/colors.go b/termui/colors.go
new file mode 100644
index 0000000..e9a7dda
--- /dev/null
+++ b/termui/colors.go
@@ -0,0 +1,28 @@
+package termui
+
+/* ---------------Port from termbox-go --------------------- */
+
+// Attribute is printable cell's color and style.
+type Attribute uint16
+
+const (
+ ColorDefault Attribute = iota
+ ColorBlack
+ ColorRed
+ ColorGreen
+ ColorYellow
+ ColorBlue
+ ColorMagenta
+ ColorCyan
+ ColorWhite
+)
+
+const NumberofColors = 8
+
+const (
+ AttrBold Attribute = 1 << (iota + 9)
+ AttrUnderline
+ AttrReverse
+)
+
+/* ----------------------- End ----------------------------- */
diff --git a/termui/events.go b/termui/events.go
new file mode 100644
index 0000000..836740b
--- /dev/null
+++ b/termui/events.go
@@ -0,0 +1,177 @@
+package termui
+
+import (
+ "strconv"
+
+ tb "github.com/nsf/termbox-go"
+)
+
+var eventStream = EventStream{
+ make(map[string]func(Event)),
+ "",
+ make(chan bool, 1),
+ make(chan tb.Event),
+}
+
+type EventStream struct {
+ eventHandlers map[string]func(Event)
+ prevKey string
+ stopLoop chan bool
+ eventQueue chan tb.Event
+}
+
+type Event struct {
+ Key string
+ Width int
+ Height int
+ MouseX int
+ MouseY int
+}
+
+// handleEvent calls the approriate callback function if there is one.
+func handleEvent(e tb.Event) {
+ if e.Type == tb.EventError {
+ panic(e.Err)
+ }
+
+ ne := convertTermboxEvent(e)
+
+ if val, ok := eventStream.eventHandlers[ne.Key]; ok {
+ val(ne)
+ eventStream.prevKey = ""
+ } else { // check if the last 2 keys form a key combo with a handler
+ // if this is a keyboard event and the previous event was unhandled
+ if e.Type == tb.EventKey && eventStream.prevKey != "" {
+ combo := eventStream.prevKey + ne.Key
+ if val, ok := eventStream.eventHandlers[combo]; ok {
+ ne.Key = combo
+ val(ne)
+ eventStream.prevKey = ""
+ } else {
+ eventStream.prevKey = ne.Key
+ }
+ } else {
+ eventStream.prevKey = ne.Key
+ }
+ }
+}
+
+// Loop gets events from termbox and passes them off to handleEvent.
+// Stops when StopLoop is called.
+func Loop() {
+ go func() {
+ for {
+ eventStream.eventQueue <- tb.PollEvent()
+ }
+ }()
+
+ for {
+ select {
+ case <-eventStream.stopLoop:
+ return
+ case e := <-eventStream.eventQueue:
+ handleEvent(e)
+ }
+ }
+}
+
+// StopLoop stops the events Loop
+func StopLoop() {
+ eventStream.stopLoop <- true
+}
+
+// On assigns event names to their handlers. Takes a string, strings, or a slice of strings, and a function.
+func On(things ...interface{}) {
+ function := things[len(things)-1].(func(Event))
+ for _, thing := range things {
+ if value, ok := thing.(string); ok {
+ eventStream.eventHandlers[value] = function
+ }
+ if value, ok := thing.([]string); ok {
+ for _, name := range value {
+ eventStream.eventHandlers[name] = function
+ }
+ }
+ }
+}
+
+// convertTermboxKeyValue converts a termbox keyboard event to a more friendly string format.
+// Combines modifiers into the string instead of having them as additional fields in an event.
+func convertTermboxKeyValue(e tb.Event) string {
+ k := string(e.Ch)
+ pre := ""
+ mod := ""
+
+ if e.Mod == tb.ModAlt {
+ mod = "M-"
+ }
+ if e.Ch == 0 {
+ if e.Key > 0xFFFF-12 {
+ k = "<f" + strconv.Itoa(0xFFFF-int(e.Key)+1) + ">"
+ } else if e.Key > 0xFFFF-25 {
+ ks := []string{"<insert>", "<delete>", "<home>", "<end>", "<previous>", "<next>", "<up>", "<down>", "<left>", "<right>"}
+ k = ks[0xFFFF-int(e.Key)-12]
+ }
+
+ if e.Key <= 0x7F {
+ pre = "C-"
+ k = string('a' - 1 + int(e.Key))
+ kmap := map[tb.Key][2]string{
+ tb.KeyCtrlSpace: {"C-", "<space>"},
+ tb.KeyBackspace: {"", "<backspace>"},
+ tb.KeyTab: {"", "<tab>"},
+ tb.KeyEnter: {"", "<enter>"},
+ tb.KeyEsc: {"", "<escape>"},
+ tb.KeyCtrlBackslash: {"C-", "\\"},
+ tb.KeyCtrlSlash: {"C-", "/"},
+ tb.KeySpace: {"", "<space>"},
+ tb.KeyCtrl8: {"C-", "8"},
+ }
+ if sk, ok := kmap[e.Key]; ok {
+ pre = sk[0]
+ k = sk[1]
+ }
+ }
+ }
+
+ return pre + mod + k
+}
+
+// convertTermboxMouseValue turns termbox mouse events into strings
+func convertTermboxMouseValue(e tb.Event) string {
+ switch e.Key {
+ case tb.MouseLeft:
+ return "MouseLeft"
+ case tb.MouseMiddle:
+ return "MouseMiddle"
+ case tb.MouseRight:
+ return "MouseRight"
+ case tb.MouseWheelUp:
+ return "MouseWheelUp"
+ case tb.MouseWheelDown:
+ return "MouseWheelDown"
+ case tb.MouseRelease:
+ return "MouseRelease"
+ }
+ return ""
+}
+
+// convertTermboxEvent turns a termbox event into a termui event
+func convertTermboxEvent(e tb.Event) Event {
+ ne := Event{} // new event
+
+ switch e.Type {
+ case tb.EventKey:
+ ne.Key = convertTermboxKeyValue(e)
+ case tb.EventMouse:
+ ne.Key = convertTermboxMouseValue(e)
+ ne.MouseX = e.MouseX
+ ne.MouseY = e.MouseY
+ case tb.EventResize:
+ ne.Key = "resize"
+ ne.Width = e.Width
+ ne.Height = e.Height
+ }
+
+ return ne
+}
diff --git a/termui/gauge.go b/termui/gauge.go
new file mode 100644
index 0000000..fbe7b8e
--- /dev/null
+++ b/termui/gauge.go
@@ -0,0 +1,54 @@
+package termui
+
+import "strconv"
+
+// Gauge is a progress bar like widget.
+type Gauge struct {
+ *Block
+ Percent int
+ BarColor Attribute
+ PercentColor Attribute
+ Description string
+}
+
+// NewGauge return a new gauge with current theme.
+func NewGauge() *Gauge {
+ return &Gauge{
+ Block: NewBlock(),
+ PercentColor: Theme.Fg,
+ BarColor: Theme.Bg,
+ }
+}
+
+// Buffer implements Bufferer interface.
+func (g *Gauge) Buffer() *Buffer {
+ buf := g.Block.Buffer()
+
+ // plot bar
+ width := g.Percent * g.X / 100
+ for y := 1; y <= g.Y; y++ {
+ for x := 1; x <= width; x++ {
+ bg := g.BarColor
+ if bg == ColorDefault {
+ bg |= AttrReverse
+ }
+ buf.SetCell(x, y, Cell{' ', ColorDefault, bg})
+ }
+ }
+
+ // plot percentage
+ s := strconv.Itoa(g.Percent) + "%" + g.Description
+ y := (g.Y + 1) / 2
+ s = MaxString(s, g.X)
+ x := ((g.X - len(s)) + 1) / 2
+
+ for i, char := range s {
+ bg := g.Bg
+ if x+i < width {
+ bg = AttrReverse
+ }
+ buf.SetCell(1+x+i, y, Cell{char, g.PercentColor, bg})
+ }
+
+ return buf
+}
diff --git a/termui/grid.go b/termui/grid.go
new file mode 100644
index 0000000..a21c685
--- /dev/null
+++ b/termui/grid.go
@@ -0,0 +1,60 @@
+package termui
+
+var Body *Grid
+
+// GridBufferer introduces a Bufferer that can be manipulated by Grid.
+type GridBufferer interface {
+ Bufferer
+ Resize(int, int, int, int)
+ SetGrid(int, int, int, int)
+}
+
+type Grid struct {
+ Widgets []GridBufferer
+ Width int
+ Height int
+ Cols int
+ Rows int
+ BgColor Attribute
+}
+
+func NewGrid() *Grid {
+ return &Grid{}
+}
+
+func (g *Grid) Set(x0, y0, x1, y1 int, widget GridBufferer) {
+ if widget == nil {
+ return
+ }
+ if x1 <= x0 || y1 <= y0 {
+ panic("Invalid widget coordinates")
+ }
+
+ widget.SetGrid(x0, y0, x1, y1)
+ widget.Resize(g.Width, g.Height, g.Cols, g.Rows)
+
+ g.Widgets = append(g.Widgets, widget)
+}
+
+func (g *Grid) Resize() {
+ for _, w := range g.Widgets {
+ w.Resize(g.Width, g.Height, g.Cols, g.Rows)
+ }
+}
+
+// Buffer implements Bufferer interface.
+func (g *Grid) Buffer() *Buffer {
+ buf := NewBuffer()
+ for _, w := range g.Widgets {
+ buf.MergeWithOffset(w.Buffer(), w.GetXOffset(), w.GetYOffset())
+ }
+ return buf
+}
+
+func (g *Grid) GetXOffset() int {
+ return 0
+}
+
+func (g *Grid) GetYOffset() int {
+ return 0
+}
diff --git a/termui/init.go b/termui/init.go
new file mode 100644
index 0000000..f9e470d
--- /dev/null
+++ b/termui/init.go
@@ -0,0 +1,28 @@
+package termui
+
+import tb "github.com/nsf/termbox-go"
+
+// Init initializes termui library. This function should be called before any others.
+// After initialization, the library must be finalized by 'Close' function.
+func Init() error {
+ if err := tb.Init(); err != nil {
+ return err
+ }
+ tb.SetInputMode(tb.InputEsc | tb.InputMouse)
+ tb.SetOutputMode(tb.Output256)
+
+ Body = NewGrid()
+ Body.BgColor = Theme.Bg
+
+ // renderLock.Lock()
+ Body.Width, Body.Height = tb.Size()
+ // renderLock.Unlock()
+
+ return nil
+}
+
+// Close finalizes termui library,
+// should be called after successful initialization when termui's functionality isn't required anymore.
+func Close() {
+ tb.Close()
+}
diff --git a/termui/linegraph.go b/termui/linegraph.go
new file mode 100644
index 0000000..556fdd4
--- /dev/null
+++ b/termui/linegraph.go
@@ -0,0 +1,100 @@
+package termui
+
+import (
+ "fmt"
+ "sort"
+
+ drawille "github.com/cjbassi/drawille-go"
+)
+
+// LineGraph implements a graph of data points.
+type LineGraph struct {
+ *Block
+ Data map[string][]float64
+ LineColor map[string]Attribute
+
+ DefaultLineColor Attribute
+}
+
+// NewLineGraph returns a new LineGraph with current theme.
+func NewLineGraph() *LineGraph {
+ return &LineGraph{
+ Block: NewBlock(),
+ Data: make(map[string][]float64),
+ LineColor: make(map[string]Attribute),
+
+ DefaultLineColor: Theme.LineGraph,
+ }
+}
+
+// renderPoints plots and interpolates data points.
+func (lc *LineGraph) Buffer() *Buffer {
+ buf := lc.Block.Buffer()
+ c := drawille.NewCanvas()
+ colors := make([][]Attribute, lc.X+2)
+ for i := range colors {
+ colors[i] = make([]Attribute, lc.Y+2)
+ }
+
+ // Sort the series so that overlapping data will overlap the same way each time
+ seriesList := make([]string, len(lc.Data))
+ i := 0
+ for seriesName := range lc.Data {
+ seriesList[i] = seriesName
+ i++
+ }
+ sort.Strings(seriesList)
+
+ for j, seriesName := range seriesList {
+ seriesData := lc.Data[seriesName]
+ seriesLineColor, ok := lc.LineColor[seriesName]
+ if !ok {
+ seriesLineColor = lc.DefaultLineColor
+ }
+
+ lastY, lastX := -1, -1
+ // assign colors to `colors` and lines/points to the canvas
+ for i := len(seriesData) - 1; i >= 0; i-- {
+ x := ((lc.X + 1) * 2) - 1 - (((len(seriesData) - 1) - i) * 5)
+ y := ((lc.Y + 1) * 4) - 1 - int((float64((lc.Y)*4)-1)*(seriesData[i]/100))
+ // stop rendering at the left-most wall
+ if x < 0 {
+ 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 drawille 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
+ if x == 0 {
+ continue
+ }
+ if char != 10240 {
+ buf.SetCell(x, y, Cell{char, colors[x][y], lc.Bg})
+ }
+ }
+ }
+
+ // Render key
+ str := fmt.Sprintf("%s %3.0f%%", seriesName, seriesData[len(seriesData)-1])
+ for k, char := range str {
+ if char != ' ' {
+ buf.SetCell(3+k, j+2, Cell{char, seriesLineColor, lc.Bg})
+ }
+ }
+
+ }
+
+ return buf
+}
diff --git a/termui/list.go b/termui/list.go
new file mode 100644
index 0000000..cb5cd8d
--- /dev/null
+++ b/termui/list.go
@@ -0,0 +1,40 @@
+package termui
+
+import "fmt"
+
+// BarChart creates multiple bars in a widget:
+type List struct {
+ *Block
+ TextColor Attribute
+ Data []int
+ DataLabels []string
+ Threshold int
+}
+
+// NewBarChart returns a new *BarChart with current theme.
+func NewList() *List {
+ return &List{
+ Block: NewBlock(),
+ TextColor: Theme.Fg,
+ }
+}
+
+// Buffer implements Bufferer interface.
+func (bc *List) Buffer() *Buffer {
+ buf := bc.Block.Buffer()
+
+ for y, text := range bc.DataLabels {
+ if y+1 > bc.Y {
+ break
+ }
+ bg := ColorGreen
+ if bc.Data[y] >= bc.Threshold {
+ bg = ColorRed
+ }
+ r := MaxString(text, (bc.X - 4))
+ buf.SetString(1, y+1, r, ColorWhite, ColorDefault)
+ buf.SetString(bc.X-2, y+1, fmt.Sprintf("%dC", bc.Data[y]), bg, ColorDefault)
+ }
+
+ return buf
+}
diff --git a/termui/render.go b/termui/render.go
new file mode 100644
index 0000000..2d900ab
--- /dev/null
+++ b/termui/render.go
@@ -0,0 +1,47 @@
+package termui
+
+import (
+ "sync"
+
+ tb "github.com/nsf/termbox-go"
+)
+
+var renderJobs chan []Bufferer
+
+// So that only one render function can flush/write to the screen at a time
+// var renderLock sync.Mutex
+
+// Bufferer should be implemented by all renderable components. Bufferers can render a Buffer.
+type Bufferer interface {
+ Buffer() *Buffer
+ GetXOffset() int
+ GetYOffset() int
+}
+
+// Render renders all Bufferer in the given order from left to right, right could overlap on left ones.
+func Render(bs ...Bufferer) {
+ var wg sync.WaitGroup
+ for _, b := range bs {
+ wg.Add(1)
+ go func(b Bufferer) {
+ defer wg.Done()
+ buf := b.Buffer()
+ // set cells in buf
+ for p, c := range buf.CellMap {
+ if p.In(buf.Area) {
+ tb.SetCell(p.X+b.GetXOffset(), p.Y+b.GetYOffset(), c.Ch, tb.Attribute(c.Fg), tb.Attribute(c.Bg))
+ }
+ }
+ }(b)
+ }
+
+ // renderLock.Lock()
+
+ wg.Wait()
+ tb.Flush()
+ // renderLock.Unlock()
+}
+
+func Clear() {
+ tb.Clear(tb.ColorDefault, tb.Attribute(Theme.Bg))
+}
diff --git a/termui/sparkline.go b/termui/sparkline.go
new file mode 100644
index 0000000..2778505
--- /dev/null
+++ b/termui/sparkline.go
@@ -0,0 +1,114 @@
+package termui
+
+import (
+ "fmt"
+ "github.com/cjbassi/gotop/utils"
+)
+
+var SPARKS = [8]rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
+
+// Sparkline is like: ▅▆▂▂▅▇▂▂▃▆▆▆▅▃. The data points should be non-negative integers.
+type Sparkline struct {
+ Data []int
+ Title string
+ TitleColor Attribute
+ Total int
+ LineColor Attribute
+}
+
+// Sparklines is a renderable widget which groups together the given sparklines.
+type Sparklines struct {
+ *Block
+ Lines []*Sparkline
+}
+
+// Add appends a given Sparkline to s *Sparklines.
+func (s *Sparklines) Add(sl Sparkline) {
+ s.Lines = append(s.Lines, &sl)
+}
+
+// NewSparkline returns a unrenderable single sparkline that intended to be added into Sparklines.
+func NewSparkline() *Sparkline {
+ return &Sparkline{
+ TitleColor: Theme.Fg,
+ LineColor: Theme.SparkLine,
+ }
+}
+
+// NewSparklines return a new *Sparklines with given Sparkline(s), you can always add a new Sparkline later.
+func NewSparklines(ss ...*Sparkline) *Sparklines {
+ return &Sparklines{
+ Block: NewBlock(),
+ Lines: ss,
+ }
+}
+
+// Buffer implements Bufferer interface.
+func (sl *Sparklines) Buffer() *Buffer {
+ buf := sl.Block.Buffer()
+
+ lc := len(sl.Lines) // lineCount
+
+ // for each line
+ for i, line := range sl.Lines {
+
+ // Total and current
+ y := 2 + (sl.Y/lc)*i
+ total := ""
+ title := ""
+ current := ""
+
+ cur := line.Data[len(line.Data)-1]
+ curMag := "B"
+ if cur >= 1000000 {
+ cur = int(utils.BytesToMB(cur))
+ curMag = "MB"
+ } else if cur >= 1000 {
+ cur = int(utils.BytesToKB(cur))
+ curMag = "kB"
+ }
+
+ t := line.Total
+ tMag := "B"
+ if t >= 1000000000 {
+ t = int(utils.BytesToGB(t))
+ tMag = "GB"
+ } else if t >= 1000000 {
+ t = int(utils.BytesToMB(t))
+ tMag = "MB"
+ }
+
+ if i == 0 {
+ total = fmt.Sprintf(" Total Rx: %3d %s", t, tMag)
+ current = fmt.Sprintf(" Rx/s: %7d %2s/s", cur, curMag)
+ } else {
+ total = fmt.Sprintf(" Total Tx: %3d %s", t, tMag)
+ current = fmt.Sprintf(" Tx/s: %7d %2s/s", cur, curMag)
+ }
+
+ total = MaxString(total, sl.X)
+ title = MaxString(current, sl.X)
+ buf.SetString(1, y, total, line.TitleColor|AttrBold, sl.Bg)
+ buf.SetString(1, y+1, title, line.TitleColor|AttrBold, sl.Bg)
+
+ // sparkline
+ y = (sl.Y / lc) * (i + 1)
+ // finds max used for relative heights
+ max := 1
+ for i := len(line.Data) - 1; i >= 0 && sl.X-((len(line.Data)-1)-i) >= 1; i-- {
+ if line.Data[i] > max {
+ max = line.Data[i]
+ }
+ }
+ // prints sparkline
+ for x := sl.X; x >= 1; x-- {
+ char := SPARKS[0]
+ if (sl.X - x) < len(line.Data) {
+ char = SPARKS[int((float64(line.Data[(len(line.Data)-1)-(sl.X-x)])/float64(max))*7)]
+ }
+ buf.SetCell(x, y, Cell{char, line.LineColor, sl.Bg})
+ }
+ }
+
+ return buf
+}
diff --git a/termui/table.go b/termui/table.go
new file mode 100644
index 0000000..6088986
--- /dev/null
+++ b/termui/table.go
@@ -0,0 +1,182 @@
+package termui
+
+import (
+ "os/exec"
+ "strings"
+)
+
+// Table tracks all the attributes of a Table instance
+type Table struct {
+ *Block
+ Header []string
+ Rows [][]string
+ Fg Attribute
+ Bg Attribute
+ Cursor Attribute
+ UniqueCol int
+ pid string
+ selected int
+ topRow int
+}
+
+// NewTable returns a new Table instance
+func NewTable() *Table {
+ return &Table{
+ Block: NewBlock(),
+ Fg: Theme.Fg,
+ Bg: Theme.Bg,
+ Cursor: Theme.TableCursor,
+ selected: 0,
+ topRow: 0,
+ UniqueCol: 0,
+ }
+}
+
+// Buffer ...
+func (t *Table) Buffer() *Buffer {
+ buf := t.Block.Buffer()
+
+ if t.topRow > len(t.Rows)-(t.Y-1) {
+ t.topRow = len(t.Rows) - (t.Y - 1)
+ }
+
+ // calculate gap size based on total width
+ gap := 3
+ if t.X < 50 {
+ gap = 1
+ } else if t.X < 75 {
+ gap = 2
+ }
+
+ cw := []int{5, 10, 4, 4} // cellWidth
+ cp := []int{ // cellPosition
+ gap,
+ gap + cw[0] + gap,
+ t.X - gap - cw[3] - gap - cw[2],
+ t.X - gap - cw[3],
+ }
+
+ // total width requires by all 4 columns
+ contentWidth := gap + cw[0] + gap + cw[1] + gap + cw[2] + gap + cw[3] + gap
+ render := 4 // number of columns to iterate through
+
+ // removes CPU and MEM if there isn't enough room
+ if t.X < (contentWidth - gap - cw[3]) {
+ render = 2
+ } else if t.X < contentWidth {
+ cp[2] = cp[3]
+ render = 3
+ }
+
+ // print header
+ for i := 0; i < render; i++ {
+ r := MaxString(t.Header[i], t.X-6)
+ buf.SetString(cp[i], 1, r, t.Fg|AttrBold, t.Bg)
+ }
+
+ // prints each row
+ // for y, row := range t.Rows {
+ // for y := t.topRow; y <= t.topRow+t.Y; y++ {
+ for rowNum := t.topRow; rowNum < t.topRow+t.Y-1 && rowNum < len(t.Rows); rowNum++ {
+ row := t.Rows[rowNum]
+ y := (rowNum + 2) - t.topRow
+
+ // cursor
+ bg := t.Bg
+ if (t.pid == "" && rowNum == t.selected) || (t.pid != "" && t.pid == row[t.UniqueCol]) {
+ bg = t.Cursor
+ for i := 0; i < render; i++ {
+ buf.SetString(1, y, strings.Repeat(" ", t.X), t.Fg, bg)
+ }
+ t.pid = row[t.UniqueCol]
+ t.selected = rowNum
+ }
+
+ // prints each string
+ for i := 0; i < render; i++ {
+ r := MaxString(row[i], t.X-6)
+ buf.SetString(cp[i], y, r, t.Fg, bg)
+ }
+ }
+
+ return buf
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+func (t *Table) calcPos() {
+ t.pid = ""
+
+ if t.selected < 0 {
+ t.selected = 0
+ }
+ if t.selected < t.topRow {
+ t.topRow = t.selected
+ }
+
+ if t.selected > len(t.Rows)-1 {
+ t.selected = len(t.Rows) - 1
+ }
+ if t.selected > t.topRow+(t.Y-2) {
+ t.topRow = t.selected - (t.Y - 2)
+ }
+}
+
+func (t *Table) Up() {
+ t.selected -= 1
+ t.calcPos()
+}
+
+func (t *Table) Down() {
+ t.selected += 1
+ t.calcPos()
+}
+
+func (t *Table) Top() {
+ t.selected = 0
+ t.calcPos()
+}
+
+func (t *Table) Bottom() {
+ t.selected = len(t.Rows) - 1
+ t.calcPos()
+}
+
+func (t *Table) HalfPageUp() {
+ t.selected = t.selected - (t.Y-2)/2
+ t.calcPos()
+}
+
+func (t *Table) HalfPageDown() {
+ t.selected = t.selected + (t.Y-2)/2
+ t.calcPos()
+}
+
+func (t *Table) PageUp() {
+ t.selected -= (t.Y - 2)
+ t.calcPos()
+}
+
+func (t *Table) PageDown() {
+ t.selected += (t.Y - 2)
+ t.calcPos()
+}
+
+func (t *Table) Click(x, y int) {
+ x = x - t.XOffset
+ y = y - t.YOffset
+ if (x > 0 && x <= t.X) && (y > 0 && y <= t.Y) {
+ t.selected = (t.topRow + y) - 2
+ t.calc