Refactor README and remove widget screenshots
65
README.md
|
@ -1,6 +1,6 @@
|
|||
# termui
|
||||
|
||||
<img src="./_assets/dashboard1.gif" alt="demo cast under osx 10.10; Terminal.app; Menlo Regular 12pt.)" width="100%">
|
||||
[<img src="./_assets/demo.gif" alt="demo cast under osx 10.10; Terminal.app; Menlo Regular 12pt.)" width="100%">](./_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.
|
||||
|
||||
[<img src="./_assets/barchart.png" alt="barchart" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_examples/barchart.go)
|
||||
[<img src="./_assets/gauge.png" alt="gauge" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_examples/gauge.go)
|
||||
[<img src="./_assets/linechart.png" alt="linechart" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_examples/linechart.go)
|
||||
[<img src="./_assets/list.png" alt="list" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_examples/list.go)
|
||||
[<img src="./_assets/paragraph.png" alt="paragraph" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_examples/paragraph.go)
|
||||
[<img src="./_assets/sparkline.png" alt="sparkline" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_examples/sparkline.go)
|
||||
[<img src="./_assets/stacked_barchart.png" alt="stacked_barchart" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_examples/stacked_barchart.go)
|
||||
[<img src="./_assets/table.png" alt="table" type="image/png" width="45%">](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)
|
||||
|
||||
|
|
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
Before Width: | Height: | Size: 52 KiB |
BIN
_assets/grid.gif
Before Width: | Height: | Size: 782 KiB |
Before Width: | Height: | Size: 31 KiB |
BIN
_assets/list.png
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 46 KiB |
|
@ -1,370 +0,0 @@
|
|||
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. 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", "<C-c>":
|
||||
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++
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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", "<C-c>":
|
||||
return
|
||||
case "<Resize>":
|
||||
payload := e.Payload.(ui.Resize)
|
||||
grid.SetRect(0, 0, payload.Width, payload.Height)
|
||||
ui.Clear()
|
||||
ui.Render(grid)
|
||||
}
|
||||
}
|
||||
}
|