diff --git a/README.md b/README.md index 8144e49..233f94e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # termui -demo cast under osx 10.10; Terminal.app; Menlo Regular 12pt.) +[demo cast under osx 10.10; Terminal.app; Menlo Regular 12pt.)](./_examples/demo.go) termui is a cross-platform and fully-customizable terminal dashboard and widget library built on top of [termbox-go](https://github.com/nsf/termbox-go). It is inspired by [blessed-contrib](https://github.com/yaronn/blessed-contrib) and written purely in Go. @@ -15,58 +15,23 @@ go get -u github.com/gizak/termui@master **Note**: termui is currently undergoing API changes so make sure to check the changelog when upgrading. If you upgrade and notice something is missing or don't like a change, revert the upgrade and open an issue. -## Usage +## Widgets -### Hello World +- [BarChart](./_examples/barchart.go) +- [Canvas](./_examples/canvas.go) +- [Gauge](./_examples/gauge.go) +- [LineChart](./_examples/linechart.go) +- [List](./_examples/list.go) +- [Paragraph](./_examples/paragraph.go) +- [PieChart](./_examples/piechart.go) +- [Sparkline](./_examples/sparkline.go) +- [StackedBarChart](./_examples/stacked_barchart.go) +- [Table](./_examples/table.go) +- [Tabs](./_examples/tabs.go) -```go -package main +Run an example with `go run _examples/{example}.go` or run all of them consecutively with `make run-examples`. -import ( - "log" - - ui "github.com/gizak/termui" - "github.com/gizak/termui/widgets" -) - -func main() { - if err := ui.Init(); err != nil { - log.Fatalf("failed to initialize termui: %v", err) - } - defer ui.Close() - - p := widgets.NewParagraph() - p.Text = "Hello World!" - p.SetRect(0, 0, 25, 5) - - ui.Render(p) - - for e := range ui.PollEvents() { - if e.Type == ui.KeyboardEvent { - break - } - } -} -``` - -### Widgets - -Click image to see the corresponding demo codes. - -[barchart](https://github.com/gizak/termui/blob/master/_examples/barchart.go) -[gauge](https://github.com/gizak/termui/blob/master/_examples/gauge.go) -[linechart](https://github.com/gizak/termui/blob/master/_examples/linechart.go) -[list](https://github.com/gizak/termui/blob/master/_examples/list.go) -[paragraph](https://github.com/gizak/termui/blob/master/_examples/paragraph.go) -[sparkline](https://github.com/gizak/termui/blob/master/_examples/sparkline.go) -[stacked_barchart](https://github.com/gizak/termui/blob/master/_examples/stacked_barchart.go) -[table](https://github.com/gizak/termui/blob/master/_examples/table.go) - -### Examples - -Examples can be found in [\_examples](./_examples). Run an example with `go run _examples/{example}.go` or run all of them consecutively with `make run-examples`. - -### Documentation +## Documentation - [wiki](https://github.com/gizak/termui/wiki) diff --git a/_assets/barchart.png b/_assets/barchart.png deleted file mode 100644 index 4c5f1ca..0000000 Binary files a/_assets/barchart.png and /dev/null differ diff --git a/_assets/dashboard1.gif b/_assets/demo.gif similarity index 100% rename from _assets/dashboard1.gif rename to _assets/demo.gif diff --git a/_assets/gauge.png b/_assets/gauge.png deleted file mode 100644 index 4009243..0000000 Binary files a/_assets/gauge.png and /dev/null differ diff --git a/_assets/grid.gif b/_assets/grid.gif deleted file mode 100644 index 7490043..0000000 Binary files a/_assets/grid.gif and /dev/null differ diff --git a/_assets/linechart.png b/_assets/linechart.png deleted file mode 100644 index b0d0057..0000000 Binary files a/_assets/linechart.png and /dev/null differ diff --git a/_assets/list.png b/_assets/list.png deleted file mode 100644 index 7c5e27c..0000000 Binary files a/_assets/list.png and /dev/null differ diff --git a/_assets/paragraph.png b/_assets/paragraph.png deleted file mode 100644 index b71fa7c..0000000 Binary files a/_assets/paragraph.png and /dev/null differ diff --git a/_assets/sparkline.png b/_assets/sparkline.png deleted file mode 100644 index dd5641b..0000000 Binary files a/_assets/sparkline.png and /dev/null differ diff --git a/_assets/stacked_barchart.png b/_assets/stacked_barchart.png deleted file mode 100644 index e51337e..0000000 Binary files a/_assets/stacked_barchart.png and /dev/null differ diff --git a/_assets/table.png b/_assets/table.png deleted file mode 100644 index 5781b1d..0000000 Binary files a/_assets/table.png and /dev/null differ diff --git a/_examples/dashboard2.go b/_examples/dashboard2.go deleted file mode 100644 index 75c4ddf..0000000 --- a/_examples/dashboard2.go +++ /dev/null @@ -1,370 +0,0 @@ -// Copyright 2017 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 ( - "bufio" - "errors" - "fmt" - "image" - "io" - "log" - "os" - "regexp" - "runtime" - "sort" - "strconv" - "strings" - "time" - - ui "github.com/gizak/termui" - "github.com/gizak/termui/widgets" -) - -const statFilePath = "/proc/stat" -const meminfoFilePath = "/proc/meminfo" - -type CpuStat struct { - user float32 - nice float32 - system float32 - idle float32 -} - -type CpusStats struct { - stat map[string]CpuStat - proc map[string]CpuStat -} - -func NewCpusStats(s map[string]CpuStat) *CpusStats { - return &CpusStats{stat: s, proc: make(map[string]CpuStat)} -} - -func (cs *CpusStats) String() (ret string) { - for key, _ := range cs.proc { - ret += fmt.Sprintf("%s: %.2f %.2f %.2f %.2f\n", key, cs.proc[key].user, cs.proc[key].nice, cs.proc[key].system, cs.proc[key].idle) - } - return -} - -func subCpuStat(m CpuStat, s CpuStat) CpuStat { - return CpuStat{user: m.user - s.user, - nice: m.nice - s.nice, - system: m.system - s.system, - idle: m.idle - s.idle} -} - -func procCpuStat(c CpuStat) CpuStat { - sum := c.user + c.nice + c.system + c.idle - return CpuStat{user: c.user / sum * 100, - nice: c.nice / sum * 100, - system: c.system / sum * 100, - idle: c.idle / sum * 100} -} - -func (cs *CpusStats) tick(ns map[string]CpuStat) { - for key, _ := range cs.stat { - proc := subCpuStat(ns[key], cs.stat[key]) - cs.proc[key] = procCpuStat(proc) - cs.stat[key] = ns[key] - } -} - -type errIntParser struct { - err error -} - -func (eip *errIntParser) parse(s string) (ret int64) { - if eip.err != nil { - return 0 - } - ret, eip.err = strconv.ParseInt(s, 10, 0) - return -} - -type LineProcessor interface { - process(string) error - finalize() interface{} -} - -type CpuLineProcessor struct { - m map[string]CpuStat -} - -func (clp *CpuLineProcessor) process(line string) (err error) { - r := regexp.MustCompile("^cpu([0-9]*)") - - if r.MatchString(line) { - tab := strings.Fields(line) - if len(tab) < 5 { - err = errors.New("cpu info line has not enough fields") - return - } - parser := errIntParser{} - cs := CpuStat{user: float32(parser.parse(tab[1])), - nice: float32(parser.parse(tab[2])), - system: float32(parser.parse(tab[3])), - idle: float32(parser.parse(tab[4]))} - clp.m[tab[0]] = cs - err = parser.err - if err != nil { - return - } - } - return -} - -func (clp *CpuLineProcessor) finalize() interface{} { - return clp.m -} - -type MemStat struct { - total int64 - free int64 -} - -func (ms MemStat) String() (ret string) { - ret = fmt.Sprintf("TotalMem: %d, FreeMem: %d\n", ms.total, ms.free) - return -} - -func (ms *MemStat) process(line string) (err error) { - rtotal := regexp.MustCompile("^MemTotal:") - rfree := regexp.MustCompile("^MemFree:") - var aux int64 - if rtotal.MatchString(line) || rfree.MatchString(line) { - tab := strings.Fields(line) - if len(tab) < 3 { - err = errors.New("mem info line has not enough fields") - return - } - aux, err = strconv.ParseInt(tab[1], 10, 0) - } - if err != nil { - return - } - - if rtotal.MatchString(line) { - ms.total = aux - } - if rfree.MatchString(line) { - ms.free = aux - } - return -} - -func (ms *MemStat) finalize() interface{} { - return *ms -} - -func processFileLines(filePath string, lp LineProcessor) (ret interface{}, err error) { - var statFile *os.File - statFile, err = os.Open(filePath) - if err != nil { - fmt.Printf("open: %v\n", err) - } - defer statFile.Close() - - statFileReader := bufio.NewReader(statFile) - - for { - var line string - line, err = statFileReader.ReadString('\n') - if err == io.EOF { - err = nil - break - } - if err != nil { - fmt.Printf("open: %v\n", err) - break - } - line = strings.TrimSpace(line) - - err = lp.process(line) - } - - ret = lp.finalize() - return -} - -func getCpusStatsMap() (m map[string]CpuStat, err error) { - var aux interface{} - aux, err = processFileLines(statFilePath, &CpuLineProcessor{m: make(map[string]CpuStat)}) - return aux.(map[string]CpuStat), err -} - -func getMemStats() (ms MemStat, err error) { - var aux interface{} - aux, err = processFileLines(meminfoFilePath, &MemStat{}) - return aux.(MemStat), err -} - -type CpuTabElems struct { - GMap map[string]*widgets.Gauge - LChart *widgets.LineChart -} - -func NewCpuTabElems(width int) *CpuTabElems { - lc := widgets.NewLineChart() - lc.SetRect(0, 0, width, 12) - lc.LineType = widgets.DotLine - lc.Title = "CPU" - return &CpuTabElems{ - GMap: make(map[string]*widgets.Gauge), - LChart: lc, - } -} - -func (cte *CpuTabElems) AddGauge(key string, Y int, width int) *widgets.Gauge { - cte.GMap[key] = widgets.NewGauge() - cte.GMap[key].SetRect(0, Y, width, Y+3) - cte.GMap[key].Title = key - cte.GMap[key].Percent = 0 //int(val.user + val.nice + val.system) - return cte.GMap[key] -} - -func (cte *CpuTabElems) Update(cs CpusStats) { - for key, val := range cs.proc { - p := int(val.user + val.nice + val.system) - cte.GMap[key].Percent = p - if key == "cpu" { - cte.LChart.Data = append(cte.LChart.Data, []float64{}) - cte.LChart.Data[0] = append(cte.LChart.Data[0], 0) - copy(cte.LChart.Data[0][1:], cte.LChart.Data[0][0:]) - cte.LChart.Data[0][0] = float64(p) - } - } -} - -type MemTabElems struct { - Gauge *widgets.Gauge - SLines *widgets.SparklineGroup -} - -func NewMemTabElems(width int) *MemTabElems { - g := widgets.NewGauge() - g.SetRect(0, 5, width, 10) - - sline := widgets.NewSparkline() - sline.Title = "MEM" - - sls := widgets.NewSparklineGroup(sline) - sls.SetRect(0, 10, width, 25) - return &MemTabElems{Gauge: g, SLines: sls} -} - -func (mte *MemTabElems) Update(ms MemStat) { - used := (ms.total - ms.free) * 100 / ms.total - mte.Gauge.Percent = int(used) - mte.SLines.Sparklines[0].Data = append(mte.SLines.Sparklines[0].Data, 0) - copy(mte.SLines.Sparklines[0].Data[1:], mte.SLines.Sparklines[0].Data[0:]) - mte.SLines.Sparklines[0].Data[0] = float64(used) - if len(mte.SLines.Sparklines[0].Data) > mte.SLines.Dx()-2 { - mte.SLines.Sparklines[0].Data = mte.SLines.Sparklines[0].Data[0 : mte.SLines.Dx()-2] - } -} - -func main() { - if runtime.GOOS != "linux" { - log.Fatalf("Currently only works on Linux") - } - if err := ui.Init(); err != nil { - log.Fatalf("failed to initialize termui: %v", err) - } - defer ui.Close() - - termWidth := 70 - - header := widgets.NewParagraph() - header.Text = "Press q to quit, Press h or l to switch tabs" - header.SetRect(0, 0, 50, 1) - header.Border = false - header.TextStyle.Bg = ui.ColorBlue - - cs, errcs := getCpusStatsMap() - cpusStats := NewCpusStats(cs) - - if errcs != nil { - panic("error") - } - - cpuTabElems := NewCpuTabElems(termWidth) - - Y := 5 - cpuKeys := make([]string, 0, len(cs)) - for key := range cs { - cpuKeys = append(cpuKeys, key) - } - sort.Strings(cpuKeys) - for _, key := range cpuKeys { - cpuTabElems.AddGauge(key, Y, termWidth) - Y += 3 - } - cpuTabElems.LChart.Rectangle = cpuTabElems.LChart.GetRect().Add(image.Pt(0, Y)) - - memTabElems := NewMemTabElems(termWidth) - ms, errm := getMemStats() - if errm != nil { - panic(errm) - } - memTabElems.Update(ms) - - tabpane := widgets.NewTabPane("CPU", "MEM") - tabpane.SetRect(0, 1, 30, 30) - tabpane.Border = false - renderTab := func() { - switch tabpane.ActiveTabIndex { - case 0: - ui.Render(cpuTabElems.LChart) - for _, gauge := range cpuTabElems.GMap { - ui.Render(gauge) - } - case 1: - ui.Render(memTabElems.Gauge, memTabElems.SLines) - } - } - - ui.Render(header, tabpane) - renderTab() - - tickerCount := 1 - uiEvents := ui.PollEvents() - ticker := time.NewTicker(time.Second).C - for { - select { - case e := <-uiEvents: - switch e.ID { - case "q", "": - return - case "h": - tabpane.FocusLeft() - ui.Render(header, tabpane) - renderTab() - case "l": - tabpane.FocusRight() - ui.Render(header, tabpane) - renderTab() - } - case <-ticker: - cs, errcs := getCpusStatsMap() - if errcs != nil { - panic(errcs) - } - cpusStats.tick(cs) - cpuTabElems.Update(*cpusStats) - - ms, errm := getMemStats() - if errm != nil { - panic(errm) - } - memTabElems.Update(ms) - ui.Render(header, tabpane) - renderTab() - tickerCount++ - } - } -} diff --git a/_examples/dashboard1.go b/_examples/demo.go similarity index 100% rename from _examples/dashboard1.go rename to _examples/demo.go diff --git a/_examples/grid_nested.go b/_examples/grid_nested.go deleted file mode 100644 index a219a4e..0000000 --- a/_examples/grid_nested.go +++ /dev/null @@ -1,48 +0,0 @@ -// +build ignore - -package main - -import ( - "log" - - ui "github.com/gizak/termui" -) - -func main() { - if err := ui.Init(); err != nil { - log.Fatalf("failed to initialize termui: %v", err) - } - defer ui.Close() - - grid := ui.NewGrid() - termWidth, termHeight := ui.TerminalDimensions() - grid.SetRect(0, 0, termWidth, termHeight) - - grid2 := ui.NewGrid() - - grid2.Set( - ui.NewCol(.5, ui.NewBlock()), - ui.NewCol(.5, ui.NewRow(.5, ui.NewBlock())), - ) - - grid.Set( - ui.NewRow(.5, ui.NewBlock()), - ui.NewRow(.5, grid2), - ) - - ui.Render(grid) - - uiEvents := ui.PollEvents() - for { - e := <-uiEvents - switch e.ID { - case "q", "": - return - case "": - payload := e.Payload.(ui.Resize) - grid.SetRect(0, 0, payload.Width, payload.Height) - ui.Clear() - ui.Render(grid) - } - } -}