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 [demo cast under osx 10.10; Terminal.app; Menlo Regular 12pt.)](./_examples/demo.go) termui is a cross-platform and fully-customizable terminal dashboard and widget library built on top of [termbox-go](https://github.com/nsf/termbox-go). It is inspired by [blessed-contrib](https://github.com/yaronn/blessed-contrib) and [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() }