diff options
author | Caleb Bassi <calebjbassi@gmail.com> | 2018-02-18 23:25:02 -0800 |
---|---|---|
committer | Caleb Bassi <calebjbassi@gmail.com> | 2018-02-19 02:00:21 -0800 |
commit | 40775db60b90cd290a206108cc4d22b236be9ba5 (patch) | |
tree | 28b5e5d7921a399cdcc8fb9559df0e69a31354be /termui |
Initial commit
Diffstat (limited to 'termui')
-rw-r--r-- | termui/block.go | 98 | ||||
-rw-r--r-- | termui/block_common.go | 18 | ||||
-rw-r--r-- | termui/block_windows.go | 18 | ||||
-rw-r--r-- | termui/buffer.go | 122 | ||||
-rw-r--r-- | termui/colors.go | 28 | ||||
-rw-r--r-- | termui/events.go | 177 | ||||
-rw-r--r-- | termui/gauge.go | 54 | ||||
-rw-r--r-- | termui/grid.go | 60 | ||||
-rw-r--r-- | termui/init.go | 28 | ||||
-rw-r--r-- | termui/linegraph.go | 100 | ||||
-rw-r--r-- | termui/list.go | 40 | ||||
-rw-r--r-- | termui/render.go | 47 | ||||
-rw-r--r-- | termui/sparkline.go | 114 | ||||
-rw-r--r-- | termui/table.go | 182 | ||||
-rw-r--r-- | termui/theme.go | 48 | ||||
-rw-r--r-- | termui/utils.go | 22 |
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 |