Repository: gizak/termui
Branch: master
Commit: 3ee54a07c7d1
Files: 54
Total size: 106.9 KB
Directory structure:
gitextract_vw2ezebp/
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── _examples/
│ ├── barchart.go
│ ├── canvas.go
│ ├── demo.go
│ ├── gauge.go
│ ├── grid.go
│ ├── hello_world.go
│ ├── image.go
│ ├── list.go
│ ├── paragraph.go
│ ├── piechart.go
│ ├── plot.go
│ ├── sparkline.go
│ ├── stacked_barchart.go
│ ├── table.go
│ ├── tabs.go
│ └── tree.go
├── _scripts/
│ └── copyright_header.py
├── _test/
│ └── log_events.go
├── alignment.go
├── backend.go
├── block.go
├── buffer.go
├── canvas.go
├── doc.go
├── drawille/
│ └── drawille.go
├── events.go
├── go.mod
├── go.sum
├── grid.go
├── render.go
├── style.go
├── style_parser.go
├── symbols.go
├── symbols_other.go
├── symbols_windows.go
├── theme.go
├── utils.go
└── widgets/
├── barchart.go
├── gauge.go
├── image.go
├── list.go
├── paragraph.go
├── piechart.go
├── plot.go
├── sparkline.go
├── stacked_barchart.go
├── table.go
├── tabs.go
└── tree.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.DS_Store
.vscode/
.mypy_cache/
.idea
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [3.1.0] - 2019-07-15
### Added
- Added Tree widget [#237]
## [3.0.0] - 2019-03-07
### Changed
- Added sync.Locker interface to Drawable interface
## 2019-03-01
### Changed
- Change scroll method names in List widget
### Fixed
- Fix List widget scrolling
## 2019-02-28
### Added
- Add `ColumnResizer` to table which allows for custom column sizing
- Add widget padding
### Changed
- Change various widget field names
- s/`TextParse`/`ParseStyles`
- Remove `AddColorMap` in place of modifying `StyleParserColorMap` directly
## 2019-01-31
### Added
- Add more scrolling options to List
### Changed
- Make list scroll automatically
### Added
## 2019-01-26
### Added
- Add scrolling to List widget
- Add WrapText option to Paragraph
- controls if text should wrap automatically
## 2019-01-24
### Added
- Add image widget [#126]
### Changed
- Change LineChart to Plot
- Added ScatterPlot mode which plots points instead of lines between points
## 2019-01-23
### Added
- Add `Canvas` which allows for drawing braille lines to a `Buffer`
### Changed
- Set `termbox-go` backend to 256 colors by default
- 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
- change 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`
- Refactor `Theme`
- `Theme` is now a large struct which holds the default `Styles` of everything
- Combine `TermWidth` and `TermHeight` functions into `TerminalDimensions`
- Rework `Block`
- Rework `Buffer` methods
- Decremente color numbers by 1 to match xterm colors
- Change text parsing
- change style items from `fg-color` to `fg:color`
- adde mod item like `mod:reverse`
## 2018-11-29
### Changed
- Move Tabpane from termui/extra to termui and rename it to TabPane
- Rename PollEvent to PollEvents
## 2018-11-28
### Changed
- Migrate from Dep to vgo
- Overhaul the event system
- check the wiki/examples for details
- Rename Par widget to Paragraph
- Rename MBarChart widget to StackedBarChart
[#237]: https://github.com/gizak/termui/pull/237
[#126]: https://github.com/gizak/termui/pull/126
[Unreleased]: https://github.com/gizak/termui/compare/v3.1.0...HEAD
[3.1.0]: https://github.com/gizak/termui/compare/v3.0.0...v3.1.0
[3.0.0]: https://github.com/gizak/termui/compare/v2.3.0...v3.0.0
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2015 Zack Guo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
.PHONY: run-examples
run-examples:
@for file in _examples/*.go; do \
go run $$file; \
done;
================================================
FILE: README.md
================================================
# termui
[
](./_examples/demo.go)
termui is a cross-platform and fully-customizable terminal dashboard and widget library built on top of [termbox-go](https://github.com/nsf/termbox-go). It is inspired by [blessed-contrib](https://github.com/yaronn/blessed-contrib) and [tui-rs](https://github.com/fdehau/tui-rs) and written purely in Go.
## Note
Please be aware that due to my fluctuating availability, the frequency of updates to this project may not always follow a consistent schedule. I would like to invite potential maintainers to contribute to this project. If you are interested in becoming a maintainer, please do not hesitate to reach out to me.
## Versions
termui is currently compatible with Go 1.15 (as in go.mod) and above (tracking the Debian's [oldstable](https://wiki.debian.org/DebianReleases)). Please use the version-numbered branch as stable release. The new changes will be pushed to master branch first and then merge to version branch.
## Features
- Several premade widgets for common use cases
- Easily create custom widgets
- Position widgets either in a relative grid or with absolute coordinates
- Keyboard, mouse, and terminal resizing events
- Colors and styling
## Installation
### Go modules
It is not necessary to `go get` termui, since Go will automatically manage any imported dependencies for you. Do note that you have to include `/v3` in the import statements as shown in the 'Hello World' example below.
### Dep
Add with `dep ensure -add github.com/gizak/termui`. With Dep, `/v3` should *not* be included in the import statements.
## Hello World
```go
package main
import (
"log"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
func main() {
if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
defer ui.Close()
p := widgets.NewParagraph()
p.Text = "Hello World!"
p.SetRect(0, 0, 25, 5)
ui.Render(p)
for e := range ui.PollEvents() {
if e.Type == ui.KeyboardEvent {
break
}
}
}
```
## Widgets
- [BarChart](./_examples/barchart.go)
- [Canvas](./_examples/canvas.go) (for drawing braille dots)
- [Gauge](./_examples/gauge.go)
- [Image](./_examples/image.go)
- [List](./_examples/list.go)
- [Tree](./_examples/tree.go)
- [Paragraph](./_examples/paragraph.go)
- [PieChart](./_examples/piechart.go)
- [Plot](./_examples/plot.go) (for scatterplots and linecharts)
- [Sparkline](./_examples/sparkline.go)
- [StackedBarChart](./_examples/stacked_barchart.go)
- [Table](./_examples/table.go)
- [Tabs](./_examples/tabs.go)
Run an example with `go run _examples/{example}.go` or run each example consecutively with `make run-examples`.
## Documentation
- [wiki](https://github.com/gizak/termui/wiki)
## Uses
- [dockdash](https://github.com/byrnedo/dockdash)
- [expvarmon](https://github.com/divan/expvarmon)
- [go-ethereum/monitorcmd](https://github.com/ethereum/go-ethereum/blob/master/cmd/geth/monitorcmd.go)
- [go-jira-ui](https://github.com/mikepea/go-jira-ui)
- [gotop](https://github.com/cjbassi/gotop)
- [termeter](https://github.com/atsaki/termeter)
- [updo](https://github.com/Owloops/updo)
## Related Works
- [blessed-contrib](https://github.com/yaronn/blessed-contrib)
- [gocui](https://github.com/jroimartin/gocui)
- [termdash](https://github.com/mum4k/termdash)
- [tui-rs](https://github.com/fdehau/tui-rs)
- [tview](https://github.com/rivo/tview)
## License
[MIT](http://opensource.org/licenses/MIT)
================================================
FILE: _examples/barchart.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build ignore
package main
import (
"log"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
func main() {
if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
defer ui.Close()
bc := widgets.NewBarChart()
bc.Data = []float64{3, 2, 5, 3, 9, 3}
bc.Labels = []string{"S0", "S1", "S2", "S3", "S4", "S5"}
bc.Title = "Bar Chart"
bc.SetRect(5, 5, 100, 25)
bc.BarWidth = 5
bc.BarColors = []ui.Color{ui.ColorRed, ui.ColorGreen}
bc.LabelStyles = []ui.Style{ui.NewStyle(ui.ColorBlue)}
bc.NumStyles = []ui.Style{ui.NewStyle(ui.ColorYellow)}
ui.Render(bc)
uiEvents := ui.PollEvents()
for {
e := <-uiEvents
switch e.ID {
case "q", "":
return
}
}
}
================================================
FILE: _examples/canvas.go
================================================
// +build ignore
package main
import (
"image"
"log"
ui "github.com/gizak/termui/v3"
)
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.SetLine(image.Pt(0, 0), image.Pt(10, 20), ui.ColorWhite)
ui.Render(c)
for e := range ui.PollEvents() {
if e.Type == ui.KeyboardEvent {
break
}
}
}
================================================
FILE: _examples/demo.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build ignore
package main
import (
"log"
"math"
"time"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/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.NewPlot()
lc.Title = "dot-marker 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.Marker = widgets.MarkerDot
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.NewPlot()
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", "":
return
}
case <-ticker:
updateParagraph(tickerCount)
draw(tickerCount)
tickerCount++
}
}
}
================================================
FILE: _examples/gauge.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build ignore
package main
import (
"fmt"
"log"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
func main() {
if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
defer ui.Close()
g0 := widgets.NewGauge()
g0.Title = "Slim Gauge"
g0.SetRect(20, 20, 30, 30)
g0.Percent = 75
g0.BarColor = ui.ColorRed
g0.BorderStyle.Fg = ui.ColorWhite
g0.TitleStyle.Fg = ui.ColorCyan
g2 := widgets.NewGauge()
g2.Title = "Slim Gauge"
g2.SetRect(0, 3, 50, 6)
g2.Percent = 60
g2.BarColor = ui.ColorYellow
g2.LabelStyle = ui.NewStyle(ui.ColorBlue)
g2.BorderStyle.Fg = ui.ColorWhite
g1 := widgets.NewGauge()
g1.Title = "Big Gauge"
g1.SetRect(0, 6, 50, 11)
g1.Percent = 30
g1.BarColor = ui.ColorGreen
g1.LabelStyle = ui.NewStyle(ui.ColorYellow)
g1.TitleStyle.Fg = ui.ColorMagenta
g1.BorderStyle.Fg = ui.ColorWhite
g3 := widgets.NewGauge()
g3.Title = "Gauge with custom label"
g3.SetRect(0, 11, 50, 14)
g3.Percent = 50
g3.Label = fmt.Sprintf("%v%% (100MBs free)", g3.Percent)
g4 := widgets.NewGauge()
g4.Title = "Gauge"
g4.SetRect(0, 14, 50, 17)
g4.Percent = 50
g4.Label = "Gauge with custom highlighted label"
g4.BarColor = ui.ColorGreen
g4.LabelStyle = ui.NewStyle(ui.ColorYellow)
ui.Render(g0, g1, g2, g3, g4)
uiEvents := ui.PollEvents()
for {
e := <-uiEvents
switch e.ID {
case "q", "":
return
}
}
}
================================================
FILE: _examples/grid.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build ignore
package main
import (
"log"
"math"
"time"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
func main() {
if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
defer ui.Close()
sinFloat64 := (func() []float64 {
n := 400
data := make([]float64, n)
for i := range data {
data[i] = 1 + math.Sin(float64(i)/5)
}
return data
})()
sl := widgets.NewSparkline()
sl.Data = sinFloat64[:100]
sl.LineColor = ui.ColorCyan
sl.TitleStyle.Fg = ui.ColorWhite
slg := widgets.NewSparklineGroup(sl)
slg.Title = "Sparkline"
lc := widgets.NewPlot()
lc.Title = "braille-mode Line Chart"
lc.Data = append(lc.Data, sinFloat64)
lc.AxesColor = ui.ColorWhite
lc.LineColors[0] = ui.ColorYellow
gs := make([]*widgets.Gauge, 3)
for i := range gs {
gs[i] = widgets.NewGauge()
gs[i].Percent = i * 10
gs[i].BarColor = ui.ColorRed
}
ls := widgets.NewList()
ls.Rows = []string{
"[1] Downloading File 1",
"",
"",
"",
"[2] Downloading File 2",
"",
"",
"",
"[3] Uploading File 3",
}
ls.Border = false
p := widgets.NewParagraph()
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.Title = "Demonstration"
grid := ui.NewGrid()
termWidth, termHeight := ui.TerminalDimensions()
grid.SetRect(0, 0, termWidth, termHeight)
grid.Set(
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(grid)
tickerCount := 1
uiEvents := ui.PollEvents()
ticker := time.NewTicker(time.Second).C
for {
select {
case e := <-uiEvents:
switch e.ID {
case "q", "":
return
case "":
payload := e.Payload.(ui.Resize)
grid.SetRect(0, 0, payload.Width, payload.Height)
ui.Clear()
ui.Render(grid)
}
case <-ticker:
if tickerCount == 100 {
return
}
for _, g := range gs {
g.Percent = (g.Percent + 3) % 100
}
slg.Sparklines[0].Data = sinFloat64[tickerCount : tickerCount+100]
lc.Data[0] = sinFloat64[2*tickerCount:]
ui.Render(grid)
tickerCount++
}
}
}
================================================
FILE: _examples/hello_world.go
================================================
// +build ignore
package main
import (
"log"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
func main() {
if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
defer ui.Close()
p := widgets.NewParagraph()
p.Text = "Hello World!"
p.SetRect(0, 0, 25, 5)
ui.Render(p)
for e := range ui.PollEvents() {
if e.Type == ui.KeyboardEvent {
break
}
}
}
================================================
FILE: _examples/image.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build ignore
package main
import (
"encoding/base64"
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"log"
"net/http"
"os"
"strings"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
func main() {
var images []image.Image
for _, arg := range os.Args[1:] {
resp, err := http.Get(arg)
if err != nil {
log.Fatalf("failed to fetch image: %v", err)
}
image, _, err := image.Decode(resp.Body)
if err != nil {
log.Fatalf("failed to decode fetched image: %v", err)
}
images = append(images, image)
}
if len(images) == 0 {
image, _, err := image.Decode(base64.NewDecoder(base64.StdEncoding, strings.NewReader(GOPHER_IMAGE)))
if err != nil {
log.Fatalf("failed to decode gopher image: %v", err)
}
images = append(images, image)
}
if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
defer ui.Close()
img := widgets.NewImage(nil)
img.SetRect(0, 0, 100, 50)
index := 0
render := func() {
img.Image = images[index]
if !img.Monochrome {
img.Title = fmt.Sprintf("Color %d/%d", index+1, len(images))
} else if !img.MonochromeInvert {
img.Title = fmt.Sprintf("Monochrome(%d) %d/%d", img.MonochromeThreshold, index+1, len(images))
} else {
img.Title = fmt.Sprintf("InverseMonochrome(%d) %d/%d", img.MonochromeThreshold, index+1, len(images))
}
ui.Render(img)
}
render()
uiEvents := ui.PollEvents()
for {
e := <-uiEvents
switch e.ID {
case "q", "":
return
case "", "h":
index = (index + len(images) - 1) % len(images)
case "", "l":
index = (index + 1) % len(images)
case "", "k":
img.MonochromeThreshold++
case "", "j":
img.MonochromeThreshold--
case "":
img.Monochrome = !img.Monochrome
case "":
img.MonochromeInvert = !img.MonochromeInvert
}
render()
}
}
const GOPHER_IMAGE = `iVBORw0KGgoAAAANSUhEUgAAAEsAAAA8CAAAAAALAhhPAAAFfUlEQVRYw62XeWwUVRzHf2+OPbo9d7tsWyiyaZti6eWGAhISoIGKECEKCAiJJkYTiUgTMYSIosYYBBIUIxoSPIINEBDi2VhwkQrVsj1ESgu9doHWdrul7ba73WNm3vOPtsseM9MdwvvrzTs+8/t95ze/33sI5BqiabU6m9En8oNjduLnAEDLUsQXFF8tQ5oxK3vmnNmDSMtrncks9Hhtt/qeWZapHb1ha3UqYSWVl2ZmpWgaXMXGohQAvmeop3bjTRtv6SgaK/Pb9/bFzUrYslbFAmHPp+3WhAYdr+7GN/YnpN46Opv55VDsJkoEpMrY/vO2BIYQ6LLvm0ThY3MzDzzeSJeeWNyTkgnIE5ePKsvKlcg/0T9QMzXalwXMlj54z4c0rh/mzEfr+FgWEz2w6uk8dkzFAgcARAgNp1ZYef8bH2AgvuStbc2/i6CiWGj98y2tw2l4FAXKkQBIf+exyRnteY83LfEwDQAYCoK+P6bxkZm/0966LxcAAILHB56kgD95PPxltuYcMtFTWw/FKkY/6Opf3GGd9ZF+Qp6mzJxzuRSractOmJrH1u8XTvWFHINNkLQLMR+XHXvfPPHw967raE1xxwtA36IMRfkAAG29/7mLuQcb2WOnsJReZGfpiHsSBX81cvMKywYZHhX5hFPtOqPGWZCXnhWGAu6lX91ElKXSalcLXu3UaOXVay57ZSe5f6Gpx7J2MXAsi7EqSp09b/MirKSyJfnfEEgeDjl8FgDAfvewP03zZ+AJ0m9aFRM8eEHBDRKjfcreDXnZdQuAxXpT2NRJ7xl3UkLBhuVGU16gZiGOgZmrSbRdqkILuL/yYoSXHHkl9KXgqNu3PB8oRg0geC5vFmLjad6mUyTKLmF3OtraWDIfACyXqmephaDABawfpi6tqqBZytfQMqOz6S09iWXhktrRaB8Xz4Yi/8gyABDm5NVe6qq/3VzPrcjELWrebVuyY2T7ar4zQyybUCtsQ5Es1FGaZVrRVQwAgHGW2ZCRZshI5bGQi7HesyE972pOSeMM0dSktlzxRdrlqb3Osa6CCS8IJoQQQgBAbTAa5l5epO34rJszibJI8rxLfGzcp1dRosutGeb2VDNgqYrwTiPNsLxXiPi3dz7LiS1WBRBDBOnqEjyy3aQb+/bLiJzz9dIkscVBBLxMfSEac7kO4Fpkngi0ruNBeSOal+u8jgOuqPz12nryMLCniEjtOOOmpt+KEIqsEdocJjYXwrh9OZqWJQyPCTo67LNS/TdxLAv6R5ZNK9npEjbYdT33gRo4o5oTqR34R+OmaSzDBWsAIPhuRcgyoteNi9gF0KzNYWVItPf2TLoXEg+7isNC7uJkgo1iQWOfRSP9NR11RtbZZ3OMG/VhL6jvx+J1m87+RCfJChAtEBQkSBX2PnSiihc/Twh3j0h7qdYQAoRVsRGmq7HU2QRbaxVGa1D6nIOqaIWRjyRZpHMQKWKpZM5feA+lzC4ZFultV8S6T0mzQGhQohi5I8iw+CsqBSxhFMuwyLgSwbghGb0AiIKkSDmGZVmJSiKihsiyOAUs70UkywooYP0bii9GdH4sfr1UNysd3fUyLLMQN+rsmo3grHl9VNJHbbwxoa47Vw5gupIqrZcjPh9R4Nye3nRDk199V+aetmvVtDRE8/+cbgAAgMIWGb3UA0MGLE9SCbWX670TDy1y98c3D27eppUjsZ6fql3jcd5rUe7+ZIlLNQny3Rd+E5Tct3WVhTM5RBCEdiEK0b6B+/ca2gYU393nFj/n1AygRQxPIUA043M42u85+z2SnssKrPl8Mx76NL3E6eXc3be7OD+H4WHbJkKI8AU8irbITQjZ+0hQcPEgId/Fn/pl9crKH02+5o2b9T/eMx7pKoskYgAAAABJRU5ErkJggg==`
================================================
FILE: _examples/list.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build ignore
package main
import (
"log"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
func main() {
if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
defer ui.Close()
l := widgets.NewList()
l.Title = "List"
l.Rows = []string{
"[0] github.com/gizak/termui/v3",
"[1] [你好,世界](fg:blue)",
"[2] [こんにちは世界](fg:red)",
"[3] [color](fg:white,bg:green) output",
"[4] output.go",
"[5] random_out.go",
"[6] dashboard.go",
"[7] foo",
"[8] bar",
"[9] baz",
}
l.TextStyle = ui.NewStyle(ui.ColorYellow)
l.WrapText = false
l.SetRect(0, 0, 25, 8)
ui.Render(l)
previousKey := ""
uiEvents := ui.PollEvents()
for {
e := <-uiEvents
switch e.ID {
case "q", "":
return
case "j", "":
l.ScrollDown()
case "k", "":
l.ScrollUp()
case "":
l.ScrollHalfPageDown()
case "":
l.ScrollHalfPageUp()
case "":
l.ScrollPageDown()
case "":
l.ScrollPageUp()
case "g":
if previousKey == "g" {
l.ScrollTop()
}
case "":
l.ScrollTop()
case "G", "":
l.ScrollBottom()
}
if previousKey == "g" {
previousKey = ""
} else {
previousKey = e.ID
}
ui.Render(l)
}
}
================================================
FILE: _examples/paragraph.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build ignore
package main
import (
"log"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
func main() {
if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
defer ui.Close()
p0 := widgets.NewParagraph()
p0.Text = "Borderless Text"
p0.SetRect(0, 0, 20, 5)
p0.Border = false
p1 := widgets.NewParagraph()
p1.Title = "标签"
p1.Text = "你好,世界。"
p1.SetRect(20, 0, 35, 5)
p2 := widgets.NewParagraph()
p2.Title = "Multiline"
p2.Text = "Simple colored text\nwith label. It [can be](fg:red) multilined with \\n or [break automatically](fg:red,fg:bold)"
p2.SetRect(0, 5, 35, 10)
p2.BorderStyle.Fg = ui.ColorYellow
p3 := widgets.NewParagraph()
p3.Title = "Auto Trim"
p3.Text = "Long text with label and it is auto trimmed."
p3.SetRect(0, 10, 40, 15)
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()
for {
e := <-uiEvents
switch e.ID {
case "q", "":
return
}
}
}
================================================
FILE: _examples/piechart.go
================================================
// +build ignore
package main
import (
"fmt"
"log"
"math"
"math/rand"
"time"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
var run = true
func main() {
if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
defer ui.Close()
rand.Seed(time.Now().UTC().UnixNano())
randomDataAndOffset := func() (data []float64, offset float64) {
noSlices := 1 + rand.Intn(5)
data = make([]float64, noSlices)
for i := range data {
data[i] = rand.Float64()
}
offset = 2.0 * math.Pi * rand.Float64()
return
}
pc := widgets.NewPieChart()
pc.Title = "Pie Chart"
pc.SetRect(5, 5, 70, 36)
pc.Data = []float64{.25, .25, .25, .25}
pc.AngleOffset = -.5 * math.Pi
pc.LabelFormatter = func(i int, v float64) string {
return fmt.Sprintf("%.02f", v)
}
pause := func() {
run = !run
if run {
pc.Title = "Pie Chart"
} else {
pc.Title = "Pie Chart (Stopped)"
}
ui.Render(pc)
}
ui.Render(pc)
uiEvents := ui.PollEvents()
ticker := time.NewTicker(time.Second).C
for {
select {
case e := <-uiEvents:
switch e.ID {
case "q", "":
return
case "s":
pause()
}
case <-ticker:
if run {
pc.Data, pc.AngleOffset = randomDataAndOffset()
ui.Render(pc)
}
}
}
}
================================================
FILE: _examples/plot.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build ignore
package main
import (
"log"
"math"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
func main() {
if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
defer ui.Close()
sinData := func() [][]float64 {
n := 220
data := make([][]float64, 2)
data[0] = make([]float64, n)
data[1] = make([]float64, n)
for i := 0; i < n; i++ {
data[0][i] = 1 + math.Sin(float64(i)/5)
data[1][i] = 1 + math.Cos(float64(i)/5)
}
return data
}()
p0 := widgets.NewPlot()
p0.Title = "braille-mode Line Chart"
p0.Data = sinData
p0.SetRect(0, 0, 50, 15)
p0.AxesColor = ui.ColorWhite
p0.LineColors[0] = ui.ColorGreen
p1 := widgets.NewPlot()
p1.Title = "dot-mode line Chart"
p1.Marker = widgets.MarkerDot
p1.Data = [][]float64{[]float64{1, 2, 3, 4, 5}}
p1.SetRect(50, 0, 75, 10)
p1.DotMarkerRune = '+'
p1.AxesColor = ui.ColorWhite
p1.LineColors[0] = ui.ColorYellow
p1.DrawDirection = widgets.DrawLeft
p2 := widgets.NewPlot()
p2.Title = "dot-mode Scatter Plot"
p2.Marker = widgets.MarkerDot
p2.Data = make([][]float64, 2)
p2.Data[0] = []float64{1, 2, 3, 4, 5}
p2.Data[1] = sinData[1][4:]
p2.SetRect(0, 15, 50, 30)
p2.AxesColor = ui.ColorWhite
p2.LineColors[0] = ui.ColorCyan
p2.PlotType = widgets.ScatterPlot
p3 := widgets.NewPlot()
p3.Title = "braille-mode Scatter Plot"
p3.Data = make([][]float64, 2)
p3.Data[0] = []float64{1, 2, 3, 4, 5}
p3.Data[1] = sinData[1][4:]
p3.SetRect(45, 15, 80, 30)
p3.AxesColor = ui.ColorWhite
p3.LineColors[0] = ui.ColorCyan
p3.Marker = widgets.MarkerBraille
p3.PlotType = widgets.ScatterPlot
ui.Render(p0, p1, p2, p3)
uiEvents := ui.PollEvents()
for {
e := <-uiEvents
switch e.ID {
case "q", "":
return
}
}
}
================================================
FILE: _examples/sparkline.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build ignore
package main
import (
"log"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/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", "":
return
}
}
}
================================================
FILE: _examples/stacked_barchart.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build ignore
package main
import (
"log"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/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", "":
return
}
}
}
================================================
FILE: _examples/table.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build ignore
package main
import (
"log"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
func main() {
if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
defer ui.Close()
table1 := widgets.NewTable()
table1.Rows = [][]string{
[]string{"header1", "header2", "header3"},
[]string{"你好吗", "Go-lang is so cool", "Im working on Ruby"},
[]string{"2016", "10", "11"},
}
table1.TextStyle = ui.NewStyle(ui.ColorWhite)
table1.SetRect(0, 0, 60, 10)
ui.Render(table1)
table2 := widgets.NewTable()
table2.Rows = [][]string{
[]string{"header1", "header2", "header3"},
[]string{"Foundations", "Go-lang is so cool", "Im working on Ruby"},
[]string{"2016", "11", "11"},
}
table2.TextStyle = ui.NewStyle(ui.ColorWhite)
table2.TextAlignment = ui.AlignCenter
table2.RowSeparator = false
table2.SetRect(0, 10, 20, 20)
ui.Render(table2)
table3 := widgets.NewTable()
table3.Rows = [][]string{
[]string{"header1", "header2", "header3"},
[]string{"AAA", "BBB", "CCC"},
[]string{"DDD", "EEE", "FFF"},
[]string{"GGG", "HHH", "III"},
}
table3.TextStyle = ui.NewStyle(ui.ColorWhite)
table3.RowSeparator = true
table3.BorderStyle = ui.NewStyle(ui.ColorGreen)
table3.SetRect(0, 30, 70, 20)
table3.FillRow = true
table3.RowStyles[0] = ui.NewStyle(ui.ColorWhite, ui.ColorBlack, ui.ModifierBold)
table3.RowStyles[2] = ui.NewStyle(ui.ColorWhite, ui.ColorRed, ui.ModifierBold)
table3.RowStyles[3] = ui.NewStyle(ui.ColorYellow)
ui.Render(table3)
uiEvents := ui.PollEvents()
for {
e := <-uiEvents
switch e.ID {
case "q", "":
return
}
}
}
================================================
FILE: _examples/tabs.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build ignore
package main
import (
"log"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
func main() {
if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
defer ui.Close()
header := widgets.NewParagraph()
header.Text = "Press q to quit, Press h or l to switch tabs"
header.SetRect(0, 0, 50, 1)
header.Border = false
header.TextStyle.Bg = ui.ColorBlue
p2 := widgets.NewParagraph()
p2.Text = "Press q to quit\nPress h or l to switch tabs\n"
p2.Title = "Keys"
p2.SetRect(5, 5, 40, 15)
p2.BorderStyle.Fg = ui.ColorYellow
bc := widgets.NewBarChart()
bc.Title = "Bar Chart"
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}
bc.SetRect(5, 5, 35, 10)
bc.Labels = []string{"S0", "S1", "S2", "S3", "S4", "S5"}
tabpane := widgets.NewTabPane("pierwszy", "drugi", "trzeci", "żółw", "four", "five")
tabpane.SetRect(0, 1, 50, 4)
tabpane.Border = true
renderTab := func() {
switch tabpane.ActiveTabIndex {
case 0:
ui.Render(p2)
case 1:
ui.Render(bc)
}
}
ui.Render(header, tabpane, p2)
uiEvents := ui.PollEvents()
for {
e := <-uiEvents
switch e.ID {
case "q", "":
return
case "h":
tabpane.FocusLeft()
ui.Clear()
ui.Render(header, tabpane)
renderTab()
case "l":
tabpane.FocusRight()
ui.Clear()
ui.Render(header, tabpane)
renderTab()
}
}
}
================================================
FILE: _examples/tree.go
================================================
/// +build ignore
package main
import (
"log"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
type nodeValue string
func (nv nodeValue) String() string {
return string(nv)
}
func main() {
if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
defer ui.Close()
nodes := []*widgets.TreeNode{
{
Value: nodeValue("Key 1"),
Nodes: []*widgets.TreeNode{
{
Value: nodeValue("Key 1.1"),
Nodes: []*widgets.TreeNode{
{
Value: nodeValue("Key 1.1.1"),
Nodes: nil,
},
{
Value: nodeValue("Key 1.1.2"),
Nodes: nil,
},
},
},
{
Value: nodeValue("Key 1.2"),
Nodes: nil,
},
},
},
{
Value: nodeValue("Key 2"),
Nodes: []*widgets.TreeNode{
{
Value: nodeValue("Key 2.1"),
Nodes: nil,
},
{
Value: nodeValue("Key 2.2"),
Nodes: nil,
},
{
Value: nodeValue("Key 2.3"),
Nodes: nil,
},
},
},
{
Value: nodeValue("Key 3"),
Nodes: nil,
},
}
l := widgets.NewTree()
l.TextStyle = ui.NewStyle(ui.ColorYellow)
l.WrapText = false
l.SetNodes(nodes)
x, y := ui.TerminalDimensions()
l.SetRect(0, 0, x, y)
ui.Render(l)
previousKey := ""
uiEvents := ui.PollEvents()
for {
e := <-uiEvents
switch e.ID {
case "q", "":
return
case "j", "":
l.ScrollDown()
case "k", "":
l.ScrollUp()
case "":
l.ScrollHalfPageDown()
case "":
l.ScrollHalfPageUp()
case "":
l.ScrollPageDown()
case "":
l.ScrollPageUp()
case "g":
if previousKey == "g" {
l.ScrollTop()
}
case "":
l.ScrollTop()
case "":
l.ToggleExpand()
case "G", "":
l.ScrollBottom()
case "E":
l.ExpandAll()
case "C":
l.CollapseAll()
case "":
x, y := ui.TerminalDimensions()
l.SetRect(0, 0, x, y)
}
if previousKey == "g" {
previousKey = ""
} else {
previousKey = e.ID
}
ui.Render(l)
}
}
================================================
FILE: _scripts/copyright_header.py
================================================
#!/usr/bin/env python3
import re
import os
import io
copyright = """// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
"""
exclude_dirs = [".git", "_docs"]
exclude_files = []
include_dirs = [".", "debug", "extra", "test", "_example"]
def is_target(fpath):
if os.path.splitext(fpath)[-1] == ".go":
return True
return False
def update_copyright(fpath):
print("processing " + fpath)
f = io.open(fpath, 'r', encoding='utf-8')
fstr = f.read()
f.close()
# remove old
m = re.search('^// Copyright .+?\r?\n\r?\n', fstr, re.MULTILINE|re.DOTALL)
if m:
fstr = fstr[m.end():]
# add new
fstr = copyright + fstr
f = io.open(fpath, 'w',encoding='utf-8')
f.write(fstr)
f.close()
def main():
for d in include_dirs:
files = [
os.path.join(d, f) for f in os.listdir(d)
if os.path.isfile(os.path.join(d, f))
]
for f in files:
if is_target(f):
update_copyright(f)
if __name__ == '__main__':
main()
================================================
FILE: _test/log_events.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build ignore
package main
import (
"fmt"
"log"
ui "github.com/gizak/termui/v3"
)
// logs all events to the termui window
// stdout can also be redirected to a file and read with `tail -f`
func main() {
if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
defer ui.Close()
events := ui.PollEvents()
for {
e := <-events
fmt.Printf("%v", e)
switch e.ID {
case "q", "":
return
case "":
return
}
}
}
================================================
FILE: alignment.go
================================================
package termui
type Alignment uint
const (
AlignLeft Alignment = iota
AlignCenter
AlignRight
)
================================================
FILE: backend.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
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))
}
================================================
FILE: block.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import (
"image"
"sync"
)
// Block is the base struct inherited by most widgets.
// Block manages size, position, border, and title.
// It implements all 3 of the methods needed for the `Drawable` interface.
// Custom widgets will override the Draw method.
type Block struct {
Border bool
BorderStyle Style
BorderLeft, BorderRight, BorderTop, BorderBottom bool
PaddingLeft, PaddingRight, PaddingTop, PaddingBottom int
image.Rectangle
Inner image.Rectangle
Title string
TitleStyle Style
sync.Mutex
}
func NewBlock() *Block {
return &Block{
Border: true,
BorderStyle: Theme.Block.Border,
BorderLeft: true,
BorderRight: true,
BorderTop: true,
BorderBottom: true,
TitleStyle: Theme.Block.Title,
}
}
func (self *Block) drawBorder(buf *Buffer) {
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 self.BorderBottom {
buf.Fill(horizontalCell, image.Rect(self.Min.X, self.Max.Y-1, self.Max.X, self.Max.Y))
}
if self.BorderLeft {
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)))
}
}
// Draw implements the Drawable interface.
func (self *Block) Draw(buf *Buffer) {
if self.Border {
self.drawBorder(buf)
}
buf.SetString(
self.Title,
self.TitleStyle,
image.Pt(self.Min.X+2, self.Min.Y),
)
}
// SetRect implements the Drawable interface.
func (self *Block) SetRect(x1, y1, x2, y2 int) {
self.Rectangle = image.Rect(x1, y1, x2, y2)
self.Inner = image.Rect(
self.Min.X+1+self.PaddingLeft,
self.Min.Y+1+self.PaddingTop,
self.Max.X-1-self.PaddingRight,
self.Max.Y-1-self.PaddingBottom,
)
}
// GetRect implements the Drawable interface.
func (self *Block) GetRect() image.Rectangle {
return self.Rectangle
}
================================================
FILE: buffer.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import (
"image"
rw "github.com/mattn/go-runewidth"
)
// Cell represents a viewable terminal cell
type Cell struct {
Rune rune
Style Style
}
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 {
image.Rectangle
CellMap map[image.Point]Cell
}
func NewBuffer(r image.Rectangle) *Buffer {
buf := &Buffer{
Rectangle: r,
CellMap: make(map[image.Point]Cell),
}
buf.Fill(CellClear, r) // clears out area
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) {
runes := []rune(s)
x := 0
for _, char := range runes {
self.SetCell(Cell{char, style}, image.Pt(p.X+x, p.Y))
x += rw.RuneWidth(char)
}
}
================================================
FILE: canvas.go
================================================
package termui
import (
"image"
"github.com/gizak/termui/v3/drawille"
)
type Canvas struct {
Block
drawille.Canvas
}
func NewCanvas() *Canvas {
return &Canvas{
Block: *NewBlock(),
Canvas: *drawille.NewCanvas(),
}
}
func (self *Canvas) SetPoint(p image.Point, color Color) {
self.Canvas.SetPoint(p, drawille.Color(color))
}
func (self *Canvas) SetLine(p0, p1 image.Point, color Color) {
self.Canvas.SetLine(p0, p1, drawille.Color(color))
}
func (self *Canvas) Draw(buf *Buffer) {
for point, cell := range self.Canvas.GetCells() {
if point.In(self.Rectangle) {
convertedCell := Cell{
cell.Rune,
Style{
Color(cell.Color),
ColorClear,
ModifierClear,
},
}
buf.SetCell(convertedCell, point)
}
}
}
================================================
FILE: doc.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
/*
Package termui is a library for creating terminal user interfaces (TUIs) using widgets.
*/
package termui
================================================
FILE: drawille/drawille.go
================================================
package drawille
import (
"image"
)
const BRAILLE_OFFSET = '\u2800'
var BRAILLE = [4][2]rune{
{'\u0001', '\u0008'},
{'\u0002', '\u0010'},
{'\u0004', '\u0020'},
{'\u0040', '\u0080'},
}
type Color int
type Cell struct {
Rune rune
Color Color
}
type Canvas struct {
CellMap map[image.Point]Cell
}
func NewCanvas() *Canvas {
return &Canvas{
CellMap: make(map[image.Point]Cell),
}
}
func (self *Canvas) SetPoint(p image.Point, color Color) {
point := image.Pt(p.X/2, p.Y/4)
self.CellMap[point] = Cell{
self.CellMap[point].Rune | BRAILLE[p.Y%4][p.X%2],
color,
}
}
func (self *Canvas) SetLine(p0, p1 image.Point, color Color) {
for _, p := range line(p0, p1) {
self.SetPoint(p, color)
}
}
func (self *Canvas) GetCells() map[image.Point]Cell {
cellMap := make(map[image.Point]Cell)
for point, cell := range self.CellMap {
cellMap[point] = Cell{cell.Rune + BRAILLE_OFFSET, cell.Color}
}
return cellMap
}
func line(p0, p1 image.Point) []image.Point {
points := []image.Point{}
leftPoint, rightPoint := p0, p1
if leftPoint.X > rightPoint.X {
leftPoint, rightPoint = rightPoint, leftPoint
}
xDistance := absInt(leftPoint.X - rightPoint.X)
yDistance := absInt(leftPoint.Y - rightPoint.Y)
slope := float64(yDistance) / float64(xDistance)
slopeSign := 1
if rightPoint.Y < leftPoint.Y {
slopeSign = -1
}
targetYCoordinate := float64(leftPoint.Y)
currentYCoordinate := leftPoint.Y
for i := leftPoint.X; i < rightPoint.X; i++ {
points = append(points, image.Pt(i, currentYCoordinate))
targetYCoordinate += (slope * float64(slopeSign))
for currentYCoordinate != int(targetYCoordinate) {
points = append(points, image.Pt(i, currentYCoordinate))
currentYCoordinate += slopeSign
}
}
return points
}
func absInt(x int) int {
if x >= 0 {
return x
}
return -x
}
================================================
FILE: events.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import (
"fmt"
tb "github.com/nsf/termbox-go"
)
/*
List of events:
mouse events:
keyboard events:
any uppercase or lowercase letter like j or J
etc
etc
> etc
terminal events:
keyboard events that do not work:
*/
type EventType uint
const (
KeyboardEvent EventType = iota
MouseEvent
ResizeEvent
)
type Event struct {
Type EventType
ID string
Payload interface{}
}
// Mouse payload.
type Mouse struct {
Drag bool
X int
Y int
}
// Resize payload.
type Resize struct {
Width int
Height int
}
// PollEvents gets events from termbox, converts them, then sends them to each of its channels.
func PollEvents() <-chan Event {
ch := make(chan Event)
go func() {
for {
ch <- convertTermboxEvent(tb.PollEvent())
}
}()
return ch
}
var keyboardMap = map[tb.Key]string{
tb.KeyF1: "",
tb.KeyF2: "",
tb.KeyF3: "",
tb.KeyF4: "",
tb.KeyF5: "",
tb.KeyF6: "",
tb.KeyF7: "",
tb.KeyF8: "",
tb.KeyF9: "",
tb.KeyF10: "",
tb.KeyF11: "",
tb.KeyF12: "",
tb.KeyInsert: "",
tb.KeyDelete: "",
tb.KeyHome: "",
tb.KeyEnd: "",
tb.KeyPgup: "",
tb.KeyPgdn: "",
tb.KeyArrowUp: "",
tb.KeyArrowDown: "",
tb.KeyArrowLeft: "",
tb.KeyArrowRight: "",
tb.KeyCtrlSpace: ">", // tb.KeyCtrl2 tb.KeyCtrlTilde
tb.KeyCtrlA: "",
tb.KeyCtrlB: "",
tb.KeyCtrlC: "",
tb.KeyCtrlD: "",
tb.KeyCtrlE: "",
tb.KeyCtrlF: "",
tb.KeyCtrlG: "",
tb.KeyBackspace: ">", // tb.KeyCtrlH
tb.KeyTab: "", // tb.KeyCtrlI
tb.KeyCtrlJ: "",
tb.KeyCtrlK: "",
tb.KeyCtrlL: "",
tb.KeyEnter: "", // tb.KeyCtrlM
tb.KeyCtrlN: "",
tb.KeyCtrlO: "",
tb.KeyCtrlP: "",
tb.KeyCtrlQ: "",
tb.KeyCtrlR: "",
tb.KeyCtrlS: "",
tb.KeyCtrlT: "",
tb.KeyCtrlU: "",
tb.KeyCtrlV: "",
tb.KeyCtrlW: "",
tb.KeyCtrlX: "",
tb.KeyCtrlY: "",
tb.KeyCtrlZ: "",
tb.KeyEsc: "", // tb.KeyCtrlLsqBracket tb.KeyCtrl3
tb.KeyCtrl4: "", // tb.KeyCtrlBackslash
tb.KeyCtrl5: "", // tb.KeyCtrlRsqBracket
tb.KeyCtrl6: "",
tb.KeyCtrl7: "", // tb.KeyCtrlSlash tb.KeyCtrlUnderscore
tb.KeySpace: "",
tb.KeyBackspace2: "", // tb.KeyCtrl8:
}
// convertTermboxKeyboardEvent converts a termbox keyboard event to a more friendly string format.
// Combines modifiers into the string instead of having them as additional fields in an event.
func convertTermboxKeyboardEvent(e tb.Event) Event {
ID := "%s"
if e.Mod == tb.ModAlt {
ID = ""
}
if e.Ch != 0 {
ID = fmt.Sprintf(ID, string(e.Ch))
} else {
converted, ok := keyboardMap[e.Key]
if !ok {
converted = ""
}
ID = fmt.Sprintf(ID, converted)
}
return Event{
Type: KeyboardEvent,
ID: ID,
}
}
var mouseButtonMap = map[tb.Key]string{
tb.MouseLeft: "",
tb.MouseMiddle: "",
tb.MouseRight: "",
tb.MouseRelease: "",
tb.MouseWheelUp: "",
tb.MouseWheelDown: "",
}
func convertTermboxMouseEvent(e tb.Event) Event {
converted, ok := mouseButtonMap[e.Key]
if !ok {
converted = "Unknown_Mouse_Button"
}
Drag := e.Mod == tb.ModMotion
return Event{
Type: MouseEvent,
ID: converted,
Payload: Mouse{
X: e.MouseX,
Y: e.MouseY,
Drag: Drag,
},
}
}
// convertTermboxEvent turns a termbox event into a termui event.
func convertTermboxEvent(e tb.Event) Event {
if e.Type == tb.EventError {
panic(e.Err)
}
switch e.Type {
case tb.EventKey:
return convertTermboxKeyboardEvent(e)
case tb.EventMouse:
return convertTermboxMouseEvent(e)
case tb.EventResize:
return Event{
Type: ResizeEvent,
ID: "",
Payload: Resize{
Width: e.Width,
Height: e.Height,
},
}
}
return Event{}
}
================================================
FILE: go.mod
================================================
module github.com/gizak/termui/v3
go 1.15
require (
github.com/mattn/go-runewidth v0.0.15
github.com/mitchellh/go-wordwrap v1.0.1
github.com/nsf/termbox-go v1.1.1
github.com/rivo/uniseg v0.4.4 // indirect
)
================================================
FILE: go.sum
================================================
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.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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 v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY=
github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
================================================
FILE: grid.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
type gridItemType uint
const (
col gridItemType = 0
row gridItemType = 1
)
type Grid struct {
Block
Items []*GridItem
}
// GridItem represents either a Row or Column in a grid.
// Holds sizing information and either an []GridItems or a widget.
type GridItem struct {
Type gridItemType
XRatio float64
YRatio float64
WidthRatio float64
HeightRatio float64
Entry interface{} // Entry.type == GridBufferer if IsLeaf else []GridItem
IsLeaf bool
ratio float64
}
func NewGrid() *Grid {
g := &Grid{
Block: *NewBlock(),
}
g.Border = false
return g
}
// NewCol takes a height percentage and either a widget or a Row or Column
func NewCol(ratio float64, i ...interface{}) GridItem {
_, ok := i[0].(Drawable)
entry := i[0]
if !ok {
entry = i
}
return GridItem{
Type: col,
Entry: entry,
IsLeaf: ok,
ratio: ratio,
}
}
// NewRow takes a width percentage and either a widget or a Row or Column
func NewRow(ratio float64, i ...interface{}) GridItem {
_, ok := i[0].(Drawable)
entry := i[0]
if !ok {
entry = i
}
return GridItem{
Type: row,
Entry: entry,
IsLeaf: ok,
ratio: ratio,
}
}
// Set is used to add Columns and Rows to the grid.
// It recursively searches the GridItems, adding leaves to the grid and calculating the dimensions of the leaves.
func (self *Grid) Set(entries ...interface{}) {
entry := GridItem{
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 {
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
}
}
self.setHelper(child, item.WidthRatio, item.HeightRatio)
}
}
}
func (self *Grid) Draw(buf *Buffer) {
width := float64(self.Dx()) + 1
height := float64(self.Dy()) + 1
for _, item := range self.Items {
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--
}
entry.SetRect(x, y, x+w, y+h)
entry.Lock()
entry.Draw(buf)
entry.Unlock()
}
}
================================================
FILE: render.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import (
"image"
"sync"
tb "github.com/nsf/termbox-go"
)
type Drawable interface {
GetRect() image.Rectangle
SetRect(int, int, int, int)
Draw(*Buffer)
sync.Locker
}
func Render(items ...Drawable) {
for _, item := range items {
buf := NewBuffer(item.GetRect())
item.Lock()
item.Draw(buf)
item.Unlock()
for point, cell := range buf.CellMap {
if point.In(buf.Rectangle) {
tb.SetCell(
point.X, point.Y,
cell.Rune,
tb.Attribute(cell.Style.Fg+1)|tb.Attribute(cell.Style.Modifier), tb.Attribute(cell.Style.Bg+1),
)
}
}
}
tb.Flush()
}
================================================
FILE: style.go
================================================
package termui
// Color is an integer from -1 to 255
// -1 = ColorClear
// 0-255 = Xterm colors
type Color int
// ColorClear clears the Fg or Bg color of a Style
const ColorClear Color = -1
// Basic terminal colors
const (
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 clears any modifiers
ModifierClear Modifier = 0
ModifierBold Modifier = 1 << 9
ModifierUnderline Modifier = 1 << 10
ModifierReverse Modifier = 1 << 11
)
// Style represents the style of one terminal cell
type Style struct {
Fg Color
Bg Color
Modifier Modifier
}
// StyleClear represents a default Style, with no colors or modifiers
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,
}
}
================================================
FILE: style_parser.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
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
)
// StyleParserColorMap can be modified to add custom color parsing to text
var StyleParserColorMap = 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,
}
// 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 = StyleParserColorMap[pair[1]]
case tokenBg:
style.Bg = StyleParserColorMap[pair[1]]
case tokenModifier:
style.Modifier = modifierMap[pair[1]]
}
}
}
return style
}
// ParseStyles parses a string for embedded Styles and returns []Cell with the correct styling.
// Uses defaultStyle for any text without an embedded style.
// Syntax is of the form [text](fg:,mod:,bg:).
// Ordering does not matter. All fields are optional.
func ParseStyles(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
}
================================================
FILE: symbols.go
================================================
package termui
const (
DOT = '•'
ELLIPSES = '…'
UP_ARROW = '▲'
DOWN_ARROW = '▼'
COLLAPSED = '+'
EXPANDED = '−'
)
var (
BARS = [...]rune{' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
SHADED_BLOCKS = [...]rune{' ', '░', '▒', '▓', '█'}
IRREGULAR_BLOCKS = [...]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', '⠠', '⠐', '⠈'}
)
================================================
FILE: symbols_other.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build !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 = '┈'
)
================================================
FILE: symbols_windows.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build 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 = '-'
)
================================================
FILE: theme.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
var StandardColors = []Color{
ColorRed,
ColorGreen,
ColorYellow,
ColorBlue,
ColorMagenta,
ColorCyan,
ColorWhite,
}
var StandardStyles = []Style{
NewStyle(ColorRed),
NewStyle(ColorGreen),
NewStyle(ColorYellow),
NewStyle(ColorBlue),
NewStyle(ColorMagenta),
NewStyle(ColorCyan),
NewStyle(ColorWhite),
}
type RootTheme struct {
Default Style
Block BlockTheme
BarChart BarChartTheme
Gauge GaugeTheme
Plot PlotTheme
List ListTheme
Tree TreeTheme
Paragraph ParagraphTheme
PieChart PieChartTheme
Sparkline SparklineTheme
StackedBarChart StackedBarChartTheme
Tab TabTheme
Table TableTheme
}
type BlockTheme struct {
Title Style
Border Style
}
type BarChartTheme struct {
Bars []Color
Nums []Style
Labels []Style
}
type GaugeTheme struct {
Bar Color
Label Style
}
type PlotTheme struct {
Lines []Color
Axes Color
}
type ListTheme struct {
Text Style
}
type TreeTheme struct {
Text Style
Collapsed rune
Expanded rune
}
type ParagraphTheme struct {
Text Style
}
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
}
// Theme holds the default Styles and Colors for all widgets.
// You can set default widget Styles by modifying the Theme before creating the widgets.
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),
},
Tree: TreeTheme{
Text: NewStyle(ColorWhite),
Collapsed: COLLAPSED,
Expanded: EXPANDED,
},
StackedBarChart: StackedBarChartTheme{
Bars: StandardColors,
Nums: StandardStyles,
Labels: StandardStyles,
},
Gauge: GaugeTheme{
Bar: ColorWhite,
Label: NewStyle(ColorWhite),
},
Sparkline: SparklineTheme{
Title: NewStyle(ColorWhite),
Line: ColorWhite,
},
Plot: PlotTheme{
Lines: StandardColors,
Axes: ColorWhite,
},
Table: TableTheme{
Text: NewStyle(ColorWhite),
},
Tab: TabTheme{
Active: NewStyle(ColorRed),
Inactive: NewStyle(ColorWhite),
},
}
================================================
FILE: utils.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import (
"fmt"
"math"
"reflect"
rw "github.com/mattn/go-runewidth"
wordwrap "github.com/mitchellh/go-wordwrap"
)
// InterfaceSlice takes an []interface{} represented as an interface{} and converts it
// https://stackoverflow.com/questions/12753805/type-converting-slices-of-interfaces-in-go
func InterfaceSlice(slice interface{}) []interface{} {
s := reflect.ValueOf(slice)
if s.Kind() != reflect.Slice {
panic("InterfaceSlice() given a non-slice type")
}
ret := make([]interface{}, s.Len())
for i := 0; i < s.Len(); i++ {
ret[i] = s.Index(i).Interface()
}
return ret
}
// TrimString trims a string to a max length and adds '…' to the end if it was trimmed.
func TrimString(s string, w int) string {
if w <= 0 {
return ""
}
if rw.StringWidth(s) > w {
return rw.Truncate(s, w, string(ELLIPSES))
}
return s
}
func SelectColor(colors []Color, index int) Color {
return colors[index%len(colors)]
}
func SelectStyle(styles []Style, index int) Style {
return styles[index%len(styles)]
}
// Math ------------------------------------------------------------------------
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 GetMaxIntFromSlice(slice []int) (int, error) {
if len(slice) == 0 {
return 0, fmt.Errorf("cannot get max value from empty slice")
}
var max int
for _, val := range slice {
if val > max {
max = val
}
}
return max, nil
}
func GetMaxFloat64FromSlice(slice []float64) (float64, error) {
if len(slice) == 0 {
return 0, fmt.Errorf("cannot get max value from empty slice")
}
var max float64
for _, val := range slice {
if val > max {
max = val
}
}
return max, nil
}
func GetMaxFloat64From2dSlice(slices [][]float64) (float64, error) {
if len(slices) == 0 {
return 0, fmt.Errorf("cannot get max value from empty slice")
}
var max float64
for _, slice := range slices {
for _, val := range slice {
if val > max {
max = val
}
}
}
return max, nil
}
func RoundFloat64(x float64) float64 {
return math.Floor(x + 0.5)
}
func FloorFloat64(x float64) float64 {
return math.Floor(x)
}
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 MaxInt(x, y int) int {
if x > y {
return x
}
return y
}
func MinInt(x, y int) int {
if x < y {
return x
}
return y
}
// []Cell ----------------------------------------------------------------------
// WrapCells takes []Cell and inserts Cells containing '\n' wherever a linebreak should go.
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 {
wrappedCells = append(wrappedCells, Cell{_rune, cells[i].Style})
}
i++
}
return wrappedCells
}
func RunesToStyledCells(runes []rune, style Style) []Cell {
cells := []Cell{}
for _, _rune := range runes {
cells = append(cells, Cell{_rune, style})
}
return cells
}
func CellsToString(cells []Cell) string {
runes := make([]rune, len(cells))
for i, cell := range cells {
runes[i] = cell.Rune
}
return string(runes)
}
func TrimCells(cells []Cell, w int) []Cell {
s := CellsToString(cells)
s = TrimString(s, w)
runes := []rune(s)
newCells := []Cell{}
for i, r := range runes {
newCells = append(newCells, Cell{r, cells[i].Style})
}
return newCells
}
func SplitCells(cells []Cell, r rune) [][]Cell {
splitCells := [][]Cell{}
temp := []Cell{}
for _, cell := range cells {
if cell.Rune == r {
splitCells = append(splitCells, temp)
temp = []Cell{}
} else {
temp = append(temp, cell)
}
}
if len(temp) > 0 {
splitCells = append(splitCells, temp)
}
return splitCells
}
type CellWithX struct {
X int
Cell Cell
}
func BuildCellWithXArray(cells []Cell) []CellWithX {
cellWithXArray := make([]CellWithX, len(cells))
index := 0
for i, cell := range cells {
cellWithXArray[i] = CellWithX{X: index, Cell: cell}
index += rw.RuneWidth(cell.Rune)
}
return cellWithXArray
}
================================================
FILE: widgets/barchart.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package widgets
import (
"fmt"
"image"
rw "github.com/mattn/go-runewidth"
. "github.com/gizak/termui/v3"
)
type BarChart struct {
Block
BarColors []Color
LabelStyles []Style
NumStyles []Style // only Fg and Modifier are used
NumFormatter 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,
NumFormatter: 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 {
if data > 0 {
// 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.NumFormatter(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)
}
}
================================================
FILE: widgets/gauge.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package widgets
import (
"fmt"
"image"
. "github.com/gizak/termui/v3"
)
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))
}
}
}
================================================
FILE: widgets/image.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package widgets
import (
"image"
"image/color"
. "github.com/gizak/termui/v3"
)
type Image struct {
Block
Image image.Image
Monochrome bool
MonochromeThreshold uint8
MonochromeInvert bool
}
func NewImage(img image.Image) *Image {
return &Image{
Block: *NewBlock(),
MonochromeThreshold: 128,
Image: img,
}
}
func (self *Image) Draw(buf *Buffer) {
self.Block.Draw(buf)
if self.Image == nil {
return
}
bufWidth := self.Inner.Dx()
bufHeight := self.Inner.Dy()
imageWidth := self.Image.Bounds().Dx()
imageHeight := self.Image.Bounds().Dy()
if self.Monochrome {
if bufWidth > imageWidth/2 {
bufWidth = imageWidth / 2
}
if bufHeight > imageHeight/2 {
bufHeight = imageHeight / 2
}
for bx := 0; bx < bufWidth; bx++ {
for by := 0; by < bufHeight; by++ {
ul := self.colorAverage(
2*bx*imageWidth/bufWidth/2,
(2*bx+1)*imageWidth/bufWidth/2,
2*by*imageHeight/bufHeight/2,
(2*by+1)*imageHeight/bufHeight/2,
)
ur := self.colorAverage(
(2*bx+1)*imageWidth/bufWidth/2,
(2*bx+2)*imageWidth/bufWidth/2,
2*by*imageHeight/bufHeight/2,
(2*by+1)*imageHeight/bufHeight/2,
)
ll := self.colorAverage(
2*bx*imageWidth/bufWidth/2,
(2*bx+1)*imageWidth/bufWidth/2,
(2*by+1)*imageHeight/bufHeight/2,
(2*by+2)*imageHeight/bufHeight/2,
)
lr := self.colorAverage(
(2*bx+1)*imageWidth/bufWidth/2,
(2*bx+2)*imageWidth/bufWidth/2,
(2*by+1)*imageHeight/bufHeight/2,
(2*by+2)*imageHeight/bufHeight/2,
)
buf.SetCell(
NewCell(blocksChar(ul, ur, ll, lr, self.MonochromeThreshold, self.MonochromeInvert)),
image.Pt(self.Inner.Min.X+bx, self.Inner.Min.Y+by),
)
}
}
} else {
if bufWidth > imageWidth {
bufWidth = imageWidth
}
if bufHeight > imageHeight {
bufHeight = imageHeight
}
for bx := 0; bx < bufWidth; bx++ {
for by := 0; by < bufHeight; by++ {
c := self.colorAverage(
bx*imageWidth/bufWidth,
(bx+1)*imageWidth/bufWidth,
by*imageHeight/bufHeight,
(by+1)*imageHeight/bufHeight,
)
buf.SetCell(
NewCell(c.ch(), NewStyle(c.fgColor(), ColorBlack)),
image.Pt(self.Inner.Min.X+bx, self.Inner.Min.Y+by),
)
}
}
}
}
func (self *Image) colorAverage(x0, x1, y0, y1 int) colorAverager {
var c colorAverager
for x := x0; x < x1; x++ {
for y := y0; y < y1; y++ {
c = c.add(
self.Image.At(
x+self.Image.Bounds().Min.X,
y+self.Image.Bounds().Min.Y,
),
)
}
}
return c
}
type colorAverager struct {
rsum, gsum, bsum, asum, count uint64
}
func (self colorAverager) add(col color.Color) colorAverager {
r, g, b, a := col.RGBA()
return colorAverager{
rsum: self.rsum + uint64(r),
gsum: self.gsum + uint64(g),
bsum: self.bsum + uint64(b),
asum: self.asum + uint64(a),
count: self.count + 1,
}
}
func (self colorAverager) RGBA() (uint32, uint32, uint32, uint32) {
if self.count == 0 {
return 0, 0, 0, 0
}
return uint32(self.rsum/self.count) & 0xffff,
uint32(self.gsum/self.count) & 0xffff,
uint32(self.bsum/self.count) & 0xffff,
uint32(self.asum/self.count) & 0xffff
}
func (self colorAverager) fgColor() Color {
return palette.Convert(self).(paletteColor).attribute
}
func (self colorAverager) ch() rune {
gray := color.GrayModel.Convert(self).(color.Gray).Y
switch {
case gray < 51:
return SHADED_BLOCKS[0]
case gray < 102:
return SHADED_BLOCKS[1]
case gray < 153:
return SHADED_BLOCKS[2]
case gray < 204:
return SHADED_BLOCKS[3]
default:
return SHADED_BLOCKS[4]
}
}
func (self colorAverager) monochrome(threshold uint8, invert bool) bool {
return self.count != 0 && (color.GrayModel.Convert(self).(color.Gray).Y < threshold != invert)
}
type paletteColor struct {
rgba color.RGBA
attribute Color
}
func (self paletteColor) RGBA() (uint32, uint32, uint32, uint32) {
return self.rgba.RGBA()
}
var palette = color.Palette([]color.Color{
paletteColor{color.RGBA{0, 0, 0, 255}, ColorBlack},
paletteColor{color.RGBA{255, 0, 0, 255}, ColorRed},
paletteColor{color.RGBA{0, 255, 0, 255}, ColorGreen},
paletteColor{color.RGBA{255, 255, 0, 255}, ColorYellow},
paletteColor{color.RGBA{0, 0, 255, 255}, ColorBlue},
paletteColor{color.RGBA{255, 0, 255, 255}, ColorMagenta},
paletteColor{color.RGBA{0, 255, 255, 255}, ColorCyan},
paletteColor{color.RGBA{255, 255, 255, 255}, ColorWhite},
})
func blocksChar(ul, ur, ll, lr colorAverager, threshold uint8, invert bool) rune {
index := 0
if ul.monochrome(threshold, invert) {
index |= 1
}
if ur.monochrome(threshold, invert) {
index |= 2
}
if ll.monochrome(threshold, invert) {
index |= 4
}
if lr.monochrome(threshold, invert) {
index |= 8
}
return IRREGULAR_BLOCKS[index]
}
================================================
FILE: widgets/list.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package widgets
import (
"image"
rw "github.com/mattn/go-runewidth"
. "github.com/gizak/termui/v3"
)
type List struct {
Block
Rows []string
WrapText bool
TextStyle Style
SelectedRow int
topRow int
SelectedRowStyle Style
}
func NewList() *List {
return &List{
Block: *NewBlock(),
TextStyle: Theme.List.Text,
SelectedRowStyle: Theme.List.Text,
}
}
func (self *List) Draw(buf *Buffer) {
self.Block.Draw(buf)
point := self.Inner.Min
// adjusts view into widget
if self.SelectedRow >= self.Inner.Dy()+self.topRow {
self.topRow = self.SelectedRow - self.Inner.Dy() + 1
} else if self.SelectedRow < self.topRow {
self.topRow = self.SelectedRow
}
// draw rows
for row := self.topRow; row < len(self.Rows) && point.Y < self.Inner.Max.Y; row++ {
cells := ParseStyles(self.Rows[row], self.TextStyle)
if self.WrapText {
cells = WrapCells(cells, uint(self.Inner.Dx()))
}
for j := 0; j < len(cells) && point.Y < self.Inner.Max.Y; j++ {
style := cells[j].Style
if row == self.SelectedRow {
style = self.SelectedRowStyle
}
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(ELLIPSES, style), point.Add(image.Pt(-1, 0)))
break
} else {
buf.SetCell(NewCell(cells[j].Rune, style), point)
point = point.Add(image.Pt(rw.RuneWidth(cells[j].Rune), 0))
}
}
}
point = image.Pt(self.Inner.Min.X, point.Y+1)
}
// draw UP_ARROW if needed
if self.topRow > 0 {
buf.SetCell(
NewCell(UP_ARROW, NewStyle(ColorWhite)),
image.Pt(self.Inner.Max.X-1, self.Inner.Min.Y),
)
}
// draw DOWN_ARROW if needed
if len(self.Rows) > int(self.topRow)+self.Inner.Dy() {
buf.SetCell(
NewCell(DOWN_ARROW, NewStyle(ColorWhite)),
image.Pt(self.Inner.Max.X-1, self.Inner.Max.Y-1),
)
}
}
// ScrollAmount scrolls by amount given. If amount is < 0, then scroll up.
// There is no need to set self.topRow, as this will be set automatically when drawn,
// since if the selected item is off screen then the topRow variable will change accordingly.
func (self *List) ScrollAmount(amount int) {
if len(self.Rows)-int(self.SelectedRow) <= amount {
self.SelectedRow = len(self.Rows) - 1
} else if int(self.SelectedRow)+amount < 0 {
self.SelectedRow = 0
} else {
self.SelectedRow += amount
}
}
func (self *List) ScrollUp() {
self.ScrollAmount(-1)
}
func (self *List) ScrollDown() {
self.ScrollAmount(1)
}
func (self *List) ScrollPageUp() {
// If an item is selected below top row, then go to the top row.
if self.SelectedRow > self.topRow {
self.SelectedRow = self.topRow
} else {
self.ScrollAmount(-self.Inner.Dy())
}
}
func (self *List) ScrollPageDown() {
self.ScrollAmount(self.Inner.Dy())
}
func (self *List) ScrollHalfPageUp() {
self.ScrollAmount(-int(FloorFloat64(float64(self.Inner.Dy()) / 2)))
}
func (self *List) ScrollHalfPageDown() {
self.ScrollAmount(int(FloorFloat64(float64(self.Inner.Dy()) / 2)))
}
func (self *List) ScrollTop() {
self.SelectedRow = 0
}
func (self *List) ScrollBottom() {
self.SelectedRow = len(self.Rows) - 1
}
================================================
FILE: widgets/paragraph.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package widgets
import (
"image"
. "github.com/gizak/termui/v3"
)
type Paragraph struct {
Block
Text string
TextStyle Style
WrapText bool
}
func NewParagraph() *Paragraph {
return &Paragraph{
Block: *NewBlock(),
TextStyle: Theme.Paragraph.Text,
WrapText: true,
}
}
func (self *Paragraph) Draw(buf *Buffer) {
self.Block.Draw(buf)
cells := ParseStyles(self.Text, self.TextStyle)
if self.WrapText {
cells = WrapCells(cells, uint(self.Inner.Dx()))
}
rows := SplitCells(cells, '\n')
for y, row := range rows {
if y+self.Inner.Min.Y >= self.Inner.Max.Y {
break
}
row = TrimCells(row, self.Inner.Dx())
for _, cx := range BuildCellWithXArray(row) {
x, cell := cx.X, cx.Cell
buf.SetCell(cell, image.Pt(x, y).Add(self.Inner.Min))
}
}
}
================================================
FILE: widgets/piechart.go
================================================
package widgets
import (
"image"
"math"
. "github.com/gizak/termui/v3"
)
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
LabelFormatter PieChartLabel // callback function for labels
AngleOffset 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,
AngleOffset: 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.AngleOffset
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_BLOCKS[1], NewStyle(SelectColor(self.Colors, i))), buf)
}
phi += size
}
// draw labels
if self.LabelFormatter != nil {
phi = self.AngleOffset
for i, size := range sliceSizes {
labelPoint := middleCircle.at(phi + size/2.0)
if len(self.Data) == 1 {
labelPoint = center
}
buf.SetString(
self.LabelFormatter(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)
}
================================================
FILE: widgets/plot.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package widgets
import (
"fmt"
"image"
. "github.com/gizak/termui/v3"
)
// Plot has two modes: line(default) and scatter.
// Plot also has two marker types: 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 Plot struct {
Block
Data [][]float64
DataLabels []string
MaxVal float64
LineColors []Color
AxesColor Color // TODO
ShowAxes bool
Marker PlotMarker
DotMarkerRune rune
PlotType PlotType
HorizontalScale int
DrawDirection DrawDirection // TODO
}
const (
xAxisLabelsHeight = 1
yAxisLabelsWidth = 4
xAxisLabelsGap = 2
yAxisLabelsGap = 1
)
type PlotType uint
const (
LineChart PlotType = iota
ScatterPlot
)
type PlotMarker uint
const (
MarkerBraille PlotMarker = iota
MarkerDot
)
type DrawDirection uint
const (
DrawLeft DrawDirection = iota
DrawRight
)
func NewPlot() *Plot {
return &Plot{
Block: *NewBlock(),
LineColors: Theme.Plot.Lines,
AxesColor: Theme.Plot.Axes,
Marker: MarkerBraille,
DotMarkerRune: DOT,
Data: [][]float64{},
HorizontalScale: 1,
DrawDirection: DrawRight,
ShowAxes: true,
PlotType: LineChart,
}
}
func (self *Plot) renderBraille(buf *Buffer, drawArea image.Rectangle, maxVal float64) {
canvas := NewCanvas()
canvas.Rectangle = drawArea
switch self.PlotType {
case ScatterPlot:
for i, line := range self.Data {
for j, val := range line {
height := int((val / maxVal) * float64(drawArea.Dy()-1))
canvas.SetPoint(
image.Pt(
(drawArea.Min.X+(j*self.HorizontalScale))*2,
(drawArea.Max.Y-height-1)*4,
),
SelectColor(self.LineColors, i),
)
}
}
case LineChart:
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.SetLine(
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 *Plot) renderDot(buf *Buffer, drawArea image.Rectangle, maxVal float64) {
switch self.PlotType {
case ScatterPlot:
for i, line := range self.Data {
for j, val := range line {
height := int((val / maxVal) * float64(drawArea.Dy()-1))
point := image.Pt(drawArea.Min.X+(j*self.HorizontalScale), drawArea.Max.Y-1-height)
if point.In(drawArea) {
buf.SetCell(
NewCell(self.DotMarkerRune, NewStyle(SelectColor(self.LineColors, i))),
point,
)
}
}
}
case LineChart:
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.DotMarkerRune, NewStyle(SelectColor(self.LineColors, i))),
image.Pt(drawArea.Min.X+(j*self.HorizontalScale), drawArea.Max.Y-1-height),
)
}
}
}
}
func (self *Plot) 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 *Plot) 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,
)
}
switch self.Marker {
case MarkerBraille:
self.renderBraille(buf, drawArea, maxVal)
case MarkerDot:
self.renderDot(buf, drawArea, maxVal)
}
}
================================================
FILE: widgets/sparkline.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package widgets
import (
"image"
. "github.com/gizak/termui/v3"
)
// 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
}
// 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))
if height > sl.MaxHeight {
height = sl.MaxHeight
}
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[1]
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),
)
}
}
}
================================================
FILE: widgets/stacked_barchart.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package widgets
import (
"fmt"
"image"
rw "github.com/mattn/go-runewidth"
. "github.com/gizak/termui/v3"
)
type StackedBarChart struct {
Block
BarColors []Color
LabelStyles []Style
NumStyles []Style // only Fg and Modifier are used
NumFormatter 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,
NumFormatter: 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.NumFormatter(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
if i < len(self.Labels) {
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)
}
}
================================================
FILE: widgets/table.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package widgets
import (
"image"
. "github.com/gizak/termui/v3"
)
/*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
TextAlignment Alignment
RowStyles map[int]Style
FillRow bool
// ColumnResizer is called on each Draw. Can be used for custom column sizing.
ColumnResizer func()
}
func NewTable() *Table {
return &Table{
Block: *NewBlock(),
TextStyle: Theme.Table.Text,
RowSeparator: true,
RowStyles: make(map[int]Style),
ColumnResizer: func() {},
}
}
func (self *Table) Draw(buf *Buffer) {
self.Block.Draw(buf)
self.ColumnResizer()
columnWidths := self.ColumnWidths
if len(columnWidths) == 0 {
columnCount := len(self.Rows[0])
columnWidth := self.Inner.Dx() / columnCount
for i := 0; i < columnCount; i++ {
columnWidths = append(columnWidths, columnWidth)
}
}
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
rowStyle := self.TextStyle
// get the row style if one exists
if style, ok := self.RowStyles[i]; ok {
rowStyle = style
}
if self.FillRow {
blankCell := NewCell(' ', rowStyle)
buf.Fill(blankCell, image.Rect(self.Inner.Min.X, yCoordinate, self.Inner.Max.X, yCoordinate+1))
}
// draw row cells
for j := 0; j < len(row); j++ {
col := ParseStyles(row[j], rowStyle)
// draw row cell
if len(col) > columnWidths[j] || self.TextAlignment == AlignLeft {
for _, cx := range BuildCellWithXArray(col) {
k, cell := cx.X, cx.Cell
if k == columnWidths[j] || colXCoordinate+k == self.Inner.Max.X {
cell.Rune = ELLIPSES
buf.SetCell(cell, image.Pt(colXCoordinate+k-1, yCoordinate))
break
} else {
buf.SetCell(cell, image.Pt(colXCoordinate+k, yCoordinate))
}
}
} else if self.TextAlignment == AlignCenter {
xCoordinateOffset := (columnWidths[j] - len(col)) / 2
stringXCoordinate := xCoordinateOffset + colXCoordinate
for _, cx := range BuildCellWithXArray(col) {
k, cell := cx.X, cx.Cell
buf.SetCell(cell, image.Pt(stringXCoordinate+k, yCoordinate))
}
} else if self.TextAlignment == AlignRight {
stringXCoordinate := MinInt(colXCoordinate+columnWidths[j], self.Inner.Max.X) - len(col)
for _, cx := range BuildCellWithXArray(col) {
k, cell := cx.X, cx.Cell
buf.SetCell(cell, image.Pt(stringXCoordinate+k, yCoordinate))
}
}
colXCoordinate += columnWidths[j] + 1
}
// draw vertical separators
separatorStyle := self.Block.BorderStyle
separatorXCoordinate := self.Inner.Min.X
verticalCell := NewCell(VERTICAL_LINE, separatorStyle)
for i, width := range columnWidths {
if self.FillRow && i < len(columnWidths)-1 {
verticalCell.Style.Bg = rowStyle.Bg
} else {
verticalCell.Style.Bg = self.Block.BorderStyle.Bg
}
separatorXCoordinate += width
buf.SetCell(verticalCell, image.Pt(separatorXCoordinate, yCoordinate))
separatorXCoordinate++
}
yCoordinate++
// draw horizontal separator
horizontalCell := NewCell(HORIZONTAL_LINE, separatorStyle)
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++
}
}
}
================================================
FILE: widgets/tabs.go
================================================
// Copyright 2017 Zack Guo . All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package widgets
import (
"image"
. "github.com/gizak/termui/v3"
)
// 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 int
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 < 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 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
}
}
================================================
FILE: widgets/tree.go
================================================
package widgets
import (
"fmt"
"image"
"strings"
. "github.com/gizak/termui/v3"
rw "github.com/mattn/go-runewidth"
)
const treeIndent = " "
// TreeNode is a tree node.
type TreeNode struct {
Value fmt.Stringer
Expanded bool
Nodes []*TreeNode
// level stores the node level in the tree.
level int
}
// TreeWalkFn is a function used for walking a Tree.
// To interrupt the walking process function should return false.
type TreeWalkFn func(*TreeNode) bool
func (self *TreeNode) parseStyles(style Style) []Cell {
var sb strings.Builder
if len(self.Nodes) == 0 {
sb.WriteString(strings.Repeat(treeIndent, self.level+1))
} else {
sb.WriteString(strings.Repeat(treeIndent, self.level))
if self.Expanded {
sb.WriteRune(Theme.Tree.Expanded)
} else {
sb.WriteRune(Theme.Tree.Collapsed)
}
sb.WriteByte(' ')
}
sb.WriteString(self.Value.String())
return ParseStyles(sb.String(), style)
}
// Tree is a tree widget.
type Tree struct {
Block
TextStyle Style
SelectedRowStyle Style
WrapText bool
SelectedRow int
nodes []*TreeNode
// rows is flatten nodes for rendering.
rows []*TreeNode
topRow int
}
// NewTree creates a new Tree widget.
func NewTree() *Tree {
return &Tree{
Block: *NewBlock(),
TextStyle: Theme.Tree.Text,
SelectedRowStyle: Theme.Tree.Text,
WrapText: true,
}
}
func (self *Tree) SetNodes(nodes []*TreeNode) {
self.nodes = nodes
self.prepareNodes()
}
func (self *Tree) prepareNodes() {
self.rows = make([]*TreeNode, 0)
for _, node := range self.nodes {
self.prepareNode(node, 0)
}
}
func (self *Tree) prepareNode(node *TreeNode, level int) {
self.rows = append(self.rows, node)
node.level = level
if node.Expanded {
for _, n := range node.Nodes {
self.prepareNode(n, level+1)
}
}
}
func (self *Tree) Walk(fn TreeWalkFn) {
for _, n := range self.nodes {
if !self.walk(n, fn) {
break
}
}
}
func (self *Tree) walk(n *TreeNode, fn TreeWalkFn) bool {
if !fn(n) {
return false
}
for _, node := range n.Nodes {
if !self.walk(node, fn) {
return false
}
}
return true
}
func (self *Tree) Draw(buf *Buffer) {
self.Block.Draw(buf)
point := self.Inner.Min
// adjusts view into widget
if self.SelectedRow >= self.Inner.Dy()+self.topRow {
self.topRow = self.SelectedRow - self.Inner.Dy() + 1
} else if self.SelectedRow < self.topRow {
self.topRow = self.SelectedRow
}
// draw rows
for row := self.topRow; row < len(self.rows) && point.Y < self.Inner.Max.Y; row++ {
cells := self.rows[row].parseStyles(self.TextStyle)
if self.WrapText {
cells = WrapCells(cells, uint(self.Inner.Dx()))
}
for j := 0; j < len(cells) && point.Y < self.Inner.Max.Y; j++ {
style := cells[j].Style
if row == self.SelectedRow {
style = self.SelectedRowStyle
}
if point.X+1 == self.Inner.Max.X+1 && len(cells) > self.Inner.Dx() {
buf.SetCell(NewCell(ELLIPSES, style), point.Add(image.Pt(-1, 0)))
} else {
buf.SetCell(NewCell(cells[j].Rune, style), point)
point = point.Add(image.Pt(rw.RuneWidth(cells[j].Rune), 0))
}
}
point = image.Pt(self.Inner.Min.X, point.Y+1)
}
// draw UP_ARROW if needed
if self.topRow > 0 {
buf.SetCell(
NewCell(UP_ARROW, NewStyle(ColorWhite)),
image.Pt(self.Inner.Max.X-1, self.Inner.Min.Y),
)
}
// draw DOWN_ARROW if needed
if len(self.rows) > int(self.topRow)+self.Inner.Dy() {
buf.SetCell(
NewCell(DOWN_ARROW, NewStyle(ColorWhite)),
image.Pt(self.Inner.Max.X-1, self.Inner.Max.Y-1),
)
}
}
// ScrollAmount scrolls by amount given. If amount is < 0, then scroll up.
// There is no need to set self.topRow, as this will be set automatically when drawn,
// since if the selected item is off screen then the topRow variable will change accordingly.
func (self *Tree) ScrollAmount(amount int) {
if len(self.rows)-int(self.SelectedRow) <= amount {
self.SelectedRow = len(self.rows) - 1
} else if int(self.SelectedRow)+amount < 0 {
self.SelectedRow = 0
} else {
self.SelectedRow += amount
}
}
func (self *Tree) SelectedNode() *TreeNode {
if len(self.rows) == 0 {
return nil
}
return self.rows[self.SelectedRow]
}
func (self *Tree) ScrollUp() {
self.ScrollAmount(-1)
}
func (self *Tree) ScrollDown() {
self.ScrollAmount(1)
}
func (self *Tree) ScrollPageUp() {
// If an item is selected below top row, then go to the top row.
if self.SelectedRow > self.topRow {
self.SelectedRow = self.topRow
} else {
self.ScrollAmount(-self.Inner.Dy())
}
}
func (self *Tree) ScrollPageDown() {
self.ScrollAmount(self.Inner.Dy())
}
func (self *Tree) ScrollHalfPageUp() {
self.ScrollAmount(-int(FloorFloat64(float64(self.Inner.Dy()) / 2)))
}
func (self *Tree) ScrollHalfPageDown() {
self.ScrollAmount(int(FloorFloat64(float64(self.Inner.Dy()) / 2)))
}
func (self *Tree) ScrollTop() {
self.SelectedRow = 0
}
func (self *Tree) ScrollBottom() {
self.SelectedRow = len(self.rows) - 1
}
func (self *Tree) Collapse() {
self.rows[self.SelectedRow].Expanded = false
self.prepareNodes()
}
func (self *Tree) Expand() {
node := self.rows[self.SelectedRow]
if len(node.Nodes) > 0 {
self.rows[self.SelectedRow].Expanded = true
}
self.prepareNodes()
}
func (self *Tree) ToggleExpand() {
node := self.rows[self.SelectedRow]
if len(node.Nodes) > 0 {
node.Expanded = !node.Expanded
}
self.prepareNodes()
}
func (self *Tree) ExpandAll() {
self.Walk(func(n *TreeNode) bool {
if len(n.Nodes) > 0 {
n.Expanded = true
}
return true
})
self.prepareNodes()
}
func (self *Tree) CollapseAll() {
self.Walk(func(n *TreeNode) bool {
n.Expanded = false
return true
})
self.prepareNodes()
}