The Great Rewrite
26
.gitignore
vendored
@ -1,28 +1,4 @@
|
|||||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
|
||||||
*.o
|
|
||||||
*.a
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Folders
|
|
||||||
_obj
|
|
||||||
_test
|
|
||||||
|
|
||||||
# Architecture specific extensions/prefixes
|
|
||||||
*.[568vq]
|
|
||||||
[568vq].out
|
|
||||||
|
|
||||||
*.cgo1.go
|
|
||||||
*.cgo2.c
|
|
||||||
_cgo_defun.c
|
|
||||||
_cgo_gotypes.go
|
|
||||||
_cgo_export.*
|
|
||||||
|
|
||||||
_testmain.go
|
|
||||||
|
|
||||||
*.exe
|
|
||||||
*.test
|
|
||||||
*.prof
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/vendor
|
|
||||||
.vscode/
|
.vscode/
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
.idea
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
language: go
|
|
||||||
|
|
||||||
go:
|
|
||||||
- tip
|
|
||||||
|
|
||||||
script: go test -v ./
|
|
29
CHANGELOG.md
@ -1,3 +1,32 @@
|
|||||||
|
Feel free to search/open an issue if something is missing or confusing from the changelog, since many things have been in flux.
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- moved widgets to `github.com/gizak/termui/widgets`
|
||||||
|
- rewrote widgets (check examples and code)
|
||||||
|
- rewrote grid
|
||||||
|
- grids are instantiated locally instead of through `termui.Body`
|
||||||
|
- grids can be nested
|
||||||
|
- changed grid layout mechanism
|
||||||
|
- columns and rows can be arbitrarily nested
|
||||||
|
- column and row size is now specified as a ratio of the available space
|
||||||
|
- `Cell`s now contain a `Style` which holds a `Fg`, `Bg`, and `Modifier`
|
||||||
|
- Change `Bufferer` interface to `Drawable`
|
||||||
|
- Add `GetRect` and `SetRect` methods to control widget sizing
|
||||||
|
- Change `Buffer` method to `Draw`
|
||||||
|
- `Draw` takes a `Buffer` and draws to it instead of returning a new `Buffer`
|
||||||
|
- Refactored `Theme`
|
||||||
|
- `Theme` is now a large struct which holds the default `Styles` of everything
|
||||||
|
- Combined `TermWidth` and `TermHeight` functions into `TerminalDimensions`
|
||||||
|
- Added `Canvas` which allows for drawing braille lines to a `Buffer`
|
||||||
|
- Refactored `Block`
|
||||||
|
- Refactored `Buffer` methods
|
||||||
|
- Set `termbox-go` backend to 256 colors by default
|
||||||
|
- Decremented color numbers by 1 to match xterm colors
|
||||||
|
- Changed text parsing
|
||||||
|
- style items changed from `fg-color` to `fg:color`
|
||||||
|
- added mod item like `mod:reverse`
|
||||||
|
|
||||||
## 18/11/29
|
## 18/11/29
|
||||||
|
|
||||||
- Move Tabpane from termui/extra to termui and rename it to TabPane
|
- Move Tabpane from termui/extra to termui and rename it to TabPane
|
||||||
|
5
Makefile
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.PHONY: run-examples
|
||||||
|
run-examples:
|
||||||
|
@for file in _examples/*.go; do \
|
||||||
|
go run $$file; \
|
||||||
|
done;
|
56
README.md
@ -1,12 +1,8 @@
|
|||||||
# termui
|
# termui
|
||||||
|
|
||||||
[](https://travis-ci.org/gizak/termui) [](https://godoc.org/github.com/gizak/termui)
|
<img src="./_assets/dashboard1.gif" alt="demo cast under osx 10.10; Terminal.app; Menlo Regular 12pt.)" width="100%">
|
||||||
|
|
||||||
<img src="./_examples/dashboard.gif" alt="demo cast under osx 10.10; Terminal.app; Menlo Regular 12pt.)" width="100%">
|
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.
|
||||||
|
|
||||||
`termui` is a cross-platform, easy-to-compile, and fully-customizable terminal dashboard 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.
|
|
||||||
|
|
||||||
**termui is currently undergoing some API changes so make sure to check the changelog when upgrading**
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -16,6 +12,9 @@ Installing from the master branch is recommended:
|
|||||||
go get -u github.com/gizak/termui@master
|
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
|
## Usage
|
||||||
|
|
||||||
### Hello World
|
### Hello World
|
||||||
@ -23,18 +22,23 @@ go get -u github.com/gizak/termui@master
|
|||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import ui "github.com/gizak/termui"
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
ui "github.com/gizak/termui"
|
||||||
|
"github.com/gizak/termui/widgets"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := ui.Init()
|
if err := ui.Init(); err != nil {
|
||||||
if err != nil {
|
log.Fatalf("failed to initialize termui: %v", err)
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
defer ui.Close()
|
defer ui.Close()
|
||||||
|
|
||||||
p := ui.NewParagraph("Hello World!")
|
p := widgets.NewParagraph()
|
||||||
p.Width = 25
|
p.Text = "Hello World!"
|
||||||
p.Height = 5
|
p.SetRect(0, 0, 25, 5)
|
||||||
|
|
||||||
ui.Render(p)
|
ui.Render(p)
|
||||||
|
|
||||||
for e := range ui.PollEvents() {
|
for e := range ui.PollEvents() {
|
||||||
@ -49,26 +53,26 @@ func main() {
|
|||||||
|
|
||||||
Click image to see the corresponding demo codes.
|
Click image to see the corresponding demo codes.
|
||||||
|
|
||||||
[<img src="./_examples/barchart.png" alt="barchart" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_examples/barchart.go)
|
[<img src="./_assets/barchart.png" alt="barchart" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_examples/barchart.go)
|
||||||
[<img src="./_examples/gauge.png" alt="gauge" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_examples/gauge.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="./_examples/linechart.png" alt="linechart" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_examples/linechart.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="./_examples/list.png" alt="list" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_examples/list.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="./_examples/paragraph.png" alt="paragraph" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_examples/paragraph.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="./_examples/sparklines.png" alt="sparklines" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_examples/sparklines.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="./_examples/stackedbarchart.png" alt="stackedbarchart" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_examples/stackedbarchart.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="./_examples/table.png" alt="table" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_examples/table.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
|
||||||
|
|
||||||
Examples can be found in [\_examples](./_examples). Run with `go run _examples/...` or run all of them consecutively with `./scripts/run_examples.py`.
|
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
|
||||||
|
|
||||||
- [godoc](https://godoc.org/github.com/gizak/termui) for code documentation
|
- [wiki](https://github.com/gizak/termui/wiki)
|
||||||
- [wiki](https://github.com/gizak/termui/wiki) for general information
|
|
||||||
|
|
||||||
## Uses
|
## Uses
|
||||||
|
|
||||||
|
- [cjbassi/gotop](https://github.com/cjbassi/gotop)
|
||||||
- [go-ethereum/monitorcmd](https://github.com/ethereum/go-ethereum/blob/96116758d22ddbff4dbef2050d6b63a7b74502d8/cmd/geth/monitorcmd.go)
|
- [go-ethereum/monitorcmd](https://github.com/ethereum/go-ethereum/blob/96116758d22ddbff4dbef2050d6b63a7b74502d8/cmd/geth/monitorcmd.go)
|
||||||
|
|
||||||
## Related Works
|
## Related Works
|
||||||
@ -79,4 +83,4 @@ Examples can be found in [\_examples](./_examples). Run with `go run _examples/.
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This library is under the [MIT License](http://opensource.org/licenses/MIT)
|
[MIT](http://opensource.org/licenses/MIT)
|
||||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 782 KiB After Width: | Height: | Size: 782 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
@ -6,24 +6,28 @@
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import ui "github.com/gizak/termui"
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
ui "github.com/gizak/termui"
|
||||||
|
"github.com/gizak/termui/widgets"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := ui.Init()
|
if err := ui.Init(); err != nil {
|
||||||
if err != nil {
|
log.Fatalf("failed to initialize termui: %v", err)
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
defer ui.Close()
|
defer ui.Close()
|
||||||
|
|
||||||
bc := ui.NewBarChart()
|
bc := widgets.NewBarChart()
|
||||||
bc.Data = []int{3, 2, 5, 3, 9, 5, 3, 2, 5, 8, 3, 2, 4, 5, 3, 2, 5, 7, 5, 3, 2, 6, 7, 4, 6, 3, 6, 7, 8, 3, 6, 4, 5, 3, 2, 4, 6, 4, 8, 5, 9, 4, 3, 6, 5, 3, 6}
|
bc.Data = []float64{3, 2, 5, 3, 9, 3}
|
||||||
bc.DataLabels = []string{"S0", "S1", "S2", "S3", "S4", "S5"}
|
bc.Labels = []string{"S0", "S1", "S2", "S3", "S4", "S5"}
|
||||||
bc.BorderLabel = "Bar Chart"
|
bc.Title = "Bar Chart"
|
||||||
bc.Width = 26
|
bc.SetRect(5, 5, 100, 25)
|
||||||
bc.Height = 10
|
bc.BarWidth = 5
|
||||||
bc.TextColor = ui.ColorGreen
|
bc.BarColors = []ui.Color{ui.ColorRed, ui.ColorGreen}
|
||||||
bc.BarColor = ui.ColorRed
|
bc.LabelStyles = []ui.Style{ui.NewStyle(ui.ColorBlue)}
|
||||||
bc.NumColor = ui.ColorYellow
|
bc.NumStyles = []ui.Style{ui.NewStyle(ui.ColorYellow)}
|
||||||
|
|
||||||
ui.Render(bc)
|
ui.Render(bc)
|
||||||
|
|
||||||
|
30
_examples/canvas.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// +build ignore
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"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()
|
||||||
|
|
||||||
|
c := ui.NewCanvas()
|
||||||
|
c.SetRect(0, 0, 50, 50)
|
||||||
|
c.Line(image.Pt(0, 0), image.Pt(80, 50), ui.ColorClear)
|
||||||
|
c.Line(image.Pt(0, 5), image.Pt(3, 10), ui.ColorClear)
|
||||||
|
|
||||||
|
ui.Render(c)
|
||||||
|
|
||||||
|
for e := range ui.PollEvents() {
|
||||||
|
if e.Type == ui.KeyboardEvent {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,159 +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 (
|
|
||||||
"math"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
ui "github.com/gizak/termui"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
err := ui.Init()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer ui.Close()
|
|
||||||
|
|
||||||
p := ui.NewParagraph("PRESS q TO QUIT DEMO")
|
|
||||||
p.Height = 3
|
|
||||||
p.Width = 50
|
|
||||||
p.TextFgColor = ui.ColorWhite
|
|
||||||
p.BorderLabel = "Text Box"
|
|
||||||
p.BorderFg = ui.ColorCyan
|
|
||||||
|
|
||||||
updateP := func(count int) {
|
|
||||||
if count%2 == 0 {
|
|
||||||
p.TextFgColor = ui.ColorRed
|
|
||||||
} else {
|
|
||||||
p.TextFgColor = ui.ColorWhite
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listData := []string{"[0] gizak/termui", "[1] editbox.go", "[2] interrupt.go", "[3] keyboard.go", "[4] output.go", "[5] random_out.go", "[6] dashboard.go", "[7] nsf/termbox-go"}
|
|
||||||
|
|
||||||
l := ui.NewList()
|
|
||||||
l.Items = listData
|
|
||||||
l.ItemFgColor = ui.ColorYellow
|
|
||||||
l.BorderLabel = "List"
|
|
||||||
l.Height = 7
|
|
||||||
l.Width = 25
|
|
||||||
l.Y = 4
|
|
||||||
|
|
||||||
g := ui.NewGauge()
|
|
||||||
g.Percent = 50
|
|
||||||
g.Width = 50
|
|
||||||
g.Height = 3
|
|
||||||
g.Y = 11
|
|
||||||
g.BorderLabel = "Gauge"
|
|
||||||
g.BarColor = ui.ColorRed
|
|
||||||
g.BorderFg = ui.ColorWhite
|
|
||||||
g.BorderLabelFg = ui.ColorCyan
|
|
||||||
|
|
||||||
sparklineData := []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}
|
|
||||||
|
|
||||||
sl := ui.Sparkline{}
|
|
||||||
sl.Height = 1
|
|
||||||
sl.Title = "srv 0:"
|
|
||||||
sl.Data = sparklineData
|
|
||||||
sl.LineColor = ui.ColorCyan
|
|
||||||
sl.TitleColor = ui.ColorWhite
|
|
||||||
|
|
||||||
sl2 := ui.Sparkline{}
|
|
||||||
sl2.Height = 1
|
|
||||||
sl2.Title = "srv 1:"
|
|
||||||
sl2.Data = sparklineData
|
|
||||||
sl2.TitleColor = ui.ColorWhite
|
|
||||||
sl2.LineColor = ui.ColorRed
|
|
||||||
|
|
||||||
sls := ui.NewSparklines(sl, sl2)
|
|
||||||
sls.Width = 25
|
|
||||||
sls.Height = 7
|
|
||||||
sls.BorderLabel = "Sparkline"
|
|
||||||
sls.Y = 4
|
|
||||||
sls.X = 25
|
|
||||||
|
|
||||||
sinData := (func() []float64 {
|
|
||||||
n := 220
|
|
||||||
ps := make([]float64, n)
|
|
||||||
for i := range ps {
|
|
||||||
ps[i] = 1 + math.Sin(float64(i)/5)
|
|
||||||
}
|
|
||||||
return ps
|
|
||||||
})()
|
|
||||||
|
|
||||||
lc := ui.NewLineChart()
|
|
||||||
lc.BorderLabel = "dot-mode Line Chart"
|
|
||||||
lc.Data["default"] = sinData
|
|
||||||
lc.Width = 50
|
|
||||||
lc.Height = 11
|
|
||||||
lc.X = 0
|
|
||||||
lc.Y = 14
|
|
||||||
lc.AxesColor = ui.ColorWhite
|
|
||||||
lc.LineColor["default"] = ui.ColorRed | ui.AttrBold
|
|
||||||
lc.Mode = "dot"
|
|
||||||
|
|
||||||
barchartData := []int{3, 2, 5, 3, 9, 5, 3, 2, 5, 8, 3, 2, 4, 5, 3, 2, 5, 7, 5, 3, 2, 6, 7, 4, 6, 3, 6, 7, 8, 3, 6, 4, 5, 3, 2, 4, 6, 4, 8, 5, 9, 4, 3, 6, 5, 3, 6}
|
|
||||||
|
|
||||||
bc := ui.NewBarChart()
|
|
||||||
bc.BorderLabel = "Bar Chart"
|
|
||||||
bc.Width = 26
|
|
||||||
bc.Height = 10
|
|
||||||
bc.X = 51
|
|
||||||
bc.Y = 0
|
|
||||||
bc.DataLabels = []string{"S0", "S1", "S2", "S3", "S4", "S5"}
|
|
||||||
bc.BarColor = ui.ColorGreen
|
|
||||||
bc.NumColor = ui.ColorBlack
|
|
||||||
|
|
||||||
lc2 := ui.NewLineChart()
|
|
||||||
lc2.BorderLabel = "braille-mode Line Chart"
|
|
||||||
lc2.Data["default"] = sinData
|
|
||||||
lc2.Width = 26
|
|
||||||
lc2.Height = 11
|
|
||||||
lc2.X = 51
|
|
||||||
lc2.Y = 14
|
|
||||||
lc2.AxesColor = ui.ColorWhite
|
|
||||||
lc2.LineColor["default"] = ui.ColorYellow | ui.AttrBold
|
|
||||||
|
|
||||||
p2 := ui.NewParagraph("Hey!\nI am a borderless block!")
|
|
||||||
p2.Border = false
|
|
||||||
p2.Width = 26
|
|
||||||
p2.Height = 2
|
|
||||||
p2.TextFgColor = ui.ColorMagenta
|
|
||||||
p2.X = 52
|
|
||||||
p2.Y = 11
|
|
||||||
|
|
||||||
draw := func(count int) {
|
|
||||||
g.Percent = count % 101
|
|
||||||
l.Items = listData[count%9:]
|
|
||||||
sls.Lines[0].Data = sparklineData[:30+count%50]
|
|
||||||
sls.Lines[1].Data = sparklineData[:35+count%50]
|
|
||||||
lc.Data["default"] = sinData[count/2%220:]
|
|
||||||
lc2.Data["default"] = sinData[2*count%220:]
|
|
||||||
bc.Data = barchartData[count/2%10:]
|
|
||||||
|
|
||||||
ui.Render(p, l, g, sls, lc, bc, lc2, p2)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 <-ticker:
|
|
||||||
updateP(tickerCount)
|
|
||||||
draw(tickerCount)
|
|
||||||
tickerCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
153
_examples/dashboard1.go
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
// 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 (
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
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.Title = "Text Box"
|
||||||
|
p.Text = "PRESS q TO QUIT DEMO"
|
||||||
|
p.SetRect(0, 0, 50, 5)
|
||||||
|
p.TextStyle.Fg = ui.ColorWhite
|
||||||
|
p.BorderStyle.Fg = ui.ColorCyan
|
||||||
|
|
||||||
|
updateParagraph := func(count int) {
|
||||||
|
if count%2 == 0 {
|
||||||
|
p.TextStyle.Fg = ui.ColorRed
|
||||||
|
} else {
|
||||||
|
p.TextStyle.Fg = ui.ColorWhite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listData := []string{
|
||||||
|
"[0] gizak/termui",
|
||||||
|
"[1] editbox.go",
|
||||||
|
"[2] interrupt.go",
|
||||||
|
"[3] keyboard.go",
|
||||||
|
"[4] output.go",
|
||||||
|
"[5] random_out.go",
|
||||||
|
"[6] dashboard.go",
|
||||||
|
"[7] nsf/termbox-go",
|
||||||
|
}
|
||||||
|
|
||||||
|
l := widgets.NewList()
|
||||||
|
l.Title = "List"
|
||||||
|
l.Rows = listData
|
||||||
|
l.SetRect(0, 5, 25, 12)
|
||||||
|
l.TextStyle.Fg = ui.ColorYellow
|
||||||
|
|
||||||
|
g := widgets.NewGauge()
|
||||||
|
g.Title = "Gauge"
|
||||||
|
g.Percent = 50
|
||||||
|
g.SetRect(0, 12, 50, 15)
|
||||||
|
g.BarColor = ui.ColorRed
|
||||||
|
g.BorderStyle.Fg = ui.ColorWhite
|
||||||
|
g.TitleStyle.Fg = ui.ColorCyan
|
||||||
|
|
||||||
|
sparklineData := []float64{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}
|
||||||
|
|
||||||
|
sl := widgets.NewSparkline()
|
||||||
|
sl.Title = "srv 0:"
|
||||||
|
sl.Data = sparklineData
|
||||||
|
sl.LineColor = ui.ColorCyan
|
||||||
|
sl.TitleStyle.Fg = ui.ColorWhite
|
||||||
|
|
||||||
|
sl2 := widgets.NewSparkline()
|
||||||
|
sl2.Title = "srv 1:"
|
||||||
|
sl2.Data = sparklineData
|
||||||
|
sl2.TitleStyle.Fg = ui.ColorWhite
|
||||||
|
sl2.LineColor = ui.ColorRed
|
||||||
|
|
||||||
|
slg := widgets.NewSparklineGroup(sl, sl2)
|
||||||
|
slg.Title = "Sparkline"
|
||||||
|
slg.SetRect(25, 5, 50, 12)
|
||||||
|
|
||||||
|
sinData := (func() []float64 {
|
||||||
|
n := 220
|
||||||
|
ps := make([]float64, n)
|
||||||
|
for i := range ps {
|
||||||
|
ps[i] = 1 + math.Sin(float64(i)/5)
|
||||||
|
}
|
||||||
|
return ps
|
||||||
|
})()
|
||||||
|
|
||||||
|
lc := widgets.NewLineChart()
|
||||||
|
lc.Title = "dot-mode Line Chart"
|
||||||
|
lc.Data = make([][]float64, 1)
|
||||||
|
lc.Data[0] = sinData
|
||||||
|
lc.SetRect(0, 15, 50, 25)
|
||||||
|
lc.AxesColor = ui.ColorWhite
|
||||||
|
lc.LineColors[0] = ui.ColorRed
|
||||||
|
lc.LineType = widgets.DotLine
|
||||||
|
|
||||||
|
barchartData := []float64{3, 2, 5, 3, 9, 5, 3, 2, 5, 8, 3, 2, 4, 5, 3, 2, 5, 7, 5, 3, 2, 6, 7, 4, 6, 3, 6, 7, 8, 3, 6, 4, 5, 3, 2, 4, 6, 4, 8, 5, 9, 4, 3, 6, 5, 3, 6}
|
||||||
|
|
||||||
|
bc := widgets.NewBarChart()
|
||||||
|
bc.Title = "Bar Chart"
|
||||||
|
bc.SetRect(50, 0, 75, 10)
|
||||||
|
bc.Labels = []string{"S0", "S1", "S2", "S3", "S4", "S5"}
|
||||||
|
bc.BarColors[0] = ui.ColorGreen
|
||||||
|
bc.NumStyles[0] = ui.NewStyle(ui.ColorBlack)
|
||||||
|
|
||||||
|
lc2 := widgets.NewLineChart()
|
||||||
|
lc2.Title = "braille-mode Line Chart"
|
||||||
|
lc2.Data = make([][]float64, 1)
|
||||||
|
lc2.Data[0] = sinData
|
||||||
|
lc2.SetRect(50, 15, 75, 25)
|
||||||
|
lc2.AxesColor = ui.ColorWhite
|
||||||
|
lc2.LineColors[0] = ui.ColorYellow
|
||||||
|
|
||||||
|
p2 := widgets.NewParagraph()
|
||||||
|
p2.Text = "Hey!\nI am a borderless block!"
|
||||||
|
p2.Border = false
|
||||||
|
p2.SetRect(50, 10, 75, 10)
|
||||||
|
p2.TextStyle.Fg = ui.ColorMagenta
|
||||||
|
|
||||||
|
draw := func(count int) {
|
||||||
|
g.Percent = count % 101
|
||||||
|
l.Rows = listData[count%9:]
|
||||||
|
slg.Sparklines[0].Data = sparklineData[:30+count%50]
|
||||||
|
slg.Sparklines[1].Data = sparklineData[:35+count%50]
|
||||||
|
lc.Data[0] = sinData[count/2%220:]
|
||||||
|
lc2.Data[0] = sinData[2*count%220:]
|
||||||
|
bc.Data = barchartData[count/2%10:]
|
||||||
|
|
||||||
|
ui.Render(p, l, g, slg, lc, bc, lc2, p2)
|
||||||
|
}
|
||||||
|
|
||||||
|
tickerCount := 1
|
||||||
|
draw(tickerCount)
|
||||||
|
tickerCount++
|
||||||
|
uiEvents := ui.PollEvents()
|
||||||
|
ticker := time.NewTicker(time.Second).C
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case e := <-uiEvents:
|
||||||
|
switch e.ID {
|
||||||
|
case "q", "<C-c>":
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-ticker:
|
||||||
|
updateParagraph(tickerCount)
|
||||||
|
draw(tickerCount)
|
||||||
|
tickerCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,9 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
@ -20,6 +22,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
|
"github.com/gizak/termui/widgets"
|
||||||
)
|
)
|
||||||
|
|
||||||
const statFilePath = "/proc/stat"
|
const statFilePath = "/proc/stat"
|
||||||
@ -201,27 +204,25 @@ func getMemStats() (ms MemStat, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CpuTabElems struct {
|
type CpuTabElems struct {
|
||||||
GMap map[string]*ui.Gauge
|
GMap map[string]*widgets.Gauge
|
||||||
LChart *ui.LineChart
|
LChart *widgets.LineChart
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCpuTabElems(width int) *CpuTabElems {
|
func NewCpuTabElems(width int) *CpuTabElems {
|
||||||
lc := ui.NewLineChart()
|
lc := widgets.NewLineChart()
|
||||||
lc.Width = width
|
lc.SetRect(0, 0, width, 12)
|
||||||
lc.Height = 12
|
lc.LineType = widgets.DotLine
|
||||||
lc.X = 0
|
lc.Title = "CPU"
|
||||||
lc.Mode = "dot"
|
return &CpuTabElems{
|
||||||
lc.BorderLabel = "CPU"
|
GMap: make(map[string]*widgets.Gauge),
|
||||||
return &CpuTabElems{GMap: make(map[string]*ui.Gauge),
|
LChart: lc,
|
||||||
LChart: lc}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cte *CpuTabElems) AddGauge(key string, Y int, width int) *ui.Gauge {
|
func (cte *CpuTabElems) AddGauge(key string, Y int, width int) *widgets.Gauge {
|
||||||
cte.GMap[key] = ui.NewGauge()
|
cte.GMap[key] = widgets.NewGauge()
|
||||||
cte.GMap[key].Width = width
|
cte.GMap[key].SetRect(0, Y, width, Y+3)
|
||||||
cte.GMap[key].Height = 3
|
cte.GMap[key].Title = key
|
||||||
cte.GMap[key].Y = Y
|
|
||||||
cte.GMap[key].BorderLabel = key
|
|
||||||
cte.GMap[key].Percent = 0 //int(val.user + val.nice + val.system)
|
cte.GMap[key].Percent = 0 //int(val.user + val.nice + val.system)
|
||||||
return cte.GMap[key]
|
return cte.GMap[key]
|
||||||
}
|
}
|
||||||
@ -231,71 +232,58 @@ func (cte *CpuTabElems) Update(cs CpusStats) {
|
|||||||
p := int(val.user + val.nice + val.system)
|
p := int(val.user + val.nice + val.system)
|
||||||
cte.GMap[key].Percent = p
|
cte.GMap[key].Percent = p
|
||||||
if key == "cpu" {
|
if key == "cpu" {
|
||||||
cte.LChart.Data["default"] = append(cte.LChart.Data["default"], 0)
|
cte.LChart.Data = append(cte.LChart.Data, []float64{})
|
||||||
copy(cte.LChart.Data["default"][1:], cte.LChart.Data["default"][0:])
|
cte.LChart.Data[0] = append(cte.LChart.Data[0], 0)
|
||||||
cte.LChart.Data["default"][0] = float64(p)
|
copy(cte.LChart.Data[0][1:], cte.LChart.Data[0][0:])
|
||||||
|
cte.LChart.Data[0][0] = float64(p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type MemTabElems struct {
|
type MemTabElems struct {
|
||||||
Gauge *ui.Gauge
|
Gauge *widgets.Gauge
|
||||||
SLines *ui.Sparklines
|
SLines *widgets.SparklineGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMemTabElems(width int) *MemTabElems {
|
func NewMemTabElems(width int) *MemTabElems {
|
||||||
g := ui.NewGauge()
|
g := widgets.NewGauge()
|
||||||
g.Width = width
|
g.SetRect(0, 5, width, 10)
|
||||||
g.Height = 3
|
|
||||||
g.Y = 0
|
|
||||||
|
|
||||||
sline := ui.NewSparkline()
|
sline := widgets.NewSparkline()
|
||||||
sline.Title = "MEM"
|
sline.Title = "MEM"
|
||||||
sline.Height = 8
|
|
||||||
|
|
||||||
sls := ui.NewSparklines(sline)
|
sls := widgets.NewSparklineGroup(sline)
|
||||||
sls.Width = width
|
sls.SetRect(0, 10, width, 25)
|
||||||
sls.Height = 12
|
|
||||||
sls.Y = 3
|
|
||||||
return &MemTabElems{Gauge: g, SLines: sls}
|
return &MemTabElems{Gauge: g, SLines: sls}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mte *MemTabElems) Update(ms MemStat) {
|
func (mte *MemTabElems) Update(ms MemStat) {
|
||||||
used := int((ms.total - ms.free) * 100 / ms.total)
|
used := (ms.total - ms.free) * 100 / ms.total
|
||||||
mte.Gauge.Percent = used
|
mte.Gauge.Percent = int(used)
|
||||||
mte.SLines.Lines[0].Data = append(mte.SLines.Lines[0].Data, 0)
|
mte.SLines.Sparklines[0].Data = append(mte.SLines.Sparklines[0].Data, 0)
|
||||||
copy(mte.SLines.Lines[0].Data[1:], mte.SLines.Lines[0].Data[0:])
|
copy(mte.SLines.Sparklines[0].Data[1:], mte.SLines.Sparklines[0].Data[0:])
|
||||||
mte.SLines.Lines[0].Data[0] = used
|
mte.SLines.Sparklines[0].Data[0] = float64(used)
|
||||||
if len(mte.SLines.Lines[0].Data) > mte.SLines.Width-2 {
|
if len(mte.SLines.Sparklines[0].Data) > mte.SLines.Dx()-2 {
|
||||||
mte.SLines.Lines[0].Data = mte.SLines.Lines[0].Data[0 : mte.SLines.Width-2]
|
mte.SLines.Sparklines[0].Data = mte.SLines.Sparklines[0].Data[0 : mte.SLines.Dx()-2]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if runtime.GOOS != "linux" {
|
if runtime.GOOS != "linux" {
|
||||||
panic("Currently works only on Linux")
|
log.Fatalf("Currently only works on Linux")
|
||||||
}
|
}
|
||||||
err := ui.Init()
|
if err := ui.Init(); err != nil {
|
||||||
if err != nil {
|
log.Fatalf("failed to initialize termui: %v", err)
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
defer ui.Close()
|
defer ui.Close()
|
||||||
|
|
||||||
termWidth := 70
|
termWidth := 70
|
||||||
|
|
||||||
header := ui.NewParagraph("Press q to quit, Press h or l to switch tabs")
|
header := widgets.NewParagraph()
|
||||||
header.Height = 1
|
header.Text = "Press q to quit, Press h or l to switch tabs"
|
||||||
header.Width = 50
|
header.SetRect(0, 0, 50, 1)
|
||||||
header.Border = false
|
header.Border = false
|
||||||
header.TextBgColor = ui.ColorBlue
|
header.TextStyle.Bg = ui.ColorBlue
|
||||||
|
|
||||||
tabCpu := ui.NewTab("CPU")
|
|
||||||
tabMem := ui.NewTab("MEM")
|
|
||||||
|
|
||||||
tabpane := ui.NewTabPane()
|
|
||||||
tabpane.Y = 1
|
|
||||||
tabpane.Width = 30
|
|
||||||
tabpane.Border = false
|
|
||||||
|
|
||||||
cs, errcs := getCpusStatsMap()
|
cs, errcs := getCpusStatsMap()
|
||||||
cpusStats := NewCpusStats(cs)
|
cpusStats := NewCpusStats(cs)
|
||||||
@ -306,19 +294,17 @@ func main() {
|
|||||||
|
|
||||||
cpuTabElems := NewCpuTabElems(termWidth)
|
cpuTabElems := NewCpuTabElems(termWidth)
|
||||||
|
|
||||||
Y := 0
|
Y := 5
|
||||||
cpuKeys := make([]string, 0, len(cs))
|
cpuKeys := make([]string, 0, len(cs))
|
||||||
for key := range cs {
|
for key := range cs {
|
||||||
cpuKeys = append(cpuKeys, key)
|
cpuKeys = append(cpuKeys, key)
|
||||||
}
|
}
|
||||||
sort.Strings(cpuKeys)
|
sort.Strings(cpuKeys)
|
||||||
for _, key := range cpuKeys {
|
for _, key := range cpuKeys {
|
||||||
g := cpuTabElems.AddGauge(key, Y, termWidth)
|
cpuTabElems.AddGauge(key, Y, termWidth)
|
||||||
Y += 3
|
Y += 3
|
||||||
tabCpu.AddBlocks(g)
|
|
||||||
}
|
}
|
||||||
cpuTabElems.LChart.Y = Y
|
cpuTabElems.LChart.Rectangle = cpuTabElems.LChart.GetRect().Add(image.Pt(0, Y))
|
||||||
tabCpu.AddBlocks(cpuTabElems.LChart)
|
|
||||||
|
|
||||||
memTabElems := NewMemTabElems(termWidth)
|
memTabElems := NewMemTabElems(termWidth)
|
||||||
ms, errm := getMemStats()
|
ms, errm := getMemStats()
|
||||||
@ -326,12 +312,24 @@ func main() {
|
|||||||
panic(errm)
|
panic(errm)
|
||||||
}
|
}
|
||||||
memTabElems.Update(ms)
|
memTabElems.Update(ms)
|
||||||
tabMem.AddBlocks(memTabElems.Gauge)
|
|
||||||
tabMem.AddBlocks(memTabElems.SLines)
|
|
||||||
|
|
||||||
tabpane.SetTabs(*tabCpu, *tabMem)
|
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)
|
ui.Render(header, tabpane)
|
||||||
|
renderTab()
|
||||||
|
|
||||||
tickerCount := 1
|
tickerCount := 1
|
||||||
uiEvents := ui.PollEvents()
|
uiEvents := ui.PollEvents()
|
||||||
@ -343,11 +341,13 @@ func main() {
|
|||||||
case "q", "<C-c>":
|
case "q", "<C-c>":
|
||||||
return
|
return
|
||||||
case "h":
|
case "h":
|
||||||
tabpane.SetActiveLeft()
|
tabpane.FocusLeft()
|
||||||
ui.Render(header, tabpane)
|
ui.Render(header, tabpane)
|
||||||
|
renderTab()
|
||||||
case "l":
|
case "l":
|
||||||
tabpane.SetActiveRight()
|
tabpane.FocusRight()
|
||||||
ui.Render(header, tabpane)
|
ui.Render(header, tabpane)
|
||||||
|
renderTab()
|
||||||
}
|
}
|
||||||
case <-ticker:
|
case <-ticker:
|
||||||
cs, errcs := getCpusStatsMap()
|
cs, errcs := getCpusStatsMap()
|
||||||
@ -363,6 +363,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
memTabElems.Update(ms)
|
memTabElems.Update(ms)
|
||||||
ui.Render(header, tabpane)
|
ui.Render(header, tabpane)
|
||||||
|
renderTab()
|
||||||
tickerCount++
|
tickerCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,71 +6,58 @@
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import ui "github.com/gizak/termui"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
ui "github.com/gizak/termui"
|
||||||
|
"github.com/gizak/termui/widgets"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := ui.Init()
|
if err := ui.Init(); err != nil {
|
||||||
if err != nil {
|
log.Fatalf("failed to initialize termui: %v", err)
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
defer ui.Close()
|
defer ui.Close()
|
||||||
|
|
||||||
g0 := ui.NewGauge()
|
g0 := widgets.NewGauge()
|
||||||
g0.Percent = 40
|
g0.Title = "Slim Gauge"
|
||||||
g0.Width = 50
|
g0.SetRect(20, 20, 30, 30)
|
||||||
g0.Height = 3
|
g0.Percent = 75
|
||||||
g0.BorderLabel = "Slim Gauge"
|
|
||||||
g0.BarColor = ui.ColorRed
|
g0.BarColor = ui.ColorRed
|
||||||
g0.BorderFg = ui.ColorWhite
|
g0.BorderStyle.Fg = ui.ColorWhite
|
||||||
g0.BorderLabelFg = ui.ColorCyan
|
g0.TitleStyle.Fg = ui.ColorCyan
|
||||||
|
|
||||||
gg := ui.NewBlock()
|
g2 := widgets.NewGauge()
|
||||||
gg.Width = 50
|
g2.Title = "Slim Gauge"
|
||||||
gg.Height = 5
|
g2.SetRect(0, 3, 50, 6)
|
||||||
gg.Y = 12
|
|
||||||
gg.BorderLabel = "TEST"
|
|
||||||
gg.Align()
|
|
||||||
|
|
||||||
g2 := ui.NewGauge()
|
|
||||||
g2.Percent = 60
|
g2.Percent = 60
|
||||||
g2.Width = 50
|
|
||||||
g2.Height = 3
|
|
||||||
g2.PercentColor = ui.ColorBlue
|
|
||||||
g2.Y = 3
|
|
||||||
g2.BorderLabel = "Slim Gauge"
|
|
||||||
g2.BarColor = ui.ColorYellow
|
g2.BarColor = ui.ColorYellow
|
||||||
g2.BorderFg = ui.ColorWhite
|
g2.LabelStyle = ui.NewStyle(ui.ColorBlue)
|
||||||
|
g2.BorderStyle.Fg = ui.ColorWhite
|
||||||
|
|
||||||
g1 := ui.NewGauge()
|
g1 := widgets.NewGauge()
|
||||||
|
g1.Title = "Big Gauge"
|
||||||
|
g1.SetRect(0, 6, 50, 11)
|
||||||
g1.Percent = 30
|
g1.Percent = 30
|
||||||
g1.Width = 50
|
|
||||||
g1.Height = 5
|
|
||||||
g1.Y = 6
|
|
||||||
g1.BorderLabel = "Big Gauge"
|
|
||||||
g1.PercentColor = ui.ColorYellow
|
|
||||||
g1.BarColor = ui.ColorGreen
|
g1.BarColor = ui.ColorGreen
|
||||||
g1.BorderFg = ui.ColorWhite
|
g1.LabelStyle = ui.NewStyle(ui.ColorYellow)
|
||||||
g1.BorderLabelFg = ui.ColorMagenta
|
g1.TitleStyle.Fg = ui.ColorMagenta
|
||||||
|
g1.BorderStyle.Fg = ui.ColorWhite
|
||||||
|
|
||||||
g3 := ui.NewGauge()
|
g3 := widgets.NewGauge()
|
||||||
|
g3.Title = "Gauge with custom label"
|
||||||
|
g3.SetRect(0, 11, 50, 14)
|
||||||
g3.Percent = 50
|
g3.Percent = 50
|
||||||
g3.Width = 50
|
g3.Label = fmt.Sprintf("%v%% (100MBs free)", g3.Percent)
|
||||||
g3.Height = 3
|
|
||||||
g3.Y = 11
|
|
||||||
g3.BorderLabel = "Gauge with custom label"
|
|
||||||
g3.Label = "{{percent}}% (100MBs free)"
|
|
||||||
g3.LabelAlign = ui.AlignRight
|
|
||||||
|
|
||||||
g4 := ui.NewGauge()
|
g4 := widgets.NewGauge()
|
||||||
|
g4.Title = "Gauge"
|
||||||
|
g4.SetRect(0, 14, 50, 17)
|
||||||
g4.Percent = 50
|
g4.Percent = 50
|
||||||
g4.Width = 50
|
|
||||||
g4.Height = 3
|
|
||||||
g4.Y = 14
|
|
||||||
g4.BorderLabel = "Gauge"
|
|
||||||
g4.Label = "Gauge with custom highlighted label"
|
g4.Label = "Gauge with custom highlighted label"
|
||||||
g4.PercentColor = ui.ColorYellow
|
|
||||||
g4.BarColor = ui.ColorGreen
|
g4.BarColor = ui.ColorGreen
|
||||||
g4.PercentColorHighlighted = ui.ColorBlack
|
g4.LabelStyle = ui.NewStyle(ui.ColorYellow)
|
||||||
|
|
||||||
ui.Render(g0, g1, g2, g3, g4)
|
ui.Render(g0, g1, g2, g3, g4)
|
||||||
|
|
||||||
|
@ -7,93 +7,88 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
|
"github.com/gizak/termui/widgets"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := ui.Init()
|
if err := ui.Init(); err != nil {
|
||||||
if err != nil {
|
log.Fatalf("failed to initialize termui: %v", err)
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
defer ui.Close()
|
defer ui.Close()
|
||||||
|
|
||||||
sinps := (func() []float64 {
|
sinFloat64 := (func() []float64 {
|
||||||
n := 400
|
n := 400
|
||||||
ps := make([]float64, n)
|
data := make([]float64, n)
|
||||||
for i := range ps {
|
for i := range data {
|
||||||
ps[i] = 1 + math.Sin(float64(i)/5)
|
data[i] = 1 + math.Sin(float64(i)/5)
|
||||||
}
|
}
|
||||||
return ps
|
return data
|
||||||
})()
|
|
||||||
sinpsint := (func() []int {
|
|
||||||
ps := make([]int, len(sinps))
|
|
||||||
for i, v := range sinps {
|
|
||||||
ps[i] = int(100*v + 10)
|
|
||||||
}
|
|
||||||
return ps
|
|
||||||
})()
|
})()
|
||||||
|
|
||||||
spark := ui.Sparkline{}
|
sl := widgets.NewSparkline()
|
||||||
spark.Height = 8
|
sl.Data = sinFloat64[:100]
|
||||||
spdata := sinpsint
|
sl.LineColor = ui.ColorCyan
|
||||||
spark.Data = spdata[:100]
|
sl.TitleStyle.Fg = ui.ColorWhite
|
||||||
spark.LineColor = ui.ColorCyan
|
|
||||||
spark.TitleColor = ui.ColorWhite
|
|
||||||
|
|
||||||
sp := ui.NewSparklines(spark)
|
slg := widgets.NewSparklineGroup(sl)
|
||||||
sp.Height = 11
|
slg.Title = "Sparkline"
|
||||||
sp.BorderLabel = "Sparkline"
|
|
||||||
|
|
||||||
lc := ui.NewLineChart()
|
lc := widgets.NewLineChart()
|
||||||
lc.BorderLabel = "braille-mode Line Chart"
|
lc.Title = "braille-mode Line Chart"
|
||||||
lc.Data["default"] = sinps
|
lc.Data = append(lc.Data, sinFloat64)
|
||||||
lc.Height = 11
|
|
||||||
lc.AxesColor = ui.ColorWhite
|
lc.AxesColor = ui.ColorWhite
|
||||||
lc.LineColor["default"] = ui.ColorYellow | ui.AttrBold
|
lc.LineColors[0] = ui.ColorYellow
|
||||||
|
|
||||||
gs := make([]*ui.Gauge, 3)
|
gs := make([]*widgets.Gauge, 3)
|
||||||
for i := range gs {
|
for i := range gs {
|
||||||
gs[i] = ui.NewGauge()
|
gs[i] = widgets.NewGauge()
|
||||||
//gs[i].LabelAlign = ui.AlignCenter
|
|
||||||
gs[i].Height = 2
|
|
||||||
gs[i].Border = false
|
|
||||||
gs[i].Percent = i * 10
|
gs[i].Percent = i * 10
|
||||||
gs[i].PaddingBottom = 1
|
|
||||||
gs[i].BarColor = ui.ColorRed
|
gs[i].BarColor = ui.ColorRed
|
||||||
}
|
}
|
||||||
|
|
||||||
ls := ui.NewList()
|
ls := widgets.NewList()
|
||||||
ls.Border = false
|
ls.Rows = []string{
|
||||||
ls.Items = []string{
|
|
||||||
"[1] Downloading File 1",
|
"[1] Downloading File 1",
|
||||||
"", // == \newline
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
"[2] Downloading File 2",
|
"[2] Downloading File 2",
|
||||||
"",
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
"[3] Uploading File 3",
|
"[3] Uploading File 3",
|
||||||
}
|
}
|
||||||
ls.Height = 5
|
|
||||||
|
|
||||||
p := ui.NewParagraph("<> This row has 3 columns\n<- Widgets can be stacked up like left side\n<- Stacked widgets are treated as a single widget")
|
p := widgets.NewParagraph()
|
||||||
p.Height = 5
|
p.Text = "<> This row has 3 columns\n<- Widgets can be stacked up like left side\n<- Stacked widgets are treated as a single widget"
|
||||||
p.BorderLabel = "Demonstration"
|
p.Title = "Demonstration"
|
||||||
|
|
||||||
// build layout
|
grid := ui.NewGrid()
|
||||||
ui.Body.AddRows(
|
termWidth, termHeight := ui.TerminalDimensions()
|
||||||
ui.NewRow(
|
grid.SetRect(0, 0, termWidth, termHeight)
|
||||||
ui.NewCol(6, 0, sp),
|
|
||||||
ui.NewCol(6, 0, lc)),
|
|
||||||
ui.NewRow(
|
|
||||||
ui.NewCol(3, 0, ls),
|
|
||||||
ui.NewCol(3, 0, gs[0], gs[1], gs[2]),
|
|
||||||
ui.NewCol(6, 0, p)))
|
|
||||||
|
|
||||||
// calculate layout
|
grid.Set(
|
||||||
ui.Body.Align()
|
ui.NewRow(1.0/2,
|
||||||
|
ui.NewCol(1.0/2, slg),
|
||||||
|
ui.NewCol(1.0/2, lc),
|
||||||
|
),
|
||||||
|
ui.NewRow(1.0/2,
|
||||||
|
ui.NewCol(1.0/4, ls),
|
||||||
|
ui.NewCol(1.0/4,
|
||||||
|
ui.NewRow(.9/3, gs[0]),
|
||||||
|
ui.NewRow(.9/3, gs[1]),
|
||||||
|
ui.NewRow(1.2/3, gs[2]),
|
||||||
|
),
|
||||||
|
ui.NewCol(1.0/2, p),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
ui.Render(ui.Body)
|
ui.Render(grid)
|
||||||
|
|
||||||
tickerCount := 1
|
tickerCount := 1
|
||||||
uiEvents := ui.PollEvents()
|
uiEvents := ui.PollEvents()
|
||||||
@ -106,21 +101,20 @@ func main() {
|
|||||||
return
|
return
|
||||||
case "<Resize>":
|
case "<Resize>":
|
||||||
payload := e.Payload.(ui.Resize)
|
payload := e.Payload.(ui.Resize)
|
||||||
ui.Body.Width = payload.Width
|
grid.SetRect(0, 0, payload.Width, payload.Height)
|
||||||
ui.Body.Align()
|
|
||||||
ui.Clear()
|
ui.Clear()
|
||||||
ui.Render(ui.Body)
|
ui.Render(grid)
|
||||||
}
|
}
|
||||||
case <-ticker:
|
case <-ticker:
|
||||||
if tickerCount > 103 {
|
if tickerCount == 100 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, g := range gs {
|
for _, g := range gs {
|
||||||
g.Percent = (g.Percent + 3) % 100
|
g.Percent = (g.Percent + 3) % 100
|
||||||
}
|
}
|
||||||
sp.Lines[0].Data = spdata[:100+tickerCount]
|
slg.Sparklines[0].Data = sinFloat64[tickerCount : tickerCount+100]
|
||||||
lc.Data["default"] = sinps[2*tickerCount:]
|
lc.Data[0] = sinFloat64[2*tickerCount:]
|
||||||
ui.Render(ui.Body)
|
ui.Render(grid)
|
||||||
tickerCount++
|
tickerCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
48
_examples/grid_nested.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// +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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +1,24 @@
|
|||||||
|
// +build ignore
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import ui "github.com/gizak/termui"
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
ui "github.com/gizak/termui"
|
||||||
|
"github.com/gizak/termui/widgets"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := ui.Init()
|
if err := ui.Init(); err != nil {
|
||||||
if err != nil {
|
log.Fatalf("failed to initialize termui: %v", err)
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
defer ui.Close()
|
defer ui.Close()
|
||||||
|
|
||||||
p := ui.NewParagraph("Hello World!")
|
p := widgets.NewParagraph()
|
||||||
p.Width = 25
|
p.Text = "Hello World!"
|
||||||
p.Height = 5
|
p.SetRect(0, 0, 25, 5)
|
||||||
|
|
||||||
ui.Render(p)
|
ui.Render(p)
|
||||||
|
|
||||||
for e := range ui.PollEvents() {
|
for e := range ui.PollEvents() {
|
||||||
|
@ -7,62 +7,57 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
|
"github.com/gizak/termui/widgets"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := ui.Init()
|
if err := ui.Init(); err != nil {
|
||||||
if err != nil {
|
log.Fatalf("failed to initialize termui: %v", err)
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
defer ui.Close()
|
defer ui.Close()
|
||||||
|
|
||||||
sinps := (func() map[string][]float64 {
|
sinData := func() [][]float64 {
|
||||||
n := 220
|
n := 220
|
||||||
ps := make(map[string][]float64)
|
data := make([][]float64, 2)
|
||||||
ps["first"] = make([]float64, n)
|
data[0] = make([]float64, n)
|
||||||
ps["second"] = make([]float64, n)
|
data[1] = make([]float64, n)
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
ps["first"][i] = 1 + math.Sin(float64(i)/5)
|
data[0][i] = 1 + math.Sin(float64(i)/5)
|
||||||
ps["second"][i] = 1 + math.Cos(float64(i)/5)
|
data[1][i] = 1 + math.Cos(float64(i)/5)
|
||||||
}
|
}
|
||||||
return ps
|
return data
|
||||||
})()
|
}()
|
||||||
|
|
||||||
lc0 := ui.NewLineChart()
|
lc0 := widgets.NewLineChart()
|
||||||
lc0.BorderLabel = "braille-mode Line Chart"
|
lc0.Title = "braille-mode Line Chart"
|
||||||
lc0.Data = sinps
|
lc0.Data = sinData
|
||||||
lc0.Width = 50
|
lc0.SetRect(0, 0, 50, 15)
|
||||||
lc0.Height = 12
|
|
||||||
lc0.X = 0
|
|
||||||
lc0.Y = 0
|
|
||||||
lc0.AxesColor = ui.ColorWhite
|
lc0.AxesColor = ui.ColorWhite
|
||||||
lc0.LineColor["first"] = ui.ColorGreen | ui.AttrBold
|
lc0.LineColors[0] = ui.ColorGreen
|
||||||
|
|
||||||
lc1 := ui.NewLineChart()
|
lc1 := widgets.NewLineChart()
|
||||||
lc1.BorderLabel = "dot-mode Line Chart"
|
lc1.Title = "custom Line Chart"
|
||||||
lc1.Mode = "dot"
|
lc1.LineType = widgets.DotLine
|
||||||
lc1.Data = sinps
|
lc1.Data = [][]float64{[]float64{1, 2, 3, 4, 5}}
|
||||||
lc1.Width = 26
|
lc1.SetRect(50, 0, 75, 10)
|
||||||
lc1.Height = 12
|
lc1.DotChar = '+'
|
||||||
lc1.X = 51
|
|
||||||
lc1.DotStyle = '+'
|
|
||||||
lc1.AxesColor = ui.ColorWhite
|
lc1.AxesColor = ui.ColorWhite
|
||||||
lc1.LineColor["first"] = ui.ColorYellow | ui.AttrBold
|
lc1.LineColors[0] = ui.ColorYellow
|
||||||
|
lc1.DrawDirection = widgets.DrawLeft
|
||||||
|
|
||||||
lc2 := ui.NewLineChart()
|
lc2 := widgets.NewLineChart()
|
||||||
lc2.BorderLabel = "dot-mode Line Chart"
|
lc2.Title = "dot-mode Line Chart"
|
||||||
lc2.Mode = "dot"
|
lc2.LineType = widgets.DotLine
|
||||||
lc2.Data["first"] = sinps["first"][4:]
|
lc2.Data = make([][]float64, 2)
|
||||||
lc2.Data["second"] = sinps["second"][4:]
|
lc2.Data[0] = sinData[0][4:]
|
||||||
lc2.Width = 77
|
lc2.Data[1] = sinData[1][4:]
|
||||||
lc2.Height = 16
|
lc2.SetRect(0, 15, 50, 30)
|
||||||
lc2.X = 0
|
|
||||||
lc2.Y = 12
|
|
||||||
lc2.AxesColor = ui.ColorWhite
|
lc2.AxesColor = ui.ColorWhite
|
||||||
lc2.LineColor["first"] = ui.ColorCyan | ui.AttrBold
|
lc2.LineColors[0] = ui.ColorCyan
|
||||||
|
|
||||||
ui.Render(lc0, lc1, lc2)
|
ui.Render(lc0, lc1, lc2)
|
||||||
|
|
||||||
|
@ -6,34 +6,36 @@
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import ui "github.com/gizak/termui"
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
ui "github.com/gizak/termui"
|
||||||
|
"github.com/gizak/termui/widgets"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := ui.Init()
|
if err := ui.Init(); err != nil {
|
||||||
if err != nil {
|
log.Fatalf("failed to initialize termui: %v", err)
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
defer ui.Close()
|
defer ui.Close()
|
||||||
|
|
||||||
strs := []string{
|
l := widgets.NewList()
|
||||||
|
l.Title = "List"
|
||||||
|
l.Rows = []string{
|
||||||
"[0] github.com/gizak/termui",
|
"[0] github.com/gizak/termui",
|
||||||
"[1] [你好,世界](fg-blue)",
|
"[1] [你好,世界](fg:blue)",
|
||||||
"[2] [こんにちは世界](fg-red)",
|
"[2] [こんにちは世界](fg:red)",
|
||||||
"[3] [color output](fg-white,bg-green)",
|
"[3] c[olor outpu](fg:white,bg:green)t",
|
||||||
"[4] output.go",
|
"[4] output.go",
|
||||||
"[5] random_out.go",
|
"[5] random_out.go",
|
||||||
"[6] dashboard.go",
|
"[6] dashboard.go",
|
||||||
"[7] nsf/termbox-go"}
|
"[7] nsf/termbox-go",
|
||||||
|
}
|
||||||
|
l.TextStyle = ui.NewStyle(ui.ColorYellow)
|
||||||
|
l.Wrap = false
|
||||||
|
l.SetRect(0, 0, 25, 50)
|
||||||
|
|
||||||
ls := ui.NewList()
|
ui.Render(l)
|
||||||
ls.Items = strs
|
|
||||||
ls.ItemFgColor = ui.ColorYellow
|
|
||||||
ls.BorderLabel = "List"
|
|
||||||
ls.Height = 7
|
|
||||||
ls.Width = 25
|
|
||||||
ls.Y = 0
|
|
||||||
|
|
||||||
ui.Render(ls)
|
|
||||||
|
|
||||||
uiEvents := ui.PollEvents()
|
uiEvents := ui.PollEvents()
|
||||||
for {
|
for {
|
||||||
|
@ -6,41 +6,47 @@
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import ui "github.com/gizak/termui"
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
ui "github.com/gizak/termui"
|
||||||
|
"github.com/gizak/termui/widgets"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := ui.Init()
|
if err := ui.Init(); err != nil {
|
||||||
if err != nil {
|
log.Fatalf("failed to initialize termui: %v", err)
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
defer ui.Close()
|
defer ui.Close()
|
||||||
|
|
||||||
p0 := ui.NewParagraph("Borderless Text")
|
p0 := widgets.NewParagraph()
|
||||||
p0.Height = 1
|
p0.Text = "Borderless Text"
|
||||||
p0.Width = 20
|
p0.SetRect(0, 0, 20, 5)
|
||||||
p0.Y = 1
|
|
||||||
p0.Border = false
|
p0.Border = false
|
||||||
|
|
||||||
p1 := ui.NewParagraph("你好,世界。")
|
p1 := widgets.NewParagraph()
|
||||||
p1.Height = 3
|
p1.Title = "标签"
|
||||||
p1.Width = 17
|
p1.Text = "你好,世界。"
|
||||||
p1.X = 20
|
p1.SetRect(20, 0, 35, 5)
|
||||||
p1.BorderLabel = "标签"
|
|
||||||
|
|
||||||
p2 := ui.NewParagraph("Simple colored text\nwith label. It [can be](fg-red) multilined with \\n or [break automatically](fg-red,fg-bold)")
|
p2 := widgets.NewParagraph()
|
||||||
p2.Height = 5
|
p2.Title = "Multiline"
|
||||||
p2.Width = 37
|
p2.Text = "Simple colored text\nwith label. It [can be](fg:red) multilined with \\n or [break automatically](fg:red,fg:bold)"
|
||||||
p2.Y = 4
|
p2.SetRect(0, 5, 35, 10)
|
||||||
p2.BorderLabel = "Multiline"
|
p2.BorderStyle.Fg = ui.ColorYellow
|
||||||
p2.BorderFg = ui.ColorYellow
|
|
||||||
|
|
||||||
p3 := ui.NewParagraph("Long text with label and it is auto trimmed.")
|
p3 := widgets.NewParagraph()
|
||||||
p3.Height = 3
|
p3.Title = "Auto Trim"
|
||||||
p3.Width = 37
|
p3.Text = "Long text with label and it is auto trimmed."
|
||||||
p3.Y = 9
|
p3.SetRect(0, 10, 40, 15)
|
||||||
p3.BorderLabel = "Auto Trim"
|
|
||||||
|
|
||||||
ui.Render(p0, p1, p2, p3)
|
p4 := widgets.NewParagraph()
|
||||||
|
p4.Title = "Text Box with Wrapping"
|
||||||
|
p4.Text = "Press q to QUIT THE DEMO. [There](fg:blue,mod:bold) are other things [that](fg:red) are going to fit in here I think. What do you think? Now is the time for all good [men to](bg:blue) come to the aid of their country. [This is going to be one really really really long line](fg:green) that is going to go together and stuffs and things. Let's see how this thing renders out.\n Here is a new paragraph and stuffs and things. There should be a tab indent at the beginning of the paragraph. Let's see if that worked as well."
|
||||||
|
p4.SetRect(40, 0, 70, 20)
|
||||||
|
p4.BorderStyle.Fg = ui.ColorBlue
|
||||||
|
|
||||||
|
ui.Render(p0, p1, p2, p3, p4)
|
||||||
|
|
||||||
uiEvents := ui.PollEvents()
|
uiEvents := ui.PollEvents()
|
||||||
for {
|
for {
|
||||||
|
@ -4,17 +4,20 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
|
"github.com/gizak/termui/widgets"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var run = true
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := ui.Init()
|
if err := ui.Init(); err != nil {
|
||||||
if err != nil {
|
log.Fatalf("failed to initialize termui: %v", err)
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
defer ui.Close()
|
defer ui.Close()
|
||||||
|
|
||||||
@ -28,12 +31,10 @@ func main() {
|
|||||||
offset = 2.0 * math.Pi * rand.Float64()
|
offset = 2.0 * math.Pi * rand.Float64()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
run := true
|
|
||||||
|
|
||||||
pc := ui.NewPieChart()
|
pc := widgets.NewPieChart()
|
||||||
pc.BorderLabel = "Pie Chart"
|
pc.Title = "Pie Chart"
|
||||||
pc.Width = 70
|
pc.SetRect(5, 5, 70, 36)
|
||||||
pc.Height = 36
|
|
||||||
pc.Data = []float64{.25, .25, .25, .25}
|
pc.Data = []float64{.25, .25, .25, .25}
|
||||||
pc.Offset = -.5 * math.Pi
|
pc.Offset = -.5 * math.Pi
|
||||||
pc.Label = func(i int, v float64) string {
|
pc.Label = func(i int, v float64) string {
|
||||||
@ -43,9 +44,9 @@ func main() {
|
|||||||
pause := func() {
|
pause := func() {
|
||||||
run = !run
|
run = !run
|
||||||
if run {
|
if run {
|
||||||
pc.BorderLabel = "Pie Chart"
|
pc.Title = "Pie Chart"
|
||||||
} else {
|
} else {
|
||||||
pc.BorderLabel = "Pie Chart (Stopped)"
|
pc.Title = "Pie Chart (Stopped)"
|
||||||
}
|
}
|
||||||
ui.Render(pc)
|
ui.Render(pc)
|
||||||
}
|
}
|
||||||
|
67
_examples/sparkline.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
// 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 (
|
||||||
|
"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()
|
||||||
|
|
||||||
|
data := []float64{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}
|
||||||
|
|
||||||
|
sl0 := widgets.NewSparkline()
|
||||||
|
sl0.Data = data[3:]
|
||||||
|
sl0.LineColor = ui.ColorGreen
|
||||||
|
|
||||||
|
// single
|
||||||
|
slg0 := widgets.NewSparklineGroup(sl0)
|
||||||
|
slg0.Title = "Sparkline 0"
|
||||||
|
slg0.SetRect(0, 0, 20, 10)
|
||||||
|
|
||||||
|
sl1 := widgets.NewSparkline()
|
||||||
|
sl1.Title = "Sparkline 1"
|
||||||
|
sl1.Data = data
|
||||||
|
sl1.LineColor = ui.ColorRed
|
||||||
|
|
||||||
|
sl2 := widgets.NewSparkline()
|
||||||
|
sl2.Title = "Sparkline 2"
|
||||||
|
sl2.Data = data[5:]
|
||||||
|
sl2.LineColor = ui.ColorMagenta
|
||||||
|
|
||||||
|
slg1 := widgets.NewSparklineGroup(sl0, sl1, sl2)
|
||||||
|
slg1.Title = "Group Sparklines"
|
||||||
|
slg1.SetRect(0, 10, 25, 25)
|
||||||
|
|
||||||
|
sl3 := widgets.NewSparkline()
|
||||||
|
sl3.Title = "Enlarged Sparkline"
|
||||||
|
sl3.Data = data
|
||||||
|
sl3.LineColor = ui.ColorYellow
|
||||||
|
|
||||||
|
slg2 := widgets.NewSparklineGroup(sl3)
|
||||||
|
slg2.Title = "Tweeked Sparkline"
|
||||||
|
slg2.SetRect(20, 0, 50, 10)
|
||||||
|
slg2.BorderStyle.Fg = ui.ColorCyan
|
||||||
|
|
||||||
|
ui.Render(slg0, slg1, slg2)
|
||||||
|
|
||||||
|
uiEvents := ui.PollEvents()
|
||||||
|
for {
|
||||||
|
e := <-uiEvents
|
||||||
|
switch e.ID {
|
||||||
|
case "q", "<C-c>":
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,70 +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 ui "github.com/gizak/termui"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
err := ui.Init()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer ui.Close()
|
|
||||||
|
|
||||||
data := []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}
|
|
||||||
spl0 := ui.NewSparkline()
|
|
||||||
spl0.Data = data[3:]
|
|
||||||
spl0.Title = "Sparkline 0"
|
|
||||||
spl0.LineColor = ui.ColorGreen
|
|
||||||
|
|
||||||
// single
|
|
||||||
spls0 := ui.NewSparklines(spl0)
|
|
||||||
spls0.Height = 2
|
|
||||||
spls0.Width = 20
|
|
||||||
spls0.Border = false
|
|
||||||
|
|
||||||
spl1 := ui.NewSparkline()
|
|
||||||
spl1.Data = data
|
|
||||||
spl1.Title = "Sparkline 1"
|
|
||||||
spl1.LineColor = ui.ColorRed
|
|
||||||
|
|
||||||
spl2 := ui.NewSparkline()
|
|
||||||
spl2.Data = data[5:]
|
|
||||||
spl2.Title = "Sparkline 2"
|
|
||||||
spl2.LineColor = ui.ColorMagenta
|
|
||||||
|
|
||||||
// group
|
|
||||||
spls1 := ui.NewSparklines(spl0, spl1, spl2)
|
|
||||||
spls1.Height = 8
|
|
||||||
spls1.Width = 20
|
|
||||||
spls1.Y = 3
|
|
||||||
spls1.BorderLabel = "Group Sparklines"
|
|
||||||
|
|
||||||
spl3 := ui.NewSparkline()
|
|
||||||
spl3.Data = data
|
|
||||||
spl3.Title = "Enlarged Sparkline"
|
|
||||||
spl3.Height = 8
|
|
||||||
spl3.LineColor = ui.ColorYellow
|
|
||||||
|
|
||||||
spls2 := ui.NewSparklines(spl3)
|
|
||||||
spls2.Height = 11
|
|
||||||
spls2.Width = 30
|
|
||||||
spls2.BorderFg = ui.ColorCyan
|
|
||||||
spls2.X = 21
|
|
||||||
spls2.BorderLabel = "Tweeked Sparkline"
|
|
||||||
|
|
||||||
ui.Render(spls0, spls1, spls2)
|
|
||||||
|
|
||||||
uiEvents := ui.PollEvents()
|
|
||||||
for {
|
|
||||||
e := <-uiEvents
|
|
||||||
switch e.ID {
|
|
||||||
case "q", "<C-c>":
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
44
_examples/stacked_barchart.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// 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 (
|
||||||
|
"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()
|
||||||
|
|
||||||
|
sbc := widgets.NewStackedBarChart()
|
||||||
|
sbc.Title = "Student's Marks: X-Axis=Name, Y-Axis=Grade% (Math, English, Science, Computer Science)"
|
||||||
|
sbc.Labels = []string{"Ken", "Rob", "Dennis", "Linus"}
|
||||||
|
|
||||||
|
sbc.Data = make([][]float64, 4)
|
||||||
|
sbc.Data[0] = []float64{90, 85, 90, 80}
|
||||||
|
sbc.Data[1] = []float64{70, 85, 75, 60}
|
||||||
|
sbc.Data[2] = []float64{75, 60, 80, 85}
|
||||||
|
sbc.Data[3] = []float64{100, 100, 100, 100}
|
||||||
|
sbc.SetRect(5, 5, 100, 30)
|
||||||
|
sbc.BarWidth = 5
|
||||||
|
|
||||||
|
ui.Render(sbc)
|
||||||
|
|
||||||
|
uiEvents := ui.PollEvents()
|
||||||
|
for {
|
||||||
|
e := <-uiEvents
|
||||||
|
switch e.ID {
|
||||||
|
case "q", "<C-c>":
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,55 +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 ui "github.com/gizak/termui"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
err := ui.Init()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer ui.Close()
|
|
||||||
|
|
||||||
sbc := ui.NewStackedBarChart()
|
|
||||||
math := []int{90, 85, 90, 80}
|
|
||||||
english := []int{70, 85, 75, 60}
|
|
||||||
science := []int{75, 60, 80, 85}
|
|
||||||
compsci := []int{100, 100, 100, 100}
|
|
||||||
sbc.Data[0] = math
|
|
||||||
sbc.Data[1] = english
|
|
||||||
sbc.Data[2] = science
|
|
||||||
sbc.Data[3] = compsci
|
|
||||||
studentsName := []string{"Ken", "Rob", "Dennis", "Linus"}
|
|
||||||
sbc.BorderLabel = "Student's Marks X-Axis=Name Y-Axis=Marks[Math,English,Science,ComputerScience] in %"
|
|
||||||
sbc.Width = 100
|
|
||||||
sbc.Height = 30
|
|
||||||
sbc.Y = 0
|
|
||||||
sbc.BarWidth = 10
|
|
||||||
sbc.DataLabels = studentsName
|
|
||||||
sbc.ShowScale = true //Show y_axis scale value (min and max)
|
|
||||||
sbc.SetMax(400)
|
|
||||||
|
|
||||||
sbc.TextColor = ui.ColorGreen //this is color for label (x-axis)
|
|
||||||
sbc.BarColor[3] = ui.ColorGreen //BarColor for computerscience
|
|
||||||
sbc.BarColor[1] = ui.ColorYellow //Bar Color for english
|
|
||||||
sbc.NumColor[3] = ui.ColorRed // Num color for computerscience
|
|
||||||
sbc.NumColor[1] = ui.ColorRed // num color for english
|
|
||||||
|
|
||||||
//Other colors are automatically populated, btw All the students seems do well in computerscience. :p
|
|
||||||
|
|
||||||
ui.Render(sbc)
|
|
||||||
|
|
||||||
uiEvents := ui.PollEvents()
|
|
||||||
for {
|
|
||||||
e := <-uiEvents
|
|
||||||
switch e.ID {
|
|
||||||
case "q", "<C-c>":
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,50 +6,40 @@
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import ui "github.com/gizak/termui"
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
ui "github.com/gizak/termui"
|
||||||
|
"github.com/gizak/termui/widgets"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := ui.Init()
|
if err := ui.Init(); err != nil {
|
||||||
if err != nil {
|
log.Fatalf("failed to initialize termui: %v", err)
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
defer ui.Close()
|
defer ui.Close()
|
||||||
|
|
||||||
rows1 := [][]string{
|
table1 := widgets.NewTable()
|
||||||
|
table1.Rows = [][]string{
|
||||||
[]string{"header1", "header2", "header3"},
|
[]string{"header1", "header2", "header3"},
|
||||||
[]string{"你好吗", "Go-lang is so cool", "Im working on Ruby"},
|
[]string{"你好吗", "Go-lang is so cool", "Im working on Ruby"},
|
||||||
[]string{"2016", "10", "11"},
|
[]string{"2016", "10", "11"},
|
||||||
}
|
}
|
||||||
|
table1.TextStyle = ui.NewStyle(ui.ColorWhite)
|
||||||
table1 := ui.NewTable()
|
table1.SetRect(0, 0, 60, 10)
|
||||||
table1.Rows = rows1
|
|
||||||
table1.FgColor = ui.ColorWhite
|
|
||||||
table1.BgColor = ui.ColorDefault
|
|
||||||
table1.Y = 0
|
|
||||||
table1.X = 0
|
|
||||||
table1.Width = 62
|
|
||||||
table1.Height = 7
|
|
||||||
|
|
||||||
ui.Render(table1)
|
ui.Render(table1)
|
||||||
|
|
||||||
rows2 := [][]string{
|
table2 := widgets.NewTable()
|
||||||
|
table2.Rows = [][]string{
|
||||||
[]string{"header1", "header2", "header3"},
|
[]string{"header1", "header2", "header3"},
|
||||||
[]string{"Foundations", "Go-lang is so cool", "Im working on Ruby"},
|
[]string{"Foundations", "Go-lang is so cool", "Im working on Ruby"},
|
||||||
[]string{"2016", "11", "11"},
|
[]string{"2016", "11", "11"},
|
||||||
}
|
}
|
||||||
|
table2.TextStyle = ui.NewStyle(ui.ColorWhite)
|
||||||
table2 := ui.NewTable()
|
|
||||||
table2.Rows = rows2
|
|
||||||
table2.FgColor = ui.ColorWhite
|
|
||||||
table2.BgColor = ui.ColorDefault
|
|
||||||
table2.TextAlign = ui.AlignCenter
|
table2.TextAlign = ui.AlignCenter
|
||||||
table2.Separator = false
|
table2.RowSeparator = false
|
||||||
table2.Analysis()
|
table2.SetRect(0, 10, 20, 20)
|
||||||
table2.SetSize()
|
|
||||||
table2.BgColors[2] = ui.ColorRed
|
|
||||||
table2.Y = 10
|
|
||||||
table2.X = 0
|
|
||||||
table2.Border = true
|
|
||||||
|
|
||||||
ui.Render(table2)
|
ui.Render(table2)
|
||||||
|
|
||||||
|
@ -7,73 +7,68 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
|
"github.com/gizak/termui/widgets"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := ui.Init()
|
if err := ui.Init(); err != nil {
|
||||||
if err != nil {
|
log.Fatalf("failed to initialize termui: %v", err)
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
defer ui.Close()
|
defer ui.Close()
|
||||||
|
|
||||||
header := ui.NewParagraph("Press q to quit, Press h or l to switch tabs")
|
header := widgets.NewParagraph()
|
||||||
header.Height = 1
|
header.Text = "Press q to quit, Press h or l to switch tabs"
|
||||||
header.Width = 50
|
header.SetRect(0, 0, 50, 1)
|
||||||
header.Border = false
|
header.Border = false
|
||||||
header.TextBgColor = ui.ColorBlue
|
header.TextStyle.Bg = ui.ColorBlue
|
||||||
|
|
||||||
tab1 := ui.NewTab("pierwszy")
|
p2 := widgets.NewParagraph()
|
||||||
p2 := ui.NewParagraph("Press q to quit\nPress h or l to switch tabs\n")
|
p2.Text = "Press q to quit\nPress h or l to switch tabs\n"
|
||||||
p2.Height = 5
|
p2.Title = "Keys"
|
||||||
p2.Width = 37
|
p2.SetRect(5, 5, 40, 15)
|
||||||
p2.Y = 0
|
p2.BorderStyle.Fg = ui.ColorYellow
|
||||||
p2.BorderLabel = "Keys"
|
|
||||||
p2.BorderFg = ui.ColorYellow
|
|
||||||
tab1.AddBlocks(p2)
|
|
||||||
|
|
||||||
tab2 := ui.NewTab("drugi")
|
bc := widgets.NewBarChart()
|
||||||
bc := ui.NewBarChart()
|
bc.Title = "Bar Chart"
|
||||||
data := []int{3, 2, 5, 3, 9, 5, 3, 2, 5, 8, 3, 2, 4, 5, 3, 2, 5, 7, 5, 3, 2, 6, 7, 4, 6, 3, 6, 7, 8, 3, 6, 4, 5, 3, 2, 4, 6, 4, 8, 5, 9, 4, 3, 6, 5, 3, 6}
|
bc.Data = []float64{3, 2, 5, 3, 9, 5, 3, 2, 5, 8, 3, 2, 4, 5, 3, 2, 5, 7, 5, 3, 2, 6, 7, 4, 6, 3, 6, 7, 8, 3, 6, 4, 5, 3, 2, 4, 6, 4, 8, 5, 9, 4, 3, 6, 5, 3, 6}
|
||||||
bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"}
|
bc.SetRect(5, 5, 35, 10)
|
||||||
bc.BorderLabel = "Bar Chart"
|
bc.Labels = []string{"S0", "S1", "S2", "S3", "S4", "S5"}
|
||||||
bc.Data = data
|
|
||||||
bc.Width = 26
|
|
||||||
bc.Height = 10
|
|
||||||
bc.DataLabels = bclabels
|
|
||||||
bc.TextColor = ui.ColorGreen
|
|
||||||
bc.BarColor = ui.ColorRed
|
|
||||||
bc.NumColor = ui.ColorYellow
|
|
||||||
tab2.AddBlocks(bc)
|
|
||||||
|
|
||||||
tab3 := ui.NewTab("trzeci")
|
tabpane := widgets.NewTabPane("pierwszy", "drugi", "trzeci", "żółw", "four", "five")
|
||||||
tab4 := ui.NewTab("żółw")
|
tabpane.SetRect(0, 1, 50, 4)
|
||||||
tab5 := ui.NewTab("four")
|
|
||||||
tab6 := ui.NewTab("five")
|
|
||||||
|
|
||||||
tabpane := ui.NewTabPane()
|
|
||||||
tabpane.Y = 1
|
|
||||||
tabpane.Width = 30
|
|
||||||
tabpane.Border = true
|
tabpane.Border = true
|
||||||
|
|
||||||
tabpane.SetTabs(*tab1, *tab2, *tab3, *tab4, *tab5, *tab6)
|
renderTab := func() {
|
||||||
|
switch tabpane.ActiveTabIndex {
|
||||||
|
case 0:
|
||||||
|
ui.Render(p2)
|
||||||
|
case 1:
|
||||||
|
ui.Render(bc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ui.Render(header, tabpane)
|
ui.Render(header, tabpane, p2)
|
||||||
|
|
||||||
uiEvents := ui.PollEvents()
|
uiEvents := ui.PollEvents()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
e := <-uiEvents
|
e := <-uiEvents
|
||||||
switch e.ID {
|
switch e.ID {
|
||||||
case "q", "<C-c>":
|
case "q", "<C-c>":
|
||||||
return
|
return
|
||||||
case "h":
|
case "h":
|
||||||
tabpane.SetActiveLeft()
|
tabpane.FocusLeft()
|
||||||
ui.Clear()
|
ui.Clear()
|
||||||
ui.Render(header, tabpane)
|
ui.Render(header, tabpane)
|
||||||
|
renderTab()
|
||||||
case "l":
|
case "l":
|
||||||
tabpane.SetActiveRight()
|
tabpane.FocusRight()
|
||||||
ui.Clear()
|
ui.Clear()
|
||||||
ui.Render(header, tabpane)
|
ui.Render(header, tabpane)
|
||||||
|
renderTab()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,156 +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 (
|
|
||||||
"math"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
ui "github.com/gizak/termui"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
err := ui.Init()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer ui.Close()
|
|
||||||
|
|
||||||
// Deprecated
|
|
||||||
//ui.UseTheme("helloworld")
|
|
||||||
ui.ColorMap = map[string]ui.Attribute{
|
|
||||||
"fg": ui.ColorWhite,
|
|
||||||
"bg": ui.ColorDefault,
|
|
||||||
"border.fg": ui.ColorYellow,
|
|
||||||
"label.fg": ui.ColorGreen,
|
|
||||||
"par.fg": ui.ColorYellow,
|
|
||||||
"par.label.bg": ui.ColorWhite,
|
|
||||||
"gauge.bar.bg": ui.ColorCyan,
|
|
||||||
"gauge.percent.fg": ui.ColorBlue,
|
|
||||||
"barchart.bar.bg": ui.ColorRed,
|
|
||||||
}
|
|
||||||
p := ui.NewParagraph(":PRESS q TO QUIT DEMO")
|
|
||||||
p.Height = 3
|
|
||||||
p.Width = 50
|
|
||||||
p.BorderLabel = "Text Box"
|
|
||||||
|
|
||||||
strs := []string{"[0] gizak/termui", "[1] editbox.go", "[2] interrupt.go", "[3] keyboard.go", "[4] output.go", "[5] random_out.go", "[6] dashboard.go", "[7] nsf/termbox-go"}
|
|
||||||
list := ui.NewList()
|
|
||||||
list.Items = strs
|
|
||||||
list.BorderLabel = "List"
|
|
||||||
list.Height = 7
|
|
||||||
list.Width = 25
|
|
||||||
list.Y = 4
|
|
||||||
|
|
||||||
g := ui.NewGauge()
|
|
||||||
g.Percent = 50
|
|
||||||
g.Width = 50
|
|
||||||
g.Height = 3
|
|
||||||
g.Y = 11
|
|
||||||
g.BorderLabel = "Gauge"
|
|
||||||
|
|
||||||
spark := ui.NewSparkline()
|
|
||||||
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}
|
|
||||||
spark.Data = spdata
|
|
||||||
|
|
||||||
spark1 := ui.NewSparkline()
|
|
||||||
spark1.Title = "srv 1:"
|
|
||||||
spark1.Data = spdata
|
|
||||||
|
|
||||||
sp := ui.NewSparklines(spark, spark1)
|
|
||||||
sp.Width = 25
|
|
||||||
sp.Height = 7
|
|
||||||
sp.BorderLabel = "Sparkline"
|
|
||||||
sp.Y = 4
|
|
||||||
sp.X = 25
|
|
||||||
|
|
||||||
lc := ui.NewLineChart()
|
|
||||||
sinps := (func() []float64 {
|
|
||||||
n := 100
|
|
||||||
ps := make([]float64, n)
|
|
||||||
for i := range ps {
|
|
||||||
ps[i] = 1 + math.Sin(float64(i)/4)
|
|
||||||
}
|
|
||||||
return ps
|
|
||||||
})()
|
|
||||||
|
|
||||||
lc.BorderLabel = "Line Chart"
|
|
||||||
lc.Data["default"] = sinps
|
|
||||||
lc.Width = 50
|
|
||||||
lc.Height = 11
|
|
||||||
lc.X = 0
|
|
||||||
lc.Y = 14
|
|
||||||
lc.Mode = "dot"
|
|
||||||
|
|
||||||
bc := ui.NewBarChart()
|
|
||||||
bcdata := []int{3, 2, 5, 3, 9, 5, 3, 2, 5, 8, 3, 2, 4, 5, 3, 2, 5, 7, 5, 3, 2, 6, 7, 4, 6, 3, 6, 7, 8, 3, 6, 4, 5, 3, 2, 4, 6, 4, 8, 5, 9, 4, 3, 6, 5, 3, 6}
|
|
||||||
bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"}
|
|
||||||
bc.BorderLabel = "Bar Chart"
|
|
||||||
bc.Width = 26
|
|
||||||
bc.Height = 10
|
|
||||||
bc.X = 51
|
|
||||||
bc.Y = 0
|
|
||||||
bc.DataLabels = bclabels
|
|
||||||
|
|
||||||
lc1 := ui.NewLineChart()
|
|
||||||
lc1.BorderLabel = "Line Chart"
|
|
||||||
rndwalk := (func() []float64 {
|
|
||||||
n := 150
|
|
||||||
d := make([]float64, n)
|
|
||||||
for i := 1; i < n; i++ {
|
|
||||||
if i < 20 {
|
|
||||||
d[i] = d[i-1] + 0.01
|
|
||||||
}
|
|
||||||
if i > 20 {
|
|
||||||
d[i] = d[i-1] - 0.05
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
})()
|
|
||||||
lc1.Data["default"] = rndwalk
|
|
||||||
lc1.Width = 26
|
|
||||||
lc1.Height = 11
|
|
||||||
lc1.X = 51
|
|
||||||
lc1.Y = 14
|
|
||||||
|
|
||||||
p1 := ui.NewParagraph("Hey!\nI am a borderless block!")
|
|
||||||
p1.Border = false
|
|
||||||
p1.Width = 26
|
|
||||||
p1.Height = 2
|
|
||||||
p1.X = 52
|
|
||||||
p1.Y = 11
|
|
||||||
|
|
||||||
draw := func(count int) {
|
|
||||||
g.Percent = count % 101
|
|
||||||
list.Items = strs[count%9:]
|
|
||||||
sp.Lines[0].Data = spdata[count%10:]
|
|
||||||
sp.Lines[1].Data = spdata[count/2%10:]
|
|
||||||
lc.Data["default"] = sinps[count/2:]
|
|
||||||
lc1.Data["default"] = rndwalk[count:]
|
|
||||||
bc.Data = bcdata[count/2%10:]
|
|
||||||
ui.Render(p, list, g, sp, lc, bc, lc1, p1)
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.Render(p, list, g, sp, lc, bc, lc1, p1)
|
|
||||||
|
|
||||||
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 <-ticker:
|
|
||||||
draw(tickerCount)
|
|
||||||
tickerCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 103 KiB |
Before Width: | Height: | Size: 88 KiB |
@ -1,38 +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 ui "github.com/gizak/termui"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
err := ui.Init()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer ui.Close()
|
|
||||||
|
|
||||||
p := ui.NewParagraph("Press q to QUIT THE DEMO. [There](fg-blue) are other things [that](fg-red) are going to fit in here I think. What do you think? Now is the time for all good [men to](bg-blue) come to the aid of their country. [This is going to be one really really really long line](fg-green) that is going to go together and stuffs and things. Let's see how this thing renders out.\n Here is a new paragraph and stuffs and things. There should be a tab indent at the beginning of the paragraph. Let's see if that worked as well.")
|
|
||||||
p.WrapLength = 48 // this should be at least p.Width - 2
|
|
||||||
p.Height = 20
|
|
||||||
p.Width = 50
|
|
||||||
p.Y = 2
|
|
||||||
p.X = 20
|
|
||||||
p.TextFgColor = ui.ColorWhite
|
|
||||||
p.BorderLabel = "Text Box with Wrapping"
|
|
||||||
p.BorderFg = ui.ColorCyan
|
|
||||||
|
|
||||||
ui.Render(p)
|
|
||||||
|
|
||||||
uiEvents := ui.PollEvents()
|
|
||||||
for {
|
|
||||||
e := <-uiEvents
|
|
||||||
switch e.ID {
|
|
||||||
case "q", "<C-c>":
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -15,8 +15,7 @@ import (
|
|||||||
// logs all events to the termui window
|
// logs all events to the termui window
|
||||||
// stdout can also be redirected to a file and read with `tail -f`
|
// stdout can also be redirected to a file and read with `tail -f`
|
||||||
func main() {
|
func main() {
|
||||||
err := ui.Init()
|
if err := ui.Init(); err != nil {
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer ui.Close()
|
defer ui.Close()
|
9
alignment.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package termui
|
||||||
|
|
||||||
|
type Alignment uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
AlignLeft Alignment = iota
|
||||||
|
AlignCenter
|
||||||
|
AlignRight
|
||||||
|
)
|
151
barchart.go
@ -1,151 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
// BarChart creates multiple bars in a widget:
|
|
||||||
/*
|
|
||||||
bc := termui.NewBarChart()
|
|
||||||
data := []int{3, 2, 5, 3, 9, 5}
|
|
||||||
bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"}
|
|
||||||
bc.BorderLabel = "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 BarChart struct {
|
|
||||||
Block
|
|
||||||
BarColor Attribute
|
|
||||||
TextColor Attribute
|
|
||||||
NumColor Attribute
|
|
||||||
NumFmt func(int) string
|
|
||||||
Data []int
|
|
||||||
DataLabels []string
|
|
||||||
BarWidth int
|
|
||||||
BarGap int
|
|
||||||
CellChar rune
|
|
||||||
labels [][]rune
|
|
||||||
dataNum [][]rune
|
|
||||||
numBar int
|
|
||||||
scale float64
|
|
||||||
max int
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBarChart returns a new *BarChart with current theme.
|
|
||||||
func NewBarChart() *BarChart {
|
|
||||||
return &BarChart{
|
|
||||||
Block: *NewBlock(),
|
|
||||||
BarColor: ThemeAttr("barchart.bar.bg"),
|
|
||||||
NumColor: ThemeAttr("barchart.num.fg"),
|
|
||||||
TextColor: ThemeAttr("barchart.text.fg"),
|
|
||||||
NumFmt: func(n int) string { return fmt.Sprint(n) },
|
|
||||||
BarGap: 1,
|
|
||||||
BarWidth: 3,
|
|
||||||
CellChar: ' ',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bc *BarChart) layout() {
|
|
||||||
bc.numBar = bc.innerArea.Dx() / (bc.BarGap + bc.BarWidth)
|
|
||||||
bc.labels = make([][]rune, bc.numBar)
|
|
||||||
bc.dataNum = make([][]rune, len(bc.Data))
|
|
||||||
|
|
||||||
for i := 0; i < bc.numBar && i < len(bc.DataLabels) && i < len(bc.Data); i++ {
|
|
||||||
bc.labels[i] = trimStr2Runes(bc.DataLabels[i], bc.BarWidth)
|
|
||||||
n := bc.Data[i]
|
|
||||||
s := bc.NumFmt(n)
|
|
||||||
bc.dataNum[i] = trimStr2Runes(s, bc.BarWidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
//bc.max = bc.Data[0] // what if Data is nil? Sometimes when bar graph is nill it produces panic with panic: runtime error: index out of range
|
|
||||||
// Assign a negative value to get maxvalue auto-populates
|
|
||||||
if bc.max == 0 {
|
|
||||||
bc.max = -1
|
|
||||||
}
|
|
||||||
for i := 0; i < len(bc.Data); i++ {
|
|
||||||
if bc.max < bc.Data[i] {
|
|
||||||
bc.max = bc.Data[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bc.scale = float64(bc.max) / float64(bc.innerArea.Dy()-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bc *BarChart) SetMax(max int) {
|
|
||||||
if max > 0 {
|
|
||||||
bc.max = max
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buffer implements Bufferer interface.
|
|
||||||
func (bc *BarChart) Buffer() Buffer {
|
|
||||||
buf := bc.Block.Buffer()
|
|
||||||
bc.layout()
|
|
||||||
|
|
||||||
for i := 0; i < bc.numBar && i < len(bc.Data) && i < len(bc.DataLabels); i++ {
|
|
||||||
h := int(float64(bc.Data[i]) / bc.scale)
|
|
||||||
oftX := i * (bc.BarWidth + bc.BarGap)
|
|
||||||
|
|
||||||
barBg := bc.Bg
|
|
||||||
barFg := bc.BarColor
|
|
||||||
|
|
||||||
if bc.CellChar == ' ' {
|
|
||||||
barBg = bc.BarColor
|
|
||||||
barFg = ColorDefault
|
|
||||||
if bc.BarColor == ColorDefault { // the same as above
|
|
||||||
barBg |= AttrReverse
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// plot bar
|
|
||||||
for j := 0; j < bc.BarWidth; j++ {
|
|
||||||
for k := 0; k < h; k++ {
|
|
||||||
c := Cell{
|
|
||||||
Ch: bc.CellChar,
|
|
||||||
Bg: barBg,
|
|
||||||
Fg: barFg,
|
|
||||||
}
|
|
||||||
|
|
||||||
x := bc.innerArea.Min.X + i*(bc.BarWidth+bc.BarGap) + j
|
|
||||||
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2 - k
|
|
||||||
buf.Set(x, y, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// plot text
|
|
||||||
for j, k := 0, 0; j < len(bc.labels[i]); j++ {
|
|
||||||
w := charWidth(bc.labels[i][j])
|
|
||||||
c := Cell{
|
|
||||||
Ch: bc.labels[i][j],
|
|
||||||
Bg: bc.Bg,
|
|
||||||
Fg: bc.TextColor,
|
|
||||||
}
|
|
||||||
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 1
|
|
||||||
x := bc.innerArea.Min.X + oftX + k
|
|
||||||
buf.Set(x, y, c)
|
|
||||||
k += w
|
|
||||||
}
|
|
||||||
// plot num
|
|
||||||
for j := 0; j < len(bc.dataNum[i]); j++ {
|
|
||||||
c := Cell{
|
|
||||||
Ch: bc.dataNum[i][j],
|
|
||||||
Fg: bc.NumColor,
|
|
||||||
Bg: barBg,
|
|
||||||
}
|
|
||||||
|
|
||||||
if h == 0 {
|
|
||||||
c.Bg = bc.Bg
|
|
||||||
}
|
|
||||||
x := bc.innerArea.Min.X + oftX + (bc.BarWidth-len(bc.dataNum[i]))/2 + j
|
|
||||||
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2
|
|
||||||
buf.Set(x, y, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
|
261
block.go
@ -4,237 +4,92 @@
|
|||||||
|
|
||||||
package termui
|
package termui
|
||||||
|
|
||||||
import "image"
|
import (
|
||||||
|
"image"
|
||||||
|
)
|
||||||
|
|
||||||
// Hline is a horizontal line.
|
// Block is the base struct inherited by all widgets.
|
||||||
type Hline struct {
|
// Block manages size, border, and title.
|
||||||
X int
|
// It implements 2 of the 3 methods needed for `Drawable` interface: `GetRect` and `SetRect`.
|
||||||
Y int
|
|
||||||
Len int
|
|
||||||
Fg Attribute
|
|
||||||
Bg Attribute
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vline is a vertical line.
|
|
||||||
type Vline struct {
|
|
||||||
X int
|
|
||||||
Y int
|
|
||||||
Len int
|
|
||||||
Fg Attribute
|
|
||||||
Bg Attribute
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buffer draws a horizontal line.
|
|
||||||
func (l Hline) Buffer() Buffer {
|
|
||||||
if l.Len <= 0 {
|
|
||||||
return NewBuffer()
|
|
||||||
}
|
|
||||||
return NewFilledBuffer(l.X, l.Y, l.X+l.Len, l.Y+1, HORIZONTAL_LINE, l.Fg, l.Bg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buffer draws a vertical line.
|
|
||||||
func (l Vline) Buffer() Buffer {
|
|
||||||
if l.Len <= 0 {
|
|
||||||
return NewBuffer()
|
|
||||||
}
|
|
||||||
return NewFilledBuffer(l.X, l.Y, l.X+1, l.Y+l.Len, VERTICAL_LINE, l.Fg, l.Bg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buffer draws a box border.
|
|
||||||
func (b Block) drawBorder(buf Buffer) {
|
|
||||||
if !b.Border {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
min := b.area.Min
|
|
||||||
max := b.area.Max
|
|
||||||
|
|
||||||
x0 := min.X
|
|
||||||
y0 := min.Y
|
|
||||||
x1 := max.X - 1
|
|
||||||
y1 := max.Y - 1
|
|
||||||
|
|
||||||
// draw lines
|
|
||||||
if b.BorderTop {
|
|
||||||
buf.Merge(Hline{x0, y0, x1 - x0, b.BorderFg, b.BorderBg}.Buffer())
|
|
||||||
}
|
|
||||||
if b.BorderBottom {
|
|
||||||
buf.Merge(Hline{x0, y1, x1 - x0, b.BorderFg, b.BorderBg}.Buffer())
|
|
||||||
}
|
|
||||||
if b.BorderLeft {
|
|
||||||
buf.Merge(Vline{x0, y0, y1 - y0, b.BorderFg, b.BorderBg}.Buffer())
|
|
||||||
}
|
|
||||||
if b.BorderRight {
|
|
||||||
buf.Merge(Vline{x1, y0, y1 - y0, b.BorderFg, b.BorderBg}.Buffer())
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw corners
|
|
||||||
if b.BorderTop && b.BorderLeft && b.area.Dx() > 0 && b.area.Dy() > 0 {
|
|
||||||
buf.Set(x0, y0, Cell{TOP_LEFT, b.BorderFg, b.BorderBg})
|
|
||||||
}
|
|
||||||
if b.BorderTop && b.BorderRight && b.area.Dx() > 1 && b.area.Dy() > 0 {
|
|
||||||
buf.Set(x1, y0, Cell{TOP_RIGHT, b.BorderFg, b.BorderBg})
|
|
||||||
}
|
|
||||||
if b.BorderBottom && b.BorderLeft && b.area.Dx() > 0 && b.area.Dy() > 1 {
|
|
||||||
buf.Set(x0, y1, Cell{BOTTOM_LEFT, b.BorderFg, b.BorderBg})
|
|
||||||
}
|
|
||||||
if b.BorderBottom && b.BorderRight && b.area.Dx() > 1 && b.area.Dy() > 1 {
|
|
||||||
buf.Set(x1, y1, Cell{BOTTOM_RIGHT, b.BorderFg, b.BorderBg})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b Block) drawBorderLabel(buf Buffer) {
|
|
||||||
maxTxtW := b.area.Dx() - 2
|
|
||||||
tx := DTrimTxCls(DefaultTxBuilder.Build(b.BorderLabel, b.BorderLabelFg, b.BorderLabelBg), maxTxtW)
|
|
||||||
|
|
||||||
for i, w := 0, 0; i < len(tx); i++ {
|
|
||||||
buf.Set(b.area.Min.X+1+w, b.area.Min.Y, tx[i])
|
|
||||||
w += tx[i].Width()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
type Block struct {
|
||||||
area image.Rectangle
|
|
||||||
innerArea image.Rectangle
|
|
||||||
X int
|
|
||||||
Y int
|
|
||||||
Border bool
|
Border bool
|
||||||
BorderFg Attribute
|
BorderStyle Style
|
||||||
BorderBg Attribute
|
|
||||||
BorderLeft bool
|
BorderLeft bool
|
||||||
BorderRight bool
|
BorderRight bool
|
||||||
BorderTop bool
|
BorderTop bool
|
||||||
BorderBottom bool
|
BorderBottom bool
|
||||||
BorderLabel string
|
|
||||||
BorderLabelFg Attribute
|
image.Rectangle
|
||||||
BorderLabelBg Attribute
|
Inner image.Rectangle
|
||||||
Display bool
|
|
||||||
Bg Attribute
|
Title string
|
||||||
Width int
|
TitleStyle Style
|
||||||
Height int
|
|
||||||
PaddingTop int
|
|
||||||
PaddingBottom int
|
|
||||||
PaddingLeft int
|
|
||||||
PaddingRight int
|
|
||||||
id string
|
|
||||||
Float Align
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBlock returns a *Block which inherits styles from current theme.
|
|
||||||
func NewBlock() *Block {
|
func NewBlock() *Block {
|
||||||
return &Block{
|
return &Block{
|
||||||
Display: true,
|
|
||||||
Border: true,
|
Border: true,
|
||||||
|
BorderStyle: Theme.Block.Border,
|
||||||
BorderLeft: true,
|
BorderLeft: true,
|
||||||
BorderRight: true,
|
BorderRight: true,
|
||||||
BorderTop: true,
|
BorderTop: true,
|
||||||
BorderBottom: true,
|
BorderBottom: true,
|
||||||
BorderBg: ThemeAttr("border.bg"),
|
|
||||||
BorderFg: ThemeAttr("border.fg"),
|
TitleStyle: Theme.Block.Title,
|
||||||
BorderLabelBg: ThemeAttr("label.bg"),
|
|
||||||
BorderLabelFg: ThemeAttr("label.fg"),
|
|
||||||
Bg: ThemeAttr("block.bg"),
|
|
||||||
Width: 2,
|
|
||||||
Height: 2,
|
|
||||||
id: GenId(),
|
|
||||||
Float: AlignNone,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b Block) Id() string {
|
func (self *Block) drawBorder(buf *Buffer) {
|
||||||
return b.id
|
if !self.Border {
|
||||||
}
|
return
|
||||||
|
|
||||||
// Align computes box model
|
|
||||||
func (b *Block) Align() {
|
|
||||||
// outer
|
|
||||||
b.area.Min.X = 0
|
|
||||||
b.area.Min.Y = 0
|
|
||||||
b.area.Max.X = b.Width
|
|
||||||
b.area.Max.Y = b.Height
|
|
||||||
|
|
||||||
// float
|
|
||||||
b.area = AlignArea(TermRect(), b.area, b.Float)
|
|
||||||
b.area = MoveArea(b.area, b.X, b.Y)
|
|
||||||
|
|
||||||
// inner
|
|
||||||
b.innerArea.Min.X = b.area.Min.X + b.PaddingLeft
|
|
||||||
b.innerArea.Min.Y = b.area.Min.Y + b.PaddingTop
|
|
||||||
b.innerArea.Max.X = b.area.Max.X - b.PaddingRight
|
|
||||||
b.innerArea.Max.Y = b.area.Max.Y - b.PaddingBottom
|
|
||||||
|
|
||||||
if b.Border {
|
|
||||||
if b.BorderLeft {
|
|
||||||
b.innerArea.Min.X++
|
|
||||||
}
|
}
|
||||||
if b.BorderRight {
|
|
||||||
b.innerArea.Max.X--
|
verticalCell := Cell{VERTICAL_LINE, self.BorderStyle}
|
||||||
|
horizontalCell := Cell{HORIZONTAL_LINE, self.BorderStyle}
|
||||||
|
|
||||||
|
// draw lines
|
||||||
|
if self.BorderTop {
|
||||||
|
buf.Fill(horizontalCell, image.Rect(self.Min.X, self.Min.Y, self.Max.X, self.Min.Y+1))
|
||||||
}
|
}
|
||||||
if b.BorderTop {
|
if self.BorderBottom {
|
||||||
b.innerArea.Min.Y++
|
buf.Fill(horizontalCell, image.Rect(self.Min.X, self.Max.Y-1, self.Max.X, self.Max.Y))
|
||||||
}
|
}
|
||||||
if b.BorderBottom {
|
if self.BorderLeft {
|
||||||
b.innerArea.Max.Y--
|
buf.Fill(verticalCell, image.Rect(self.Min.X, self.Min.Y, self.Min.X+1, self.Max.Y))
|
||||||
}
|
}
|
||||||
|
if self.BorderRight {
|
||||||
|
buf.Fill(verticalCell, image.Rect(self.Max.X-1, self.Min.Y, self.Max.X, self.Max.Y))
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw corners
|
||||||
|
if self.BorderTop && self.BorderLeft {
|
||||||
|
buf.SetCell(Cell{TOP_LEFT, self.BorderStyle}, self.Min)
|
||||||
|
}
|
||||||
|
if self.BorderTop && self.BorderRight {
|
||||||
|
buf.SetCell(Cell{TOP_RIGHT, self.BorderStyle}, image.Pt(self.Max.X-1, self.Min.Y))
|
||||||
|
}
|
||||||
|
if self.BorderBottom && self.BorderLeft {
|
||||||
|
buf.SetCell(Cell{BOTTOM_LEFT, self.BorderStyle}, image.Pt(self.Min.X, self.Max.Y-1))
|
||||||
|
}
|
||||||
|
if self.BorderBottom && self.BorderRight {
|
||||||
|
buf.SetCell(Cell{BOTTOM_RIGHT, self.BorderStyle}, self.Max.Sub(image.Pt(1, 1)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// InnerBounds returns the internal bounds of the block after aligning and
|
func (self *Block) Draw(buf *Buffer) {
|
||||||
// calculating the padding and border, if any.
|
self.drawBorder(buf)
|
||||||
func (b *Block) InnerBounds() image.Rectangle {
|
buf.SetString(
|
||||||
b.Align()
|
self.Title,
|
||||||
return b.innerArea
|
self.TitleStyle,
|
||||||
|
image.Pt(self.Min.X+2, self.Min.Y),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buffer implements Bufferer interface.
|
func (self *Block) SetRect(x1, y1, x2, y2 int) {
|
||||||
// Draw background and border (if any).
|
self.Rectangle = image.Rect(x1, y1, x2, y2)
|
||||||
func (b *Block) Buffer() Buffer {
|
self.Inner = image.Rect(self.Min.X+1, self.Min.Y+1, self.Max.X-1, self.Max.Y-1)
|
||||||
b.Align()
|
|
||||||
|
|
||||||
buf := NewBuffer()
|
|
||||||
buf.SetArea(b.area)
|
|
||||||
buf.Fill(' ', ColorDefault, b.Bg)
|
|
||||||
|
|
||||||
b.drawBorder(buf)
|
|
||||||
b.drawBorderLabel(buf)
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHeight implements GridBufferer.
|
func (self *Block) GetRect() image.Rectangle {
|
||||||
// It returns current height of the block.
|
return self.Rectangle
|
||||||
func (b Block) GetHeight() int {
|
|
||||||
return b.Height
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetX implements GridBufferer interface, which sets block's x position.
|
|
||||||
func (b *Block) SetX(x int) {
|
|
||||||
b.X = x
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetY implements GridBufferer interface, it sets y position for block.
|
|
||||||
func (b *Block) SetY(y int) {
|
|
||||||
b.Y = y
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetWidth implements GridBuffer interface, it sets block's width.
|
|
||||||
func (b *Block) SetWidth(w int) {
|
|
||||||
b.Width = w
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b Block) InnerWidth() int {
|
|
||||||
return b.innerArea.Dx()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b Block) InnerHeight() int {
|
|
||||||
return b.innerArea.Dy()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b Block) InnerX() int {
|
|
||||||
return b.innerArea.Min.X
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b Block) InnerY() int { return b.innerArea.Min.Y }
|
|
||||||
|
@ -1,20 +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 !windows
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
const TOP_RIGHT = '┐'
|
|
||||||
const VERTICAL_LINE = '│'
|
|
||||||
const HORIZONTAL_LINE = '─'
|
|
||||||
const TOP_LEFT = '┌'
|
|
||||||
const BOTTOM_RIGHT = '┘'
|
|
||||||
const BOTTOM_LEFT = '└'
|
|
||||||
const VERTICAL_LEFT = '┤'
|
|
||||||
const VERTICAL_RIGHT = '├'
|
|
||||||
const HORIZONTAL_DOWN = '┬'
|
|
||||||
const HORIZONTAL_UP = '┴'
|
|
||||||
const QUOTA_LEFT = '«'
|
|
||||||
const QUOTA_RIGHT = '»'
|
|
@ -1,70 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestBlockFloat(t *testing.T) {
|
|
||||||
Init()
|
|
||||||
defer Close()
|
|
||||||
|
|
||||||
b := NewBlock()
|
|
||||||
b.X = 10
|
|
||||||
b.Y = 20
|
|
||||||
|
|
||||||
b.Float = AlignCenter
|
|
||||||
b.Align()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBlockInnerBounds(t *testing.T) {
|
|
||||||
Init()
|
|
||||||
defer Close()
|
|
||||||
|
|
||||||
b := NewBlock()
|
|
||||||
b.X = 10
|
|
||||||
b.Y = 11
|
|
||||||
b.Width = 12
|
|
||||||
b.Height = 13
|
|
||||||
|
|
||||||
assert := func(name string, x, y, w, h int) {
|
|
||||||
t.Log(name)
|
|
||||||
area := b.InnerBounds()
|
|
||||||
cx := area.Min.X
|
|
||||||
cy := area.Min.Y
|
|
||||||
cw := area.Dx()
|
|
||||||
ch := area.Dy()
|
|
||||||
|
|
||||||
if cx != x {
|
|
||||||
t.Errorf("expected x to be %d but got %d", x, cx)
|
|
||||||
}
|
|
||||||
if cy != y {
|
|
||||||
t.Errorf("expected y to be %d but got %d\n%+v", y, cy, area)
|
|
||||||
}
|
|
||||||
if cw != w {
|
|
||||||
t.Errorf("expected width to be %d but got %d", w, cw)
|
|
||||||
}
|
|
||||||
if ch != h {
|
|
||||||
t.Errorf("expected height to be %d but got %d", h, ch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.Border = false
|
|
||||||
assert("no border, no padding", 10, 11, 12, 13)
|
|
||||||
|
|
||||||
b.Border = true
|
|
||||||
assert("border, no padding", 11, 12, 10, 11)
|
|
||||||
|
|
||||||
b.PaddingBottom = 2
|
|
||||||
assert("border, 2b padding", 11, 12, 10, 9)
|
|
||||||
|
|
||||||
b.PaddingTop = 3
|
|
||||||
assert("border, 2b 3t padding", 11, 15, 10, 6)
|
|
||||||
|
|
||||||
b.PaddingLeft = 4
|
|
||||||
assert("border, 2b 3t 4l padding", 15, 15, 6, 6)
|
|
||||||
|
|
||||||
b.PaddingRight = 5
|
|
||||||
assert("border, 2b 3t 4l 5r padding", 15, 15, 1, 6)
|
|
||||||
}
|
|
@ -1,20 +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 windows
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
const TOP_RIGHT = '+'
|
|
||||||
const VERTICAL_LINE = '|'
|
|
||||||
const HORIZONTAL_LINE = '-'
|
|
||||||
const TOP_LEFT = '+'
|
|
||||||
const BOTTOM_RIGHT = '+'
|
|
||||||
const BOTTOM_LEFT = '+'
|
|
||||||
const VERTICAL_LEFT = '+'
|
|
||||||
const VERTICAL_RIGHT = '+'
|
|
||||||
const HORIZONTAL_DOWN = '+'
|
|
||||||
const HORIZONTAL_UP = '+'
|
|
||||||
const QUOTA_LEFT = '<'
|
|
||||||
const QUOTA_RIGHT = '>'
|
|
141
buffer.go
@ -4,103 +4,68 @@
|
|||||||
|
|
||||||
package termui
|
package termui
|
||||||
|
|
||||||
import "image"
|
import (
|
||||||
|
"image"
|
||||||
|
)
|
||||||
|
|
||||||
// Cell is a rune with assigned Fg and Bg
|
// Cell represents a viewable terminal cell
|
||||||
type Cell struct {
|
type Cell struct {
|
||||||
Ch rune
|
Rune rune
|
||||||
Fg Attribute
|
Style Style
|
||||||
Bg Attribute
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buffer is a renderable rectangle cell data container.
|
var CellClear = Cell{
|
||||||
|
Rune: ' ',
|
||||||
|
Style: StyleClear,
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCell takes 1 to 2 arguments
|
||||||
|
// 1st argument = rune
|
||||||
|
// 2nd argument = optional style
|
||||||
|
func NewCell(rune rune, args ...interface{}) Cell {
|
||||||
|
style := StyleClear
|
||||||
|
if len(args) == 1 {
|
||||||
|
style = args[0].(Style)
|
||||||
|
}
|
||||||
|
return Cell{
|
||||||
|
Rune: rune,
|
||||||
|
Style: style,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer represents a section of a terminal and is a renderable rectangle of cells.
|
||||||
type Buffer struct {
|
type Buffer struct {
|
||||||
Area image.Rectangle // selected drawing area
|
image.Rectangle
|
||||||
CellMap map[image.Point]Cell
|
CellMap map[image.Point]Cell
|
||||||
}
|
}
|
||||||
|
|
||||||
// At returns the cell at (x,y).
|
func NewBuffer(r image.Rectangle) *Buffer {
|
||||||
func (b Buffer) At(x, y int) Cell {
|
buf := &Buffer{
|
||||||
return b.CellMap[image.Pt(x, y)]
|
Rectangle: r,
|
||||||
}
|
|
||||||
|
|
||||||
// Set assigns a char to (x,y)
|
|
||||||
func (b Buffer) Set(x, y int, c Cell) {
|
|
||||||
b.CellMap[image.Pt(x, y)] = c
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync sets drawing area to the buffer's bound
|
|
||||||
func (b *Buffer) Sync() {
|
|
||||||
b.SetArea(b.Bounds())
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCell returns a new cell
|
|
||||||
func NewCell(ch rune, fg, bg Attribute) Cell {
|
|
||||||
return Cell{ch, fg, bg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge merges bs Buffers onto b
|
|
||||||
func (b *Buffer) Merge(bs ...Buffer) {
|
|
||||||
for _, buf := range bs {
|
|
||||||
for p, v := range buf.CellMap {
|
|
||||||
b.Set(p.X, p.Y, v)
|
|
||||||
}
|
|
||||||
b.SetArea(b.Area.Union(buf.Area))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBuffer returns a new Buffer
|
|
||||||
func NewBuffer() Buffer {
|
|
||||||
return Buffer{
|
|
||||||
CellMap: make(map[image.Point]Cell),
|
CellMap: make(map[image.Point]Cell),
|
||||||
Area: image.Rectangle{}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill fills the Buffer b with ch,fg and bg.
|
|
||||||
func (b Buffer) Fill(ch rune, fg, bg Attribute) {
|
|
||||||
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.Set(x, y, Cell{ch, fg, bg})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFilledBuffer returns a new Buffer filled with ch, fb and bg.
|
|
||||||
func NewFilledBuffer(x0, y0, x1, y1 int, ch rune, fg, bg Attribute) 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.Set(x, y, Cell{ch, fg, bg})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
buf.Fill(CellClear, r) // clears out area
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *Buffer) GetCell(p image.Point) Cell {
|
||||||
|
return self.CellMap[p]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Buffer) SetCell(c Cell, p image.Point) {
|
||||||
|
self.CellMap[p] = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Buffer) Fill(c Cell, rect image.Rectangle) {
|
||||||
|
for x := rect.Min.X; x < rect.Max.X; x++ {
|
||||||
|
for y := rect.Min.Y; y < rect.Max.Y; y++ {
|
||||||
|
self.SetCell(c, image.Pt(x, y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Buffer) SetString(s string, style Style, p image.Point) {
|
||||||
|
for i, char := range s {
|
||||||
|
self.SetCell(Cell{char, style}, image.Pt(p.X+i, p.Y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,23 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"image"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBufferUnion(t *testing.T) {
|
|
||||||
b0 := NewBuffer()
|
|
||||||
b1 := NewBuffer()
|
|
||||||
|
|
||||||
b1.Area.Max.X = 100
|
|
||||||
b1.Area.Max.Y = 100
|
|
||||||
b0.Area.Max.X = 50
|
|
||||||
b0.Merge(b1)
|
|
||||||
if b0.Area.Max.X != 100 {
|
|
||||||
t.Errorf("Buffer.Merge unions Area failed: should:%v, actual %v,%v", image.Rect(0, 0, 50, 0).Union(image.Rect(0, 0, 100, 100)), b1.Area, b0.Area)
|
|
||||||
}
|
|
||||||
}
|
|
115
canvas.go
@ -1,72 +1,63 @@
|
|||||||
// 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.
|
|
||||||
|
|
||||||
package termui
|
package termui
|
||||||
|
|
||||||
/*
|
import (
|
||||||
dots:
|
"image"
|
||||||
,___,
|
)
|
||||||
|1 4|
|
|
||||||
|2 5|
|
|
||||||
|3 6|
|
|
||||||
|7 8|
|
|
||||||
`````
|
|
||||||
*/
|
|
||||||
|
|
||||||
var brailleBase = '\u2800'
|
type Canvas struct {
|
||||||
|
CellMap map[image.Point]Cell
|
||||||
var brailleOftMap = [4][2]rune{
|
Block
|
||||||
{'\u0001', '\u0008'},
|
|
||||||
{'\u0002', '\u0010'},
|
|
||||||
{'\u0004', '\u0020'},
|
|
||||||
{'\u0040', '\u0080'}}
|
|
||||||
|
|
||||||
// Canvas contains drawing map: i,j -> rune
|
|
||||||
type Canvas map[[2]int]rune
|
|
||||||
|
|
||||||
// NewCanvas returns an empty Canvas
|
|
||||||
func NewCanvas() Canvas {
|
|
||||||
return make(map[[2]int]rune)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func chOft(x, y int) rune {
|
func NewCanvas() *Canvas {
|
||||||
return brailleOftMap[y%4][x%2]
|
return &Canvas{
|
||||||
}
|
Block: *NewBlock(),
|
||||||
|
CellMap: make(map[image.Point]Cell),
|
||||||
func (c Canvas) rawCh(x, y int) rune {
|
|
||||||
if ch, ok := c[[2]int{x, y}]; ok {
|
|
||||||
return ch
|
|
||||||
}
|
}
|
||||||
return '\u0000' //brailleOffset
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// return coordinate in terminal
|
// points given as arguments correspond to dots within a braille character
|
||||||
func chPos(x, y int) (int, int) {
|
// and therefore have 2x4 times the resolution of a normal cell
|
||||||
return y / 4, x / 2
|
func (self *Canvas) Line(p0, p1 image.Point, color Color) {
|
||||||
}
|
leftPoint, rightPoint := p0, p1
|
||||||
|
if leftPoint.X > rightPoint.X {
|
||||||
// Set sets a point (x,y) in the virtual coordinate
|
leftPoint, rightPoint = rightPoint, leftPoint
|
||||||
func (c Canvas) Set(x, y int) {
|
}
|
||||||
i, j := chPos(x, y)
|
|
||||||
ch := c.rawCh(i, j)
|
xDistance := AbsInt(leftPoint.X - rightPoint.X)
|
||||||
ch |= chOft(x, y)
|
yDistance := AbsInt(leftPoint.Y - rightPoint.Y)
|
||||||
c[[2]int{i, j}] = ch
|
slope := float64(yDistance) / float64(xDistance)
|
||||||
}
|
slopeDirection := 1
|
||||||
|
if rightPoint.Y < leftPoint.Y {
|
||||||
// Unset removes point (x,y)
|
slopeDirection = -1
|
||||||
func (c Canvas) Unset(x, y int) {
|
}
|
||||||
i, j := chPos(x, y)
|
|
||||||
ch := c.rawCh(i, j)
|
targetYCoordinate := float64(leftPoint.Y)
|
||||||
ch &= ^chOft(x, y)
|
currentYCoordinate := leftPoint.Y
|
||||||
c[[2]int{i, j}] = ch
|
for i := leftPoint.X; i < rightPoint.X; i++ {
|
||||||
}
|
targetYCoordinate += (slope * float64(slopeDirection))
|
||||||
|
if currentYCoordinate == int(targetYCoordinate) {
|
||||||
// Buffer returns un-styled points
|
point := image.Pt(i/2, currentYCoordinate/4)
|
||||||
func (c Canvas) Buffer() Buffer {
|
self.CellMap[point] = Cell{
|
||||||
buf := NewBuffer()
|
self.CellMap[point].Rune | BRAILLE[currentYCoordinate%4][i%2],
|
||||||
for k, v := range c {
|
NewStyle(color),
|
||||||
buf.Set(k[0], k[1], Cell{Ch: v + brailleBase})
|
}
|
||||||
|
}
|
||||||
|
for currentYCoordinate != int(targetYCoordinate) {
|
||||||
|
point := image.Pt(i/2, currentYCoordinate/4)
|
||||||
|
self.CellMap[point] = Cell{
|
||||||
|
self.CellMap[point].Rune | BRAILLE[currentYCoordinate%4][i%2],
|
||||||
|
NewStyle(color),
|
||||||
|
}
|
||||||
|
currentYCoordinate += slopeDirection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Canvas) Draw(buf *Buffer) {
|
||||||
|
for point, cell := range self.CellMap {
|
||||||
|
if point.In(self.Rectangle) {
|
||||||
|
buf.SetCell(Cell{cell.Rune + BRAILLE_OFFSET, cell.Style}, point)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return buf
|
|
||||||
}
|
}
|
||||||
|
@ -1,55 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCanvasSet(t *testing.T) {
|
|
||||||
c := NewCanvas()
|
|
||||||
c.Set(0, 0)
|
|
||||||
c.Set(0, 1)
|
|
||||||
c.Set(0, 2)
|
|
||||||
c.Set(0, 3)
|
|
||||||
c.Set(1, 3)
|
|
||||||
c.Set(2, 3)
|
|
||||||
c.Set(3, 3)
|
|
||||||
c.Set(4, 3)
|
|
||||||
c.Set(5, 3)
|
|
||||||
spew.Dump(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCanvasUnset(t *testing.T) {
|
|
||||||
c := NewCanvas()
|
|
||||||
c.Set(0, 0)
|
|
||||||
c.Set(0, 1)
|
|
||||||
c.Set(0, 2)
|
|
||||||
c.Unset(0, 2)
|
|
||||||
spew.Dump(c)
|
|
||||||
c.Unset(0, 3)
|
|
||||||
spew.Dump(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCanvasBuffer(t *testing.T) {
|
|
||||||
c := NewCanvas()
|
|
||||||
c.Set(0, 0)
|
|
||||||
c.Set(0, 1)
|
|
||||||
c.Set(0, 2)
|
|
||||||
c.Set(0, 3)
|
|
||||||
c.Set(1, 3)
|
|
||||||
c.Set(2, 3)
|
|
||||||
c.Set(3, 3)
|
|
||||||
c.Set(4, 3)
|
|
||||||
c.Set(5, 3)
|
|
||||||
c.Set(6, 3)
|
|
||||||
c.Set(7, 2)
|
|
||||||
c.Set(8, 1)
|
|
||||||
c.Set(9, 0)
|
|
||||||
bufs := c.Buffer()
|
|
||||||
spew.Dump(bufs)
|
|
||||||
}
|
|
@ -16,7 +16,7 @@ List of events:
|
|||||||
<MouseLeft> <MouseRight> <MouseMiddle>
|
<MouseLeft> <MouseRight> <MouseMiddle>
|
||||||
<MouseWheelUp> <MouseWheelDown>
|
<MouseWheelUp> <MouseWheelDown>
|
||||||
keyboard events:
|
keyboard events:
|
||||||
any uppercase or lowercase letter or a set of two letters like j or jj or J or JJ
|
any uppercase or lowercase letter like j or J
|
||||||
<C-d> etc
|
<C-d> etc
|
||||||
<M-d> etc
|
<M-d> etc
|
||||||
<Up> <Down> <Left> <Right>
|
<Up> <Down> <Left> <Right>
|
||||||
|
@ -1,37 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
var ps = []string{
|
|
||||||
"",
|
|
||||||
"/",
|
|
||||||
"/a",
|
|
||||||
"/b",
|
|
||||||
"/a/c",
|
|
||||||
"/a/b",
|
|
||||||
"/a/b/c",
|
|
||||||
"/a/b/c/d",
|
|
||||||
"/a/b/c/d/"}
|
|
||||||
|
|
||||||
func TestMatchScore(t *testing.T) {
|
|
||||||
chk := func(a, b string, s bool) {
|
|
||||||
if c := isPathMatch(a, b); c != s {
|
|
||||||
t.Errorf("\na:%s\nb:%s\nshould:%t\nactual:%t", a, b, s, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chk(ps[1], ps[1], true)
|
|
||||||
chk(ps[1], ps[2], true)
|
|
||||||
chk(ps[2], ps[1], false)
|
|
||||||
chk(ps[4], ps[1], false)
|
|
||||||
chk(ps[6], ps[2], false)
|
|
||||||
chk(ps[4], ps[5], false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCrtEvt(t *testing.T) {
|
|
||||||
|
|
||||||
}
|
|
108
gauge.go
@ -1,108 +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.
|
|
||||||
|
|
||||||
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.BorderLabel = "Slim Gauge"
|
|
||||||
g.BarColor = termui.ColorRed
|
|
||||||
g.PercentColor = termui.ColorBlue
|
|
||||||
*/
|
|
||||||
|
|
||||||
const ColorUndef Attribute = Attribute(^uint16(0))
|
|
||||||
|
|
||||||
type Gauge struct {
|
|
||||||
Block
|
|
||||||
Percent int
|
|
||||||
BarColor Attribute
|
|
||||||
PercentColor Attribute
|
|
||||||
PercentColorHighlighted Attribute
|
|
||||||
Label string
|
|
||||||
LabelAlign Align
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewGauge return a new gauge with current theme.
|
|
||||||
func NewGauge() *Gauge {
|
|
||||||
g := &Gauge{
|
|
||||||
Block: *NewBlock(),
|
|
||||||
PercentColor: ThemeAttr("gauge.percent.fg"),
|
|
||||||
BarColor: ThemeAttr("gauge.bar.bg"),
|
|
||||||
Label: "{{percent}}%",
|
|
||||||
LabelAlign: AlignCenter,
|
|
||||||
PercentColorHighlighted: ColorUndef,
|
|
||||||
}
|
|
||||||
g.Width = 12
|
|
||||||
g.Height = 5
|
|
||||||
return g
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buffer implements Bufferer interface.
|
|
||||||
func (g *Gauge) Buffer() Buffer {
|
|
||||||
buf := g.Block.Buffer()
|
|
||||||
|
|
||||||
// plot bar
|
|
||||||
w := g.Percent * g.innerArea.Dx() / 100
|
|
||||||
for i := 0; i < g.innerArea.Dy(); i++ {
|
|
||||||
for j := 0; j < w; j++ {
|
|
||||||
c := Cell{}
|
|
||||||
c.Ch = ' '
|
|
||||||
c.Bg = g.BarColor
|
|
||||||
if c.Bg == ColorDefault {
|
|
||||||
c.Bg |= AttrReverse
|
|
||||||
}
|
|
||||||
buf.Set(g.innerArea.Min.X+j, g.innerArea.Min.Y+i, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// plot percentage
|
|
||||||
s := strings.Replace(g.Label, "{{percent}}", strconv.Itoa(g.Percent), -1)
|
|
||||||
pry := g.innerArea.Min.Y + g.innerArea.Dy()/2
|
|
||||||
rs := str2runes(s)
|
|
||||||
var pos int
|
|
||||||
switch g.LabelAlign {
|
|
||||||
case AlignLeft:
|
|
||||||
pos = 0
|
|
||||||
|
|
||||||
case AlignCenter:
|
|
||||||
pos = (g.innerArea.Dx() - strWidth(s)) / 2
|
|
||||||
|
|
||||||
case AlignRight:
|
|
||||||
pos = g.innerArea.Dx() - strWidth(s) - 1
|
|
||||||
}
|
|
||||||
pos += g.innerArea.Min.X
|
|
||||||
|
|
||||||
for i, v := range rs {
|
|
||||||
c := Cell{
|
|
||||||
Ch: v,
|
|
||||||
Fg: g.PercentColor,
|
|
||||||
}
|
|
||||||
|
|
||||||
if w+g.innerArea.Min.X > pos+i {
|
|
||||||
c.Bg = g.BarColor
|
|
||||||
if c.Bg == ColorDefault {
|
|
||||||
c.Bg |= AttrReverse
|
|
||||||
}
|
|
||||||
|
|
||||||
if g.PercentColorHighlighted != ColorUndef {
|
|
||||||
c.Fg = g.PercentColorHighlighted
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
c.Bg = g.Block.Bg
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.Set(1+pos+i, pry, c)
|
|
||||||
}
|
|
||||||
return buf
|
|
||||||
}
|
|
9
go.mod
@ -1,11 +1,12 @@
|
|||||||
module github.com/gizak/termui
|
module github.com/gizak/termui
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.0
|
github.com/google/pprof v0.0.0-20190109223431-e84dfd68c163 // indirect
|
||||||
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.2
|
github.com/mattn/go-runewidth v0.0.2
|
||||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7
|
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7
|
||||||
github.com/nsf/termbox-go v0.0.0-20180613055208-5c94acc5e6eb
|
github.com/nsf/termbox-go v0.0.0-20180613055208-5c94acc5e6eb
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
golang.org/x/arch v0.0.0-20181203225421-5a4828bb7045 // indirect
|
||||||
github.com/stretchr/testify v1.2.2
|
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc // indirect
|
||||||
golang.org/x/net v0.0.0-20180801234040-f4c29de78a2a
|
golang.org/x/sys v0.0.0-20190116161447-11f53e031339 // indirect
|
||||||
)
|
)
|
||||||
|
18
go.sum
@ -1,14 +1,16 @@
|
|||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
github.com/google/pprof v0.0.0-20190109223431-e84dfd68c163 h1:beB+Da4k9B1zmgag78k3k1Bx4L/fdWr5FwNa0f8RxmY=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/google/pprof v0.0.0-20190109223431-e84dfd68c163/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c=
|
||||||
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
|
github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
|
||||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
|
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
|
||||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||||
github.com/nsf/termbox-go v0.0.0-20180613055208-5c94acc5e6eb h1:YahEjAGkJtCrkqgVHhX6n8ZX+CZ3hDRL9fjLYugLfSs=
|
github.com/nsf/termbox-go v0.0.0-20180613055208-5c94acc5e6eb h1:YahEjAGkJtCrkqgVHhX6n8ZX+CZ3hDRL9fjLYugLfSs=
|
||||||
github.com/nsf/termbox-go v0.0.0-20180613055208-5c94acc5e6eb/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
|
github.com/nsf/termbox-go v0.0.0-20180613055208-5c94acc5e6eb/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
golang.org/x/arch v0.0.0-20181203225421-5a4828bb7045 h1:Pn8fQdvx+z1avAi7fdM2kRYWQNxGlavNDSyzrQg2SsU=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
golang.org/x/arch v0.0.0-20181203225421-5a4828bb7045/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8=
|
||||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc h1:F5tKCVGp+MUAHhKp5MZtGqAlGX3+oCsiL1Q629FL90M=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/net v0.0.0-20180801234040-f4c29de78a2a h1:8fCF9zjAir2SP3N+axz9xs+0r4V8dqPzqsWO10t8zoo=
|
golang.org/x/sys v0.0.0-20190116161447-11f53e031339 h1:g/Jesu8+QLnA0CPzF3E1pURg0Byr7i6jLoX5sqjcAh0=
|
||||||
golang.org/x/net v0.0.0-20180801234040-f4c29de78a2a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
378
grid.go
@ -4,276 +4,154 @@
|
|||||||
|
|
||||||
package termui
|
package termui
|
||||||
|
|
||||||
// GridBufferer introduces a Bufferer that can be manipulated by Grid.
|
type gridItemType uint
|
||||||
type GridBufferer interface {
|
|
||||||
Bufferer
|
|
||||||
GetHeight() int
|
|
||||||
SetWidth(int)
|
|
||||||
SetX(int)
|
|
||||||
SetY(int)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Row builds a layout tree
|
const (
|
||||||
type Row struct {
|
col gridItemType = 0
|
||||||
Cols []*Row //children
|
row gridItemType = 1
|
||||||
Widget GridBufferer // root
|
)
|
||||||
X int
|
|
||||||
Y int
|
|
||||||
Width int
|
|
||||||
Height int
|
|
||||||
Span int
|
|
||||||
Offset int
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate and set the underlying layout tree's x, y, height and width.
|
|
||||||
func (r *Row) calcLayout() {
|
|
||||||
r.assignWidth(r.Width)
|
|
||||||
r.Height = r.solveHeight()
|
|
||||||
r.assignX(r.X)
|
|
||||||
r.assignY(r.Y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// tell if the node is leaf in the tree.
|
|
||||||
func (r *Row) isLeaf() bool {
|
|
||||||
return r.Cols == nil || len(r.Cols) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Row) isRenderableLeaf() bool {
|
|
||||||
return r.isLeaf() && r.Widget != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// assign widgets' (and their parent rows') width recursively.
|
|
||||||
func (r *Row) assignWidth(w int) {
|
|
||||||
r.SetWidth(w)
|
|
||||||
|
|
||||||
accW := 0 // acc span and offset
|
|
||||||
calcW := make([]int, len(r.Cols)) // calculated width
|
|
||||||
calcOftX := make([]int, len(r.Cols)) // computed start position of x
|
|
||||||
|
|
||||||
for i, c := range r.Cols {
|
|
||||||
accW += c.Span + c.Offset
|
|
||||||
cw := int(float64(c.Span*r.Width) / 12.0)
|
|
||||||
|
|
||||||
if i >= 1 {
|
|
||||||
calcOftX[i] = calcOftX[i-1] +
|
|
||||||
calcW[i-1] +
|
|
||||||
int(float64(r.Cols[i-1].Offset*r.Width)/12.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// use up the space if it is the last col
|
|
||||||
if i == len(r.Cols)-1 && accW == 12 {
|
|
||||||
cw = r.Width - calcOftX[i]
|
|
||||||
}
|
|
||||||
calcW[i] = cw
|
|
||||||
r.Cols[i].assignWidth(cw)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// bottom up calc and set rows' (and their widgets') height,
|
|
||||||
// return r's total height.
|
|
||||||
func (r *Row) solveHeight() int {
|
|
||||||
if r.isRenderableLeaf() {
|
|
||||||
r.Height = r.Widget.GetHeight()
|
|
||||||
return r.Widget.GetHeight()
|
|
||||||
}
|
|
||||||
|
|
||||||
maxh := 0
|
|
||||||
if !r.isLeaf() {
|
|
||||||
for _, c := range r.Cols {
|
|
||||||
nh := c.solveHeight()
|
|
||||||
// when embed rows in Cols, row widgets stack up
|
|
||||||
if r.Widget != nil {
|
|
||||||
nh += r.Widget.GetHeight()
|
|
||||||
}
|
|
||||||
if nh > maxh {
|
|
||||||
maxh = nh
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Height = maxh
|
|
||||||
return maxh
|
|
||||||
}
|
|
||||||
|
|
||||||
// recursively assign x position for r tree.
|
|
||||||
func (r *Row) assignX(x int) {
|
|
||||||
r.SetX(x)
|
|
||||||
|
|
||||||
if !r.isLeaf() {
|
|
||||||
acc := 0
|
|
||||||
for i, c := range r.Cols {
|
|
||||||
if c.Offset != 0 {
|
|
||||||
acc += int(float64(c.Offset*r.Width) / 12.0)
|
|
||||||
}
|
|
||||||
r.Cols[i].assignX(x + acc)
|
|
||||||
acc += c.Width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// recursively assign y position to r.
|
|
||||||
func (r *Row) assignY(y int) {
|
|
||||||
r.SetY(y)
|
|
||||||
|
|
||||||
if r.isLeaf() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range r.Cols {
|
|
||||||
acc := 0
|
|
||||||
if r.Widget != nil {
|
|
||||||
acc = r.Widget.GetHeight()
|
|
||||||
}
|
|
||||||
r.Cols[i].assignY(y + acc)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHeight implements GridBufferer interface.
|
|
||||||
func (r Row) GetHeight() int {
|
|
||||||
return r.Height
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetX implements GridBufferer interface.
|
|
||||||
func (r *Row) SetX(x int) {
|
|
||||||
r.X = x
|
|
||||||
if r.Widget != nil {
|
|
||||||
r.Widget.SetX(x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetY implements GridBufferer interface.
|
|
||||||
func (r *Row) SetY(y int) {
|
|
||||||
r.Y = y
|
|
||||||
if r.Widget != nil {
|
|
||||||
r.Widget.SetY(y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetWidth implements GridBufferer interface.
|
|
||||||
func (r *Row) SetWidth(w int) {
|
|
||||||
r.Width = w
|
|
||||||
if r.Widget != nil {
|
|
||||||
r.Widget.SetWidth(w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buffer implements Bufferer interface,
|
|
||||||
// recursively merge all widgets buffer
|
|
||||||
func (r *Row) Buffer() Buffer {
|
|
||||||
merged := NewBuffer()
|
|
||||||
|
|
||||||
if r.isRenderableLeaf() {
|
|
||||||
return r.Widget.Buffer()
|
|
||||||
}
|
|
||||||
|
|
||||||
// for those are not leaves but have a renderable widget
|
|
||||||
if r.Widget != nil {
|
|
||||||
merged.Merge(r.Widget.Buffer())
|
|
||||||
}
|
|
||||||
|
|
||||||
// collect buffer from children
|
|
||||||
if !r.isLeaf() {
|
|
||||||
for _, c := range r.Cols {
|
|
||||||
merged.Merge(c.Buffer())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grid implements 12 columns system.
|
|
||||||
// A simple example:
|
|
||||||
/*
|
|
||||||
import ui "github.com/gizak/termui"
|
|
||||||
// init and create widgets...
|
|
||||||
|
|
||||||
// build
|
|
||||||
ui.Body.AddRows(
|
|
||||||
ui.NewRow(
|
|
||||||
ui.NewCol(6, 0, widget0),
|
|
||||||
ui.NewCol(6, 0, widget1)),
|
|
||||||
ui.NewRow(
|
|
||||||
ui.NewCol(3, 0, widget2),
|
|
||||||
ui.NewCol(3, 0, widget30, widget31, widget32),
|
|
||||||
ui.NewCol(6, 0, widget4)))
|
|
||||||
|
|
||||||
// calculate layout
|
|
||||||
ui.Body.Align()
|
|
||||||
|
|
||||||
ui.Render(ui.Body)
|
|
||||||
*/
|
|
||||||
type Grid struct {
|
type Grid struct {
|
||||||
Rows []*Row
|
Block
|
||||||
Width int
|
Items []*GridItem
|
||||||
X int
|
|
||||||
Y int
|
|
||||||
BgColor Attribute
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGrid returns *Grid with given rows.
|
// GridItem represents either a Row or Column in a grid and holds sizing information and other GridItems or widgets
|
||||||
func NewGrid(rows ...*Row) *Grid {
|
type GridItem struct {
|
||||||
return &Grid{Rows: rows}
|
Type gridItemType
|
||||||
|
XRatio float64
|
||||||
|
YRatio float64
|
||||||
|
WidthRatio float64
|
||||||
|
HeightRatio float64
|
||||||
|
Entry interface{} // Entry.type == GridBufferer if IsLeaf else []GridItem
|
||||||
|
IsLeaf bool
|
||||||
|
ratio float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddRows appends given rows to Grid.
|
func NewGrid() *Grid {
|
||||||
func (g *Grid) AddRows(rs ...*Row) {
|
g := &Grid{
|
||||||
g.Rows = append(g.Rows, rs...)
|
Block: *NewBlock(),
|
||||||
|
}
|
||||||
|
g.Border = false
|
||||||
|
return g
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRow creates a new row out of given columns.
|
// NewCol takes a height percentage and either a widget or a Row or Column
|
||||||
func NewRow(cols ...*Row) *Row {
|
func NewCol(ratio float64, i ...interface{}) GridItem {
|
||||||
rs := &Row{Span: 12, Cols: cols}
|
_, ok := i[0].(Drawable)
|
||||||
return rs
|
entry := i[0]
|
||||||
|
if !ok {
|
||||||
|
entry = i
|
||||||
|
}
|
||||||
|
return GridItem{
|
||||||
|
Type: col,
|
||||||
|
Entry: entry,
|
||||||
|
IsLeaf: ok,
|
||||||
|
ratio: ratio,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCol accepts: widgets are LayoutBufferer or widgets is A NewRow.
|
// NewRow takes a width percentage and either a widget or a Row or Column
|
||||||
// Note that if multiple widgets are provided, they will stack up in the col.
|
func NewRow(ratio float64, i ...interface{}) GridItem {
|
||||||
func NewCol(span, offset int, widgets ...GridBufferer) *Row {
|
_, ok := i[0].(Drawable)
|
||||||
r := &Row{Span: span, Offset: offset}
|
entry := i[0]
|
||||||
|
if !ok {
|
||||||
|
entry = i
|
||||||
|
}
|
||||||
|
return GridItem{
|
||||||
|
Type: row,
|
||||||
|
Entry: entry,
|
||||||
|
IsLeaf: ok,
|
||||||
|
ratio: ratio,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if widgets != nil && len(widgets) == 1 {
|
// Set is used to add Columns and Rows to the grid.
|
||||||
wgt := widgets[0]
|
// It recursively searches the GridItems, adding leaves to the grid and calculating the dimensions of the leaves.
|
||||||
nw, isRow := wgt.(*Row)
|
func (self *Grid) Set(entries ...interface{}) {
|
||||||
if isRow {
|
entry := GridItem{
|
||||||
r.Cols = nw.Cols
|
Type: row,
|
||||||
|
Entry: entries,
|
||||||
|
IsLeaf: false,
|
||||||
|
ratio: 1.0,
|
||||||
|
}
|
||||||
|
self.setHelper(entry, 1.0, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Grid) setHelper(item GridItem, parentWidthRatio, parentHeightRatio float64) {
|
||||||
|
var HeightRatio float64
|
||||||
|
var WidthRatio float64
|
||||||
|
switch item.Type {
|
||||||
|
case col:
|
||||||
|
HeightRatio = 1.0
|
||||||
|
WidthRatio = item.ratio
|
||||||
|
case row:
|
||||||
|
HeightRatio = item.ratio
|
||||||
|
WidthRatio = 1.0
|
||||||
|
}
|
||||||
|
item.WidthRatio = parentWidthRatio * WidthRatio
|
||||||
|
item.HeightRatio = parentHeightRatio * HeightRatio
|
||||||
|
|
||||||
|
if item.IsLeaf {
|
||||||
|
self.Items = append(self.Items, &item)
|
||||||
} else {
|
} else {
|
||||||
r.Widget = wgt
|
XRatio := 0.0
|
||||||
|
YRatio := 0.0
|
||||||
|
cols := false
|
||||||
|
rows := false
|
||||||
|
|
||||||
|
children := InterfaceSlice(item.Entry)
|
||||||
|
|
||||||
|
for i := 0; i < len(children); i++ {
|
||||||
|
if children[i] == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
child, _ := children[i].(GridItem)
|
||||||
|
|
||||||
|
child.XRatio = item.XRatio + (item.WidthRatio * XRatio)
|
||||||
|
child.YRatio = item.YRatio + (item.HeightRatio * YRatio)
|
||||||
|
|
||||||
|
switch child.Type {
|
||||||
|
case col:
|
||||||
|
cols = true
|
||||||
|
XRatio += child.ratio
|
||||||
|
if rows {
|
||||||
|
item.HeightRatio /= 2
|
||||||
|
}
|
||||||
|
case row:
|
||||||
|
rows = true
|
||||||
|
YRatio += child.ratio
|
||||||
|
if cols {
|
||||||
|
item.WidthRatio /= 2
|
||||||
}
|
}
|
||||||
return r
|
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Cols = []*Row{}
|
self.setHelper(child, item.WidthRatio, item.HeightRatio)
|
||||||
ir := r
|
|
||||||
for _, w := range widgets {
|
|
||||||
nr := &Row{Span: 12, Widget: w}
|
|
||||||
ir.Cols = []*Row{nr}
|
|
||||||
ir = nr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// Align calculate each rows' layout.
|
|
||||||
func (g *Grid) Align() {
|
|
||||||
h := 0
|
|
||||||
for _, r := range g.Rows {
|
|
||||||
r.SetWidth(g.Width)
|
|
||||||
r.SetX(g.X)
|
|
||||||
r.SetY(g.Y + h)
|
|
||||||
r.calcLayout()
|
|
||||||
h += r.GetHeight()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buffer implements Bufferer interface.
|
func (self *Grid) Draw(buf *Buffer) {
|
||||||
func (g Grid) Buffer() Buffer {
|
width := float64(self.Dx()) + 1
|
||||||
buf := NewBuffer()
|
height := float64(self.Dy()) + 1
|
||||||
|
|
||||||
for _, r := range g.Rows {
|
for _, item := range self.Items {
|
||||||
buf.Merge(r.Buffer())
|
entry, _ := item.Entry.(Drawable)
|
||||||
|
|
||||||
|
x := int(width*item.XRatio) + self.Min.X
|
||||||
|
y := int(height*item.YRatio) + self.Min.Y
|
||||||
|
w := int(width * item.WidthRatio)
|
||||||
|
h := int(height * item.HeightRatio)
|
||||||
|
|
||||||
|
if x+w > self.Dx() {
|
||||||
|
w--
|
||||||
|
}
|
||||||
|
if y+h > self.Dy() {
|
||||||
|
h--
|
||||||
}
|
}
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
var Body *Grid
|
entry.SetRect(x, y, x+w, y+h)
|
||||||
|
|
||||||
|
entry.Draw(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
80
grid_test.go
@ -1,80 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
)
|
|
||||||
|
|
||||||
var r *Row
|
|
||||||
|
|
||||||
func TestRowWidth(t *testing.T) {
|
|
||||||
p0 := NewBlock()
|
|
||||||
p0.Height = 1
|
|
||||||
p1 := NewBlock()
|
|
||||||
p1.Height = 1
|
|
||||||
p2 := NewBlock()
|
|
||||||
p2.Height = 1
|
|
||||||
p3 := NewBlock()
|
|
||||||
p3.Height = 1
|
|
||||||
|
|
||||||
/* test against tree:
|
|
||||||
|
|
||||||
r
|
|
||||||
/ \
|
|
||||||
0:w 1
|
|
||||||
/ \
|
|
||||||
10:w 11
|
|
||||||
/
|
|
||||||
110:w
|
|
||||||
/
|
|
||||||
1100:w
|
|
||||||
*/
|
|
||||||
|
|
||||||
r = NewRow(
|
|
||||||
NewCol(6, 0, p0),
|
|
||||||
NewCol(6, 0,
|
|
||||||
NewRow(
|
|
||||||
NewCol(6, 0, p1),
|
|
||||||
NewCol(6, 0, p2, p3))))
|
|
||||||
|
|
||||||
r.assignWidth(100)
|
|
||||||
if r.Width != 100 ||
|
|
||||||
(r.Cols[0].Width) != 50 ||
|
|
||||||
(r.Cols[1].Width) != 50 ||
|
|
||||||
(r.Cols[1].Cols[0].Width) != 25 ||
|
|
||||||
(r.Cols[1].Cols[1].Width) != 25 ||
|
|
||||||
(r.Cols[1].Cols[1].Cols[0].Width) != 25 ||
|
|
||||||
(r.Cols[1].Cols[1].Cols[0].Cols[0].Width) != 25 {
|
|
||||||
t.Error("assignWidth fails")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRowHeight(t *testing.T) {
|
|
||||||
spew.Dump()
|
|
||||||
|
|
||||||
if (r.solveHeight()) != 2 ||
|
|
||||||
(r.Cols[1].Cols[1].Height) != 2 ||
|
|
||||||
(r.Cols[1].Cols[1].Cols[0].Height) != 2 ||
|
|
||||||
(r.Cols[1].Cols[0].Height) != 1 {
|
|
||||||
t.Error("solveHeight fails")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAssignXY(t *testing.T) {
|
|
||||||
r.assignX(0)
|
|
||||||
r.assignY(0)
|
|
||||||
if (r.Cols[0].X) != 0 ||
|
|
||||||
(r.Cols[1].Cols[0].X) != 50 ||
|
|
||||||
(r.Cols[1].Cols[1].X) != 75 ||
|
|
||||||
(r.Cols[1].Cols[1].Cols[0].X) != 75 ||
|
|
||||||
(r.Cols[1].Cols[0].Y) != 0 ||
|
|
||||||
(r.Cols[1].Cols[1].Cols[0].Y) != 0 ||
|
|
||||||
(r.Cols[1].Cols[1].Cols[0].Cols[0].Y) != 1 {
|
|
||||||
t.Error("assignXY fails")
|
|
||||||
}
|
|
||||||
}
|
|
407
linechart.go
@ -1,407 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"sort"
|
|
||||||
)
|
|
||||||
|
|
||||||
// only 16 possible combinations, why bother
|
|
||||||
var braillePatterns = map[[2]int]rune{
|
|
||||||
[2]int{0, 0}: '⣀',
|
|
||||||
[2]int{0, 1}: '⡠',
|
|
||||||
[2]int{0, 2}: '⡐',
|
|
||||||
[2]int{0, 3}: '⡈',
|
|
||||||
|
|
||||||
[2]int{1, 0}: '⢄',
|
|
||||||
[2]int{1, 1}: '⠤',
|
|
||||||
[2]int{1, 2}: '⠔',
|
|
||||||
[2]int{1, 3}: '⠌',
|
|
||||||
|
|
||||||
[2]int{2, 0}: '⢂',
|
|
||||||
[2]int{2, 1}: '⠢',
|
|
||||||
[2]int{2, 2}: '⠒',
|
|
||||||
[2]int{2, 3}: '⠊',
|
|
||||||
|
|
||||||
[2]int{3, 0}: '⢁',
|
|
||||||
[2]int{3, 1}: '⠡',
|
|
||||||
[2]int{3, 2}: '⠑',
|
|
||||||
[2]int{3, 3}: '⠉',
|
|
||||||
}
|
|
||||||
|
|
||||||
var lSingleBraille = [4]rune{'\u2840', '⠄', '⠂', '⠁'}
|
|
||||||
var rSingleBraille = [4]rune{'\u2880', '⠠', '⠐', '⠈'}
|
|
||||||
|
|
||||||
// LineChart has two modes: braille(default) and dot.
|
|
||||||
// A single braille character is a 2x4 grid of dots, so Using braille
|
|
||||||
// gives 2x X resolution and 4x Y resolution over dot mode.
|
|
||||||
/*
|
|
||||||
lc := termui.NewLineChart()
|
|
||||||
lc.Border.Label = "braille-mode Line Chart"
|
|
||||||
lc.Data["name'] = [1.2, 1.3, 1.5, 1.7, 1.5, 1.6, 1.8, 2.0]
|
|
||||||
lc.Width = 50
|
|
||||||
lc.Height = 12
|
|
||||||
lc.AxesColor = termui.ColorWhite
|
|
||||||
lc.LineColor = termui.ColorGreen | termui.AttrBold
|
|
||||||
// termui.Render(lc)...
|
|
||||||
*/
|
|
||||||
type LineChart struct {
|
|
||||||
Block
|
|
||||||
Data map[string][]float64
|
|
||||||
DataLabels []string // if unset, the data indices will be used
|
|
||||||
Mode string // braille | dot
|
|
||||||
DotStyle rune
|
|
||||||
LineColor map[string]Attribute
|
|
||||||
defaultLineColor Attribute
|
|
||||||
scale float64 // data span per cell on y-axis
|
|
||||||
AxesColor Attribute
|
|
||||||
drawingX int
|
|
||||||
drawingY int
|
|
||||||
axisYHeight int
|
|
||||||
axisXWidth int
|
|
||||||
axisYLabelGap int
|
|
||||||
axisXLabelGap int
|
|
||||||
topValue float64
|
|
||||||
bottomValue float64
|
|
||||||
labelX [][]rune
|
|
||||||
labelY [][]rune
|
|
||||||
labelYSpace int
|
|
||||||
maxY float64
|
|
||||||
minY float64
|
|
||||||
YPadding float64
|
|
||||||
YFloor float64
|
|
||||||
YCeil float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLineChart returns a new LineChart with current theme.
|
|
||||||
func NewLineChart() *LineChart {
|
|
||||||
return &LineChart{
|
|
||||||
Block: *NewBlock(),
|
|
||||||
AxesColor: ThemeAttr("linechart.axes.fg"),
|
|
||||||
defaultLineColor: ThemeAttr("linechart.line.fg"),
|
|
||||||
Mode: "braille",
|
|
||||||
DotStyle: '•',
|
|
||||||
Data: make(map[string][]float64),
|
|
||||||
LineColor: make(map[string]Attribute),
|
|
||||||
axisXLabelGap: 2,
|
|
||||||
axisYLabelGap: 1,
|
|
||||||
bottomValue: math.Inf(1),
|
|
||||||
topValue: math.Inf(-1),
|
|
||||||
YPadding: 0.2,
|
|
||||||
YFloor: math.Inf(-1),
|
|
||||||
YCeil: math.Inf(1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// one cell contains two data points, so capicity is 2x dot mode
|
|
||||||
func (lc *LineChart) renderBraille() Buffer {
|
|
||||||
buf := NewBuffer()
|
|
||||||
|
|
||||||
// return: b -> which cell should the point be in
|
|
||||||
// m -> in the cell, divided into 4 equal height levels, which subcell?
|
|
||||||
getPos := func(d float64) (b, m int) {
|
|
||||||
cnt4 := int((d-lc.bottomValue)/(lc.scale/4) + 0.5)
|
|
||||||
b = cnt4 / 4
|
|
||||||
m = cnt4 % 4
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
// plot points
|
|
||||||
for _, seriesName := range seriesList {
|
|
||||||
seriesData := lc.Data[seriesName]
|
|
||||||
if len(seriesData) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
thisLineColor, ok := lc.LineColor[seriesName]
|
|
||||||
if !ok {
|
|
||||||
thisLineColor = lc.defaultLineColor
|
|
||||||
}
|
|
||||||
|
|
||||||
minCell := lc.innerArea.Min.X + lc.labelYSpace
|
|
||||||
cellPos := lc.innerArea.Max.X - 1
|
|
||||||
for dataPos := len(seriesData) - 1; dataPos >= 0 && cellPos > minCell; {
|
|
||||||
b0, m0 := getPos(seriesData[dataPos])
|
|
||||||
var b1, m1 int
|
|
||||||
|
|
||||||
if dataPos > 0 {
|
|
||||||
b1, m1 = getPos(seriesData[dataPos-1])
|
|
||||||
|
|
||||||
if b0 == b1 {
|
|
||||||
c := Cell{
|
|
||||||
Ch: braillePatterns[[2]int{m1, m0}],
|
|
||||||
Bg: lc.Bg,
|
|
||||||
Fg: thisLineColor,
|
|
||||||
}
|
|
||||||
y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
|
|
||||||
buf.Set(cellPos, y, c)
|
|
||||||
} else {
|
|
||||||
c0 := Cell{
|
|
||||||
Ch: rSingleBraille[m0],
|
|
||||||
Fg: thisLineColor,
|
|
||||||
Bg: lc.Bg,
|
|
||||||
}
|
|
||||||
y0 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
|
|
||||||
buf.Set(cellPos, y0, c0)
|
|
||||||
|
|
||||||
c1 := Cell{
|
|
||||||
Ch: lSingleBraille[m1],
|
|
||||||
Fg: thisLineColor,
|
|
||||||
Bg: lc.Bg,
|
|
||||||
}
|
|
||||||
y1 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b1
|
|
||||||
buf.Set(cellPos, y1, c1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
c0 := Cell{
|
|
||||||
Ch: rSingleBraille[m0],
|
|
||||||
Fg: thisLineColor,
|
|
||||||
Bg: lc.Bg,
|
|
||||||
}
|
|
||||||
x0 := cellPos
|
|
||||||
y0 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
|
|
||||||
buf.Set(x0, y0, c0)
|
|
||||||
}
|
|
||||||
dataPos -= 2
|
|
||||||
cellPos--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lc *LineChart) renderDot() Buffer {
|
|
||||||
buf := NewBuffer()
|
|
||||||
for seriesName, seriesData := range lc.Data {
|
|
||||||
thisLineColor, ok := lc.LineColor[seriesName]
|
|
||||||
if !ok {
|
|
||||||
thisLineColor = lc.defaultLineColor
|
|
||||||
}
|
|
||||||
minCell := lc.innerArea.Min.X + lc.labelYSpace
|
|
||||||
cellPos := lc.innerArea.Max.X - 1
|
|
||||||
for dataPos := len(seriesData) - 1; dataPos >= 0 && cellPos > minCell; {
|
|
||||||
c := Cell{
|
|
||||||
Ch: lc.DotStyle,
|
|
||||||
Fg: thisLineColor,
|
|
||||||
Bg: lc.Bg,
|
|
||||||
}
|
|
||||||
x := cellPos
|
|
||||||
y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - int((seriesData[dataPos]-lc.bottomValue)/lc.scale+0.5)
|
|
||||||
buf.Set(x, y, c)
|
|
||||||
|
|
||||||
cellPos--
|
|
||||||
dataPos--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lc *LineChart) calcLabelX() {
|
|
||||||
lc.labelX = [][]rune{}
|
|
||||||
|
|
||||||
for i, l := 0, 0; i < len(lc.DataLabels) && l < lc.axisXWidth; i++ {
|
|
||||||
if lc.Mode == "dot" {
|
|
||||||
if l >= len(lc.DataLabels) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
s := str2runes(lc.DataLabels[l])
|
|
||||||
w := strWidth(lc.DataLabels[l])
|
|
||||||
if l+w <= lc.axisXWidth {
|
|
||||||
lc.labelX = append(lc.labelX, s)
|
|
||||||
}
|
|
||||||
l += w + lc.axisXLabelGap
|
|
||||||
} else { // braille
|
|
||||||
if 2*l >= len(lc.DataLabels) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
s := str2runes(lc.DataLabels[2*l])
|
|
||||||
w := strWidth(lc.DataLabels[2*l])
|
|
||||||
if l+w <= lc.axisXWidth {
|
|
||||||
lc.labelX = append(lc.labelX, s)
|
|
||||||
}
|
|
||||||
l += w + lc.axisXLabelGap
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func shortenFloatVal(x float64) string {
|
|
||||||
s := fmt.Sprintf("%.2f", x)
|
|
||||||
if len(s)-3 > 3 {
|
|
||||||
s = fmt.Sprintf("%.2e", x)
|
|
||||||
}
|
|
||||||
|
|
||||||
if x < 0 {
|
|
||||||
s = fmt.Sprintf("%.2f", x)
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lc *LineChart) calcLabelY() {
|
|
||||||
span := lc.topValue - lc.bottomValue
|
|
||||||
// where does -2 come from? Without it, we might draw on the top border or past the block
|
|
||||||
lc.scale = span / float64(lc.axisYHeight-2)
|
|
||||||
|
|
||||||
n := (1 + lc.axisYHeight) / (lc.axisYLabelGap + 1)
|
|
||||||
lc.labelY = make([][]rune, n)
|
|
||||||
maxLen := 0
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
s := str2runes(shortenFloatVal(lc.bottomValue + float64(i)*span/float64(n)))
|
|
||||||
if len(s) > maxLen {
|
|
||||||
maxLen = len(s)
|
|
||||||
}
|
|
||||||
lc.labelY[i] = s
|
|
||||||
}
|
|
||||||
|
|
||||||
lc.labelYSpace = maxLen
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lc *LineChart) calcLayout() {
|
|
||||||
for _, seriesData := range lc.Data {
|
|
||||||
if seriesData == nil || len(seriesData) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// set datalabels if not provided
|
|
||||||
if lc.DataLabels == nil || len(lc.DataLabels) == 0 {
|
|
||||||
lc.DataLabels = make([]string, len(seriesData))
|
|
||||||
for i := range seriesData {
|
|
||||||
lc.DataLabels[i] = fmt.Sprint(i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// lazy increase, to avoid y shaking frequently
|
|
||||||
lc.minY = seriesData[0]
|
|
||||||
lc.maxY = seriesData[0]
|
|
||||||
|
|
||||||
// valid visible range
|
|
||||||
vrange := lc.innerArea.Dx()
|
|
||||||
if lc.Mode == "braille" {
|
|
||||||
vrange = 2 * lc.innerArea.Dx()
|
|
||||||
}
|
|
||||||
if vrange > len(seriesData) {
|
|
||||||
vrange = len(seriesData)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range seriesData[:vrange] {
|
|
||||||
if v > lc.maxY {
|
|
||||||
lc.maxY = v
|
|
||||||
}
|
|
||||||
if v < lc.minY {
|
|
||||||
lc.minY = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span := lc.maxY - lc.minY
|
|
||||||
|
|
||||||
// allow some padding unless we are beyond the flor/ceil
|
|
||||||
if lc.minY <= lc.bottomValue {
|
|
||||||
lc.bottomValue = lc.minY - lc.YPadding*span
|
|
||||||
if lc.bottomValue < lc.YFloor {
|
|
||||||
lc.bottomValue = lc.YFloor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if lc.maxY >= lc.topValue {
|
|
||||||
lc.topValue = lc.maxY + lc.YPadding*span
|
|
||||||
if lc.topValue > lc.YCeil {
|
|
||||||
lc.topValue = lc.YCeil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lc.axisYHeight = lc.innerArea.Dy() - 1
|
|
||||||
lc.calcLabelY()
|
|
||||||
|
|
||||||
lc.axisXWidth = lc.innerArea.Dx() - 1 - lc.labelYSpace
|
|
||||||
lc.calcLabelX()
|
|
||||||
|
|
||||||
lc.drawingX = lc.innerArea.Min.X + 1 + lc.labelYSpace
|
|
||||||
lc.drawingY = lc.innerArea.Min.Y
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lc *LineChart) plotAxes() Buffer {
|
|
||||||
buf := NewBuffer()
|
|
||||||
|
|
||||||
origY := lc.innerArea.Min.Y + lc.innerArea.Dy() - 2
|
|
||||||
origX := lc.innerArea.Min.X + lc.labelYSpace
|
|
||||||
|
|
||||||
buf.Set(origX, origY, Cell{Ch: ORIGIN, Fg: lc.AxesColor, Bg: lc.Bg})
|
|
||||||
|
|
||||||
for x := origX + 1; x < origX+lc.axisXWidth; x++ {
|
|
||||||
buf.Set(x, origY, Cell{Ch: HDASH, Fg: lc.AxesColor, Bg: lc.Bg})
|
|
||||||
}
|
|
||||||
|
|
||||||
for y := origY - 1; y > origY-lc.axisYHeight; y-- {
|
|
||||||
buf.Set(origX, y, Cell{Ch: VDASH, Fg: lc.AxesColor, Bg: lc.Bg})
|
|
||||||
}
|
|
||||||
|
|
||||||
// x label
|
|
||||||
oft := 0
|
|
||||||
for _, rs := range lc.labelX {
|
|
||||||
if oft+len(rs) > lc.axisXWidth {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
for j, r := range rs {
|
|
||||||
c := Cell{
|
|
||||||
Ch: r,
|
|
||||||
Fg: lc.AxesColor,
|
|
||||||
Bg: lc.Bg,
|
|
||||||
}
|
|
||||||
x := origX + oft + j
|
|
||||||
y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 1
|
|
||||||
buf.Set(x, y, c)
|
|
||||||
}
|
|
||||||
oft += len(rs) + lc.axisXLabelGap
|
|
||||||
}
|
|
||||||
|
|
||||||
// y labels
|
|
||||||
for i, rs := range lc.labelY {
|
|
||||||
for j, r := range rs {
|
|
||||||
buf.Set(
|
|
||||||
lc.innerArea.Min.X+j,
|
|
||||||
origY-i*(lc.axisYLabelGap+1),
|
|
||||||
Cell{Ch: r, Fg: lc.AxesColor, Bg: lc.Bg})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buffer implements Bufferer interface.
|
|
||||||
func (lc *LineChart) Buffer() Buffer {
|
|
||||||
buf := lc.Block.Buffer()
|
|
||||||
|
|
||||||
seriesCount := 0
|
|
||||||
for _, data := range lc.Data {
|
|
||||||
if len(data) > 0 {
|
|
||||||
seriesCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if seriesCount == 0 {
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
lc.calcLayout()
|
|
||||||
buf.Merge(lc.plotAxes())
|
|
||||||
|
|
||||||
if lc.Mode == "dot" {
|
|
||||||
buf.Merge(lc.renderDot())
|
|
||||||
} else {
|
|
||||||
buf.Merge(lc.renderBraille())
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
|
@ -1,11 +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 !windows
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
const VDASH = '┊'
|
|
||||||
const HDASH = '┈'
|
|
||||||
const ORIGIN = '└'
|
|
@ -1,11 +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 windows
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
const VDASH = '|'
|
|
||||||
const HDASH = '-'
|
|
||||||
const ORIGIN = '+'
|
|
91
list.go
@ -1,91 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
// List displays []string as its items,
|
|
||||||
// it has a Overflow option (default is "hidden"), when set to "hidden",
|
|
||||||
// the item exceeding List's width is truncated, but when set to "wrap",
|
|
||||||
// the overflowed text breaks into next line.
|
|
||||||
/*
|
|
||||||
strs := []string{
|
|
||||||
"[0] github.com/gizak/termui",
|
|
||||||
"[1] editbox.go",
|
|
||||||
"[2] interrupt.go",
|
|
||||||
"[3] keyboard.go",
|
|
||||||
"[4] output.go",
|
|
||||||
"[5] random_out.go",
|
|
||||||
"[6] dashboard.go",
|
|
||||||
"[7] nsf/termbox-go"}
|
|
||||||
|
|
||||||
ls := termui.NewList()
|
|
||||||
ls.Items = strs
|
|
||||||
ls.ItemFgColor = termui.ColorYellow
|
|
||||||
ls.BorderLabel = "List"
|
|
||||||
ls.Height = 7
|
|
||||||
ls.Width = 25
|
|
||||||
ls.Y = 0
|
|
||||||
*/
|
|
||||||
type List struct {
|
|
||||||
Block
|
|
||||||
Items []string
|
|
||||||
Overflow string
|
|
||||||
ItemFgColor Attribute
|
|
||||||
ItemBgColor Attribute
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewList returns a new *List with current theme.
|
|
||||||
func NewList() *List {
|
|
||||||
return &List{
|
|
||||||
Block: *NewBlock(),
|
|
||||||
Overflow: "hidden",
|
|
||||||
ItemFgColor: ThemeAttr("list.item.fg"),
|
|
||||||
ItemBgColor: ThemeAttr("list.item.bg"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buffer implements Bufferer interface.
|
|
||||||
func (l *List) Buffer() Buffer {
|
|
||||||
buf := l.Block.Buffer()
|
|
||||||
|
|
||||||
switch l.Overflow {
|
|
||||||
case "wrap":
|
|
||||||
cs := DefaultTxBuilder.Build(strings.Join(l.Items, "\n"), l.ItemFgColor, l.ItemBgColor)
|
|
||||||
i, j, k := 0, 0, 0
|
|
||||||
for i < l.innerArea.Dy() && k < len(cs) {
|
|
||||||
w := cs[k].Width()
|
|
||||||
if cs[k].Ch == '\n' || j+w > l.innerArea.Dx() {
|
|
||||||
i++
|
|
||||||
j = 0
|
|
||||||
if cs[k].Ch == '\n' {
|
|
||||||
k++
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
buf.Set(l.innerArea.Min.X+j, l.innerArea.Min.Y+i, cs[k])
|
|
||||||
|
|
||||||
k++
|
|
||||||
j++
|
|
||||||
}
|
|
||||||
|
|
||||||
case "hidden":
|
|
||||||
trimItems := l.Items
|
|
||||||
if len(trimItems) > l.innerArea.Dy() {
|
|
||||||
trimItems = trimItems[:l.innerArea.Dy()]
|
|
||||||
}
|
|
||||||
for i, v := range trimItems {
|
|
||||||
cs := DTrimTxCls(DefaultTxBuilder.Build(v, l.ItemFgColor, l.ItemBgColor), l.innerArea.Dx())
|
|
||||||
j := 0
|
|
||||||
for _, vv := range cs {
|
|
||||||
w := vv.Width()
|
|
||||||
buf.Set(l.innerArea.Min.X+j, l.innerArea.Min.Y+i, vv)
|
|
||||||
j += w
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
|
73
paragraph.go
@ -1,73 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
// Paragraph displays a paragraph.
|
|
||||||
/*
|
|
||||||
par := termui.NewParagraph("Simple Text")
|
|
||||||
par.Height = 3
|
|
||||||
par.Width = 17
|
|
||||||
par.BorderLabel = "Label"
|
|
||||||
*/
|
|
||||||
type Paragraph struct {
|
|
||||||
Block
|
|
||||||
Text string
|
|
||||||
TextFgColor Attribute
|
|
||||||
TextBgColor Attribute
|
|
||||||
WrapLength int // words wrap limit. Note it may not work properly with multi-width char
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewParagraph returns a new *Paragraph with given text as its content.
|
|
||||||
func NewParagraph(s string) *Paragraph {
|
|
||||||
return &Paragraph{
|
|
||||||
Block: *NewBlock(),
|
|
||||||
Text: s,
|
|
||||||
TextFgColor: ThemeAttr("par.text.fg"),
|
|
||||||
TextBgColor: ThemeAttr("par.text.bg"),
|
|
||||||
WrapLength: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buffer implements Bufferer interface.
|
|
||||||
func (p *Paragraph) Buffer() Buffer {
|
|
||||||
buf := p.Block.Buffer()
|
|
||||||
|
|
||||||
fg, bg := p.TextFgColor, p.TextBgColor
|
|
||||||
cs := DefaultTxBuilder.Build(p.Text, fg, bg)
|
|
||||||
|
|
||||||
// wrap if WrapLength set
|
|
||||||
if p.WrapLength < 0 {
|
|
||||||
cs = wrapTx(cs, p.Width-2)
|
|
||||||
} else if p.WrapLength > 0 {
|
|
||||||
cs = wrapTx(cs, p.WrapLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
y, x, n := 0, 0, 0
|
|
||||||
for y < p.innerArea.Dy() && n < len(cs) {
|
|
||||||
w := cs[n].Width()
|
|
||||||
if cs[n].Ch == '\n' || x+w > p.innerArea.Dx() {
|
|
||||||
y++
|
|
||||||
x = 0 // set x = 0
|
|
||||||
if cs[n].Ch == '\n' {
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
|
|
||||||
if y >= p.innerArea.Dy() {
|
|
||||||
buf.Set(p.innerArea.Min.X+p.innerArea.Dx()-1,
|
|
||||||
p.innerArea.Min.Y+p.innerArea.Dy()-1,
|
|
||||||
Cell{Ch: '…', Fg: p.TextFgColor, Bg: p.TextBgColor})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.Set(p.innerArea.Min.X+x, p.innerArea.Min.Y+y, cs[n])
|
|
||||||
|
|
||||||
n++
|
|
||||||
x += w
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
|
@ -1,24 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestPar_NoBorderBackground(t *testing.T) {
|
|
||||||
par := NewPar("a")
|
|
||||||
par.Border = false
|
|
||||||
par.Bg = ColorBlue
|
|
||||||
par.TextBgColor = ColorBlue
|
|
||||||
par.Width = 2
|
|
||||||
par.Height = 2
|
|
||||||
|
|
||||||
pts := par.Buffer()
|
|
||||||
for _, p := range pts.CellMap {
|
|
||||||
t.Log(p)
|
|
||||||
if p.Bg != par.Bg {
|
|
||||||
t.Errorf("expected color to be %v but got %v", par.Bg, p.Bg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
290
piechart.go
@ -1,290 +0,0 @@
|
|||||||
package termui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"container/list"
|
|
||||||
"image"
|
|
||||||
"math"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
piechartOffsetUp = -.5 * math.Pi // the northward angle
|
|
||||||
sectorFactor = 7.0 // circle resolution: precision vs. performance
|
|
||||||
fullCircle = 2.0 * math.Pi // the full circle angle
|
|
||||||
xStretch = 2.0 // horizontal adjustment
|
|
||||||
solidBlock = '░'
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
defaultColors = []Attribute{
|
|
||||||
ColorRed,
|
|
||||||
ColorGreen,
|
|
||||||
ColorYellow,
|
|
||||||
ColorBlue,
|
|
||||||
ColorMagenta,
|
|
||||||
ColorCyan,
|
|
||||||
ColorWhite,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// PieChartLabel callback, i is the current data index, v the current value
|
|
||||||
type PieChartLabel func(i int, v float64) string
|
|
||||||
|
|
||||||
type PieChart struct {
|
|
||||||
Block
|
|
||||||
Data []float64 // list of data items
|
|
||||||
Colors []Attribute // colors to by cycled through (see defaultColors)
|
|
||||||
BorderColor Attribute // color of the pie-border
|
|
||||||
Label PieChartLabel // callback function for labels
|
|
||||||
Offset float64 // which angle to start drawing at? (see piechartOffsetUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPieChart Creates a new pie chart with reasonable defaults and no labels
|
|
||||||
func NewPieChart() *PieChart {
|
|
||||||
return &PieChart{
|
|
||||||
Block: *NewBlock(),
|
|
||||||
Colors: defaultColors,
|
|
||||||
Offset: piechartOffsetUp,
|
|
||||||
BorderColor: ColorDefault,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// computes the color for a given data index
|
|
||||||
func (pc *PieChart) colorFor(i int) Attribute {
|
|
||||||
return pc.Colors[i%len(pc.Colors)]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buffer creates the buffer for the pie chart
|
|
||||||
func (pc *PieChart) Buffer() Buffer {
|
|
||||||
buf := pc.Block.Buffer()
|
|
||||||
w, h := pc.innerArea.Dx(), pc.innerArea.Dy()
|
|
||||||
center := image.Point{X: w / 2, Y: h / 2}
|
|
||||||
|
|
||||||
// radius for the border
|
|
||||||
r := float64(w/2/xStretch) - 1.0
|
|
||||||
if h < w/xStretch {
|
|
||||||
r = float64(h/2) - 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
// make border
|
|
||||||
borderCircle := &circle{Point: center, radius: r}
|
|
||||||
drawBorder := func() {
|
|
||||||
borderCircle.draw(&buf, Cell{Ch: solidBlock, Fg: pc.BorderColor, Bg: ColorDefault})
|
|
||||||
}
|
|
||||||
drawBorder()
|
|
||||||
|
|
||||||
if len(pc.Data) == 0 { // nothing to draw?
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
// compute slice sizes
|
|
||||||
sum := sum(pc.Data)
|
|
||||||
sliceSizes := make([]float64, len(pc.Data))
|
|
||||||
for i, v := range pc.Data {
|
|
||||||
sliceSizes[i] = v / sum * fullCircle
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw slice borders
|
|
||||||
phi := pc.Offset
|
|
||||||
for i, v := range sliceSizes {
|
|
||||||
p := borderCircle.at(phi)
|
|
||||||
l := line{P1: center, P2: p}
|
|
||||||
l.draw(&buf, &Cell{Ch: solidBlock, Fg: pc.colorFor(i), Bg: ColorDefault})
|
|
||||||
phi += v
|
|
||||||
}
|
|
||||||
|
|
||||||
// fill slices
|
|
||||||
middleCircle := circle{Point: center, radius: r / 2.0}
|
|
||||||
_, sectorSize := borderCircle.sectors()
|
|
||||||
phi = pc.Offset
|
|
||||||
for i, v := range sliceSizes {
|
|
||||||
if v > sectorSize { // do not render if slice is too small
|
|
||||||
cell := Cell{Ch: solidBlock, Fg: pc.colorFor(i), Bg: ColorDefault}
|
|
||||||
halfSlice := phi + v/2.0
|
|
||||||
fill(borderCircle.inner(halfSlice), &cell, &buf)
|
|
||||||
fill(middleCircle.at(halfSlice), &cell, &buf)
|
|
||||||
for f := phi; f < phi+v; f += sectorSize {
|
|
||||||
line{P1: center, P2: borderCircle.inner(f)}.draw(&buf, &cell)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
phi += v
|
|
||||||
}
|
|
||||||
|
|
||||||
// labels
|
|
||||||
if pc.Label != nil {
|
|
||||||
drawLabel := func(p image.Point, label string, fg, bg Attribute) {
|
|
||||||
offset := p.Add(image.Point{X: -int(round(float64(len(label)) / 2.0)), Y: 0})
|
|
||||||
for i, v := range []rune(label) {
|
|
||||||
buf.Set(offset.X+i, offset.Y, Cell{Ch: v, Fg: fg, Bg: bg})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
phi = pc.Offset
|
|
||||||
for i, v := range sliceSizes {
|
|
||||||
labelAt := middleCircle.at(phi + v/2.0)
|
|
||||||
if len(pc.Data) == 1 {
|
|
||||||
labelAt = center
|
|
||||||
}
|
|
||||||
drawLabel(labelAt, pc.Label(i, pc.Data[i]), pc.colorFor(i), ColorDefault)
|
|
||||||
phi += v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
drawBorder()
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
// fills empty cells from position
|
|
||||||
func fill(p image.Point, c *Cell, buf *Buffer) {
|
|
||||||
empty := func(x, y int) bool {
|
|
||||||
return buf.At(x, y).Ch == ' '
|
|
||||||
}
|
|
||||||
if !empty(p.X, p.Y) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
q := list.New()
|
|
||||||
q.PushBack(p)
|
|
||||||
buf.Set(p.X, p.Y, *c)
|
|
||||||
for q.Front() != nil {
|
|
||||||
p := q.Remove(q.Front()).(image.Point)
|
|
||||||
w, e, row := p.X, p.X, p.Y
|
|
||||||
|
|
||||||
for empty(w-1, row) {
|
|
||||||
w--
|
|
||||||
}
|
|
||||||
for empty(e+1, row) {
|
|
||||||
e++
|
|
||||||
}
|
|
||||||
for x := w; x <= e; x++ {
|
|
||||||
buf.Set(x, row, *c)
|
|
||||||
if empty(x, row-1) {
|
|
||||||
q.PushBack(image.Point{X: x, Y: row - 1})
|
|
||||||
}
|
|
||||||
if empty(x, row+1) {
|
|
||||||
q.PushBack(image.Point{X: x, Y: row + 1})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type circle struct {
|
|
||||||
image.Point
|
|
||||||
radius float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// computes the point at a given angle phi
|
|
||||||
func (c circle) at(phi float64) image.Point {
|
|
||||||
x := c.X + int(round(xStretch*c.radius*math.Cos(phi)))
|
|
||||||
y := c.Y + int(round(c.radius*math.Sin(phi)))
|
|
||||||
return image.Point{X: x, Y: y}
|
|
||||||
}
|
|
||||||
|
|
||||||
// computes the "inner" point at a given angle phi
|
|
||||||
func (c circle) inner(phi float64) image.Point {
|
|
||||||
p := image.Point{X: 0, Y: 0}
|
|
||||||
outer := c.at(phi)
|
|
||||||
if c.X < outer.X {
|
|
||||||
p.X = -1
|
|
||||||
} else if c.X > outer.X {
|
|
||||||
p.X = 1
|
|
||||||
}
|
|
||||||
if c.Y < outer.Y {
|
|
||||||
p.Y = -1
|
|
||||||
} else if c.Y > outer.Y {
|
|
||||||
p.Y = 1
|
|
||||||
}
|
|
||||||
return outer.Add(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// computes the perimeter of a circle
|
|
||||||
func (c circle) perimeter() float64 {
|
|
||||||
return 2.0 * math.Pi * c.radius
|
|
||||||
}
|
|
||||||
|
|
||||||
// computes the number of sectors and the size of each sector
|
|
||||||
func (c circle) sectors() (sectors float64, sectorSize float64) {
|
|
||||||
sectors = c.perimeter() * sectorFactor
|
|
||||||
sectorSize = fullCircle / sectors
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// draws the circle
|
|
||||||
func (c circle) draw(buf *Buffer, cell Cell) {
|
|
||||||
sectors, sectorSize := c.sectors()
|
|
||||||
for i := 0; i < int(round(sectors)); i++ {
|
|
||||||
phi := float64(i) * sectorSize
|
|
||||||
point := c.at(float64(phi))
|
|
||||||
buf.Set(point.X, point.Y, cell)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// a line between two points
|
|
||||||
type line struct {
|
|
||||||
P1, P2 image.Point
|
|
||||||
}
|
|
||||||
|
|
||||||
// draws the line
|
|
||||||
func (l line) draw(buf *Buffer, cell *Cell) {
|
|
||||||
isLeftOf := func(p1, p2 image.Point) bool {
|
|
||||||
return p1.X <= p2.X
|
|
||||||
}
|
|
||||||
isTopOf := func(p1, p2 image.Point) bool {
|
|
||||||
return p1.Y <= p2.Y
|
|
||||||
}
|
|
||||||
p1, p2 := l.P1, l.P2
|
|
||||||
buf.Set(l.P2.X, l.P2.Y, Cell{Ch: '*', Fg: cell.Fg, Bg: cell.Bg})
|
|
||||||
width, height := l.size()
|
|
||||||
if width > height { // paint left to right
|
|
||||||
if !isLeftOf(p1, p2) {
|
|
||||||
p1, p2 = p2, p1
|
|
||||||
}
|
|
||||||
flip := 1.0
|
|
||||||
if !isTopOf(p1, p2) {
|
|
||||||
flip = -1.0
|
|
||||||
}
|
|
||||||
for x := p1.X; x <= p2.X; x++ {
|
|
||||||
ratio := float64(height) / float64(width)
|
|
||||||
factor := float64(x - p1.X)
|
|
||||||
y := ratio * factor * flip
|
|
||||||
buf.Set(x, int(round(y))+p1.Y, *cell)
|
|
||||||
}
|
|
||||||
} else { // paint top to bottom
|
|
||||||
if !isTopOf(p1, p2) {
|
|
||||||
p1, p2 = p2, p1
|
|
||||||
}
|
|
||||||
flip := 1.0
|
|
||||||
if !isLeftOf(p1, p2) {
|
|
||||||
flip = -1.0
|
|
||||||
}
|
|
||||||
for y := p1.Y; y <= p2.Y; y++ {
|
|
||||||
ratio := float64(width) / float64(height)
|
|
||||||
factor := float64(y - p1.Y)
|
|
||||||
x := ratio * factor * flip
|
|
||||||
buf.Set(int(round(x))+p1.X, y, *cell)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// width and height of a line
|
|
||||||
func (l line) size() (w, h int) {
|
|
||||||
return abs(l.P2.X - l.P1.X), abs(l.P2.Y - l.P1.Y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// rounds a value
|
|
||||||
func round(x float64) float64 {
|
|
||||||
return math.Floor(x + 0.5)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fold a sum
|
|
||||||
func sum(data []float64) float64 {
|
|
||||||
sum := 0.0
|
|
||||||
for _, v := range data {
|
|
||||||
sum += v
|
|
||||||
}
|
|
||||||
return sum
|
|
||||||
}
|
|
||||||
|
|
||||||
// math.Abs for ints
|
|
||||||
func abs(x int) int {
|
|
||||||
if x >= 0 {
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
return -x
|
|
||||||
}
|
|
78
position.go
@ -1,78 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
import "image"
|
|
||||||
|
|
||||||
// Align is the position of the gauge's label.
|
|
||||||
type Align uint
|
|
||||||
|
|
||||||
// All supported positions.
|
|
||||||
const (
|
|
||||||
AlignNone Align = 0
|
|
||||||
AlignLeft Align = 1 << iota
|
|
||||||
AlignRight
|
|
||||||
AlignBottom
|
|
||||||
AlignTop
|
|
||||||
AlignCenterVertical
|
|
||||||
AlignCenterHorizontal
|
|
||||||
AlignCenter = AlignCenterVertical | AlignCenterHorizontal
|
|
||||||
)
|
|
||||||
|
|
||||||
func AlignArea(parent, child image.Rectangle, a Align) image.Rectangle {
|
|
||||||
w, h := child.Dx(), child.Dy()
|
|
||||||
|
|
||||||
// parent center
|
|
||||||
pcx, pcy := parent.Min.X+parent.Dx()/2, parent.Min.Y+parent.Dy()/2
|
|
||||||
// child center
|
|
||||||
ccx, ccy := child.Min.X+child.Dx()/2, child.Min.Y+child.Dy()/2
|
|
||||||
|
|
||||||
if a&AlignLeft == AlignLeft {
|
|
||||||
child.Min.X = parent.Min.X
|
|
||||||
child.Max.X = child.Min.X + w
|
|
||||||
}
|
|
||||||
|
|
||||||
if a&AlignRight == AlignRight {
|
|
||||||
child.Max.X = parent.Max.X
|
|
||||||
child.Min.X = child.Max.X - w
|
|
||||||
}
|
|
||||||
|
|
||||||
if a&AlignBottom == AlignBottom {
|
|
||||||
child.Max.Y = parent.Max.Y
|
|
||||||
child.Min.Y = child.Max.Y - h
|
|
||||||
}
|
|
||||||
|
|
||||||
if a&AlignTop == AlignRight {
|
|
||||||
child.Min.Y = parent.Min.Y
|
|
||||||
child.Max.Y = child.Min.Y + h
|
|
||||||
}
|
|
||||||
|
|
||||||
if a&AlignCenterHorizontal == AlignCenterHorizontal {
|
|
||||||
child.Min.X += pcx - ccx
|
|
||||||
child.Max.X = child.Min.X + w
|
|
||||||
}
|
|
||||||
|
|
||||||
if a&AlignCenterVertical == AlignCenterVertical {
|
|
||||||
child.Min.Y += pcy - ccy
|
|
||||||
child.Max.Y = child.Min.Y + h
|
|
||||||
}
|
|
||||||
|
|
||||||
return child
|
|
||||||
}
|
|
||||||
|
|
||||||
func MoveArea(a image.Rectangle, dx, dy int) image.Rectangle {
|
|
||||||
a.Min.X += dx
|
|
||||||
a.Max.X += dx
|
|
||||||
a.Min.Y += dy
|
|
||||||
a.Max.Y += dy
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
var termWidth int
|
|
||||||
var termHeight int
|
|
||||||
|
|
||||||
func TermRect() image.Rectangle {
|
|
||||||
return image.Rect(0, 0, termWidth, termHeight)
|
|
||||||
}
|
|
@ -1,38 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"image"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAlignArea(t *testing.T) {
|
|
||||||
p := image.Rect(0, 0, 100, 100)
|
|
||||||
c := image.Rect(10, 10, 20, 20)
|
|
||||||
|
|
||||||
nc := AlignArea(p, c, AlignLeft)
|
|
||||||
if nc.Min.X != 0 || nc.Max.Y != 20 {
|
|
||||||
t.Errorf("AlignLeft failed:\n%+v", nc)
|
|
||||||
}
|
|
||||||
|
|
||||||
nc = AlignArea(p, c, AlignCenter)
|
|
||||||
if nc.Min.X != 45 || nc.Max.Y != 55 {
|
|
||||||
t.Error("AlignCenter failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
nc = AlignArea(p, c, AlignBottom|AlignRight)
|
|
||||||
if nc.Min.X != 90 || nc.Max.Y != 100 {
|
|
||||||
t.Errorf("AlignBottom|AlignRight failed\n%+v", nc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMoveArea(t *testing.T) {
|
|
||||||
a := image.Rect(10, 10, 20, 20)
|
|
||||||
a = MoveArea(a, 5, 10)
|
|
||||||
if a.Min.X != 15 || a.Min.Y != 20 || a.Max.X != 25 || a.Max.Y != 30 {
|
|
||||||
t.Error("MoveArea failed")
|
|
||||||
}
|
|
||||||
}
|
|
135
render.go
@ -6,134 +6,29 @@ package termui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
"runtime/debug"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
tb "github.com/nsf/termbox-go"
|
tb "github.com/nsf/termbox-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Bufferer should be implemented by all renderable components.
|
type Drawable interface {
|
||||||
type Bufferer interface {
|
GetRect() image.Rectangle
|
||||||
Buffer() Buffer
|
SetRect(int, int, int, int)
|
||||||
|
Draw(*Buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes termui library. This function should be called before any others.
|
func Render(items ...Drawable) {
|
||||||
// After initialization, the library must be finalized by 'Close' function.
|
for _, item := range items {
|
||||||
func Init() error {
|
buf := NewBuffer(item.GetRect())
|
||||||
if err := tb.Init(); err != nil {
|
item.Draw(buf)
|
||||||
return err
|
for point, cell := range buf.CellMap {
|
||||||
}
|
if point.In(buf.Rectangle) {
|
||||||
tb.SetInputMode(tb.InputEsc | tb.InputMouse)
|
tb.SetCell(
|
||||||
// DefaultEvtStream = NewEvtStream()
|
point.X, point.Y,
|
||||||
|
cell.Rune,
|
||||||
// sysEvtChs = make([]chan Event, 0)
|
tb.Attribute(cell.Style.Fg+1)|tb.Attribute(cell.Style.Modifier), tb.Attribute(cell.Style.Bg+1),
|
||||||
// go hookTermboxEvt()
|
)
|
||||||
|
|
||||||
renderJobs = make(chan []Bufferer)
|
|
||||||
//renderLock = new(sync.RWMutex)
|
|
||||||
|
|
||||||
Body = NewGrid()
|
|
||||||
Body.X = 0
|
|
||||||
Body.Y = 0
|
|
||||||
Body.BgColor = ThemeAttr("bg")
|
|
||||||
Body.Width = TermWidth()
|
|
||||||
|
|
||||||
// resizeCh := Handle("<Resize>")
|
|
||||||
// go func() {
|
|
||||||
// for e := range resizeCh {
|
|
||||||
// payload := e.Payload.(Resize)
|
|
||||||
// Body.Width = payload.Width
|
|
||||||
// }
|
|
||||||
// }()
|
|
||||||
|
|
||||||
// DefaultWgtMgr = NewWgtMgr()
|
|
||||||
// EventHook(DefaultWgtMgr.WgtHandlersHook())
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for bs := range renderJobs {
|
|
||||||
render(bs...)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close finalizes termui library,
|
|
||||||
// should be called after successful initialization when termui's functionality isn't required anymore.
|
|
||||||
func Close() {
|
|
||||||
tb.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
var renderLock sync.Mutex
|
|
||||||
|
|
||||||
func termSync() {
|
|
||||||
renderLock.Lock()
|
|
||||||
tb.Sync()
|
|
||||||
termWidth, termHeight = tb.Size()
|
|
||||||
renderLock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TermWidth returns the current terminal's width.
|
|
||||||
func TermWidth() int {
|
|
||||||
termSync()
|
|
||||||
return termWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
// TermHeight returns the current terminal's height.
|
|
||||||
func TermHeight() int {
|
|
||||||
termSync()
|
|
||||||
return termHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render renders all Bufferer in the given order from left to right,
|
|
||||||
// right could overlap on left ones.
|
|
||||||
func render(bs ...Bufferer) {
|
|
||||||
defer func() {
|
|
||||||
if e := recover(); e != nil {
|
|
||||||
Close()
|
|
||||||
panic(debug.Stack())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
for _, b := range bs {
|
|
||||||
|
|
||||||
buf := b.Buffer()
|
|
||||||
// set cels in buf
|
|
||||||
for p, c := range buf.CellMap {
|
|
||||||
if p.In(buf.Area) {
|
|
||||||
|
|
||||||
tb.SetCell(p.X, p.Y, c.Ch, toTmAttr(c.Fg), toTmAttr(c.Bg))
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLock.Lock()
|
|
||||||
// render
|
|
||||||
tb.Flush()
|
|
||||||
renderLock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func Clear() {
|
|
||||||
tb.Clear(tb.ColorDefault, toTmAttr(ThemeAttr("bg")))
|
|
||||||
}
|
|
||||||
|
|
||||||
func clearArea(r image.Rectangle, bg Attribute) {
|
|
||||||
for i := r.Min.X; i < r.Max.X; i++ {
|
|
||||||
for j := r.Min.Y; j < r.Max.Y; j++ {
|
|
||||||
tb.SetCell(i, j, ' ', tb.ColorDefault, toTmAttr(bg))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ClearArea(r image.Rectangle, bg Attribute) {
|
|
||||||
clearArea(r, bg)
|
|
||||||
tb.Flush()
|
tb.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
var renderJobs chan []Bufferer
|
|
||||||
|
|
||||||
func Render(bs ...Bufferer) {
|
|
||||||
//go func() { renderJobs <- bs }()
|
|
||||||
renderJobs <- bs
|
|
||||||
}
|
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import signal
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from subprocess import call
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
signal.signal(signal.SIGINT, lambda sig, frame: sys.exit(0))
|
|
||||||
|
|
||||||
p = Path('.') / '_examples'
|
|
||||||
files = p.glob('*.go')
|
|
||||||
for file in files:
|
|
||||||
command = f'go run {file}'
|
|
||||||
print(command)
|
|
||||||
call(command.split())
|
|
166
sparkline.go
@ -1,166 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
// Sparkline is like: ▅▆▂▂▅▇▂▂▃▆▆▆▅▃. The data points should be non-negative integers.
|
|
||||||
/*
|
|
||||||
data := []int{4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1}
|
|
||||||
spl := termui.NewSparkline()
|
|
||||||
spl.Data = data
|
|
||||||
spl.Title = "Sparkline 0"
|
|
||||||
spl.LineColor = termui.ColorGreen
|
|
||||||
*/
|
|
||||||
type Sparkline struct {
|
|
||||||
Data []int
|
|
||||||
Height int
|
|
||||||
Title string
|
|
||||||
TitleColor Attribute
|
|
||||||
LineColor Attribute
|
|
||||||
displayHeight int
|
|
||||||
scale float32
|
|
||||||
max int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sparklines is a renderable widget which groups together the given sparklines.
|
|
||||||
/*
|
|
||||||
spls := termui.NewSparklines(spl0,spl1,spl2) //...
|
|
||||||
spls.Height = 2
|
|
||||||
spls.Width = 20
|
|
||||||
*/
|
|
||||||
type Sparklines struct {
|
|
||||||
Block
|
|
||||||
Lines []Sparkline
|
|
||||||
displayLines int
|
|
||||||
displayWidth int
|
|
||||||
}
|
|
||||||
|
|
||||||
var sparks = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
|
||||||
|
|
||||||
// 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{
|
|
||||||
Height: 1,
|
|
||||||
TitleColor: ThemeAttr("sparkline.title.fg"),
|
|
||||||
LineColor: ThemeAttr("sparkline.line.fg")}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sl *Sparklines) update() {
|
|
||||||
for i, v := range sl.Lines {
|
|
||||||
if v.Title == "" {
|
|
||||||
sl.Lines[i].displayHeight = v.Height
|
|
||||||
} else {
|
|
||||||
sl.Lines[i].displayHeight = v.Height + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sl.displayWidth = sl.innerArea.Dx()
|
|
||||||
|
|
||||||
// get how many lines gotta display
|
|
||||||
h := 0
|
|
||||||
sl.displayLines = 0
|
|
||||||
for _, v := range sl.Lines {
|
|
||||||
if h+v.displayHeight <= sl.innerArea.Dy() {
|
|
||||||
sl.displayLines++
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
h += v.displayHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < sl.displayLines; i++ {
|
|
||||||
data := sl.Lines[i].Data
|
|
||||||
|
|
||||||
max := 0
|
|
||||||
for _, v := range data {
|
|
||||||
if max < v {
|
|
||||||
max = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sl.Lines[i].max = max
|
|
||||||
if max != 0 {
|
|
||||||
sl.Lines[i].scale = float32(8*sl.Lines[i].Height) / float32(max)
|
|
||||||
} else { // when all negative
|
|
||||||
sl.Lines[i].scale = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buffer implements Bufferer interface.
|
|
||||||
func (sl *Sparklines) Buffer() Buffer {
|
|
||||||
buf := sl.Block.Buffer()
|
|
||||||
sl.update()
|
|
||||||
|
|
||||||
oftY := 0
|
|
||||||
for i := 0; i < sl.displayLines; i++ {
|
|
||||||
l := sl.Lines[i]
|
|
||||||
data := l.Data
|
|
||||||
|
|
||||||
if len(data) > sl.innerArea.Dx() {
|
|
||||||
data = data[len(data)-sl.innerArea.Dx():]
|
|
||||||
}
|
|
||||||
|
|
||||||
if l.Title != "" {
|
|
||||||
rs := trimStr2Runes(l.Title, sl.innerArea.Dx())
|
|
||||||
oftX := 0
|
|
||||||
for _, v := range rs {
|
|
||||||
w := charWidth(v)
|
|
||||||
c := Cell{
|
|
||||||
Ch: v,
|
|
||||||
Fg: l.TitleColor,
|
|
||||||
Bg: sl.Bg,
|
|
||||||
}
|
|
||||||
x := sl.innerArea.Min.X + oftX
|
|
||||||
y := sl.innerArea.Min.Y + oftY
|
|
||||||
buf.Set(x, y, c)
|
|
||||||
oftX += w
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for j, v := range data {
|
|
||||||
// display height of the data point, zero when data is negative
|
|
||||||
h := int(float32(v)*l.scale + 0.5)
|
|
||||||
if v < 0 {
|
|
||||||
h = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
barCnt := h / 8
|
|
||||||
barMod := h % 8
|
|
||||||
for jj := 0; jj < barCnt; jj++ {
|
|
||||||
c := Cell{
|
|
||||||
Ch: ' ', // => sparks[7]
|
|
||||||
Bg: l.LineColor,
|
|
||||||
}
|
|
||||||
x := sl.innerArea.Min.X + j
|
|
||||||
y := sl.innerArea.Min.Y + oftY + l.Height - jj
|
|
||||||
|
|
||||||
//p.Bg = sl.BgColor
|
|
||||||
buf.Set(x, y, c)
|
|
||||||
}
|
|
||||||
if barMod != 0 {
|
|
||||||
c := Cell{
|
|
||||||
Ch: sparks[barMod-1],
|
|
||||||
Fg: l.LineColor,
|
|
||||||
Bg: sl.Bg,
|
|
||||||
}
|
|
||||||
x := sl.innerArea.Min.X + j
|
|
||||||
y := sl.innerArea.Min.Y + oftY + l.Height - barCnt
|
|
||||||
buf.Set(x, y, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
oftY += l.displayHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
|
@ -1,247 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This is the implementation 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.NewStackedBarChart()
|
|
||||||
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.BorderLabel = "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 StackedBarChart struct {
|
|
||||||
Block
|
|
||||||
BarColor [NumberofColors]Attribute
|
|
||||||
TextColor Attribute
|
|
||||||
NumColor [NumberofColors]Attribute
|
|
||||||
NumFmt func(int) string
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStackedBarChart returns a new *StackedBarChart with current theme.
|
|
||||||
func NewStackedBarChart() *StackedBarChart {
|
|
||||||
mbc := &StackedBarChart{
|
|
||||||
Block: *NewBlock(),
|
|
||||||
TextColor: ThemeAttr("mbarchart.text.fg"),
|
|
||||||
NumFmt: func(n int) string { return fmt.Sprint(n) },
|
|
||||||
BarGap: 1,
|
|
||||||
BarWidth: 3,
|
|
||||||
}
|
|
||||||
mbc.BarColor[0] = ThemeAttr("mbarchart.bar.bg")
|
|
||||||
mbc.NumColor[0] = ThemeAttr("mbarchart.num.fg")
|
|
||||||
return mbc
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bc *StackedBarChart) layout() {
|
|
||||||
bc.numBar = bc.innerArea.Dx() / (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 minimum 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 calculate the rune
|
|
||||||
for j := 0; j < LabelLen && i < bc.numBar; j++ {
|
|
||||||
n := bc.Data[i][j]
|
|
||||||
s := bc.NumFmt(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 previous 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 := bc.NumFmt(bc.max)
|
|
||||||
bc.maxScale = trimStr2Runes(s, len(s))
|
|
||||||
bc.scale = float64(bc.max) / float64(bc.innerArea.Dy()-2)
|
|
||||||
} else {
|
|
||||||
bc.scale = float64(bc.max) / float64(bc.innerArea.Dy()-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bc *StackedBarChart) SetMax(max int) {
|
|
||||||
if max > 0 {
|
|
||||||
bc.max = max
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buffer implements Bufferer interface.
|
|
||||||
func (bc *StackedBarChart) Buffer() Buffer {
|
|
||||||
buf := 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++ {
|
|
||||||
c := Cell{
|
|
||||||
Ch: ' ',
|
|
||||||
Bg: bc.BarColor[i1],
|
|
||||||
}
|
|
||||||
if bc.BarColor[i1] == ColorDefault { // when color is default, space char treated as transparent!
|
|
||||||
c.Bg |= AttrReverse
|
|
||||||
}
|
|
||||||
x := bc.innerArea.Min.X + i*(bc.BarWidth+bc.BarGap) + j
|
|
||||||
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2 - k - ph
|
|
||||||
buf.Set(x, y, c)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ph += h
|
|
||||||
}
|
|
||||||
// plot text
|
|
||||||
for j, k := 0, 0; j < len(bc.labels[i]); j++ {
|
|
||||||
w := charWidth(bc.labels[i][j])
|
|
||||||
c := Cell{
|
|
||||||
Ch: bc.labels[i][j],
|
|
||||||
Bg: bc.Bg,
|
|
||||||
Fg: bc.TextColor,
|
|
||||||
}
|
|
||||||
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 1
|
|
||||||
x := bc.innerArea.Max.X + oftX + ((bc.BarWidth - len(bc.labels[i])) / 2) + k
|
|
||||||
buf.Set(x, y, c)
|
|
||||||
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++ {
|
|
||||||
c := Cell{
|
|
||||||
Ch: bc.dataNum[i1][i][j],
|
|
||||||
Fg: bc.NumColor[i1],
|
|
||||||
Bg: bc.BarColor[i1],
|
|
||||||
}
|
|
||||||
if bc.BarColor[i1] == ColorDefault { // the same as above
|
|
||||||
c.Bg |= AttrReverse
|
|
||||||
}
|
|
||||||
if h == 0 {
|
|
||||||
c.Bg = bc.Bg
|
|
||||||
}
|
|
||||||
x := bc.innerArea.Min.X + oftX + (bc.BarWidth-len(bc.dataNum[i1][i]))/2 + j
|
|
||||||
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2 - ph
|
|
||||||
buf.Set(x, y, c)
|
|
||||||
}
|
|
||||||
ph += h
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if bc.ShowScale {
|
|
||||||
//Currently bar graph only supprts data range from 0 to MAX
|
|
||||||
//Plot 0
|
|
||||||
c := Cell{
|
|
||||||
Ch: '0',
|
|
||||||
Bg: bc.Bg,
|
|
||||||
Fg: bc.TextColor,
|
|
||||||
}
|
|
||||||
|
|
||||||
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2
|
|
||||||
x := bc.X
|
|
||||||
buf.Set(x, y, c)
|
|
||||||
|
|
||||||
//Plot the maximum sacle value
|
|
||||||
for i := 0; i < len(bc.maxScale); i++ {
|
|
||||||
c := Cell{
|
|
||||||
Ch: bc.maxScale[i],
|
|
||||||
Bg: bc.Bg,
|
|
||||||
Fg: bc.TextColor,
|
|
||||||
}
|
|
||||||
|
|
||||||
y := bc.innerArea.Min.Y
|
|
||||||
x := bc.X + i
|
|
||||||
|
|
||||||
buf.Set(x, y, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
|
59
style.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package termui
|
||||||
|
|
||||||
|
// Color is an integer from -1 to 255
|
||||||
|
type Color int
|
||||||
|
|
||||||
|
// Basic terminal colors
|
||||||
|
const (
|
||||||
|
ColorClear Color = -1
|
||||||
|
ColorBlack Color = 0
|
||||||
|
ColorRed Color = 1
|
||||||
|
ColorGreen Color = 2
|
||||||
|
ColorYellow Color = 3
|
||||||
|
ColorBlue Color = 4
|
||||||
|
ColorMagenta Color = 5
|
||||||
|
ColorCyan Color = 6
|
||||||
|
ColorWhite Color = 7
|
||||||
|
)
|
||||||
|
|
||||||
|
type Modifier uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModifierClear Modifier = 0
|
||||||
|
ModifierBold Modifier = 1 << 9
|
||||||
|
ModifierUnderline Modifier = 1 << 10
|
||||||
|
ModifierReverse Modifier = 1 << 11
|
||||||
|
)
|
||||||
|
|
||||||
|
// Style represents the look of the text of one terminal cell
|
||||||
|
type Style struct {
|
||||||
|
Fg Color
|
||||||
|
Bg Color
|
||||||
|
Modifier Modifier
|
||||||
|
}
|
||||||
|
|
||||||
|
var StyleClear = Style{
|
||||||
|
Fg: ColorClear,
|
||||||
|
Bg: ColorClear,
|
||||||
|
Modifier: ModifierClear,
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStyle takes 1 to 3 arguments.
|
||||||
|
// 1st argument = Fg
|
||||||
|
// 2nd argument = optional Bg
|
||||||
|
// 3rd argument = optional Modifier
|
||||||
|
func NewStyle(fg Color, args ...interface{}) Style {
|
||||||
|
bg := ColorClear
|
||||||
|
modifier := ModifierClear
|
||||||
|
if len(args) >= 1 {
|
||||||
|
bg = args[0].(Color)
|
||||||
|
}
|
||||||
|
if len(args) == 2 {
|
||||||
|
modifier = args[1].(Modifier)
|
||||||
|
}
|
||||||
|
return Style{
|
||||||
|
fg,
|
||||||
|
bg,
|
||||||
|
modifier,
|
||||||
|
}
|
||||||
|
}
|
44
symbols.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package termui
|
||||||
|
|
||||||
|
const (
|
||||||
|
SHADED_BLOCK = '░'
|
||||||
|
DOT = '•'
|
||||||
|
DOTS = '…'
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
BARS = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
||||||
|
|
||||||
|
BRAILLE_OFFSET = '\u2800'
|
||||||
|
BRAILLE = [4][2]rune{
|
||||||
|
{'\u0001', '\u0008'},
|
||||||
|
{'\u0002', '\u0010'},
|
||||||
|
{'\u0004', '\u0020'},
|
||||||
|
{'\u0040', '\u0080'},
|
||||||
|
}
|
||||||
|
|
||||||
|
DOUBLE_BRAILLE = map[[2]int]rune{
|
||||||
|
[2]int{0, 0}: '⣀',
|
||||||
|
[2]int{0, 1}: '⡠',
|
||||||
|
[2]int{0, 2}: '⡐',
|
||||||
|
[2]int{0, 3}: '⡈',
|
||||||
|
|
||||||
|
[2]int{1, 0}: '⢄',
|
||||||
|
[2]int{1, 1}: '⠤',
|
||||||
|
[2]int{1, 2}: '⠔',
|
||||||
|
[2]int{1, 3}: '⠌',
|
||||||
|
|
||||||
|
[2]int{2, 0}: '⢂',
|
||||||
|
[2]int{2, 1}: '⠢',
|
||||||
|
[2]int{2, 2}: '⠒',
|
||||||
|
[2]int{2, 3}: '⠊',
|
||||||
|
|
||||||
|
[2]int{3, 0}: '⢁',
|
||||||
|
[2]int{3, 1}: '⠡',
|
||||||
|
[2]int{3, 2}: '⠑',
|
||||||
|
[2]int{3, 3}: '⠉',
|
||||||
|
}
|
||||||
|
|
||||||
|
SINGLE_BRAILLE_LEFT = [4]rune{'\u2840', '⠄', '⠂', '⠁'}
|
||||||
|
SINGLE_BRAILLE_RIGHT = [4]rune{'\u2880', '⠠', '⠐', '⠈'}
|
||||||
|
)
|
28
symbols_other.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// 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 !windows
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
const (
|
||||||
|
TOP_LEFT = '┌'
|
||||||
|
TOP_RIGHT = '┐'
|
||||||
|
BOTTOM_LEFT = '└'
|
||||||
|
BOTTOM_RIGHT = '┘'
|
||||||
|
|
||||||
|
VERTICAL_LINE = '│'
|
||||||
|
HORIZONTAL_LINE = '─'
|
||||||
|
|
||||||
|
VERTICAL_LEFT = '┤'
|
||||||
|
VERTICAL_RIGHT = '├'
|
||||||
|
HORIZONTAL_UP = '┴'
|
||||||
|
HORIZONTAL_DOWN = '┬'
|
||||||
|
|
||||||
|
QUOTA_LEFT = '«'
|
||||||
|
QUOTA_RIGHT = '»'
|
||||||
|
|
||||||
|
VERTICAL_DASH = '┊'
|
||||||
|
HORIZONTAL_DASH = '┈'
|
||||||
|
)
|
28
symbols_windows.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// 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 windows
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
const (
|
||||||
|
TOP_LEFT = '+'
|
||||||
|
TOP_RIGHT = '+'
|
||||||
|
BOTTOM_LEFT = '+'
|
||||||
|
BOTTOM_RIGHT = '+'
|
||||||
|
|
||||||
|
VERTICAL_LINE = '|'
|
||||||
|
HORIZONTAL_LINE = '-'
|
||||||
|
|
||||||
|
VERTICAL_LEFT = '+'
|
||||||
|
VERTICAL_RIGHT = '+'
|
||||||
|
HORIZONTAL_UP = '+'
|
||||||
|
HORIZONTAL_DOWN = '+'
|
||||||
|
|
||||||
|
QUOTA_LEFT = '<'
|
||||||
|
QUOTA_RIGHT = '>'
|
||||||
|
|
||||||
|
VERTICAL_DASH = '|'
|
||||||
|
HORIZONTAL_DASH = '-'
|
||||||
|
)
|
186
table.go
@ -1,186 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
/* Table is like:
|
|
||||||
|
|
||||||
┌Awesome Table ────────────────────────────────────────────────┐
|
|
||||||
│ Col0 | Col1 | Col2 | Col3 | Col4 | Col5 | Col6 |
|
|
||||||
│──────────────────────────────────────────────────────────────│
|
|
||||||
│ Some Item #1 | AAA | 123 | CCCCC | EEEEE | GGGGG | IIIII |
|
|
||||||
│──────────────────────────────────────────────────────────────│
|
|
||||||
│ Some Item #2 | BBB | 456 | DDDDD | FFFFF | HHHHH | JJJJJ |
|
|
||||||
└──────────────────────────────────────────────────────────────┘
|
|
||||||
|
|
||||||
Datapoints are a two dimensional array of strings: [][]string
|
|
||||||
|
|
||||||
Example:
|
|
||||||
data := [][]string{
|
|
||||||
{"Col0", "Col1", "Col3", "Col4", "Col5", "Col6"},
|
|
||||||
{"Some Item #1", "AAA", "123", "CCCCC", "EEEEE", "GGGGG", "IIIII"},
|
|
||||||
{"Some Item #2", "BBB", "456", "DDDDD", "FFFFF", "HHHHH", "JJJJJ"},
|
|
||||||
}
|
|
||||||
|
|
||||||
table := termui.NewTable()
|
|
||||||
table.Rows = data // type [][]string
|
|
||||||
table.FgColor = termui.ColorWhite
|
|
||||||
table.BgColor = termui.ColorDefault
|
|
||||||
table.Height = 7
|
|
||||||
table.Width = 62
|
|
||||||
table.Y = 0
|
|
||||||
table.X = 0
|
|
||||||
table.Border = true
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Table tracks all the attributes of a Table instance
|
|
||||||
type Table struct {
|
|
||||||
Block
|
|
||||||
Rows [][]string
|
|
||||||
CellWidth []int
|
|
||||||
FgColor Attribute
|
|
||||||
BgColor Attribute
|
|
||||||
FgColors []Attribute
|
|
||||||
BgColors []Attribute
|
|
||||||
Separator bool
|
|
||||||
TextAlign Align
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTable returns a new Table instance
|
|
||||||
func NewTable() *Table {
|
|
||||||
return &Table{
|
|
||||||
Block: *NewBlock(),
|
|
||||||
FgColor: ColorWhite,
|
|
||||||
BgColor: ColorDefault,
|
|
||||||
Separator: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CellsWidth calculates the width of a cell array and returns an int
|
|
||||||
func cellsWidth(cells []Cell) int {
|
|
||||||
width := 0
|
|
||||||
for _, c := range cells {
|
|
||||||
width += c.Width()
|
|
||||||
}
|
|
||||||
return width
|
|
||||||
}
|
|
||||||
|
|
||||||
// Analysis generates and returns an array of []Cell that represent all columns in the Table
|
|
||||||
func (table *Table) Analysis() [][]Cell {
|
|
||||||
var rowCells [][]Cell
|
|
||||||
length := len(table.Rows)
|
|
||||||
if length < 1 {
|
|
||||||
return rowCells
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(table.FgColors) == 0 {
|
|
||||||
table.FgColors = make([]Attribute, len(table.Rows))
|
|
||||||
}
|
|
||||||
if len(table.BgColors) == 0 {
|
|
||||||
table.BgColors = make([]Attribute, len(table.Rows))
|
|
||||||
}
|
|
||||||
|
|
||||||
cellWidths := make([]int, len(table.Rows[0]))
|
|
||||||
|
|
||||||
for y, row := range table.Rows {
|
|
||||||
if table.FgColors[y] == 0 {
|
|
||||||
table.FgColors[y] = table.FgColor
|
|
||||||
}
|
|
||||||
if table.BgColors[y] == 0 {
|
|
||||||
table.BgColors[y] = table.BgColor
|
|
||||||
}
|
|
||||||
for x, str := range row {
|
|
||||||
cells := DefaultTxBuilder.Build(str, table.FgColors[y], table.BgColors[y])
|
|
||||||
cw := cellsWidth(cells)
|
|
||||||
if cellWidths[x] < cw {
|
|
||||||
cellWidths[x] = cw
|
|
||||||
}
|
|
||||||
rowCells = append(rowCells, cells)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
table.CellWidth = cellWidths
|
|
||||||
return rowCells
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSize calculates the table size and sets the internal value
|
|
||||||
func (table *Table) SetSize() {
|
|
||||||
length := len(table.Rows)
|
|
||||||
if table.Separator {
|
|
||||||
table.Height = length*2 + 1
|
|
||||||
} else {
|
|
||||||
table.Height = length + 2
|
|
||||||
}
|
|
||||||
table.Width = 2
|
|
||||||
if length != 0 {
|
|
||||||
for _, cellWidth := range table.CellWidth {
|
|
||||||
table.Width += cellWidth + 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CalculatePosition ...
|
|
||||||
func (table *Table) CalculatePosition(x int, y int, coordinateX *int, coordinateY *int, cellStart *int) {
|
|
||||||
if table.Separator {
|
|
||||||
*coordinateY = table.innerArea.Min.Y + y*2
|
|
||||||
} else {
|
|
||||||
*coordinateY = table.innerArea.Min.Y + y
|
|
||||||
}
|
|
||||||
if x == 0 {
|
|
||||||
*cellStart = table.innerArea.Min.X
|
|
||||||
} else {
|
|
||||||
*cellStart += table.CellWidth[x-1] + 3
|
|
||||||
}
|
|
||||||
|
|
||||||
switch table.TextAlign {
|
|
||||||
case AlignRight:
|
|
||||||
*coordinateX = *cellStart + (table.CellWidth[x] - len(table.Rows[y][x])) + 2
|
|
||||||
case AlignCenter:
|
|
||||||
*coordinateX = *cellStart + (table.CellWidth[x]-len(table.Rows[y][x]))/2 + 2
|
|
||||||
default:
|
|
||||||
*coordinateX = *cellStart + 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buffer ...
|
|
||||||
func (table *Table) Buffer() Buffer {
|
|
||||||
buffer := table.Block.Buffer()
|
|
||||||
rowCells := table.Analysis()
|
|
||||||
pointerX := table.innerArea.Min.X + 2
|
|
||||||
pointerY := table.innerArea.Min.Y
|
|
||||||
borderPointerX := table.innerArea.Min.X
|
|
||||||
for y, row := range table.Rows {
|
|
||||||
for x := range row {
|
|
||||||
table.CalculatePosition(x, y, &pointerX, &pointerY, &borderPointerX)
|
|
||||||
background := DefaultTxBuilder.Build(strings.Repeat(" ", table.CellWidth[x]+3), table.BgColors[y], table.BgColors[y])
|
|
||||||
cells := rowCells[y*len(row)+x]
|
|
||||||
for i, back := range background {
|
|
||||||
buffer.Set(borderPointerX+i, pointerY, back)
|
|
||||||
}
|
|
||||||
|
|
||||||
coordinateX := pointerX
|
|
||||||
for _, printer := range cells {
|
|
||||||
buffer.Set(coordinateX, pointerY, printer)
|
|
||||||
coordinateX += printer.Width()
|
|
||||||
}
|
|
||||||
|
|
||||||
if x != 0 {
|
|
||||||
dividors := DefaultTxBuilder.Build("|", table.FgColors[y], table.BgColors[y])
|
|
||||||
for _, dividor := range dividors {
|
|
||||||
buffer.Set(borderPointerX, pointerY, dividor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if table.Separator {
|
|
||||||
border := DefaultTxBuilder.Build(strings.Repeat("─", table.Width-2), table.FgColor, table.BgColor)
|
|
||||||
for i, cell := range border {
|
|
||||||
buffer.Set(table.innerArea.Min.X+i, pointerY+1, cell)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer
|
|
||||||
}
|
|
261
tabpane.go
@ -1,261 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
import "unicode/utf8"
|
|
||||||
|
|
||||||
type Tab struct {
|
|
||||||
Label string
|
|
||||||
RuneLen int
|
|
||||||
Blocks []Bufferer
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTab(label string) *Tab {
|
|
||||||
return &Tab{
|
|
||||||
Label: label,
|
|
||||||
RuneLen: utf8.RuneCount([]byte(label))}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tab *Tab) AddBlocks(rs ...Bufferer) {
|
|
||||||
for _, r := range rs {
|
|
||||||
tab.Blocks = append(tab.Blocks, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tab *Tab) Buffer() Buffer {
|
|
||||||
buf := NewBuffer()
|
|
||||||
for blockNum := 0; blockNum < len(tab.Blocks); blockNum++ {
|
|
||||||
b := tab.Blocks[blockNum]
|
|
||||||
buf.Merge(b.Buffer())
|
|
||||||
}
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
type TabPane struct {
|
|
||||||
Block
|
|
||||||
Tabs []Tab
|
|
||||||
activeTabIndex int
|
|
||||||
ActiveTabBg Attribute
|
|
||||||
posTabText []int
|
|
||||||
offTabText int
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTabPane() *TabPane {
|
|
||||||
tp := TabPane{
|
|
||||||
Block: *NewBlock(),
|
|
||||||
activeTabIndex: 0,
|
|
||||||
offTabText: 0,
|
|
||||||
ActiveTabBg: ThemeAttr("bg.tab.active")}
|
|
||||||
return &tp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tp *TabPane) SetTabs(tabs ...Tab) {
|
|
||||||
tp.Tabs = make([]Tab, len(tabs))
|
|
||||||
tp.posTabText = make([]int, len(tabs)+1)
|
|
||||||
off := 0
|
|
||||||
for i := 0; i < len(tp.Tabs); i++ {
|
|
||||||
tp.Tabs[i] = tabs[i]
|
|
||||||
tp.posTabText[i] = off
|
|
||||||
off += tp.Tabs[i].RuneLen + 1 //+1 for space between tabs
|
|
||||||
}
|
|
||||||
tp.posTabText[len(tabs)] = off - 1 //total length of Tab's text
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tp *TabPane) SetActiveLeft() {
|
|
||||||
if tp.activeTabIndex == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tp.activeTabIndex -= 1
|
|
||||||
if tp.posTabText[tp.activeTabIndex] < tp.offTabText {
|
|
||||||
tp.offTabText = tp.posTabText[tp.activeTabIndex]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tp *TabPane) SetActiveRight() {
|
|
||||||
if tp.activeTabIndex == len(tp.Tabs)-1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tp.activeTabIndex += 1
|
|
||||||
endOffset := tp.posTabText[tp.activeTabIndex] + tp.Tabs[tp.activeTabIndex].RuneLen
|
|
||||||
if endOffset+tp.offTabText > tp.InnerWidth() {
|
|
||||||
tp.offTabText = endOffset - tp.InnerWidth()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if left and right tabs are fully visible
|
|
||||||
// if only left tabs are not visible return -1
|
|
||||||
// if only right tabs are not visible return 1
|
|
||||||
// if both return 0
|
|
||||||
// use only if fitsWidth() returns false
|
|
||||||
func (tp *TabPane) checkAlignment() int {
|
|
||||||
ret := 0
|
|
||||||
if tp.offTabText > 0 {
|
|
||||||
ret = -1
|
|
||||||
}
|
|
||||||
if tp.offTabText+tp.InnerWidth() < tp.posTabText[len(tp.Tabs)] {
|
|
||||||
ret += 1
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if all tabs fits innerWidth of TabPane
|
|
||||||
func (tp *TabPane) fitsWidth() bool {
|
|
||||||
return tp.InnerWidth() >= tp.posTabText[len(tp.Tabs)]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tp *TabPane) align() {
|
|
||||||
if !tp.fitsWidth() && !tp.Border {
|
|
||||||
tp.PaddingLeft += 1
|
|
||||||
tp.PaddingRight += 1
|
|
||||||
tp.Block.Align()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// bridge the old Point stuct
|
|
||||||
type point struct {
|
|
||||||
X int
|
|
||||||
Y int
|
|
||||||
Ch rune
|
|
||||||
Fg Attribute
|
|
||||||
Bg Attribute
|
|
||||||
}
|
|
||||||
|
|
||||||
func buf2pt(b Buffer) []point {
|
|
||||||
ps := make([]point, 0, len(b.CellMap))
|
|
||||||
for k, c := range b.CellMap {
|
|
||||||
ps = append(ps, point{X: k.X, Y: k.Y, Ch: c.Ch, Fg: c.Fg, Bg: c.Bg})
|
|
||||||
}
|
|
||||||
|
|
||||||
return ps
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adds the point only if it is visible in TabPane.
|
|
||||||
// Point can be invisible if concatenation of Tab's texts is widther then
|
|
||||||
// innerWidth of TabPane
|
|
||||||
func (tp *TabPane) addPoint(ptab []point, charOffset *int, oftX *int, points ...point) []point {
|
|
||||||
if *charOffset < tp.offTabText || tp.offTabText+tp.InnerWidth() < *charOffset {
|
|
||||||
*charOffset++
|
|
||||||
return ptab
|
|
||||||
}
|
|
||||||
for _, p := range points {
|
|
||||||
p.X = *oftX
|
|
||||||
ptab = append(ptab, p)
|
|
||||||
}
|
|
||||||
*oftX++
|
|
||||||
*charOffset++
|
|
||||||
return ptab
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draws the point and redraws upper and lower border points (if it has one)
|
|
||||||
func (tp *TabPane) drawPointWithBorder(p point, ch rune, chbord rune, chdown rune, chup rune) []point {
|
|
||||||
var addp []point
|
|
||||||
p.Ch = ch
|
|
||||||
if tp.Border {
|
|
||||||
p.Ch = chdown
|
|
||||||
p.Y = tp.InnerY() - 1
|
|
||||||
addp = append(addp, p)
|
|
||||||
p.Ch = chup
|
|
||||||
p.Y = tp.InnerY() + 1
|
|
||||||
addp = append(addp, p)
|
|
||||||
p.Ch = chbord
|
|
||||||
}
|
|
||||||
p.Y = tp.InnerY()
|
|
||||||
return append(addp, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tp *TabPane) Buffer() Buffer {
|
|
||||||
if tp.Border {
|
|
||||||
tp.Height = 3
|
|
||||||
} else {
|
|
||||||
tp.Height = 1
|
|
||||||
}
|
|
||||||
if tp.Width > tp.posTabText[len(tp.Tabs)]+2 {
|
|
||||||
tp.Width = tp.posTabText[len(tp.Tabs)] + 2
|
|
||||||
}
|
|
||||||
buf := tp.Block.Buffer()
|
|
||||||
ps := []point{}
|
|
||||||
|
|
||||||
tp.align()
|
|
||||||
if tp.InnerHeight() <= 0 || tp.InnerWidth() <= 0 {
|
|
||||||
return NewBuffer()
|
|
||||||
}
|
|
||||||
oftX := tp.InnerX()
|
|
||||||
charOffset := 0
|
|
||||||
pt := point{Bg: tp.BorderBg, Fg: tp.BorderFg}
|
|
||||||
for i, tab := range tp.Tabs {
|
|
||||||
|
|
||||||
if i != 0 {
|
|
||||||
pt.X = oftX
|
|
||||||
pt.Y = tp.InnerY()
|
|
||||||
addp := tp.drawPointWithBorder(pt, ' ', VERTICAL_LINE, HORIZONTAL_DOWN, HORIZONTAL_UP)
|
|
||||||
ps = tp.addPoint(ps, &charOffset, &oftX, addp...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if i == tp.activeTabIndex {
|
|
||||||
pt.Bg = tp.ActiveTabBg
|
|
||||||
}
|
|
||||||
rs := []rune(tab.Label)
|
|
||||||
for k := 0; k < len(rs); k++ {
|
|
||||||
|
|
||||||
addp := make([]point, 0, 2)
|
|
||||||
if i == tp.activeTabIndex && tp.Border {
|
|
||||||
pt.Ch = ' '
|
|
||||||
pt.Y = tp.InnerY() + 1
|
|
||||||
pt.Bg = tp.BorderBg
|
|
||||||
addp = append(addp, pt)
|
|
||||||
pt.Bg = tp.ActiveTabBg
|
|
||||||
}
|
|
||||||
|
|
||||||
pt.Y = tp.InnerY()
|
|
||||||
pt.Ch = rs[k]
|
|
||||||
|
|
||||||
addp = append(addp, pt)
|
|
||||||
ps = tp.addPoint(ps, &charOffset, &oftX, addp...)
|
|
||||||
}
|
|
||||||
pt.Bg = tp.BorderBg
|
|
||||||
|
|
||||||
if !tp.fitsWidth() {
|
|
||||||
all := tp.checkAlignment()
|
|
||||||
pt.X = tp.InnerX() - 1
|
|
||||||
|
|
||||||
pt.Ch = '*'
|
|
||||||
if tp.Border {
|
|
||||||
pt.Ch = VERTICAL_LINE
|
|
||||||
}
|
|
||||||
ps = append(ps, pt)
|
|
||||||
|
|
||||||
if all <= 0 {
|
|
||||||
addp := tp.drawPointWithBorder(pt, '<', QUOTA_LEFT, HORIZONTAL_LINE, HORIZONTAL_LINE)
|
|
||||||
ps = append(ps, addp...)
|
|
||||||
}
|
|
||||||
|
|
||||||
pt.X = tp.InnerX() + tp.InnerWidth()
|
|
||||||
pt.Ch = '*'
|
|
||||||
if tp.Border {
|
|
||||||
pt.Ch = VERTICAL_LINE
|
|
||||||
}
|
|
||||||
ps = append(ps, pt)
|
|
||||||
if all >= 0 {
|
|
||||||
addp := tp.drawPointWithBorder(pt, '>', QUOTA_RIGHT, HORIZONTAL_LINE, HORIZONTAL_LINE)
|
|
||||||
ps = append(ps, addp...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//draw tab content below the TabPane
|
|
||||||
if i == tp.activeTabIndex {
|
|
||||||
blockPoints := buf2pt(tab.Buffer())
|
|
||||||
for i := 0; i < len(blockPoints); i++ {
|
|
||||||
blockPoints[i].Y += tp.Height + tp.Y
|
|
||||||
}
|
|
||||||
ps = append(ps, blockPoints...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range ps {
|
|
||||||
buf.Set(v.X, v.Y, NewCell(v.Ch, v.Fg, v.Bg))
|
|
||||||
}
|
|
||||||
buf.Sync()
|
|
||||||
return buf
|
|
||||||
}
|
|
35
termbox.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
tb "github.com/nsf/termbox-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Init initializes termbox-go and is required to render anything.
|
||||||
|
// After initialization, the library must be finalized with `Close`.
|
||||||
|
func Init() error {
|
||||||
|
if err := tb.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tb.SetInputMode(tb.InputEsc | tb.InputMouse)
|
||||||
|
tb.SetOutputMode(tb.Output256)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes termbox-go.
|
||||||
|
func Close() {
|
||||||
|
tb.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TerminalDimensions() (int, int) {
|
||||||
|
tb.Sync()
|
||||||
|
width, height := tb.Size()
|
||||||
|
return width, height
|
||||||
|
}
|
||||||
|
|
||||||
|
func Clear() {
|
||||||
|
tb.Clear(tb.ColorDefault, tb.Attribute(Theme.Default.Bg+1))
|
||||||
|
}
|
156
text_parser.go
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tokenFg = "fg"
|
||||||
|
tokenBg = "bg"
|
||||||
|
tokenModifier = "mod"
|
||||||
|
|
||||||
|
tokenItemSeparator = ","
|
||||||
|
tokenValueSeparator = ":"
|
||||||
|
|
||||||
|
tokenBeginStyledText = '['
|
||||||
|
tokenEndStyledText = ']'
|
||||||
|
|
||||||
|
tokenBeginStyle = '('
|
||||||
|
tokenEndStyle = ')'
|
||||||
|
)
|
||||||
|
|
||||||
|
type parserState uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
parserStateDefault parserState = iota
|
||||||
|
parserStateStyleItems
|
||||||
|
parserStateStyledText
|
||||||
|
)
|
||||||
|
|
||||||
|
var colorMap = map[string]Color{
|
||||||
|
"red": ColorRed,
|
||||||
|
"blue": ColorBlue,
|
||||||
|
"black": ColorBlack,
|
||||||
|
"cyan": ColorCyan,
|
||||||
|
"yellow": ColorYellow,
|
||||||
|
"white": ColorWhite,
|
||||||
|
"clear": ColorClear,
|
||||||
|
"green": ColorGreen,
|
||||||
|
"magenta": ColorMagenta,
|
||||||
|
}
|
||||||
|
|
||||||
|
var modifierMap = map[string]Modifier{
|
||||||
|
"bold": ModifierBold,
|
||||||
|
"underline": ModifierUnderline,
|
||||||
|
"reverse": ModifierReverse,
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddColorMap allows users to add/override the string to Coloribute mapping
|
||||||
|
func AddColorMap(str string, color Color) {
|
||||||
|
colorMap[str] = color
|
||||||
|
}
|
||||||
|
|
||||||
|
// readStyle translates an []rune like `fg:red,mod:bold,bg:white` to a style
|
||||||
|
func readStyle(runes []rune, defaultStyle Style) Style {
|
||||||
|
style := defaultStyle
|
||||||
|
split := strings.Split(string(runes), tokenItemSeparator)
|
||||||
|
for _, item := range split {
|
||||||
|
pair := strings.Split(item, tokenValueSeparator)
|
||||||
|
if len(pair) == 2 {
|
||||||
|
switch pair[0] {
|
||||||
|
case tokenFg:
|
||||||
|
style.Fg = colorMap[pair[1]]
|
||||||
|
case tokenBg:
|
||||||
|
style.Bg = colorMap[pair[1]]
|
||||||
|
case tokenModifier:
|
||||||
|
style.Modifier = modifierMap[pair[1]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseText(s string, defaultStyle Style) []Cell {
|
||||||
|
cells := []Cell{}
|
||||||
|
runes := []rune(s)
|
||||||
|
state := parserStateDefault
|
||||||
|
styledText := []rune{}
|
||||||
|
styleItems := []rune{}
|
||||||
|
squareCount := 0
|
||||||
|
|
||||||
|
reset := func() {
|
||||||
|
styledText = []rune{}
|
||||||
|
styleItems = []rune{}
|
||||||
|
state = parserStateDefault
|
||||||
|
squareCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
rollback := func() {
|
||||||
|
cells = append(cells, RunesToStyledCells(styledText, defaultStyle)...)
|
||||||
|
cells = append(cells, RunesToStyledCells(styleItems, defaultStyle)...)
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
// chop first and last runes
|
||||||
|
chop := func(s []rune) []rune {
|
||||||
|
return s[1 : len(s)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, _rune := range runes {
|
||||||
|
switch state {
|
||||||
|
case parserStateDefault:
|
||||||
|
if _rune == tokenBeginStyledText {
|
||||||
|
state = parserStateStyledText
|
||||||
|
squareCount = 1
|
||||||
|
styledText = append(styledText, _rune)
|
||||||
|
} else {
|
||||||
|
cells = append(cells, Cell{_rune, defaultStyle})
|
||||||
|
}
|
||||||
|
case parserStateStyledText:
|
||||||
|
switch {
|
||||||
|
case squareCount == 0:
|
||||||
|
switch _rune {
|
||||||
|
case tokenBeginStyle:
|
||||||
|
state = parserStateStyleItems
|
||||||
|
styleItems = append(styleItems, _rune)
|
||||||
|
default:
|
||||||
|
rollback()
|
||||||
|
switch _rune {
|
||||||
|
case tokenBeginStyledText:
|
||||||
|
state = parserStateStyledText
|
||||||
|
squareCount = 1
|
||||||
|
styleItems = append(styleItems, _rune)
|
||||||
|
default:
|
||||||
|
cells = append(cells, Cell{_rune, defaultStyle})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case len(runes) == i+1:
|
||||||
|
rollback()
|
||||||
|
styledText = append(styledText, _rune)
|
||||||
|
case _rune == tokenBeginStyledText:
|
||||||
|
squareCount++
|
||||||
|
styledText = append(styledText, _rune)
|
||||||
|
case _rune == tokenEndStyledText:
|
||||||
|
squareCount--
|
||||||
|
styledText = append(styledText, _rune)
|
||||||
|
default:
|
||||||
|
styledText = append(styledText, _rune)
|
||||||
|
}
|
||||||
|
case parserStateStyleItems:
|
||||||
|
styleItems = append(styleItems, _rune)
|
||||||
|
if _rune == tokenEndStyle {
|
||||||
|
style := readStyle(chop(styleItems), defaultStyle)
|
||||||
|
cells = append(cells, RunesToStyledCells(chop(styledText), style)...)
|
||||||
|
reset()
|
||||||
|
} else if len(runes) == i+1 {
|
||||||
|
rollback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cells
|
||||||
|
}
|
283
textbuilder.go
@ -1,283 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mitchellh/go-wordwrap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TextBuilder is a minimal interface to produce text []Cell using specific syntax (markdown).
|
|
||||||
type TextBuilder interface {
|
|
||||||
Build(s string, fg, bg Attribute) []Cell
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultTxBuilder is set to be MarkdownTxBuilder.
|
|
||||||
var DefaultTxBuilder = NewMarkdownTxBuilder()
|
|
||||||
|
|
||||||
// MarkdownTxBuilder implements TextBuilder interface, using markdown syntax.
|
|
||||||
type MarkdownTxBuilder struct {
|
|
||||||
baseFg Attribute
|
|
||||||
baseBg Attribute
|
|
||||||
plainTx []rune
|
|
||||||
markers []marker
|
|
||||||
}
|
|
||||||
|
|
||||||
type marker struct {
|
|
||||||
st int
|
|
||||||
ed int
|
|
||||||
fg Attribute
|
|
||||||
bg Attribute
|
|
||||||
}
|
|
||||||
|
|
||||||
var colorMap = map[string]Attribute{
|
|
||||||
"red": ColorRed,
|
|
||||||
"blue": ColorBlue,
|
|
||||||
"black": ColorBlack,
|
|
||||||
"cyan": ColorCyan,
|
|
||||||
"yellow": ColorYellow,
|
|
||||||
"white": ColorWhite,
|
|
||||||
"default": ColorDefault,
|
|
||||||
"green": ColorGreen,
|
|
||||||
"magenta": ColorMagenta,
|
|
||||||
}
|
|
||||||
|
|
||||||
var attrMap = map[string]Attribute{
|
|
||||||
"bold": AttrBold,
|
|
||||||
"underline": AttrUnderline,
|
|
||||||
"reverse": AttrReverse,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow users to add/override the string to attribute mapping
|
|
||||||
func AddColorMap(str string, attr Attribute) {
|
|
||||||
colorMap[str] = attr
|
|
||||||
}
|
|
||||||
|
|
||||||
func rmSpc(s string) string {
|
|
||||||
reg := regexp.MustCompile(`\s+`)
|
|
||||||
return reg.ReplaceAllString(s, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// readAttr translates strings like `fg-red,fg-bold,bg-white` to fg and bg Attribute
|
|
||||||
func (mtb MarkdownTxBuilder) readAttr(s string) (Attribute, Attribute) {
|
|
||||||
fg := mtb.baseFg
|
|
||||||
bg := mtb.baseBg
|
|
||||||
|
|
||||||
updateAttr := func(a Attribute, attrs []string) Attribute {
|
|
||||||
for _, s := range attrs {
|
|
||||||
// replace the color
|
|
||||||
if c, ok := colorMap[s]; ok {
|
|
||||||
a &= 0xFF00 // erase clr 0 ~ 8 bits
|
|
||||||
a |= c // set clr
|
|
||||||
}
|
|
||||||
// add attrs
|
|
||||||
if c, ok := attrMap[s]; ok {
|
|
||||||
a |= c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
ss := strings.Split(s, ",")
|
|
||||||
fgs := []string{}
|
|
||||||
bgs := []string{}
|
|
||||||
for _, v := range ss {
|
|
||||||
subs := strings.Split(v, "-")
|
|
||||||
if len(subs) > 1 {
|
|
||||||
if subs[0] == "fg" {
|
|
||||||
fgs = append(fgs, subs[1])
|
|
||||||
} else if subs[0] == "bg" {
|
|
||||||
bgs = append(bgs, subs[1])
|
|
||||||
}
|
|
||||||
// else maybe error somehow?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fg = updateAttr(fg, fgs)
|
|
||||||
bg = updateAttr(bg, bgs)
|
|
||||||
return fg, bg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mtb *MarkdownTxBuilder) reset() {
|
|
||||||
mtb.plainTx = []rune{}
|
|
||||||
mtb.markers = []marker{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse streams and parses text into normalized text and render sequence.
|
|
||||||
func (mtb *MarkdownTxBuilder) parse(str string) {
|
|
||||||
rs := str2runes(str)
|
|
||||||
normTx := []rune{}
|
|
||||||
square := []rune{}
|
|
||||||
brackt := []rune{}
|
|
||||||
accSquare := false
|
|
||||||
accBrackt := false
|
|
||||||
cntSquare := 0
|
|
||||||
|
|
||||||
reset := func() {
|
|
||||||
square = []rune{}
|
|
||||||
brackt = []rune{}
|
|
||||||
accSquare = false
|
|
||||||
accBrackt = false
|
|
||||||
cntSquare = 0
|
|
||||||
}
|
|
||||||
// pipe stacks into normTx and clear
|
|
||||||
rollback := func() {
|
|
||||||
normTx = append(normTx, square...)
|
|
||||||
normTx = append(normTx, brackt...)
|
|
||||||
reset()
|
|
||||||
}
|
|
||||||
// chop first and last
|
|
||||||
chop := func(s []rune) []rune {
|
|
||||||
return s[1 : len(s)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, r := range rs {
|
|
||||||
switch {
|
|
||||||
// stacking brackt
|
|
||||||
case accBrackt:
|
|
||||||
brackt = append(brackt, r)
|
|
||||||
if ')' == r {
|
|
||||||
fg, bg := mtb.readAttr(string(chop(brackt)))
|
|
||||||
st := len(normTx)
|
|
||||||
ed := len(normTx) + len(square) - 2
|
|
||||||
mtb.markers = append(mtb.markers, marker{st, ed, fg, bg})
|
|
||||||
normTx = append(normTx, chop(square)...)
|
|
||||||
reset()
|
|
||||||
} else if i+1 == len(rs) {
|
|
||||||
rollback()
|
|
||||||
}
|
|
||||||
// stacking square
|
|
||||||
case accSquare:
|
|
||||||
switch {
|
|
||||||
// squares closed and followed by a '('
|
|
||||||
case cntSquare == 0 && '(' == r:
|
|
||||||
accBrackt = true
|
|
||||||
brackt = append(brackt, '(')
|
|
||||||
// squares closed but not followed by a '('
|
|
||||||
case cntSquare == 0:
|
|
||||||
rollback()
|
|
||||||
if '[' == r {
|
|
||||||
accSquare = true
|
|
||||||
cntSquare = 1
|
|
||||||
brackt = append(brackt, '[')
|
|
||||||
} else {
|
|
||||||
normTx = append(normTx, r)
|
|
||||||
}
|
|
||||||
// hit the end
|
|
||||||
case i+1 == len(rs):
|
|
||||||
square = append(square, r)
|
|
||||||
rollback()
|
|
||||||
case '[' == r:
|
|
||||||
cntSquare++
|
|
||||||
square = append(square, '[')
|
|
||||||
case ']' == r:
|
|
||||||
cntSquare--
|
|
||||||
square = append(square, ']')
|
|
||||||
// normal char
|
|
||||||
default:
|
|
||||||
square = append(square, r)
|
|
||||||
}
|
|
||||||
// stacking normTx
|
|
||||||
default:
|
|
||||||
if '[' == r {
|
|
||||||
accSquare = true
|
|
||||||
cntSquare = 1
|
|
||||||
square = append(square, '[')
|
|
||||||
} else {
|
|
||||||
normTx = append(normTx, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mtb.plainTx = normTx
|
|
||||||
}
|
|
||||||
|
|
||||||
func wrapTx(cs []Cell, wl int) []Cell {
|
|
||||||
tmpCell := make([]Cell, len(cs))
|
|
||||||
copy(tmpCell, cs)
|
|
||||||
|
|
||||||
// get the plaintext
|
|
||||||
plain := CellsToStr(cs)
|
|
||||||
|
|
||||||
// wrap
|
|
||||||
plainWrapped := wordwrap.WrapString(plain, uint(wl))
|
|
||||||
|
|
||||||
// find differences and insert
|
|
||||||
finalCell := tmpCell // finalcell will get the inserts and is what is returned
|
|
||||||
|
|
||||||
plainRune := []rune(plain)
|
|
||||||
plainWrappedRune := []rune(plainWrapped)
|
|
||||||
trigger := "go"
|
|
||||||
plainRuneNew := plainRune
|
|
||||||
|
|
||||||
for trigger != "stop" {
|
|
||||||
plainRune = plainRuneNew
|
|
||||||
for i := range plainRune {
|
|
||||||
if plainRune[i] == plainWrappedRune[i] {
|
|
||||||
trigger = "stop"
|
|
||||||
} else if plainRune[i] != plainWrappedRune[i] && plainWrappedRune[i] == 10 {
|
|
||||||
trigger = "go"
|
|
||||||
cell := Cell{10, 0, 0}
|
|
||||||
j := i - 0
|
|
||||||
|
|
||||||
// insert a cell into the []Cell in correct position
|
|
||||||
tmpCell[i] = cell
|
|
||||||
|
|
||||||
// insert the newline into plain so we avoid indexing errors
|
|
||||||
plainRuneNew = append(plainRune, 10)
|
|
||||||
copy(plainRuneNew[j+1:], plainRuneNew[j:])
|
|
||||||
plainRuneNew[j] = plainWrappedRune[j]
|
|
||||||
|
|
||||||
// restart the inner for loop until plain and plain wrapped are
|
|
||||||
// the same; yeah, it's inefficient, but the text amounts
|
|
||||||
// should be small
|
|
||||||
break
|
|
||||||
|
|
||||||
} else if plainRune[i] != plainWrappedRune[i] &&
|
|
||||||
plainWrappedRune[i-1] == 10 && // if the prior rune is a newline
|
|
||||||
plainRune[i] == 32 { // and this rune is a space
|
|
||||||
trigger = "go"
|
|
||||||
// need to delete plainRune[i] because it gets rid of an extra
|
|
||||||
// space
|
|
||||||
plainRuneNew = append(plainRune[:i], plainRune[i+1:]...)
|
|
||||||
break
|
|
||||||
|
|
||||||
} else {
|
|
||||||
trigger = "stop" // stops the outer for loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finalCell = tmpCell
|
|
||||||
|
|
||||||
return finalCell
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build implements TextBuilder interface.
|
|
||||||
func (mtb MarkdownTxBuilder) Build(s string, fg, bg Attribute) []Cell {
|
|
||||||
mtb.baseFg = fg
|
|
||||||
mtb.baseBg = bg
|
|
||||||
mtb.reset()
|
|
||||||
mtb.parse(s)
|
|
||||||
cs := make([]Cell, len(mtb.plainTx))
|
|
||||||
for i := range cs {
|
|
||||||
cs[i] = Cell{Ch: mtb.plainTx[i], Fg: fg, Bg: bg}
|
|
||||||
}
|
|
||||||
for _, mrk := range mtb.markers {
|
|
||||||
for i := mrk.st; i < mrk.ed; i++ {
|
|
||||||
cs[i].Fg = mrk.fg
|
|
||||||
cs[i].Bg = mrk.bg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cs
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMarkdownTxBuilder returns a TextBuilder employing markdown syntax.
|
|
||||||
func NewMarkdownTxBuilder() TextBuilder {
|
|
||||||
return MarkdownTxBuilder{}
|
|
||||||
}
|
|
@ -1,70 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestReadAttr(t *testing.T) {
|
|
||||||
m := MarkdownTxBuilder{}
|
|
||||||
m.baseFg = ColorCyan | AttrUnderline
|
|
||||||
m.baseBg = ColorBlue | AttrBold
|
|
||||||
fg, bg := m.readAttr("fg-red,bg-reverse")
|
|
||||||
if fg != ColorRed|AttrUnderline || bg != ColorBlue|AttrBold|AttrReverse {
|
|
||||||
t.Error("readAttr failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMTBParse(t *testing.T) {
|
|
||||||
/*
|
|
||||||
str := func(cs []Cell) string {
|
|
||||||
rs := make([]rune, len(cs))
|
|
||||||
for i := range cs {
|
|
||||||
rs[i] = cs[i].Ch
|
|
||||||
}
|
|
||||||
return string(rs)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
tbls := [][]string{
|
|
||||||
{"hello world", "hello world"},
|
|
||||||
{"[hello](fg-red) world", "hello world"},
|
|
||||||
{"[[hello]](bg-red) world", "[hello] world"},
|
|
||||||
{"[1] hello world", "[1] hello world"},
|
|
||||||
{"[[1]](bg-white) [hello] world", "[1] [hello] world"},
|
|
||||||
{"[hello world]", "[hello world]"},
|
|
||||||
{"", ""},
|
|
||||||
{"[hello world)", "[hello world)"},
|
|
||||||
{"[0] [hello](bg-red)[ world](fg-blue)!", "[0] hello world!"},
|
|
||||||
}
|
|
||||||
|
|
||||||
m := MarkdownTxBuilder{}
|
|
||||||
m.baseFg = ColorWhite
|
|
||||||
m.baseBg = ColorDefault
|
|
||||||
for _, s := range tbls {
|
|
||||||
m.reset()
|
|
||||||
m.parse(s[0])
|
|
||||||
res := string(m.plainTx)
|
|
||||||
if s[1] != res {
|
|
||||||
t.Errorf("\ninput :%s\nshould:%s\noutput:%s", s[0], s[1], res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m.reset()
|
|
||||||
m.parse("[0] [hello](bg-red)[ world](fg-blue)")
|
|
||||||
if len(m.markers) != 2 &&
|
|
||||||
m.markers[0].st == 4 &&
|
|
||||||
m.markers[0].ed == 11 &&
|
|
||||||
m.markers[0].fg == ColorWhite &&
|
|
||||||
m.markers[0].bg == ColorRed {
|
|
||||||
t.Error("markers dismatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
m2 := NewMarkdownTxBuilder()
|
|
||||||
cs := m2.Build("[0] [hellob-e) wrd]fgblue)!", ColorWhite, ColorBlack)
|
|
||||||
cs = m2.Build("[0] [hello](bg-red) [world](fg-blue)!", ColorWhite, ColorBlack)
|
|
||||||
if cs[4].Ch != 'h' && cs[4].Bg != ColorRed && cs[4].Fg != ColorWhite {
|
|
||||||
t.Error("dismatch in Build")
|
|
||||||
}
|
|
||||||
}
|
|
248
theme.go
@ -4,141 +4,149 @@
|
|||||||
|
|
||||||
package termui
|
package termui
|
||||||
|
|
||||||
import "strings"
|
var StandardColors = []Color{
|
||||||
|
ColorRed,
|
||||||
/*
|
ColorGreen,
|
||||||
// A ColorScheme represents the current look-and-feel of the dashboard.
|
ColorYellow,
|
||||||
type ColorScheme struct {
|
ColorBlue,
|
||||||
BodyBg Attribute
|
ColorMagenta,
|
||||||
BlockBg Attribute
|
ColorCyan,
|
||||||
HasBorder bool
|
ColorWhite,
|
||||||
BorderFg Attribute
|
|
||||||
BorderBg Attribute
|
|
||||||
BorderLabelTextFg Attribute
|
|
||||||
BorderLabelTextBg Attribute
|
|
||||||
ParTextFg Attribute
|
|
||||||
ParTextBg Attribute
|
|
||||||
SparklineLine Attribute
|
|
||||||
SparklineTitle Attribute
|
|
||||||
GaugeBar Attribute
|
|
||||||
GaugePercent Attribute
|
|
||||||
LineChartLine Attribute
|
|
||||||
LineChartAxes Attribute
|
|
||||||
ListItemFg Attribute
|
|
||||||
ListItemBg Attribute
|
|
||||||
BarChartBar Attribute
|
|
||||||
BarChartText Attribute
|
|
||||||
BarChartNum Attribute
|
|
||||||
MBarChartBar Attribute
|
|
||||||
MBarChartText Attribute
|
|
||||||
MBarChartNum Attribute
|
|
||||||
TabActiveBg Attribute
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// default color scheme depends on the user's terminal setting.
|
var StandardStyles = []Style{
|
||||||
var themeDefault = ColorScheme{HasBorder: true}
|
NewStyle(ColorRed),
|
||||||
|
NewStyle(ColorGreen),
|
||||||
var themeHelloWorld = ColorScheme{
|
NewStyle(ColorYellow),
|
||||||
BodyBg: ColorBlack,
|
NewStyle(ColorBlue),
|
||||||
BlockBg: ColorBlack,
|
NewStyle(ColorMagenta),
|
||||||
HasBorder: true,
|
NewStyle(ColorCyan),
|
||||||
BorderFg: ColorWhite,
|
NewStyle(ColorWhite),
|
||||||
BorderBg: ColorBlack,
|
|
||||||
BorderLabelTextBg: ColorBlack,
|
|
||||||
BorderLabelTextFg: ColorGreen,
|
|
||||||
ParTextBg: ColorBlack,
|
|
||||||
ParTextFg: ColorWhite,
|
|
||||||
SparklineLine: ColorMagenta,
|
|
||||||
SparklineTitle: ColorWhite,
|
|
||||||
GaugeBar: ColorRed,
|
|
||||||
GaugePercent: ColorWhite,
|
|
||||||
LineChartLine: ColorYellow | AttrBold,
|
|
||||||
LineChartAxes: ColorWhite,
|
|
||||||
ListItemBg: ColorBlack,
|
|
||||||
ListItemFg: ColorYellow,
|
|
||||||
BarChartBar: ColorRed,
|
|
||||||
BarChartNum: ColorWhite,
|
|
||||||
BarChartText: ColorCyan,
|
|
||||||
MBarChartBar: ColorRed,
|
|
||||||
MBarChartNum: ColorWhite,
|
|
||||||
MBarChartText: ColorCyan,
|
|
||||||
TabActiveBg: ColorMagenta,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var theme = themeDefault // global dep
|
type RootTheme struct {
|
||||||
|
Default Style
|
||||||
|
|
||||||
// Theme returns the currently used theme.
|
Block BlockTheme
|
||||||
func Theme() ColorScheme {
|
|
||||||
return theme
|
BarChart BarChartTheme
|
||||||
|
Gauge GaugeTheme
|
||||||
|
LineChart LineChartTheme
|
||||||
|
List ListTheme
|
||||||
|
Paragraph ParagraphTheme
|
||||||
|
PieChart PieChartTheme
|
||||||
|
Sparkline SparklineTheme
|
||||||
|
StackedBarChart StackedBarChartTheme
|
||||||
|
Tab TabTheme
|
||||||
|
Table TableTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTheme sets a new, custom theme.
|
type BlockTheme struct {
|
||||||
func SetTheme(newTheme ColorScheme) {
|
Title Style
|
||||||
theme = newTheme
|
Border Style
|
||||||
}
|
}
|
||||||
|
|
||||||
// UseTheme sets a predefined scheme. Currently available: "hello-world" and
|
type BarChartTheme struct {
|
||||||
// "black-and-white".
|
Bars []Color
|
||||||
func UseTheme(th string) {
|
Nums []Style
|
||||||
switch th {
|
Labels []Style
|
||||||
case "helloworld":
|
|
||||||
theme = themeHelloWorld
|
|
||||||
default:
|
|
||||||
theme = themeDefault
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
var ColorMap = map[string]Attribute{
|
|
||||||
"fg": ColorWhite,
|
|
||||||
"bg": ColorDefault,
|
|
||||||
"border.fg": ColorWhite,
|
|
||||||
"label.fg": ColorGreen,
|
|
||||||
"par.fg": ColorYellow,
|
|
||||||
"par.label.bg": ColorWhite,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ThemeAttr(name string) Attribute {
|
type GaugeTheme struct {
|
||||||
return lookUpAttr(ColorMap, name)
|
Bar Color
|
||||||
|
Label Style
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookUpAttr(clrmap map[string]Attribute, name string) Attribute {
|
type LineChartTheme struct {
|
||||||
a, ok := clrmap[name]
|
Lines []Color
|
||||||
if ok {
|
Axes Color
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
ns := strings.Split(name, ".")
|
|
||||||
for i := range ns {
|
|
||||||
nn := strings.Join(ns[i:len(ns)], ".")
|
|
||||||
a, ok = ColorMap[nn]
|
|
||||||
if ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return a
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 0<=r,g,b <= 5
|
type ListTheme struct {
|
||||||
func ColorRGB(r, g, b int) Attribute {
|
Text Style
|
||||||
within := func(n int) int {
|
|
||||||
if n < 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if n > 5 {
|
|
||||||
return 5
|
|
||||||
}
|
|
||||||
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
r, b, g = within(r), within(b), within(g)
|
|
||||||
return Attribute(0x0f + 36*r + 6*g + b)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert from familiar 24 bit colors into 6 bit terminal colors
|
type ParagraphTheme struct {
|
||||||
func ColorRGB24(r, g, b int) Attribute {
|
Text Style
|
||||||
return ColorRGB(r/51, g/51, b/51)
|
}
|
||||||
|
|
||||||
|
type PieChartTheme struct {
|
||||||
|
Slices []Color
|
||||||
|
}
|
||||||
|
|
||||||
|
type SparklineTheme struct {
|
||||||
|
Title Style
|
||||||
|
Line Color
|
||||||
|
}
|
||||||
|
|
||||||
|
type StackedBarChartTheme struct {
|
||||||
|
Bars []Color
|
||||||
|
Nums []Style
|
||||||
|
Labels []Style
|
||||||
|
}
|
||||||
|
|
||||||
|
type TabTheme struct {
|
||||||
|
Active Style
|
||||||
|
Inactive Style
|
||||||
|
}
|
||||||
|
|
||||||
|
type TableTheme struct {
|
||||||
|
Text Style
|
||||||
|
}
|
||||||
|
|
||||||
|
var Theme = RootTheme{
|
||||||
|
Default: NewStyle(ColorWhite),
|
||||||
|
|
||||||
|
Block: BlockTheme{
|
||||||
|
Title: NewStyle(ColorWhite),
|
||||||
|
Border: NewStyle(ColorWhite),
|
||||||
|
},
|
||||||
|
|
||||||
|
BarChart: BarChartTheme{
|
||||||
|
Bars: StandardColors,
|
||||||
|
Nums: StandardStyles,
|
||||||
|
Labels: StandardStyles,
|
||||||
|
},
|
||||||
|
|
||||||
|
Paragraph: ParagraphTheme{
|
||||||
|
Text: NewStyle(ColorWhite),
|
||||||
|
},
|
||||||
|
|
||||||
|
PieChart: PieChartTheme{
|
||||||
|
Slices: StandardColors,
|
||||||
|
},
|
||||||
|
|
||||||
|
List: ListTheme{
|
||||||
|
Text: NewStyle(ColorWhite),
|
||||||
|
},
|
||||||
|
|
||||||
|
StackedBarChart: StackedBarChartTheme{
|
||||||
|
Bars: StandardColors,
|
||||||
|
Nums: StandardStyles,
|
||||||
|
Labels: StandardStyles,
|
||||||
|
},
|
||||||
|
|
||||||
|
Gauge: GaugeTheme{
|
||||||
|
Bar: ColorWhite,
|
||||||
|
Label: NewStyle(ColorWhite),
|
||||||
|
},
|
||||||
|
|
||||||
|
Sparkline: SparklineTheme{
|
||||||
|
Line: ColorBlack,
|
||||||
|
Title: NewStyle(ColorBlue),
|
||||||
|
},
|
||||||
|
|
||||||
|
LineChart: LineChartTheme{
|
||||||
|
Lines: StandardColors,
|
||||||
|
Axes: ColorBlue,
|
||||||
|
},
|
||||||
|
|
||||||
|
Table: TableTheme{
|
||||||
|
Text: NewStyle(ColorWhite),
|
||||||
|
},
|
||||||
|
|
||||||
|
Tab: TabTheme{
|
||||||
|
Active: NewStyle(ColorRed),
|
||||||
|
Inactive: NewStyle(ColorWhite),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,35 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
var cmap = map[string]Attribute{
|
|
||||||
"fg": ColorWhite,
|
|
||||||
"bg": ColorDefault,
|
|
||||||
"border.fg": ColorWhite,
|
|
||||||
"label.fg": ColorGreen,
|
|
||||||
"par.fg": ColorYellow,
|
|
||||||
"par.label.bg": ColorWhite,
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoopUpAttr(t *testing.T) {
|
|
||||||
tbl := []struct {
|
|
||||||
name string
|
|
||||||
should Attribute
|
|
||||||
}{
|
|
||||||
{"par.label.bg", ColorWhite},
|
|
||||||
{"par.label.fg", ColorGreen},
|
|
||||||
{"par.bg", ColorDefault},
|
|
||||||
{"bar.border.fg", ColorWhite},
|
|
||||||
{"bar.label.bg", ColorDefault},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range tbl {
|
|
||||||
if lookUpAttr(cmap, v.name) != v.should {
|
|
||||||
t.Error(v.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
344
utils.go
@ -5,246 +5,172 @@
|
|||||||
package termui
|
package termui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"fmt"
|
||||||
"strings"
|
"math"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
rw "github.com/mattn/go-runewidth"
|
rw "github.com/mattn/go-runewidth"
|
||||||
tb "github.com/nsf/termbox-go"
|
wordwrap "github.com/mitchellh/go-wordwrap"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* ---------------Port from termbox-go --------------------- */
|
// https://stackoverflow.com/questions/12753805/type-converting-slices-of-interfaces-in-go
|
||||||
|
func InterfaceSlice(slice interface{}) []interface{} {
|
||||||
// Attribute is printable cell's color and style.
|
s := reflect.ValueOf(slice)
|
||||||
type Attribute uint16
|
if s.Kind() != reflect.Slice {
|
||||||
|
panic("InterfaceSlice() given a non-slice type")
|
||||||
// 8 basic clolrs
|
|
||||||
const (
|
|
||||||
ColorDefault Attribute = iota
|
|
||||||
ColorBlack
|
|
||||||
ColorRed
|
|
||||||
ColorGreen
|
|
||||||
ColorYellow
|
|
||||||
ColorBlue
|
|
||||||
ColorMagenta
|
|
||||||
ColorCyan
|
|
||||||
ColorWhite
|
|
||||||
)
|
|
||||||
|
|
||||||
// Have a constant that defines number of colors
|
|
||||||
const NumberofColors = 8
|
|
||||||
|
|
||||||
// Text style
|
|
||||||
const (
|
|
||||||
AttrBold Attribute = 1 << (iota + 9)
|
|
||||||
AttrUnderline
|
|
||||||
AttrReverse
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
dot = "…"
|
|
||||||
dotw = rw.StringWidth(dot)
|
|
||||||
)
|
|
||||||
|
|
||||||
// termbox passthrough
|
|
||||||
type OutputMode int
|
|
||||||
|
|
||||||
// termbox passthrough
|
|
||||||
const (
|
|
||||||
OutputCurrent OutputMode = iota
|
|
||||||
OutputNormal
|
|
||||||
Output256
|
|
||||||
Output216
|
|
||||||
OutputGrayscale
|
|
||||||
)
|
|
||||||
|
|
||||||
/* ----------------------- End ----------------------------- */
|
|
||||||
|
|
||||||
func toTmAttr(x Attribute) tb.Attribute {
|
|
||||||
return tb.Attribute(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func str2runes(s string) []rune {
|
|
||||||
return []rune(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Here for backwards-compatibility.
|
|
||||||
func trimStr2Runes(s string, w int) []rune {
|
|
||||||
return TrimStr2Runes(s, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TrimStr2Runes trims string to w[-1 rune], appends …, and returns the runes
|
|
||||||
// of that string if string is grather then n. If string is small then w,
|
|
||||||
// return the runes.
|
|
||||||
func TrimStr2Runes(s string, w int) []rune {
|
|
||||||
if w <= 0 {
|
|
||||||
return []rune{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sw := rw.StringWidth(s)
|
ret := make([]interface{}, s.Len())
|
||||||
if sw > w {
|
|
||||||
return []rune(rw.Truncate(s, w, dot))
|
for i := 0; i < s.Len(); i++ {
|
||||||
|
ret[i] = s.Index(i).Interface()
|
||||||
}
|
}
|
||||||
return str2runes(s)
|
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrimStrIfAppropriate trim string to "s[:-1] + …"
|
func MaxInt(x, y int) int {
|
||||||
// if string > width otherwise return string
|
if x > y {
|
||||||
func TrimStrIfAppropriate(s string, w int) string {
|
return x
|
||||||
|
}
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
|
||||||
|
func MinInt(x, y int) int {
|
||||||
|
if x < y {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
|
||||||
|
func TrimString(s string, w int) string {
|
||||||
if w <= 0 {
|
if w <= 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
if rw.StringWidth(s) > w {
|
||||||
sw := rw.StringWidth(s)
|
return rw.Truncate(s, w, string(DOTS))
|
||||||
if sw > w {
|
|
||||||
return rw.Truncate(s, w, dot)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func strWidth(s string) int {
|
func GetMaxIntFromSlice(slice []int) (int, error) {
|
||||||
return rw.StringWidth(s)
|
if len(slice) == 0 {
|
||||||
}
|
return 0, fmt.Errorf("cannot get max value from empty slice")
|
||||||
|
|
||||||
func charWidth(ch rune) int {
|
|
||||||
return rw.RuneWidth(ch)
|
|
||||||
}
|
|
||||||
|
|
||||||
var whiteSpaceRegex = regexp.MustCompile(`\s`)
|
|
||||||
|
|
||||||
// StringToAttribute converts text to a termui attribute. You may specify more
|
|
||||||
// then one attribute like that: "BLACK, BOLD, ...". All whitespaces
|
|
||||||
// are ignored.
|
|
||||||
func StringToAttribute(text string) Attribute {
|
|
||||||
text = whiteSpaceRegex.ReplaceAllString(strings.ToLower(text), "")
|
|
||||||
attributes := strings.Split(text, ",")
|
|
||||||
result := Attribute(0)
|
|
||||||
|
|
||||||
for _, theAttribute := range attributes {
|
|
||||||
var match Attribute
|
|
||||||
switch theAttribute {
|
|
||||||
case "reset", "default":
|
|
||||||
match = ColorDefault
|
|
||||||
|
|
||||||
case "black":
|
|
||||||
match = ColorBlack
|
|
||||||
|
|
||||||
case "red":
|
|
||||||
match = ColorRed
|
|
||||||
|
|
||||||
case "green":
|
|
||||||
match = ColorGreen
|
|
||||||
|
|
||||||
case "yellow":
|
|
||||||
match = ColorYellow
|
|
||||||
|
|
||||||
case "blue":
|
|
||||||
match = ColorBlue
|
|
||||||
|
|
||||||
case "magenta":
|
|
||||||
match = ColorMagenta
|
|
||||||
|
|
||||||
case "cyan":
|
|
||||||
match = ColorCyan
|
|
||||||
|
|
||||||
case "white":
|
|
||||||
match = ColorWhite
|
|
||||||
|
|
||||||
case "bold":
|
|
||||||
match = AttrBold
|
|
||||||
|
|
||||||
case "underline":
|
|
||||||
match = AttrUnderline
|
|
||||||
|
|
||||||
case "reverse":
|
|
||||||
match = AttrReverse
|
|
||||||
}
|
}
|
||||||
|
var max int
|
||||||
result |= match
|
for _, val := range slice {
|
||||||
|
if val > max {
|
||||||
|
max = val
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// TextCells returns a coloured text cells []Cell
|
|
||||||
func TextCells(s string, fg, bg Attribute) []Cell {
|
|
||||||
cs := make([]Cell, 0, len(s))
|
|
||||||
|
|
||||||
// sequence := MarkdownTextRendererFactory{}.TextRenderer(s).Render(fg, bg)
|
|
||||||
// runes := []rune(sequence.NormalizedText)
|
|
||||||
runes := str2runes(s)
|
|
||||||
|
|
||||||
for n := range runes {
|
|
||||||
// point, _ := sequence.PointAt(n, 0, 0)
|
|
||||||
// cs = append(cs, Cell{point.Ch, point.Fg, point.Bg})
|
|
||||||
cs = append(cs, Cell{runes[n], fg, bg})
|
|
||||||
}
|
}
|
||||||
return cs
|
return max, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Width returns the actual screen space the cell takes (usually 1 or 2).
|
func GetMaxFloat64FromSlice(slice []float64) (float64, error) {
|
||||||
func (c Cell) Width() int {
|
if len(slice) == 0 {
|
||||||
return charWidth(c.Ch)
|
return 0, fmt.Errorf("cannot get max value from empty slice")
|
||||||
}
|
|
||||||
|
|
||||||
// Copy return a copy of c
|
|
||||||
func (c Cell) Copy() Cell {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// TrimTxCells trims the overflowed text cells sequence.
|
|
||||||
func TrimTxCells(cs []Cell, w int) []Cell {
|
|
||||||
if len(cs) <= w {
|
|
||||||
return cs
|
|
||||||
}
|
}
|
||||||
return cs[:w]
|
var max float64
|
||||||
|
for _, val := range slice {
|
||||||
|
if val > max {
|
||||||
|
max = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DTrimTxCls trims the overflowed text cells sequence and append dots at the end.
|
func GetMaxFloat64From2dSlice(slices [][]float64) (float64, error) {
|
||||||
func DTrimTxCls(cs []Cell, w int) []Cell {
|
if len(slices) == 0 {
|
||||||
l := len(cs)
|
return 0, fmt.Errorf("cannot get max value from empty slice")
|
||||||
if l <= 0 {
|
|
||||||
return []Cell{}
|
|
||||||
}
|
}
|
||||||
|
var max float64
|
||||||
|
for _, slice := range slices {
|
||||||
|
for _, val := range slice {
|
||||||
|
if val > max {
|
||||||
|
max = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max, nil
|
||||||
|
}
|
||||||
|
|
||||||
rt := make([]Cell, 0, w)
|
func SelectColor(colors []Color, index int) Color {
|
||||||
csw := 0
|
return colors[index%len(colors)]
|
||||||
for i := 0; i < l && csw <= w; i++ {
|
}
|
||||||
c := cs[i]
|
|
||||||
cw := c.Width()
|
|
||||||
|
|
||||||
if cw+csw < w {
|
func SelectStyle(styles []Style, index int) Style {
|
||||||
rt = append(rt, c)
|
return styles[index%len(styles)]
|
||||||
csw += cw
|
}
|
||||||
|
|
||||||
|
func CellsToString(cells []Cell) string {
|
||||||
|
runes := make([]rune, len(cells))
|
||||||
|
for i, cell := range cells {
|
||||||
|
runes[i] = cell.Rune
|
||||||
|
}
|
||||||
|
return string(runes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RoundFloat64(x float64) float64 {
|
||||||
|
return math.Floor(x + 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SumIntSlice(slice []int) int {
|
||||||
|
sum := 0
|
||||||
|
for _, val := range slice {
|
||||||
|
sum += val
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
|
||||||
|
func SumFloat64Slice(data []float64) float64 {
|
||||||
|
sum := 0.0
|
||||||
|
for _, v := range data {
|
||||||
|
sum += v
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
|
||||||
|
func AbsInt(x int) int {
|
||||||
|
if x >= 0 {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
return -x
|
||||||
|
}
|
||||||
|
|
||||||
|
func MinFloat64(x, y float64) float64 {
|
||||||
|
if x < y {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
|
||||||
|
func MaxFloat64(x, y float64) float64 {
|
||||||
|
if x > y {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
|
||||||
|
func WrapCells(cells []Cell, width uint) []Cell {
|
||||||
|
str := CellsToString(cells)
|
||||||
|
wrapped := wordwrap.WrapString(str, width)
|
||||||
|
wrappedCells := []Cell{}
|
||||||
|
i := 0
|
||||||
|
for _, _rune := range wrapped {
|
||||||
|
if _rune == '\n' {
|
||||||
|
wrappedCells = append(wrappedCells, Cell{_rune, StyleClear})
|
||||||
} else {
|
} else {
|
||||||
rt = append(rt, Cell{'…', c.Fg, c.Bg})
|
wrappedCells = append(wrappedCells, Cell{_rune, cells[i].Style})
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
i++
|
||||||
}
|
}
|
||||||
|
return wrappedCells
|
||||||
return rt
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CellsToStr(cs []Cell) string {
|
func RunesToStyledCells(runes []rune, style Style) []Cell {
|
||||||
str := ""
|
cells := []Cell{}
|
||||||
for _, c := range cs {
|
for _, _rune := range runes {
|
||||||
str += string(c.Ch)
|
cells = append(cells, Cell{_rune, style})
|
||||||
}
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
// Passthrough to termbox using termbox constants above
|
|
||||||
func SetOutputMode(mode OutputMode) {
|
|
||||||
switch mode {
|
|
||||||
case OutputCurrent:
|
|
||||||
tb.SetOutputMode(tb.OutputCurrent)
|
|
||||||
case OutputNormal:
|
|
||||||
tb.SetOutputMode(tb.OutputNormal)
|
|
||||||
case Output256:
|
|
||||||
tb.SetOutputMode(tb.Output256)
|
|
||||||
case Output216:
|
|
||||||
tb.SetOutputMode(tb.Output216)
|
|
||||||
case OutputGrayscale:
|
|
||||||
tb.SetOutputMode(tb.OutputGrayscale)
|
|
||||||
}
|
}
|
||||||
|
return cells
|
||||||
}
|
}
|
||||||
|
@ -1,70 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestStr2Rune(t *testing.T) {
|
|
||||||
s := "你好,世界."
|
|
||||||
rs := str2runes(s)
|
|
||||||
if len(rs) != 6 {
|
|
||||||
t.Error(t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWidth(t *testing.T) {
|
|
||||||
s0 := "つのだ☆HIRO"
|
|
||||||
s1 := "11111111111"
|
|
||||||
// above not align for setting East Asian Ambiguous to wide!!
|
|
||||||
|
|
||||||
if strWidth(s0) != strWidth(s1) {
|
|
||||||
t.Error("str len failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
len1 := []rune{'a', '2', '&', '「', 'オ', '。'} //will false: 'ᆵ', 'ᄚ', 'ᄒ'
|
|
||||||
for _, v := range len1 {
|
|
||||||
if charWidth(v) != 1 {
|
|
||||||
t.Error("len1 failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
len2 := []rune{'漢', '字', '한', '자', '你', '好', 'だ', '。', '%', 's', 'E', 'ョ', '、', 'ヲ'}
|
|
||||||
for _, v := range len2 {
|
|
||||||
if charWidth(v) != 2 {
|
|
||||||
t.Error("len2 failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTrim(t *testing.T) {
|
|
||||||
s := "つのだ☆HIRO"
|
|
||||||
if string(trimStr2Runes(s, 10)) != "つのだ☆HI"+dot {
|
|
||||||
t.Error("trim failed")
|
|
||||||
}
|
|
||||||
if string(trimStr2Runes(s, 11)) != "つのだ☆HIRO" {
|
|
||||||
t.Error("avoid tail trim failed")
|
|
||||||
}
|
|
||||||
if string(trimStr2Runes(s, 15)) != "つのだ☆HIRO" {
|
|
||||||
t.Error("avoid trim failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTrimStrIfAppropriate_NoTrim(t *testing.T) {
|
|
||||||
assert.Equal(t, "hello", TrimStrIfAppropriate("hello", 5))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTrimStrIfAppropriate(t *testing.T) {
|
|
||||||
assert.Equal(t, "hel…", TrimStrIfAppropriate("hello", 4))
|
|
||||||
assert.Equal(t, "h…", TrimStrIfAppropriate("hello", 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStringToAttribute(t *testing.T) {
|
|
||||||
assert.Equal(t, ColorRed, StringToAttribute("ReD"))
|
|
||||||
assert.Equal(t, ColorRed|AttrBold, StringToAttribute("RED, bold"))
|
|
||||||
}
|
|
93
widget.go
@ -1,93 +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.
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// event mixins
|
|
||||||
type WgtMgr map[string]WgtInfo
|
|
||||||
|
|
||||||
type WgtInfo struct {
|
|
||||||
Handlers map[string]func(Event)
|
|
||||||
WgtRef Widget
|
|
||||||
Id string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Widget interface {
|
|
||||||
Id() string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWgtInfo(wgt Widget) WgtInfo {
|
|
||||||
return WgtInfo{
|
|
||||||
Handlers: make(map[string]func(Event)),
|
|
||||||
WgtRef: wgt,
|
|
||||||
Id: wgt.Id(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWgtMgr() WgtMgr {
|
|
||||||
wm := WgtMgr(make(map[string]WgtInfo))
|
|
||||||
return wm
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wm WgtMgr) AddWgt(wgt Widget) {
|
|
||||||
wm[wgt.Id()] = NewWgtInfo(wgt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wm WgtMgr) RmWgt(wgt Widget) {
|
|
||||||
wm.RmWgtById(wgt.Id())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wm WgtMgr) RmWgtById(id string) {
|
|
||||||
delete(wm, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wm WgtMgr) AddWgtHandler(id, path string, h func(Event)) {
|
|
||||||
if w, ok := wm[id]; ok {
|
|
||||||
w.Handlers[path] = h
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wm WgtMgr) RmWgtHandler(id, path string) {
|
|
||||||
if w, ok := wm[id]; ok {
|
|
||||||
delete(w.Handlers, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var counter struct {
|
|
||||||
sync.RWMutex
|
|
||||||
count int
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenId() string {
|
|
||||||
counter.Lock()
|
|
||||||
defer counter.Unlock()
|
|
||||||
|
|
||||||
counter.count += 1
|
|
||||||
return fmt.Sprintf("%d", counter.count)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wm WgtMgr) WgtHandlersHook() func(Event) {
|
|
||||||
return func(e Event) {
|
|
||||||
for _, v := range wm {
|
|
||||||
if val, ok := v.Handlers[e.ID]; ok {
|
|
||||||
val(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var DefaultWgtMgr WgtMgr
|
|
||||||
|
|
||||||
func (b *Block) Handle(path string, handler func(Event)) {
|
|
||||||
if _, ok := DefaultWgtMgr[b.Id()]; !ok {
|
|
||||||
DefaultWgtMgr.AddWgt(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
DefaultWgtMgr.AddWgtHandler(b.Id(), path, handler)
|
|
||||||
}
|
|
89
widgets/barchart.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
|
||||||
|
rw "github.com/mattn/go-runewidth"
|
||||||
|
|
||||||
|
. "github.com/gizak/termui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BarChart struct {
|
||||||
|
Block
|
||||||
|
BarColors []Color
|
||||||
|
LabelStyles []Style
|
||||||
|
NumStyles []Style // only Fg and Modifier are used
|
||||||
|
NumFmt func(float64) string
|
||||||
|
Data []float64
|
||||||
|
Labels []string
|
||||||
|
BarWidth int
|
||||||
|
BarGap int
|
||||||
|
MaxVal float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBarChart() *BarChart {
|
||||||
|
return &BarChart{
|
||||||
|
Block: *NewBlock(),
|
||||||
|
BarColors: Theme.BarChart.Bars,
|
||||||
|
NumStyles: Theme.BarChart.Nums,
|
||||||
|
LabelStyles: Theme.BarChart.Labels,
|
||||||
|
NumFmt: func(n float64) string { return fmt.Sprint(n) },
|
||||||
|
BarGap: 1,
|
||||||
|
BarWidth: 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *BarChart) Draw(buf *Buffer) {
|
||||||
|
self.Block.Draw(buf)
|
||||||
|
|
||||||
|
maxVal := self.MaxVal
|
||||||
|
if maxVal == 0 {
|
||||||
|
maxVal, _ = GetMaxFloat64FromSlice(self.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
barXCoordinate := self.Inner.Min.X
|
||||||
|
|
||||||
|
for i, data := range self.Data {
|
||||||
|
// draw bar
|
||||||
|
height := int((data / maxVal) * float64(self.Inner.Dy()-1))
|
||||||
|
for x := barXCoordinate; x < MinInt(barXCoordinate+self.BarWidth, self.Inner.Max.X); x++ {
|
||||||
|
for y := self.Inner.Max.Y - 2; y > (self.Inner.Max.Y-2)-height; y-- {
|
||||||
|
c := NewCell(' ', NewStyle(ColorClear, SelectColor(self.BarColors, i)))
|
||||||
|
buf.SetCell(c, image.Pt(x, y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw label
|
||||||
|
if i < len(self.Labels) {
|
||||||
|
labelXCoordinate := barXCoordinate +
|
||||||
|
int((float64(self.BarWidth) / 2)) -
|
||||||
|
int((float64(rw.StringWidth(self.Labels[i])) / 2))
|
||||||
|
buf.SetString(
|
||||||
|
self.Labels[i],
|
||||||
|
SelectStyle(self.LabelStyles, i),
|
||||||
|
image.Pt(labelXCoordinate, self.Inner.Max.Y-1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw number
|
||||||
|
numberXCoordinate := barXCoordinate + int((float64(self.BarWidth) / 2))
|
||||||
|
if numberXCoordinate <= self.Inner.Max.X {
|
||||||
|
buf.SetString(
|
||||||
|
self.NumFmt(data),
|
||||||
|
NewStyle(
|
||||||
|
SelectStyle(self.NumStyles, i+1).Fg,
|
||||||
|
SelectColor(self.BarColors, i),
|
||||||
|
SelectStyle(self.NumStyles, i+1).Modifier,
|
||||||
|
),
|
||||||
|
image.Pt(numberXCoordinate, self.Inner.Max.Y-2),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
barXCoordinate += (self.BarWidth + self.BarGap)
|
||||||
|
}
|
||||||
|
}
|
57
widgets/gauge.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
|
||||||
|
. "github.com/gizak/termui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Gauge struct {
|
||||||
|
Block
|
||||||
|
Percent int
|
||||||
|
BarColor Color
|
||||||
|
Label string
|
||||||
|
LabelStyle Style
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGauge() *Gauge {
|
||||||
|
return &Gauge{
|
||||||
|
Block: *NewBlock(),
|
||||||
|
BarColor: Theme.Gauge.Bar,
|
||||||
|
LabelStyle: Theme.Gauge.Label,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Gauge) Draw(buf *Buffer) {
|
||||||
|
self.Block.Draw(buf)
|
||||||
|
|
||||||
|
label := self.Label
|
||||||
|
if label == "" {
|
||||||
|
label = fmt.Sprintf("%d%%", self.Percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// plot bar
|
||||||
|
barWidth := int((float64(self.Percent) / 100) * float64(self.Inner.Dx()))
|
||||||
|
buf.Fill(
|
||||||
|
NewCell(' ', NewStyle(ColorClear, self.BarColor)),
|
||||||
|
image.Rect(self.Inner.Min.X, self.Inner.Min.Y, self.Inner.Min.X+barWidth, self.Inner.Max.Y),
|
||||||
|
)
|
||||||
|
|
||||||
|
// plot label
|
||||||
|
labelXCoordinate := self.Inner.Min.X + (self.Inner.Dx() / 2) - int(float64(len(label))/2)
|
||||||
|
labelYCoordinate := self.Inner.Min.Y + ((self.Inner.Dy() - 1) / 2)
|
||||||
|
if labelYCoordinate < self.Inner.Max.Y {
|
||||||
|
for i, char := range label {
|
||||||
|
style := self.LabelStyle
|
||||||
|
if labelXCoordinate+i+1 <= self.Inner.Min.X+barWidth {
|
||||||
|
style = NewStyle(self.BarColor, ColorClear, ModifierReverse)
|
||||||
|
}
|
||||||
|
buf.SetCell(NewCell(char, style), image.Pt(labelXCoordinate+i, labelYCoordinate))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
181
widgets/linechart.go
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
|
||||||
|
. "github.com/gizak/termui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LineChart has two modes: braille(default) and dot.
|
||||||
|
// A single braille character is a 2x4 grid of dots, so using braille
|
||||||
|
// gives 2x X resolution and 4x Y resolution over dot mode.
|
||||||
|
type LineChart struct {
|
||||||
|
Block
|
||||||
|
Data [][]float64
|
||||||
|
DataLabels []string
|
||||||
|
HorizontalScale int
|
||||||
|
LineType LineType
|
||||||
|
DotChar rune
|
||||||
|
LineColors []Color
|
||||||
|
AxesColor Color // TODO
|
||||||
|
MaxVal float64
|
||||||
|
ShowAxes bool
|
||||||
|
DrawDirection DrawDirection // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
xAxisLabelsHeight = 1
|
||||||
|
yAxisLabelsWidth = 4
|
||||||
|
xAxisLabelsGap = 2
|
||||||
|
yAxisLabelsGap = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
BrailleLine LineType = iota
|
||||||
|
DotLine
|
||||||
|
)
|
||||||
|
|
||||||
|
type DrawDirection uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
DrawLeft DrawDirection = iota
|
||||||
|
DrawRight
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewLineChart() *LineChart {
|
||||||
|
return &LineChart{
|
||||||
|
Block: *NewBlock(),
|
||||||
|
LineColors: Theme.LineChart.Lines,
|
||||||
|
AxesColor: Theme.LineChart.Axes,
|
||||||
|
LineType: BrailleLine,
|
||||||
|
DotChar: DOT,
|
||||||
|
Data: [][]float64{},
|
||||||
|
HorizontalScale: 1,
|
||||||
|
DrawDirection: DrawRight,
|
||||||
|
ShowAxes: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *LineChart) renderBraille(buf *Buffer, drawArea image.Rectangle, maxVal float64) {
|
||||||
|
canvas := NewCanvas()
|
||||||
|
canvas.Rectangle = drawArea
|
||||||
|
|
||||||
|
for i, line := range self.Data {
|
||||||
|
previousHeight := int((line[1] / maxVal) * float64(drawArea.Dy()-1))
|
||||||
|
for j, val := range line[1:] {
|
||||||
|
height := int((val / maxVal) * float64(drawArea.Dy()-1))
|
||||||
|
canvas.Line(
|
||||||
|
image.Pt(
|
||||||
|
(drawArea.Min.X+(j*self.HorizontalScale))*2,
|
||||||
|
(drawArea.Max.Y-previousHeight-1)*4,
|
||||||
|
),
|
||||||
|
image.Pt(
|
||||||
|
(drawArea.Min.X+((j+1)*self.HorizontalScale))*2,
|
||||||
|
(drawArea.Max.Y-height-1)*4,
|
||||||
|
),
|
||||||
|
SelectColor(self.LineColors, i),
|
||||||
|
)
|
||||||
|
previousHeight = height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.Draw(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *LineChart) renderDot(buf *Buffer, drawArea image.Rectangle, maxVal float64) {
|
||||||
|
for i, line := range self.Data {
|
||||||
|
for j := 0; j < len(line) && j*self.HorizontalScale < drawArea.Dx(); j++ {
|
||||||
|
val := line[j]
|
||||||
|
height := int((val / maxVal) * float64(drawArea.Dy()-1))
|
||||||
|
buf.SetCell(
|
||||||
|
NewCell(self.DotChar, NewStyle(SelectColor(self.LineColors, i))),
|
||||||
|
image.Pt(drawArea.Min.X+(j*self.HorizontalScale), drawArea.Max.Y-1-height),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *LineChart) plotAxes(buf *Buffer, maxVal float64) {
|
||||||
|
// draw origin cell
|
||||||
|
buf.SetCell(
|
||||||
|
NewCell(BOTTOM_LEFT, NewStyle(ColorWhite)),
|
||||||
|
image.Pt(self.Inner.Min.X+yAxisLabelsWidth, self.Inner.Max.Y-xAxisLabelsHeight-1),
|
||||||
|
)
|
||||||
|
// draw x axis line
|
||||||
|
for i := yAxisLabelsWidth + 1; i < self.Inner.Dx(); i++ {
|
||||||
|
buf.SetCell(
|
||||||
|
NewCell(HORIZONTAL_DASH, NewStyle(ColorWhite)),
|
||||||
|
image.Pt(i+self.Inner.Min.X, self.Inner.Max.Y-xAxisLabelsHeight-1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// draw y axis line
|
||||||
|
for i := 0; i < self.Inner.Dy()-xAxisLabelsHeight-1; i++ {
|
||||||
|
buf.SetCell(
|
||||||
|
NewCell(VERTICAL_DASH, NewStyle(ColorWhite)),
|
||||||
|
image.Pt(self.Inner.Min.X+yAxisLabelsWidth, i+self.Inner.Min.Y),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// draw x axis labels
|
||||||
|
// draw 0
|
||||||
|
buf.SetString(
|
||||||
|
"0",
|
||||||
|
NewStyle(ColorWhite),
|
||||||
|
image.Pt(self.Inner.Min.X+yAxisLabelsWidth, self.Inner.Max.Y-1),
|
||||||
|
)
|
||||||
|
// draw rest
|
||||||
|
for x := self.Inner.Min.X + yAxisLabelsWidth + (xAxisLabelsGap)*self.HorizontalScale + 1; x < self.Inner.Max.X-1; {
|
||||||
|
label := fmt.Sprintf(
|
||||||
|
"%d",
|
||||||
|
(x-(self.Inner.Min.X+yAxisLabelsWidth)-1)/(self.HorizontalScale)+1,
|
||||||
|
)
|
||||||
|
buf.SetString(
|
||||||
|
label,
|
||||||
|
NewStyle(ColorWhite),
|
||||||
|
image.Pt(x, self.Inner.Max.Y-1),
|
||||||
|
)
|
||||||
|
x += (len(label) + xAxisLabelsGap) * self.HorizontalScale
|
||||||
|
}
|
||||||
|
// draw y axis labels
|
||||||
|
verticalScale := maxVal / float64(self.Inner.Dy()-xAxisLabelsHeight-1)
|
||||||
|
for i := 0; i*(yAxisLabelsGap+1) < self.Inner.Dy()-1; i++ {
|
||||||
|
buf.SetString(
|
||||||
|
fmt.Sprintf("%.2f", float64(i)*verticalScale*(yAxisLabelsGap+1)),
|
||||||
|
NewStyle(ColorWhite),
|
||||||
|
image.Pt(self.Inner.Min.X, self.Inner.Max.Y-(i*(yAxisLabelsGap+1))-2),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *LineChart) Draw(buf *Buffer) {
|
||||||
|
self.Block.Draw(buf)
|
||||||
|
|
||||||
|
maxVal := self.MaxVal
|
||||||
|
if maxVal == 0 {
|
||||||
|
maxVal, _ = GetMaxFloat64From2dSlice(self.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.ShowAxes {
|
||||||
|
self.plotAxes(buf, maxVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
drawArea := self.Inner
|
||||||
|
if self.ShowAxes {
|
||||||
|
drawArea = image.Rect(
|
||||||
|
self.Inner.Min.X+yAxisLabelsWidth+1, self.Inner.Min.Y,
|
||||||
|
self.Inner.Max.X, self.Inner.Max.Y-xAxisLabelsHeight-1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.LineType == BrailleLine {
|
||||||
|
self.renderBraille(buf, drawArea, maxVal)
|
||||||
|
} else {
|
||||||
|
self.renderDot(buf, drawArea, maxVal)
|
||||||
|
}
|
||||||
|
}
|
52
widgets/list.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
. "github.com/gizak/termui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type List struct {
|
||||||
|
Block
|
||||||
|
Rows []string
|
||||||
|
Wrap bool
|
||||||
|
TextStyle Style
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewList() *List {
|
||||||
|
return &List{
|
||||||
|
Block: *NewBlock(),
|
||||||
|
TextStyle: Theme.List.Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *List) Draw(buf *Buffer) {
|
||||||
|
self.Block.Draw(buf)
|
||||||
|
|
||||||
|
point := self.Inner.Min
|
||||||
|
|
||||||
|
for row := 0; row < len(self.Rows) && point.Y < self.Inner.Max.Y; row++ {
|
||||||
|
cells := ParseText(self.Rows[row], self.TextStyle)
|
||||||
|
if self.Wrap {
|
||||||
|
cells = WrapCells(cells, uint(self.Inner.Dx()))
|
||||||
|
}
|
||||||
|
for j := 0; j < len(cells) && point.Y < self.Inner.Max.Y; j++ {
|
||||||
|
if cells[j].Rune == '\n' {
|
||||||
|
point = image.Pt(self.Inner.Min.X, point.Y+1)
|
||||||
|
} else {
|
||||||
|
if point.X+1 == self.Inner.Max.X+1 && len(cells) > self.Inner.Dx() {
|
||||||
|
buf.SetCell(NewCell(DOTS, cells[j].Style), point.Add(image.Pt(-1, 0)))
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
buf.SetCell(cells[j], point)
|
||||||
|
point = point.Add(image.Pt(1, 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
point = image.Pt(self.Inner.Min.X, point.Y+1)
|
||||||
|
}
|
||||||
|
}
|
40
widgets/paragraph.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
. "github.com/gizak/termui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Paragraph struct {
|
||||||
|
Block
|
||||||
|
Text string
|
||||||
|
TextStyle Style
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewParagraph() *Paragraph {
|
||||||
|
return &Paragraph{
|
||||||
|
Block: *NewBlock(),
|
||||||
|
TextStyle: Theme.Paragraph.Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Paragraph) Draw(buf *Buffer) {
|
||||||
|
self.Block.Draw(buf)
|
||||||
|
|
||||||
|
point := self.Inner.Min
|
||||||
|
cells := WrapCells(ParseText(self.Text, self.TextStyle), uint(self.Inner.Dx()))
|
||||||
|
|
||||||
|
for i := 0; i < len(cells) && point.Y < self.Inner.Max.Y; i++ {
|
||||||
|
if cells[i].Rune == '\n' {
|
||||||
|
point = image.Pt(self.Inner.Min.X, point.Y+1)
|
||||||
|
} else {
|
||||||
|
buf.SetCell(cells[i], point)
|
||||||
|
point = point.Add(image.Pt(1, 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
149
widgets/piechart.go
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
. "github.com/gizak/termui"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
piechartOffsetUp = -.5 * math.Pi // the northward angle
|
||||||
|
resolutionFactor = .0001 // circle resolution: precision vs. performance
|
||||||
|
fullCircle = 2.0 * math.Pi // the full circle angle
|
||||||
|
xStretch = 2.0 // horizontal adjustment
|
||||||
|
)
|
||||||
|
|
||||||
|
// PieChartLabel callback
|
||||||
|
type PieChartLabel func(dataIndex int, currentValue float64) string
|
||||||
|
|
||||||
|
type PieChart struct {
|
||||||
|
Block
|
||||||
|
Data []float64 // list of data items
|
||||||
|
Colors []Color // colors to by cycled through
|
||||||
|
Label PieChartLabel // callback function for labels
|
||||||
|
Offset float64 // which angle to start drawing at? (see piechartOffsetUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPieChart Creates a new pie chart with reasonable defaults and no labels.
|
||||||
|
func NewPieChart() *PieChart {
|
||||||
|
return &PieChart{
|
||||||
|
Block: *NewBlock(),
|
||||||
|
Colors: Theme.PieChart.Slices,
|
||||||
|
Offset: piechartOffsetUp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *PieChart) Draw(buf *Buffer) {
|
||||||
|
self.Block.Draw(buf)
|
||||||
|
|
||||||
|
center := self.Inner.Min.Add(self.Inner.Size().Div(2))
|
||||||
|
radius := MinFloat64(float64(self.Inner.Dx()/2/xStretch), float64(self.Inner.Dy()/2))
|
||||||
|
|
||||||
|
// compute slice sizes
|
||||||
|
sum := SumFloat64Slice(self.Data)
|
||||||
|
sliceSizes := make([]float64, len(self.Data))
|
||||||
|
for i, v := range self.Data {
|
||||||
|
sliceSizes[i] = v / sum * fullCircle
|
||||||
|
}
|
||||||
|
|
||||||
|
borderCircle := &circle{center, radius}
|
||||||
|
middleCircle := circle{Point: center, radius: radius / 2.0}
|
||||||
|
|
||||||
|
// draw sectors
|
||||||
|
phi := self.Offset
|
||||||
|
for i, size := range sliceSizes {
|
||||||
|
for j := 0.0; j < size; j += resolutionFactor {
|
||||||
|
borderPoint := borderCircle.at(phi + j)
|
||||||
|
line := line{P1: center, P2: borderPoint}
|
||||||
|
line.draw(NewCell(SHADED_BLOCK, NewStyle(SelectColor(self.Colors, i))), buf)
|
||||||
|
}
|
||||||
|
phi += size
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw labels
|
||||||
|
if self.Label != nil {
|
||||||
|
phi = self.Offset
|
||||||
|
for i, size := range sliceSizes {
|
||||||
|
labelPoint := middleCircle.at(phi + size/2.0)
|
||||||
|
if len(self.Data) == 1 {
|
||||||
|
labelPoint = center
|
||||||
|
}
|
||||||
|
buf.SetString(
|
||||||
|
self.Label(i, self.Data[i]),
|
||||||
|
NewStyle(SelectColor(self.Colors, i)),
|
||||||
|
image.Pt(labelPoint.X, labelPoint.Y),
|
||||||
|
)
|
||||||
|
phi += size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type circle struct {
|
||||||
|
image.Point
|
||||||
|
radius float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// computes the point at a given angle phi
|
||||||
|
func (self circle) at(phi float64) image.Point {
|
||||||
|
x := self.X + int(RoundFloat64(xStretch*self.radius*math.Cos(phi)))
|
||||||
|
y := self.Y + int(RoundFloat64(self.radius*math.Sin(phi)))
|
||||||
|
return image.Point{X: x, Y: y}
|
||||||
|
}
|
||||||
|
|
||||||
|
// computes the perimeter of a circle
|
||||||
|
func (self circle) perimeter() float64 {
|
||||||
|
return 2.0 * math.Pi * self.radius
|
||||||
|
}
|
||||||
|
|
||||||
|
// a line between two points
|
||||||
|
type line struct {
|
||||||
|
P1, P2 image.Point
|
||||||
|
}
|
||||||
|
|
||||||
|
// draws the line
|
||||||
|
func (self line) draw(cell Cell, buf *Buffer) {
|
||||||
|
isLeftOf := func(p1, p2 image.Point) bool {
|
||||||
|
return p1.X <= p2.X
|
||||||
|
}
|
||||||
|
isTopOf := func(p1, p2 image.Point) bool {
|
||||||
|
return p1.Y <= p2.Y
|
||||||
|
}
|
||||||
|
p1, p2 := self.P1, self.P2
|
||||||
|
buf.SetCell(NewCell('*', cell.Style), self.P2)
|
||||||
|
width, height := self.size()
|
||||||
|
if width > height { // paint left to right
|
||||||
|
if !isLeftOf(p1, p2) {
|
||||||
|
p1, p2 = p2, p1
|
||||||
|
}
|
||||||
|
flip := 1.0
|
||||||
|
if !isTopOf(p1, p2) {
|
||||||
|
flip = -1.0
|
||||||
|
}
|
||||||
|
for x := p1.X; x <= p2.X; x++ {
|
||||||
|
ratio := float64(height) / float64(width)
|
||||||
|
factor := float64(x - p1.X)
|
||||||
|
y := ratio * factor * flip
|
||||||
|
buf.SetCell(cell, image.Pt(x, int(RoundFloat64(y))+p1.Y))
|
||||||
|
}
|
||||||
|
} else { // paint top to bottom
|
||||||
|
if !isTopOf(p1, p2) {
|
||||||
|
p1, p2 = p2, p1
|
||||||
|
}
|
||||||
|
flip := 1.0
|
||||||
|
if !isLeftOf(p1, p2) {
|
||||||
|
flip = -1.0
|
||||||
|
}
|
||||||
|
for y := p1.Y; y <= p2.Y; y++ {
|
||||||
|
ratio := float64(width) / float64(height)
|
||||||
|
factor := float64(y - p1.Y)
|
||||||
|
x := ratio * factor * flip
|
||||||
|
buf.SetCell(cell, image.Pt(int(RoundFloat64(x))+p1.X, y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// width and height of a line
|
||||||
|
func (self line) size() (w, h int) {
|
||||||
|
return AbsInt(self.P2.X - self.P1.X), AbsInt(self.P2.Y - self.P1.Y)
|
||||||
|
}
|
94
widgets/sparkline.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
. "github.com/gizak/termui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sparkline is like: ▅▆▂▂▅▇▂▂▃▆▆▆▅▃. The data points should be non-negative integers.
|
||||||
|
type Sparkline struct {
|
||||||
|
Data []float64
|
||||||
|
Title string
|
||||||
|
TitleStyle Style
|
||||||
|
LineColor Color
|
||||||
|
MaxVal float64
|
||||||
|
MaxHeight int // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
// SparklineGroup is a renderable widget which groups together the given sparklines.
|
||||||
|
type SparklineGroup struct {
|
||||||
|
Block
|
||||||
|
Sparklines []*Sparkline
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSparkline returns a unrenderable single sparkline that needs to be added to a SparklineGroup
|
||||||
|
func NewSparkline() *Sparkline {
|
||||||
|
return &Sparkline{
|
||||||
|
TitleStyle: Theme.Sparkline.Title,
|
||||||
|
LineColor: Theme.Sparkline.Line,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSparklineGroup(sls ...*Sparkline) *SparklineGroup {
|
||||||
|
return &SparklineGroup{
|
||||||
|
Block: *NewBlock(),
|
||||||
|
Sparklines: sls,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SparklineGroup) Draw(buf *Buffer) {
|
||||||
|
self.Block.Draw(buf)
|
||||||
|
|
||||||
|
sparklineHeight := self.Inner.Dy() / len(self.Sparklines)
|
||||||
|
|
||||||
|
for i, sl := range self.Sparklines {
|
||||||
|
heightOffset := (sparklineHeight * (i + 1))
|
||||||
|
barHeight := sparklineHeight
|
||||||
|
if i == len(self.Sparklines)-1 {
|
||||||
|
heightOffset = self.Inner.Dy()
|
||||||
|
barHeight = self.Inner.Dy() - (sparklineHeight * i)
|
||||||
|
}
|
||||||
|
if sl.Title != "" {
|
||||||
|
barHeight--
|
||||||
|
}
|
||||||
|
|
||||||
|
maxVal := sl.MaxVal
|
||||||
|
if maxVal == 0 {
|
||||||
|
maxVal, _ = GetMaxFloat64FromSlice(sl.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw line
|
||||||
|
for j := 0; j < len(sl.Data) && j < self.Inner.Dx(); j++ {
|
||||||
|
data := sl.Data[j]
|
||||||
|
height := int((data / maxVal) * float64(barHeight))
|
||||||
|
sparkChar := BARS[len(BARS)-1]
|
||||||
|
for k := 0; k < height; k++ {
|
||||||
|
buf.SetCell(
|
||||||
|
NewCell(sparkChar, NewStyle(sl.LineColor)),
|
||||||
|
image.Pt(j+self.Inner.Min.X, self.Inner.Min.Y-1+heightOffset-k),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if height == 0 {
|
||||||
|
sparkChar = BARS[0]
|
||||||
|
buf.SetCell(
|
||||||
|
NewCell(sparkChar, NewStyle(sl.LineColor)),
|
||||||
|
image.Pt(j+self.Inner.Min.X, self.Inner.Min.Y-1+heightOffset),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sl.Title != "" {
|
||||||
|
// draw title
|
||||||
|
buf.SetString(
|
||||||
|
TrimString(sl.Title, self.Inner.Dx()),
|
||||||
|
sl.TitleStyle,
|
||||||
|
image.Pt(self.Inner.Min.X, self.Inner.Min.Y-1+heightOffset-barHeight),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
94
widgets/stacked_barchart.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
|
||||||
|
rw "github.com/mattn/go-runewidth"
|
||||||
|
|
||||||
|
. "github.com/gizak/termui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StackedBarChart struct {
|
||||||
|
Block
|
||||||
|
BarColors []Color
|
||||||
|
LabelStyles []Style
|
||||||
|
NumStyles []Style // only Fg and Modifier are used
|
||||||
|
NumFmt func(float64) string
|
||||||
|
Data [][]float64
|
||||||
|
Labels []string
|
||||||
|
BarWidth int
|
||||||
|
BarGap int
|
||||||
|
MaxVal float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStackedBarChart() *StackedBarChart {
|
||||||
|
return &StackedBarChart{
|
||||||
|
Block: *NewBlock(),
|
||||||
|
BarColors: Theme.StackedBarChart.Bars,
|
||||||
|
LabelStyles: Theme.StackedBarChart.Labels,
|
||||||
|
NumStyles: Theme.StackedBarChart.Nums,
|
||||||
|
NumFmt: func(n float64) string { return fmt.Sprint(n) },
|
||||||
|
BarGap: 1,
|
||||||
|
BarWidth: 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *StackedBarChart) Draw(buf *Buffer) {
|
||||||
|
self.Block.Draw(buf)
|
||||||
|
|
||||||
|
maxVal := self.MaxVal
|
||||||
|
if maxVal == 0 {
|
||||||
|
for _, data := range self.Data {
|
||||||
|
maxVal = MaxFloat64(maxVal, SumFloat64Slice(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
barXCoordinate := self.Inner.Min.X
|
||||||
|
|
||||||
|
for i, bar := range self.Data {
|
||||||
|
// draw stacked bars
|
||||||
|
stackedBarYCoordinate := 0
|
||||||
|
for j, data := range bar {
|
||||||
|
// draw each stacked bar
|
||||||
|
height := int((data / maxVal) * float64(self.Inner.Dy()-1))
|
||||||
|
for x := barXCoordinate; x < MinInt(barXCoordinate+self.BarWidth, self.Inner.Max.X); x++ {
|
||||||
|
for y := (self.Inner.Max.Y - 2) - stackedBarYCoordinate; y > (self.Inner.Max.Y-2)-stackedBarYCoordinate-height; y-- {
|
||||||
|
c := NewCell(' ', NewStyle(ColorClear, SelectColor(self.BarColors, j)))
|
||||||
|
buf.SetCell(c, image.Pt(x, y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw number
|
||||||
|
numberXCoordinate := barXCoordinate + int((float64(self.BarWidth) / 2)) - 1
|
||||||
|
buf.SetString(
|
||||||
|
self.NumFmt(data),
|
||||||
|
NewStyle(
|
||||||
|
SelectStyle(self.NumStyles, j+1).Fg,
|
||||||
|
SelectColor(self.BarColors, j),
|
||||||
|
SelectStyle(self.NumStyles, j+1).Modifier,
|
||||||
|
),
|
||||||
|
image.Pt(numberXCoordinate, (self.Inner.Max.Y-2)-stackedBarYCoordinate),
|
||||||
|
)
|
||||||
|
|
||||||
|
stackedBarYCoordinate += height
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw label
|
||||||
|
labelXCoordinate := barXCoordinate + MaxInt(
|
||||||
|
int((float64(self.BarWidth)/2))-int((float64(rw.StringWidth(self.Labels[i]))/2)),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
buf.SetString(
|
||||||
|
TrimString(self.Labels[i], self.BarWidth),
|
||||||
|
SelectStyle(self.LabelStyles, i),
|
||||||
|
image.Pt(labelXCoordinate, self.Inner.Max.Y-1),
|
||||||
|
)
|
||||||
|
|
||||||
|
barXCoordinate += (self.BarWidth + self.BarGap)
|
||||||
|
}
|
||||||
|
}
|
106
widgets/table.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
. "github.com/gizak/termui"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Table is like:
|
||||||
|
|
||||||
|
┌ Awesome Table ───────────────────────────────────────────────┐
|
||||||
|
│ Col0 | Col1 | Col2 | Col3 | Col4 | Col5 | Col6 |
|
||||||
|
│──────────────────────────────────────────────────────────────│
|
||||||
|
│ Some Item #1 | AAA | 123 | CCCCC | EEEEE | GGGGG | IIIII |
|
||||||
|
│──────────────────────────────────────────────────────────────│
|
||||||
|
│ Some Item #2 | BBB | 456 | DDDDD | FFFFF | HHHHH | JJJJJ |
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Table struct {
|
||||||
|
Block
|
||||||
|
Rows [][]string
|
||||||
|
ColumnWidths []int
|
||||||
|
TextStyle Style
|
||||||
|
RowSeparator bool
|
||||||
|
TextAlign Alignment
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTable() *Table {
|
||||||
|
return &Table{
|
||||||
|
Block: *NewBlock(),
|
||||||
|
TextStyle: Theme.Table.Text,
|
||||||
|
RowSeparator: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Table) Draw(buf *Buffer) {
|
||||||
|
self.Block.Draw(buf)
|
||||||
|
|
||||||
|
columnWidths := self.ColumnWidths
|
||||||
|
if len(columnWidths) == 0 {
|
||||||
|
columnCount := len(self.Rows[0])
|
||||||
|
colWidth := self.Inner.Dx() / columnCount
|
||||||
|
for i := 0; i < columnCount; i++ {
|
||||||
|
columnWidths = append(columnWidths, colWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yCoordinate := self.Inner.Min.Y
|
||||||
|
|
||||||
|
// draw rows
|
||||||
|
for i := 0; i < len(self.Rows) && yCoordinate < self.Inner.Max.Y; i++ {
|
||||||
|
row := self.Rows[i]
|
||||||
|
colXCoordinate := self.Inner.Min.X
|
||||||
|
// draw row cells
|
||||||
|
for j := 0; j < len(row); j++ {
|
||||||
|
col := ParseText(row[j], self.TextStyle)
|
||||||
|
// draw row cell
|
||||||
|
if len(col) > columnWidths[j] || self.TextAlign == AlignLeft {
|
||||||
|
for k, cell := range col {
|
||||||
|
if k == columnWidths[j] || colXCoordinate+k == self.Inner.Max.X {
|
||||||
|
cell.Rune = DOTS
|
||||||
|
buf.SetCell(cell, image.Pt(colXCoordinate+k-1, yCoordinate))
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
buf.SetCell(cell, image.Pt(colXCoordinate+k, yCoordinate))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if self.TextAlign == AlignCenter {
|
||||||
|
xCoordinateOffset := (columnWidths[j] - len(col)) / 2
|
||||||
|
stringXCoordinate := xCoordinateOffset + colXCoordinate
|
||||||
|
for k, cell := range col {
|
||||||
|
buf.SetCell(cell, image.Pt(stringXCoordinate+k, yCoordinate))
|
||||||
|
}
|
||||||
|
} else if self.TextAlign == AlignRight {
|
||||||
|
stringXCoordinate := MinInt(colXCoordinate+columnWidths[j], self.Inner.Max.X) - len(col)
|
||||||
|
for k, cell := range col {
|
||||||
|
buf.SetCell(cell, image.Pt(stringXCoordinate+k, yCoordinate))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
colXCoordinate += columnWidths[j] + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw vertical separators
|
||||||
|
separatorXCoordinate := self.Inner.Min.X
|
||||||
|
verticalCell := NewCell(VERTICAL_LINE, NewStyle(ColorWhite))
|
||||||
|
for _, width := range columnWidths {
|
||||||
|
separatorXCoordinate += width
|
||||||
|
buf.SetCell(verticalCell, image.Pt(separatorXCoordinate, yCoordinate))
|
||||||
|
separatorXCoordinate++
|
||||||
|
}
|
||||||
|
|
||||||
|
yCoordinate++
|
||||||
|
|
||||||
|
// draw horizontal separator
|
||||||
|
horizontalCell := NewCell(HORIZONTAL_LINE, NewStyle(ColorWhite))
|
||||||
|
if self.RowSeparator && yCoordinate < self.Inner.Max.Y && i != len(self.Rows)-1 {
|
||||||
|
buf.Fill(horizontalCell, image.Rect(self.Inner.Min.X, yCoordinate, self.Inner.Max.X, yCoordinate+1))
|
||||||
|
yCoordinate++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
71
widgets/tabs.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
. "github.com/gizak/termui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TabPane is a renderable widget which can be used to conditionally render certain tabs/views.
|
||||||
|
// TabPane shows a list of Tab names.
|
||||||
|
// The currently selected tab can be found through the `ActiveTabIndex` field.
|
||||||
|
type TabPane struct {
|
||||||
|
Block
|
||||||
|
TabNames []string
|
||||||
|
ActiveTabIndex uint
|
||||||
|
ActiveTabStyle Style
|
||||||
|
InactiveTabStyle Style
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTabPane(names ...string) *TabPane {
|
||||||
|
return &TabPane{
|
||||||
|
Block: *NewBlock(),
|
||||||
|
TabNames: names,
|
||||||
|
ActiveTabStyle: Theme.Tab.Active,
|
||||||
|
InactiveTabStyle: Theme.Tab.Inactive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *TabPane) FocusLeft() {
|
||||||
|
if self.ActiveTabIndex > 0 {
|
||||||
|
self.ActiveTabIndex--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *TabPane) FocusRight() {
|
||||||
|
if self.ActiveTabIndex < uint(len(self.TabNames)-1) {
|
||||||
|
self.ActiveTabIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *TabPane) Draw(buf *Buffer) {
|
||||||
|
self.Block.Draw(buf)
|
||||||
|
|
||||||
|
xCoordinate := self.Inner.Min.X
|
||||||
|
for i, name := range self.TabNames {
|
||||||
|
ColorPair := self.InactiveTabStyle
|
||||||
|
if uint(i) == self.ActiveTabIndex {
|
||||||
|
ColorPair = self.ActiveTabStyle
|
||||||
|
}
|
||||||
|
buf.SetString(
|
||||||
|
TrimString(name, self.Inner.Max.X-xCoordinate),
|
||||||
|
ColorPair,
|
||||||
|
image.Pt(xCoordinate, self.Inner.Min.Y),
|
||||||
|
)
|
||||||
|
|
||||||
|
xCoordinate += 1 + len(name)
|
||||||
|
|
||||||
|
if i < len(self.TabNames)-1 && xCoordinate < self.Inner.Max.X {
|
||||||
|
buf.SetCell(
|
||||||
|
NewCell(VERTICAL_LINE, NewStyle(ColorWhite)),
|
||||||
|
image.Pt(xCoordinate, self.Inner.Min.Y),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
xCoordinate += 2
|
||||||
|
}
|
||||||
|
}
|