diff --git a/README.md b/README.md index 55c12f1..bcf57ef 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ -# termui [![Build Status](https://travis-ci.org/gizak/termui.svg)](https://travis-ci.org/gizak/termui) [![Doc Status](https://godoc.org/github.com/gizak/termui?status.png)](https://godoc.org/github.com/gizak/termui) +# termui [![Build Status](https://travis-ci.org/gizak/termui.svg?branch=master)](https://travis-ci.org/gizak/termui) [![Doc Status](https://godoc.org/github.com/gizak/termui?status.png)](https://godoc.org/github.com/gizak/termui) ---- +## Notice +termui comes with ABSOLUTELY NO WARRANTY, and there is a breaking change coming up (see refactoring branch) which will change the `Bufferer` interface and many others. These changes reduce calculation overhead and introduce a new drawing buffer with better capacibilities. We will step into the next stage (call it beta) after merging these changes. +## Introduction Go terminal dashboard. Inspired by [blessed-contrib](https://github.com/yaronn/blessed-contrib), but purely in Go. Cross-platform, easy to compile, and fully-customizable. @@ -132,6 +134,11 @@ TODO: Image (let's wait until the implementation is finished). barchart +#### Mult-Bar / Stacked-Bar Chart +[demo code](https://github.com/gizak/termui/blob/master/example/mbarchart.go) + +barchart + #### Sparklines [demo code](https://github.com/gizak/termui/blob/master/example/sparklines.go) diff --git a/_grid_test.go b/_grid_test.go index 3230033..cdafb20 100644 --- a/_grid_test.go +++ b/_grid_test.go @@ -10,7 +10,7 @@ import ( "github.com/davecgh/go-spew/spew" ) -var r *row +var r *Row func TestRowWidth(t *testing.T) { p0 := NewPar("p0") diff --git a/example/dashboard.go b/example/dashboard.go index d855ef2..c14bb44 100644 --- a/example/dashboard.go +++ b/example/dashboard.go @@ -47,7 +47,7 @@ func main() { spark := ui.Sparkline{} spark.Height = 1 spark.Title = "srv 0:" - spdata := []int{4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6} + spdata := []int{4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6, 4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6, 4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6, 4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6} spark.Data = spdata spark.LineColor = ui.ColorCyan spark.TitleColor = ui.ColorWhite @@ -119,8 +119,8 @@ func main() { draw := func(t int) { g.Percent = t % 101 list.Items = strs[t%9:] - sp.Lines[0].Data = spdata[t%10:] - sp.Lines[1].Data = spdata[t/2%10:] + sp.Lines[0].Data = spdata[:30+t%50] + sp.Lines[1].Data = spdata[:35+t%50] lc.Data = sinps[t/2:] lc1.Data = sinps[2*t:] bc.Data = bcdata[t/2%10:] diff --git a/example/gauge.go b/example/gauge.go index f598d0e..26a2f12 100644 --- a/example/gauge.go +++ b/example/gauge.go @@ -55,7 +55,15 @@ func main() { g1.Border.Fg = termui.ColorWhite g1.Border.LabelFgClr = termui.ColorMagenta - termui.Render(g0, g1, g2, gg) + g3 := termui.NewGauge() + g3.Percent = 50 + g3.Width = 50 + g3.Height = 3 + g3.Y = 11 + g3.Border.Label = "Gauge with custom label" + g3.Label = "{{percent}}% (100MBs free)" + g3.LabelAlign = termui.AlignRight + termui.Render(g0, g1, g2, g3) <-termui.EventCh() } diff --git a/example/gauge.png b/example/gauge.png index 17eb3ea..5c20e6e 100644 Binary files a/example/gauge.png and b/example/gauge.png differ diff --git a/example/grid.go b/example/grid.go index c010b4b..4912141 100644 --- a/example/grid.go +++ b/example/grid.go @@ -8,7 +8,6 @@ package main import ui "github.com/gizak/termui" import "math" - import "time" func main() { @@ -39,7 +38,7 @@ func main() { spark := ui.Sparkline{} spark.Height = 8 spdata := sinpsint - spark.Data = spdata + spark.Data = spdata[:100] spark.LineColor = ui.ColorCyan spark.TitleColor = ui.ColorWhite @@ -92,34 +91,44 @@ func main() { // calculate layout ui.Body.Align() - draw := func(t int) { - sp.Lines[0].Data = spdata[t:] - lc.Data = sinps[2*t:] - ui.Render(ui.Body) + done := make(chan bool) + redraw := make(chan bool) + + update := func() { + for i := 0; i < 103; i++ { + for _, g := range gs { + g.Percent = (g.Percent + 3) % 100 + } + + sp.Lines[0].Data = spdata[:100+i] + lc.Data = sinps[2*i:] + + time.Sleep(time.Second / 2) + redraw <- true + } + done <- true } evt := ui.EventCh() - i := 0 + ui.Render(ui.Body) + go update() + for { select { case e := <-evt: if e.Type == ui.EventKey && e.Ch == 'q' { return } - default: - for _, g := range gs { - g.Percent = (g.Percent + 3) % 100 + if e.Type == ui.EventResize { + ui.Body.Width = ui.TermWidth() + ui.Body.Align() + go func() { redraw <- true }() } - ui.Body.Width = ui.TermWidth() - ui.Body.Align() - - draw(i) - i++ - if i == 102 { - return - } - time.Sleep(time.Second / 2) + case <-done: + return + case <-redraw: + ui.Render(ui.Body) } } } diff --git a/example/mbarchart.go b/example/mbarchart.go new file mode 100644 index 0000000..a32a28e --- /dev/null +++ b/example/mbarchart.go @@ -0,0 +1,50 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +// +build ignore + +package main + +import "github.com/gizak/termui" + +func main() { + err := termui.Init() + if err != nil { + panic(err) + } + defer termui.Close() + + termui.UseTheme("helloworld") + + bc := termui.NewMBarChart() + math := []int{90, 85, 90, 80} + english := []int{70, 85, 75, 60} + science := []int{75, 60, 80, 85} + compsci := []int{100, 100, 100, 100} + bc.Data[0] = math + bc.Data[1] = english + bc.Data[2] = science + bc.Data[3] = compsci + studentsName := []string{"Ken", "Rob", "Dennis", "Linus"} + bc.Border.Label = "Student's Marks X-Axis=Name Y-Axis=Marks[Math,English,Science,ComputerScience] in %" + bc.Width = 100 + bc.Height = 50 + bc.Y = 10 + bc.BarWidth = 10 + bc.DataLabels = studentsName + bc.ShowScale = true //Show y_axis scale value (min and max) + bc.SetMax(400) + + bc.TextColor = termui.ColorGreen //this is color for label (x-axis) + bc.BarColor[3] = termui.ColorGreen //BarColor for computerscience + bc.BarColor[1] = termui.ColorYellow //Bar Color for english + bc.NumColor[3] = termui.ColorRed // Num color for computerscience + bc.NumColor[1] = termui.ColorRed // num color for english + + //Other colors are automatically populated, btw All the students seems do well in computerscience. :p + + termui.Render(bc) + + <-termui.EventCh() +} diff --git a/example/mbarchart.png b/example/mbarchart.png new file mode 100644 index 0000000..9a42526 Binary files /dev/null and b/example/mbarchart.png differ diff --git a/gauge.go b/gauge.go new file mode 100644 index 0000000..986f4f3 --- /dev/null +++ b/gauge.go @@ -0,0 +1,113 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +package termui + +import ( + "strconv" + "strings" +) + +// Gauge is a progress bar like widget. +// A simple example: +/* + g := termui.NewGauge() + g.Percent = 40 + g.Width = 50 + g.Height = 3 + g.Border.Label = "Slim Gauge" + g.BarColor = termui.ColorRed + g.PercentColor = termui.ColorBlue +*/ + +// Align is the position of the gauge's label. +type Align int + +// All supported positions. +const ( + AlignLeft Align = iota + AlignCenter + AlignRight +) + +type Gauge struct { + Block + Percent int + BarColor Attribute + PercentColor Attribute + Label string + LabelAlign Align +} + +// NewGauge return a new gauge with current theme. +func NewGauge() *Gauge { + g := &Gauge{ + Block: *NewBlock(), + PercentColor: theme.GaugePercent, + BarColor: theme.GaugeBar, + Label: "{{percent}}%", + LabelAlign: AlignCenter, + } + + g.Width = 12 + g.Height = 5 + return g +} + +// Buffer implements Bufferer interface. +func (g *Gauge) Buffer() []Point { + ps := g.Block.Buffer() + + // plot bar + w := g.Percent * g.innerWidth / 100 + for i := 0; i < g.innerHeight; i++ { + for j := 0; j < w; j++ { + p := Point{} + p.X = g.innerX + j + p.Y = g.innerY + i + p.Ch = ' ' + p.Bg = g.BarColor + if p.Bg == ColorDefault { + p.Bg |= AttrReverse + } + ps = append(ps, p) + } + } + + // plot percentage + s := strings.Replace(g.Label, "{{percent}}", strconv.Itoa(g.Percent), -1) + pry := g.innerY + g.innerHeight/2 + rs := str2runes(s) + var pos int + switch g.LabelAlign { + case AlignLeft: + pos = 0 + + case AlignCenter: + pos = (g.innerWidth - strWidth(s)) / 2 + + case AlignRight: + pos = g.innerWidth - strWidth(s) + } + + for i, v := range rs { + p := Point{} + p.X = 1 + pos + i + p.Y = pry + p.Ch = v + p.Fg = g.PercentColor + if w+g.innerX > pos+i { + p.Bg = g.BarColor + if p.Bg == ColorDefault { + p.Bg |= AttrReverse + } + + } else { + p.Bg = g.Block.BgColor + } + + ps = append(ps, p) + } + return g.Block.chopOverflow(ps) +} diff --git a/helper.go b/helper.go index b73010c..1c8f5ef 100644 --- a/helper.go +++ b/helper.go @@ -29,6 +29,7 @@ const ( ColorWhite ) +const NumberofColors = 8 //Have a constant that defines number of colors const ( AttrBold Attribute = 1 << (iota + 9) AttrUnderline diff --git a/mbar.go b/mbar.go new file mode 100644 index 0000000..9d18c2c --- /dev/null +++ b/mbar.go @@ -0,0 +1,233 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +package termui + +import ( + "fmt" +) + +// This is the implemetation of multi-colored or stacked bar graph. This is different from default barGraph which is implemented in bar.go +// Multi-Colored-BarChart creates multiple bars in a widget: +/* + bc := termui.NewMBarChart() + data := make([][]int, 2) + data[0] := []int{3, 2, 5, 7, 9, 4} + data[1] := []int{7, 8, 5, 3, 1, 6} + bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"} + bc.Border.Label = "Bar Chart" + bc.Data = data + bc.Width = 26 + bc.Height = 10 + bc.DataLabels = bclabels + bc.TextColor = termui.ColorGreen + bc.BarColor = termui.ColorRed + bc.NumColor = termui.ColorYellow +*/ +type MBarChart struct { + Block + BarColor [NumberofColors]Attribute + TextColor Attribute + NumColor [NumberofColors]Attribute + Data [NumberofColors][]int + DataLabels []string + BarWidth int + BarGap int + labels [][]rune + dataNum [NumberofColors][][]rune + numBar int + scale float64 + max int + minDataLen int + numStack int + ShowScale bool + maxScale []rune +} + +// NewBarChart returns a new *BarChart with current theme. +func NewMBarChart() *MBarChart { + bc := &MBarChart{Block: *NewBlock()} + bc.BarColor[0] = theme.MBarChartBar + bc.NumColor[0] = theme.MBarChartNum + bc.TextColor = theme.MBarChartText + bc.BarGap = 1 + bc.BarWidth = 3 + return bc +} + +func (bc *MBarChart) layout() { + bc.numBar = bc.innerWidth / (bc.BarGap + bc.BarWidth) + bc.labels = make([][]rune, bc.numBar) + DataLen := 0 + LabelLen := len(bc.DataLabels) + bc.minDataLen = 9999 //Set this to some very hight value so that we find the minimum one We want to know which array among data[][] has got the least length + + // We need to know how many stack/data array data[0] , data[1] are there + for i := 0; i < len(bc.Data); i++ { + if bc.Data[i] == nil { + break + } + DataLen++ + } + bc.numStack = DataLen + + //We need to know what is the mimimum size of data array data[0] could have 10 elements data[1] could have only 5, so we plot only 5 bar graphs + + for i := 0; i < DataLen; i++ { + if bc.minDataLen > len(bc.Data[i]) { + bc.minDataLen = len(bc.Data[i]) + } + } + + if LabelLen > bc.minDataLen { + LabelLen = bc.minDataLen + } + + for i := 0; i < LabelLen && i < bc.numBar; i++ { + bc.labels[i] = trimStr2Runes(bc.DataLabels[i], bc.BarWidth) + } + + for i := 0; i < bc.numStack; i++ { + bc.dataNum[i] = make([][]rune, len(bc.Data[i])) + //For each stack of bar calcualte the rune + for j := 0; j < LabelLen && i < bc.numBar; j++ { + n := bc.Data[i][j] + s := fmt.Sprint(n) + bc.dataNum[i][j] = trimStr2Runes(s, bc.BarWidth) + } + //If color is not defined by default then populate a color that is different from the prevous bar + if bc.BarColor[i] == ColorDefault && bc.NumColor[i] == ColorDefault { + if i == 0 { + bc.BarColor[i] = ColorBlack + } else { + bc.BarColor[i] = bc.BarColor[i-1] + 1 + if bc.BarColor[i] > NumberofColors { + bc.BarColor[i] = ColorBlack + } + } + bc.NumColor[i] = (NumberofColors + 1) - bc.BarColor[i] //Make NumColor opposite of barColor for visibility + } + } + + //If Max value is not set then we have to populate, this time the max value will be max(sum(d1[0],d2[0],d3[0]) .... sum(d1[n], d2[n], d3[n])) + + if bc.max == 0 { + bc.max = -1 + } + for i := 0; i < bc.minDataLen && i < LabelLen; i++ { + var dsum int + for j := 0; j < bc.numStack; j++ { + dsum += bc.Data[j][i] + } + if dsum > bc.max { + bc.max = dsum + } + } + + //Finally Calculate max sale + if bc.ShowScale { + s := fmt.Sprintf("%d", bc.max) + bc.maxScale = trimStr2Runes(s, len(s)) + bc.scale = float64(bc.max) / float64(bc.innerHeight-2) + } else { + bc.scale = float64(bc.max) / float64(bc.innerHeight-1) + } + +} + +func (bc *MBarChart) SetMax(max int) { + + if max > 0 { + bc.max = max + } +} + +// Buffer implements Bufferer interface. +func (bc *MBarChart) Buffer() []Point { + ps := bc.Block.Buffer() + bc.layout() + var oftX int + + for i := 0; i < bc.numBar && i < bc.minDataLen && i < len(bc.DataLabels); i++ { + ph := 0 //Previous Height to stack up + oftX = i * (bc.BarWidth + bc.BarGap) + for i1 := 0; i1 < bc.numStack; i1++ { + h := int(float64(bc.Data[i1][i]) / bc.scale) + // plot bars + for j := 0; j < bc.BarWidth; j++ { + for k := 0; k < h; k++ { + p := Point{} + p.Ch = ' ' + p.Bg = bc.BarColor[i1] + if bc.BarColor[i1] == ColorDefault { // when color is default, space char treated as transparent! + p.Bg |= AttrReverse + } + p.X = bc.innerX + i*(bc.BarWidth+bc.BarGap) + j + p.Y = bc.innerY + bc.innerHeight - 2 - k - ph + ps = append(ps, p) + } + } + ph += h + } + // plot text + for j, k := 0, 0; j < len(bc.labels[i]); j++ { + w := charWidth(bc.labels[i][j]) + p := Point{} + p.Ch = bc.labels[i][j] + p.Bg = bc.BgColor + p.Fg = bc.TextColor + p.Y = bc.innerY + bc.innerHeight - 1 + p.X = bc.innerX + oftX + ((bc.BarWidth - len(bc.labels[i])) / 2) + k + ps = append(ps, p) + k += w + } + // plot num + ph = 0 //re-initialize previous height + for i1 := 0; i1 < bc.numStack; i1++ { + h := int(float64(bc.Data[i1][i]) / bc.scale) + for j := 0; j < len(bc.dataNum[i1][i]) && h > 0; j++ { + p := Point{} + p.Ch = bc.dataNum[i1][i][j] + p.Fg = bc.NumColor[i1] + p.Bg = bc.BarColor[i1] + if bc.BarColor[i1] == ColorDefault { // the same as above + p.Bg |= AttrReverse + } + if h == 0 { + p.Bg = bc.BgColor + } + p.X = bc.innerX + oftX + (bc.BarWidth-len(bc.dataNum[i1][i]))/2 + j + p.Y = bc.innerY + bc.innerHeight - 2 - ph + ps = append(ps, p) + } + ph += h + } + } + + if bc.ShowScale { + //Currently bar graph only supprts data range from 0 to MAX + //Plot 0 + p := Point{} + p.Ch = '0' + p.Bg = bc.BgColor + p.Fg = bc.TextColor + p.Y = bc.innerY + bc.innerHeight - 2 + p.X = bc.X + ps = append(ps, p) + + //Plot the maximum sacle value + for i := 0; i < len(bc.maxScale); i++ { + p := Point{} + p.Ch = bc.maxScale[i] + p.Bg = bc.BgColor + p.Fg = bc.TextColor + p.Y = bc.innerY + p.X = bc.X + i + ps = append(ps, p) + } + + } + + return bc.Block.chopOverflow(ps) +} diff --git a/render.go b/render.go index c471238..ce3bdb3 100644 --- a/render.go +++ b/render.go @@ -35,12 +35,14 @@ func Close() { // TermWidth returns the current terminal's width. func TermWidth() int { + tm.Sync() w, _ := tm.Size() return w } // TermHeight returns the current terminal's height. func TermHeight() int { + tm.Sync() _, h := tm.Size() return h } diff --git a/theme.go b/theme.go index 092d06b..c8ad947 100644 --- a/theme.go +++ b/theme.go @@ -26,6 +26,9 @@ type ColorScheme struct { BarChartBar Attribute BarChartText Attribute BarChartNum Attribute + MBarChartBar Attribute + MBarChartText Attribute + MBarChartNum Attribute } // default color scheme depends on the user's terminal setting. @@ -52,6 +55,9 @@ var themeHelloWorld = ColorScheme{ BarChartBar: ColorRed, BarChartNum: ColorWhite, BarChartText: ColorCyan, + MBarChartBar: ColorRed, + MBarChartNum: ColorWhite, + MBarChartText: ColorCyan, } var theme = themeDefault // global dep diff --git a/widget/sparkline.go b/widget/sparkline.go index 809b9e6..cabfd32 100644 --- a/widget/sparkline.go +++ b/widget/sparkline.go @@ -108,7 +108,7 @@ func (sl *Sparklines) Buffer() []Point { data := l.Data if len(data) > sl.innerWidth { - data = data[:sl.innerWidth] + data = data[len(data)-sl.innerWidth:] } if l.Title != "" {