summaryrefslogtreecommitdiff
path: root/examples/go-dashboard/src/github.com
diff options
context:
space:
mode:
Diffstat (limited to 'examples/go-dashboard/src/github.com')
-rw-r--r--examples/go-dashboard/src/github.com/mattn/go-runewidth/.travis.yml16
-rw-r--r--examples/go-dashboard/src/github.com/mattn/go-runewidth/LICENSE21
-rw-r--r--examples/go-dashboard/src/github.com/mattn/go-runewidth/README.md27
-rw-r--r--examples/go-dashboard/src/github.com/mattn/go-runewidth/go.mod3
-rw-r--r--examples/go-dashboard/src/github.com/mattn/go-runewidth/go.test.sh12
-rw-r--r--examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth.go257
-rw-r--r--examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth_appengine.go8
-rw-r--r--examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth_js.go9
-rw-r--r--examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth_posix.go82
-rw-r--r--examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth_table.go437
-rw-r--r--examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth_windows.go28
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/.gitignore2
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/.travis.yml17
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/CHANGELOG.md361
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/CONTRIBUTING.md38
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/LICENSE201
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/README.md215
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/align/align.go70
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/cell/cell.go64
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/cell/color.go106
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/container/container.go471
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/container/draw.go175
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/container/focus.go116
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/container/options.go817
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/container/traversal.go86
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/go.mod10
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/keyboard/keyboard.go172
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/linestyle/linestyle.go51
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/mouse/mouse.go48
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/alignfor/alignfor.go128
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/area/area.go258
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/button/button.go135
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/canvas/braille/braille.go284
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/canvas/buffer/buffer.go188
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/canvas/canvas.go247
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/border.go182
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/braille_circle.go263
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/braille_fill.go160
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/braille_line.go204
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/draw.go17
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/hv_line.go207
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/hv_line_graph.go206
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/line_style.go129
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/rectangle.go93
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/text.go195
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/vertical_text.go120
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/event/event.go260
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/event/eventqueue/eventqueue.go231
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/numbers/numbers.go222
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/numbers/trig/trig.go224
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/runewidth/runewidth.go98
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/private/wrap/wrap.go409
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/termdash.go362
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/terminal/termbox/cell_options.go37
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/terminal/termbox/color_mode.go38
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/terminal/termbox/event.go179
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/terminal/termbox/termbox.go164
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/terminal/terminalapi/color_mode.go60
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/terminal/terminalapi/event.go106
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/terminal/terminalapi/terminalapi.go56
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/widgetapi/widgetapi.go185
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/widgets/text/line_trim.go117
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/widgets/text/options.go156
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/widgets/text/scroll.go165
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/widgets/text/text.go286
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/widgets/text/write_options.go67
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/AUTHORS4
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/LICENSE19
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/README.md51
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/api.go500
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/api_common.go187
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/api_windows.go257
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/collect_terminfo.py110
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/escwait.go11
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/escwait_darwin.go9
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_darwin.go41
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_darwin_amd64.go40
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_dragonfly.go39
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_freebsd.go39
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_linux.go33
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_netbsd.go39
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_openbsd.go39
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_windows.go61
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/termbox.go529
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/termbox_common.go59
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/termbox_windows.go952
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/terminfo.go232
-rw-r--r--examples/go-dashboard/src/github.com/nsf/termbox-go/terminfo_builtin.go64
88 files changed, 13673 insertions, 0 deletions
diff --git a/examples/go-dashboard/src/github.com/mattn/go-runewidth/.travis.yml b/examples/go-dashboard/src/github.com/mattn/go-runewidth/.travis.yml
new file mode 100644
index 000000000..6a21813a3
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mattn/go-runewidth/.travis.yml
@@ -0,0 +1,16 @@
+language: go
+sudo: false
+go:
+ - 1.13.x
+ - tip
+
+before_install:
+ - go get -t -v ./...
+
+script:
+ - go generate
+ - git diff --cached --exit-code
+ - ./go.test.sh
+
+after_success:
+ - bash <(curl -s https://codecov.io/bash)
diff --git a/examples/go-dashboard/src/github.com/mattn/go-runewidth/LICENSE b/examples/go-dashboard/src/github.com/mattn/go-runewidth/LICENSE
new file mode 100644
index 000000000..91b5cef30
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mattn/go-runewidth/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Yasuhiro Matsumoto
+
+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.
diff --git a/examples/go-dashboard/src/github.com/mattn/go-runewidth/README.md b/examples/go-dashboard/src/github.com/mattn/go-runewidth/README.md
new file mode 100644
index 000000000..aa56ab96c
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mattn/go-runewidth/README.md
@@ -0,0 +1,27 @@
+go-runewidth
+============
+
+[![Build Status](https://travis-ci.org/mattn/go-runewidth.png?branch=master)](https://travis-ci.org/mattn/go-runewidth)
+[![Codecov](https://codecov.io/gh/mattn/go-runewidth/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-runewidth)
+[![GoDoc](https://godoc.org/github.com/mattn/go-runewidth?status.svg)](http://godoc.org/github.com/mattn/go-runewidth)
+[![Go Report Card](https://goreportcard.com/badge/github.com/mattn/go-runewidth)](https://goreportcard.com/report/github.com/mattn/go-runewidth)
+
+Provides functions to get fixed width of the character or string.
+
+Usage
+-----
+
+```go
+runewidth.StringWidth("つのだ☆HIRO") == 12
+```
+
+
+Author
+------
+
+Yasuhiro Matsumoto
+
+License
+-------
+
+under the MIT License: http://mattn.mit-license.org/2013
diff --git a/examples/go-dashboard/src/github.com/mattn/go-runewidth/go.mod b/examples/go-dashboard/src/github.com/mattn/go-runewidth/go.mod
new file mode 100644
index 000000000..fa7f4d864
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mattn/go-runewidth/go.mod
@@ -0,0 +1,3 @@
+module github.com/mattn/go-runewidth
+
+go 1.9
diff --git a/examples/go-dashboard/src/github.com/mattn/go-runewidth/go.test.sh b/examples/go-dashboard/src/github.com/mattn/go-runewidth/go.test.sh
new file mode 100644
index 000000000..012162b07
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mattn/go-runewidth/go.test.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+set -e
+echo "" > coverage.txt
+
+for d in $(go list ./... | grep -v vendor); do
+ go test -race -coverprofile=profile.out -covermode=atomic "$d"
+ if [ -f profile.out ]; then
+ cat profile.out >> coverage.txt
+ rm profile.out
+ fi
+done
diff --git a/examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth.go b/examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth.go
new file mode 100644
index 000000000..19f8e0449
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth.go
@@ -0,0 +1,257 @@
+package runewidth
+
+import (
+ "os"
+)
+
+//go:generate go run script/generate.go
+
+var (
+ // EastAsianWidth will be set true if the current locale is CJK
+ EastAsianWidth bool
+
+ // ZeroWidthJoiner is flag to set to use UTR#51 ZWJ
+ ZeroWidthJoiner bool
+
+ // DefaultCondition is a condition in current locale
+ DefaultCondition = &Condition{}
+)
+
+func init() {
+ handleEnv()
+}
+
+func handleEnv() {
+ env := os.Getenv("RUNEWIDTH_EASTASIAN")
+ if env == "" {
+ EastAsianWidth = IsEastAsian()
+ } else {
+ EastAsianWidth = env == "1"
+ }
+ // update DefaultCondition
+ DefaultCondition.EastAsianWidth = EastAsianWidth
+ DefaultCondition.ZeroWidthJoiner = ZeroWidthJoiner
+}
+
+type interval struct {
+ first rune
+ last rune
+}
+
+type table []interval
+
+func inTables(r rune, ts ...table) bool {
+ for _, t := range ts {
+ if inTable(r, t) {
+ return true
+ }
+ }
+ return false
+}
+
+func inTable(r rune, t table) bool {
+ if r < t[0].first {
+ return false
+ }
+
+ bot := 0
+ top := len(t) - 1
+ for top >= bot {
+ mid := (bot + top) >> 1
+
+ switch {
+ case t[mid].last < r:
+ bot = mid + 1
+ case t[mid].first > r:
+ top = mid - 1
+ default:
+ return true
+ }
+ }
+
+ return false
+}
+
+var private = table{
+ {0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD},
+}
+
+var nonprint = table{
+ {0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD},
+ {0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F},
+ {0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF},
+ {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF},
+}
+
+// Condition have flag EastAsianWidth whether the current locale is CJK or not.
+type Condition struct {
+ EastAsianWidth bool
+ ZeroWidthJoiner bool
+}
+
+// NewCondition return new instance of Condition which is current locale.
+func NewCondition() *Condition {
+ return &Condition{
+ EastAsianWidth: EastAsianWidth,
+ ZeroWidthJoiner: ZeroWidthJoiner,
+ }
+}
+
+// RuneWidth returns the number of cells in r.
+// See http://www.unicode.org/reports/tr11/
+func (c *Condition) RuneWidth(r rune) int {
+ switch {
+ case r < 0 || r > 0x10FFFF || inTables(r, nonprint, combining, notassigned):
+ return 0
+ case (c.EastAsianWidth && IsAmbiguousWidth(r)) || inTables(r, doublewidth):
+ return 2
+ default:
+ return 1
+ }
+}
+
+func (c *Condition) stringWidth(s string) (width int) {
+ for _, r := range []rune(s) {
+ width += c.RuneWidth(r)
+ }
+ return width
+}
+
+func (c *Condition) stringWidthZeroJoiner(s string) (width int) {
+ r1, r2 := rune(0), rune(0)
+ for _, r := range []rune(s) {
+ if r == 0xFE0E || r == 0xFE0F {
+ continue
+ }
+ w := c.RuneWidth(r)
+ if r2 == 0x200D && inTables(r, emoji) && inTables(r1, emoji) {
+ if width < w {
+ width = w
+ }
+ } else {
+ width += w
+ }
+ r1, r2 = r2, r
+ }
+ return width
+}
+
+// StringWidth return width as you can see
+func (c *Condition) StringWidth(s string) (width int) {
+ if c.ZeroWidthJoiner {
+ return c.stringWidthZeroJoiner(s)
+ }
+ return c.stringWidth(s)
+}
+
+// Truncate return string truncated with w cells
+func (c *Condition) Truncate(s string, w int, tail string) string {
+ if c.StringWidth(s) <= w {
+ return s
+ }
+ r := []rune(s)
+ tw := c.StringWidth(tail)
+ w -= tw
+ width := 0
+ i := 0
+ for ; i < len(r); i++ {
+ cw := c.RuneWidth(r[i])
+ if width+cw > w {
+ break
+ }
+ width += cw
+ }
+ return string(r[0:i]) + tail
+}
+
+// Wrap return string wrapped with w cells
+func (c *Condition) Wrap(s string, w int) string {
+ width := 0
+ out := ""
+ for _, r := range []rune(s) {
+ cw := RuneWidth(r)
+ if r == '\n' {
+ out += string(r)
+ width = 0
+ continue
+ } else if width+cw > w {
+ out += "\n"
+ width = 0
+ out += string(r)
+ width += cw
+ continue
+ }
+ out += string(r)
+ width += cw
+ }
+ return out
+}
+
+// FillLeft return string filled in left by spaces in w cells
+func (c *Condition) FillLeft(s string, w int) string {
+ width := c.StringWidth(s)
+ count := w - width
+ if count > 0 {
+ b := make([]byte, count)
+ for i := range b {
+ b[i] = ' '
+ }
+ return string(b) + s
+ }
+ return s
+}
+
+// FillRight return string filled in left by spaces in w cells
+func (c *Condition) FillRight(s string, w int) string {
+ width := c.StringWidth(s)
+ count := w - width
+ if count > 0 {
+ b := make([]byte, count)
+ for i := range b {
+ b[i] = ' '
+ }
+ return s + string(b)
+ }
+ return s
+}
+
+// RuneWidth returns the number of cells in r.
+// See http://www.unicode.org/reports/tr11/
+func RuneWidth(r rune) int {
+ return DefaultCondition.RuneWidth(r)
+}
+
+// IsAmbiguousWidth returns whether is ambiguous width or not.
+func IsAmbiguousWidth(r rune) bool {
+ return inTables(r, private, ambiguous)
+}
+
+// IsNeutralWidth returns whether is neutral width or not.
+func IsNeutralWidth(r rune) bool {
+ return inTable(r, neutral)
+}
+
+// StringWidth return width as you can see
+func StringWidth(s string) (width int) {
+ return DefaultCondition.StringWidth(s)
+}
+
+// Truncate return string truncated with w cells
+func Truncate(s string, w int, tail string) string {
+ return DefaultCondition.Truncate(s, w, tail)
+}
+
+// Wrap return string wrapped with w cells
+func Wrap(s string, w int) string {
+ return DefaultCondition.Wrap(s, w)
+}
+
+// FillLeft return string filled in left by spaces in w cells
+func FillLeft(s string, w int) string {
+ return DefaultCondition.FillLeft(s, w)
+}
+
+// FillRight return string filled in left by spaces in w cells
+func FillRight(s string, w int) string {
+ return DefaultCondition.FillRight(s, w)
+}
diff --git a/examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth_appengine.go b/examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth_appengine.go
new file mode 100644
index 000000000..7d99f6e52
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth_appengine.go
@@ -0,0 +1,8 @@
+// +build appengine
+
+package runewidth
+
+// IsEastAsian return true if the current locale is CJK
+func IsEastAsian() bool {
+ return false
+}
diff --git a/examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth_js.go b/examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth_js.go
new file mode 100644
index 000000000..c5fdf40ba
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth_js.go
@@ -0,0 +1,9 @@
+// +build js
+// +build !appengine
+
+package runewidth
+
+func IsEastAsian() bool {
+ // TODO: Implement this for the web. Detect east asian in a compatible way, and return true.
+ return false
+}
diff --git a/examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth_posix.go b/examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth_posix.go
new file mode 100644
index 000000000..480ad7485
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth_posix.go
@@ -0,0 +1,82 @@
+// +build !windows
+// +build !js
+// +build !appengine
+
+package runewidth
+
+import (
+ "os"
+ "regexp"
+ "strings"
+)
+
+var reLoc = regexp.MustCompile(`^[a-z][a-z][a-z]?(?:_[A-Z][A-Z])?\.(.+)`)
+
+var mblenTable = map[string]int{
+ "utf-8": 6,
+ "utf8": 6,
+ "jis": 8,
+ "eucjp": 3,
+ "euckr": 2,
+ "euccn": 2,
+ "sjis": 2,
+ "cp932": 2,
+ "cp51932": 2,
+ "cp936": 2,
+ "cp949": 2,
+ "cp950": 2,
+ "big5": 2,
+ "gbk": 2,
+ "gb2312": 2,
+}
+
+func isEastAsian(locale string) bool {
+ charset := strings.ToLower(locale)
+ r := reLoc.FindStringSubmatch(locale)
+ if len(r) == 2 {
+ charset = strings.ToLower(r[1])
+ }
+
+ if strings.HasSuffix(charset, "@cjk_narrow") {
+ return false
+ }
+
+ for pos, b := range []byte(charset) {
+ if b == '@' {
+ charset = charset[:pos]
+ break
+ }
+ }
+ max := 1
+ if m, ok := mblenTable[charset]; ok {
+ max = m
+ }
+ if max > 1 && (charset[0] != 'u' ||
+ strings.HasPrefix(locale, "ja") ||
+ strings.HasPrefix(locale, "ko") ||
+ strings.HasPrefix(locale, "zh")) {
+ return true
+ }
+ return false
+}
+
+// IsEastAsian return true if the current locale is CJK
+func IsEastAsian() bool {
+ locale := os.Getenv("LC_ALL")
+ if locale == "" {
+ locale = os.Getenv("LC_CTYPE")
+ }
+ if locale == "" {
+ locale = os.Getenv("LANG")
+ }
+
+ // ignore C locale
+ if locale == "POSIX" || locale == "C" {
+ return false
+ }
+ if len(locale) > 1 && locale[0] == 'C' && (locale[1] == '.' || locale[1] == '-') {
+ return false
+ }
+
+ return isEastAsian(locale)
+}
diff --git a/examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth_table.go b/examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth_table.go
new file mode 100644
index 000000000..b27d77d89
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth_table.go
@@ -0,0 +1,437 @@
+// Code generated by script/generate.go. DO NOT EDIT.
+
+package runewidth
+
+var combining = table{
+ {0x0300, 0x036F}, {0x0483, 0x0489}, {0x07EB, 0x07F3},
+ {0x0C00, 0x0C00}, {0x0C04, 0x0C04}, {0x0D00, 0x0D01},
+ {0x135D, 0x135F}, {0x1A7F, 0x1A7F}, {0x1AB0, 0x1AC0},
+ {0x1B6B, 0x1B73}, {0x1DC0, 0x1DF9}, {0x1DFB, 0x1DFF},
+ {0x20D0, 0x20F0}, {0x2CEF, 0x2CF1}, {0x2DE0, 0x2DFF},
+ {0x3099, 0x309A}, {0xA66F, 0xA672}, {0xA674, 0xA67D},
+ {0xA69E, 0xA69F}, {0xA6F0, 0xA6F1}, {0xA8E0, 0xA8F1},
+ {0xFE20, 0xFE2F}, {0x101FD, 0x101FD}, {0x10376, 0x1037A},
+ {0x10EAB, 0x10EAC}, {0x10F46, 0x10F50}, {0x11300, 0x11301},
+ {0x1133B, 0x1133C}, {0x11366, 0x1136C}, {0x11370, 0x11374},
+ {0x16AF0, 0x16AF4}, {0x1D165, 0x1D169}, {0x1D16D, 0x1D172},
+ {0x1D17B, 0x1D182}, {0x1D185, 0x1D18B}, {0x1D1AA, 0x1D1AD},
+ {0x1D242, 0x1D244}, {0x1E000, 0x1E006}, {0x1E008, 0x1E018},
+ {0x1E01B, 0x1E021}, {0x1E023, 0x1E024}, {0x1E026, 0x1E02A},
+ {0x1E8D0, 0x1E8D6},
+}
+
+var doublewidth = table{
+ {0x1100, 0x115F}, {0x231A, 0x231B}, {0x2329, 0x232A},
+ {0x23E9, 0x23EC}, {0x23F0, 0x23F0}, {0x23F3, 0x23F3},
+ {0x25FD, 0x25FE}, {0x2614, 0x2615}, {0x2648, 0x2653},
+ {0x267F, 0x267F}, {0x2693, 0x2693}, {0x26A1, 0x26A1},
+ {0x26AA, 0x26AB}, {0x26BD, 0x26BE}, {0x26C4, 0x26C5},
+ {0x26CE, 0x26CE}, {0x26D4, 0x26D4}, {0x26EA, 0x26EA},
+ {0x26F2, 0x26F3}, {0x26F5, 0x26F5}, {0x26FA, 0x26FA},
+ {0x26FD, 0x26FD}, {0x2705, 0x2705}, {0x270A, 0x270B},
+ {0x2728, 0x2728}, {0x274C, 0x274C}, {0x274E, 0x274E},
+ {0x2753, 0x2755}, {0x2757, 0x2757}, {0x2795, 0x2797},
+ {0x27B0, 0x27B0}, {0x27BF, 0x27BF}, {0x2B1B, 0x2B1C},
+ {0x2B50, 0x2B50}, {0x2B55, 0x2B55}, {0x2E80, 0x2E99},
+ {0x2E9B, 0x2EF3}, {0x2F00, 0x2FD5}, {0x2FF0, 0x2FFB},
+ {0x3000, 0x303E}, {0x3041, 0x3096}, {0x3099, 0x30FF},
+ {0x3105, 0x312F}, {0x3131, 0x318E}, {0x3190, 0x31E3},
+ {0x31F0, 0x321E}, {0x3220, 0x3247}, {0x3250, 0x4DBF},
+ {0x4E00, 0xA48C}, {0xA490, 0xA4C6}, {0xA960, 0xA97C},
+ {0xAC00, 0xD7A3}, {0xF900, 0xFAFF}, {0xFE10, 0xFE19},
+ {0xFE30, 0xFE52}, {0xFE54, 0xFE66}, {0xFE68, 0xFE6B},
+ {0xFF01, 0xFF60}, {0xFFE0, 0xFFE6}, {0x16FE0, 0x16FE4},
+ {0x16FF0, 0x16FF1}, {0x17000, 0x187F7}, {0x18800, 0x18CD5},
+ {0x18D00, 0x18D08}, {0x1B000, 0x1B11E}, {0x1B150, 0x1B152},
+ {0x1B164, 0x1B167}, {0x1B170, 0x1B2FB}, {0x1F004, 0x1F004},
+ {0x1F0CF, 0x1F0CF}, {0x1F18E, 0x1F18E}, {0x1F191, 0x1F19A},
+ {0x1F200, 0x1F202}, {0x1F210, 0x1F23B}, {0x1F240, 0x1F248},
+ {0x1F250, 0x1F251}, {0x1F260, 0x1F265}, {0x1F300, 0x1F320},
+ {0x1F32D, 0x1F335}, {0x1F337, 0x1F37C}, {0x1F37E, 0x1F393},
+ {0x1F3A0, 0x1F3CA}, {0x1F3CF, 0x1F3D3}, {0x1F3E0, 0x1F3F0},
+ {0x1F3F4, 0x1F3F4}, {0x1F3F8, 0x1F43E}, {0x1F440, 0x1F440},
+ {0x1F442, 0x1F4FC}, {0x1F4FF, 0x1F53D}, {0x1F54B, 0x1F54E},
+ {0x1F550, 0x1F567}, {0x1F57A, 0x1F57A}, {0x1F595, 0x1F596},
+ {0x1F5A4, 0x1F5A4}, {0x1F5FB, 0x1F64F}, {0x1F680, 0x1F6C5},
+ {0x1F6CC, 0x1F6CC}, {0x1F6D0, 0x1F6D2}, {0x1F6D5, 0x1F6D7},
+ {0x1F6EB, 0x1F6EC}, {0x1F6F4, 0x1F6FC}, {0x1F7E0, 0x1F7EB},
+ {0x1F90C, 0x1F93A}, {0x1F93C, 0x1F945}, {0x1F947, 0x1F978},
+ {0x1F97A, 0x1F9CB}, {0x1F9CD, 0x1F9FF}, {0x1FA70, 0x1FA74},
+ {0x1FA78, 0x1FA7A}, {0x1FA80, 0x1FA86}, {0x1FA90, 0x1FAA8},
+ {0x1FAB0, 0x1FAB6}, {0x1FAC0, 0x1FAC2}, {0x1FAD0, 0x1FAD6},
+ {0x20000, 0x2FFFD}, {0x30000, 0x3FFFD},
+}
+
+var ambiguous = table{
+ {0x00A1, 0x00A1}, {0x00A4, 0x00A4}, {0x00A7, 0x00A8},
+ {0x00AA, 0x00AA}, {0x00AD, 0x00AE}, {0x00B0, 0x00B4},
+ {0x00B6, 0x00BA}, {0x00BC, 0x00BF}, {0x00C6, 0x00C6},
+ {0x00D0, 0x00D0}, {0x00D7, 0x00D8}, {0x00DE, 0x00E1},
+ {0x00E6, 0x00E6}, {0x00E8, 0x00EA}, {0x00EC, 0x00ED},
+ {0x00F0, 0x00F0}, {0x00F2, 0x00F3}, {0x00F7, 0x00FA},
+ {0x00FC, 0x00FC}, {0x00FE, 0x00FE}, {0x0101, 0x0101},
+ {0x0111, 0x0111}, {0x0113, 0x0113}, {0x011B, 0x011B},
+ {0x0126, 0x0127}, {0x012B, 0x012B}, {0x0131, 0x0133},
+ {0x0138, 0x0138}, {0x013F, 0x0142}, {0x0144, 0x0144},
+ {0x0148, 0x014B}, {0x014D, 0x014D}, {0x0152, 0x0153},
+ {0x0166, 0x0167}, {0x016B, 0x016B}, {0x01CE, 0x01CE},
+ {0x01D0, 0x01D0}, {0x01D2, 0x01D2}, {0x01D4, 0x01D4},
+ {0x01D6, 0x01D6}, {0x01D8, 0x01D8}, {0x01DA, 0x01DA},
+ {0x01DC, 0x01DC}, {0x0251, 0x0251}, {0x0261, 0x0261},
+ {0x02C4, 0x02C4}, {0x02C7, 0x02C7}, {0x02C9, 0x02CB},
+ {0x02CD, 0x02CD}, {0x02D0, 0x02D0}, {0x02D8, 0x02DB},
+ {0x02DD, 0x02DD}, {0x02DF, 0x02DF}, {0x0300, 0x036F},
+ {0x0391, 0x03A1}, {0x03A3, 0x03A9}, {0x03B1, 0x03C1},
+ {0x03C3, 0x03C9}, {0x0401, 0x0401}, {0x0410, 0x044F},
+ {0x0451, 0x0451}, {0x2010, 0x2010}, {0x2013, 0x2016},
+ {0x2018, 0x2019}, {0x201C, 0x201D}, {0x2020, 0x2022},
+ {0x2024, 0x2027}, {0x2030, 0x2030}, {0x2032, 0x2033},
+ {0x2035, 0x2035}, {0x203B, 0x203B}, {0x203E, 0x203E},
+ {0x2074, 0x2074}, {0x207F, 0x207F}, {0x2081, 0x2084},
+ {0x20AC, 0x20AC}, {0x2103, 0x2103}, {0x2105, 0x2105},
+ {0x2109, 0x2109}, {0x2113, 0x2113}, {0x2116, 0x2116},
+ {0x2121, 0x2122}, {0x2126, 0x2126}, {0x212B, 0x212B},
+ {0x2153, 0x2154}, {0x215B, 0x215E}, {0x2160, 0x216B},
+ {0x2170, 0x2179}, {0x2189, 0x2189}, {0x2190, 0x2199},
+ {0x21B8, 0x21B9}, {0x21D2, 0x21D2}, {0x21D4, 0x21D4},
+ {0x21E7, 0x21E7}, {0x2200, 0x2200}, {0x2202, 0x2203},
+ {0x2207, 0x2208}, {0x220B, 0x220B}, {0x220F, 0x220F},
+ {0x2211, 0x2211}, {0x2215, 0x2215}, {0x221A, 0x221A},
+ {0x221D, 0x2220}, {0x2223, 0x2223}, {0x2225, 0x2225},
+ {0x2227, 0x222C}, {0x222E, 0x222E}, {0x2234, 0x2237},
+ {0x223C, 0x223D}, {0x2248, 0x2248}, {0x224C, 0x224C},
+ {0x2252, 0x2252}, {0x2260, 0x2261}, {0x2264, 0x2267},
+ {0x226A, 0x226B}, {0x226E, 0x226F}, {0x2282, 0x2283},
+ {0x2286, 0x2287}, {0x2295, 0x2295}, {0x2299, 0x2299},
+ {0x22A5, 0x22A5}, {0x22BF, 0x22BF}, {0x2312, 0x2312},
+ {0x2460, 0x24E9}, {0x24EB, 0x254B}, {0x2550, 0x2573},
+ {0x2580, 0x258F}, {0x2592, 0x2595}, {0x25A0, 0x25A1},
+ {0x25A3, 0x25A9}, {0x25B2, 0x25B3}, {0x25B6, 0x25B7},
+ {0x25BC, 0x25BD}, {0x25C0, 0x25C1}, {0x25C6, 0x25C8},
+ {0x25CB, 0x25CB}, {0x25CE, 0x25D1}, {0x25E2, 0x25E5},
+ {0x25EF, 0x25EF}, {0x2605, 0x2606}, {0x2609, 0x2609},
+ {0x260E, 0x260F}, {0x261C, 0x261C}, {0x261E, 0x261E},
+ {0x2640, 0x2640}, {0x2642, 0x2642}, {0x2660, 0x2661},
+ {0x2663, 0x2665}, {0x2667, 0x266A}, {0x266C, 0x266D},
+ {0x266F, 0x266F}, {0x269E, 0x269F}, {0x26BF, 0x26BF},
+ {0x26C6, 0x26CD}, {0x26CF, 0x26D3}, {0x26D5, 0x26E1},
+ {0x26E3, 0x26E3}, {0x26E8, 0x26E9}, {0x26EB, 0x26F1},
+ {0x26F4, 0x26F4}, {0x26F6, 0x26F9}, {0x26FB, 0x26FC},
+ {0x26FE, 0x26FF}, {0x273D, 0x273D}, {0x2776, 0x277F},
+ {0x2B56, 0x2B59}, {0x3248, 0x324F}, {0xE000, 0xF8FF},
+ {0xFE00, 0xFE0F}, {0xFFFD, 0xFFFD}, {0x1F100, 0x1F10A},
+ {0x1F110, 0x1F12D}, {0x1F130, 0x1F169}, {0x1F170, 0x1F18D},
+ {0x1F18F, 0x1F190}, {0x1F19B, 0x1F1AC}, {0xE0100, 0xE01EF},
+ {0xF0000, 0xFFFFD}, {0x100000, 0x10FFFD},
+}
+var notassigned = table{
+ {0x27E6, 0x27ED}, {0x2985, 0x2986},
+}
+
+var neutral = table{
+ {0x0000, 0x001F}, {0x007F, 0x00A0}, {0x00A9, 0x00A9},
+ {0x00AB, 0x00AB}, {0x00B5, 0x00B5}, {0x00BB, 0x00BB},
+ {0x00C0, 0x00C5}, {0x00C7, 0x00CF}, {0x00D1, 0x00D6},
+ {0x00D9, 0x00DD}, {0x00E2, 0x00E5}, {0x00E7, 0x00E7},
+ {0x00EB, 0x00EB}, {0x00EE, 0x00EF}, {0x00F1, 0x00F1},
+ {0x00F4, 0x00F6}, {0x00FB, 0x00FB}, {0x00FD, 0x00FD},
+ {0x00FF, 0x0100}, {0x0102, 0x0110}, {0x0112, 0x0112},
+ {0x0114, 0x011A}, {0x011C, 0x0125}, {0x0128, 0x012A},
+ {0x012C, 0x0130}, {0x0134, 0x0137}, {0x0139, 0x013E},
+ {0x0143, 0x0143}, {0x0145, 0x0147}, {0x014C, 0x014C},
+ {0x014E, 0x0151}, {0x0154, 0x0165}, {0x0168, 0x016A},
+ {0x016C, 0x01CD}, {0x01CF, 0x01CF}, {0x01D1, 0x01D1},
+ {0x01D3, 0x01D3}, {0x01D5, 0x01D5}, {0x01D7, 0x01D7},
+ {0x01D9, 0x01D9}, {0x01DB, 0x01DB}, {0x01DD, 0x0250},
+ {0x0252, 0x0260}, {0x0262, 0x02C3}, {0x02C5, 0x02C6},
+ {0x02C8, 0x02C8}, {0x02CC, 0x02CC}, {0x02CE, 0x02CF},
+ {0x02D1, 0x02D7}, {0x02DC, 0x02DC}, {0x02DE, 0x02DE},
+ {0x02E0, 0x02FF}, {0x0370, 0x0377}, {0x037A, 0x037F},
+ {0x0384, 0x038A}, {0x038C, 0x038C}, {0x038E, 0x0390},
+ {0x03AA, 0x03B0}, {0x03C2, 0x03C2}, {0x03CA, 0x0400},
+ {0x0402, 0x040F}, {0x0450, 0x0450}, {0x0452, 0x052F},
+ {0x0531, 0x0556}, {0x0559, 0x058A}, {0x058D, 0x058F},
+ {0x0591, 0x05C7}, {0x05D0, 0x05EA}, {0x05EF, 0x05F4},
+ {0x0600, 0x061C}, {0x061E, 0x070D}, {0x070F, 0x074A},
+ {0x074D, 0x07B1}, {0x07C0, 0x07FA}, {0x07FD, 0x082D},
+ {0x0830, 0x083E}, {0x0840, 0x085B}, {0x085E, 0x085E},
+ {0x0860, 0x086A}, {0x08A0, 0x08B4}, {0x08B6, 0x08C7},
+ {0x08D3, 0x0983}, {0x0985, 0x098C}, {0x098F, 0x0990},
+ {0x0993, 0x09A8}, {0x09AA, 0x09B0}, {0x09B2, 0x09B2},
+ {0x09B6, 0x09B9}, {0x09BC, 0x09C4}, {0x09C7, 0x09C8},
+ {0x09CB, 0x09CE}, {0x09D7, 0x09D7}, {0x09DC, 0x09DD},
+ {0x09DF, 0x09E3}, {0x09E6, 0x09FE}, {0x0A01, 0x0A03},
+ {0x0A05, 0x0A0A}, {0x0A0F, 0x0A10}, {0x0A13, 0x0A28},
+ {0x0A2A, 0x0A30}, {0x0A32, 0x0A33}, {0x0A35, 0x0A36},
+ {0x0A38, 0x0A39}, {0x0A3C, 0x0A3C}, {0x0A3E, 0x0A42},
+ {0x0A47, 0x0A48}, {0x0A4B, 0x0A4D}, {0x0A51, 0x0A51},
+ {0x0A59, 0x0A5C}, {0x0A5E, 0x0A5E}, {0x0A66, 0x0A76},
+ {0x0A81, 0x0A83}, {0x0A85, 0x0A8D}, {0x0A8F, 0x0A91},
+ {0x0A93, 0x0AA8}, {0x0AAA, 0x0AB0}, {0x0AB2, 0x0AB3},
+ {0x0AB5, 0x0AB9}, {0x0ABC, 0x0AC5}, {0x0AC7, 0x0AC9},
+ {0x0ACB, 0x0ACD}, {0x0AD0, 0x0AD0}, {0x0AE0, 0x0AE3},
+ {0x0AE6, 0x0AF1}, {0x0AF9, 0x0AFF}, {0x0B01, 0x0B03},
+ {0x0B05, 0x0B0C}, {0x0B0F, 0x0B10}, {0x0B13, 0x0B28},
+ {0x0B2A, 0x0B30}, {0x0B32, 0x0B33}, {0x0B35, 0x0B39},
+ {0x0B3C, 0x0B44}, {0x0B47, 0x0B48}, {0x0B4B, 0x0B4D},
+ {0x0B55, 0x0B57}, {0x0B5C, 0x0B5D}, {0x0B5F, 0x0B63},
+ {0x0B66, 0x0B77}, {0x0B82, 0x0B83}, {0x0B85, 0x0B8A},
+ {0x0B8E, 0x0B90}, {0x0B92, 0x0B95}, {0x0B99, 0x0B9A},
+ {0x0B9C, 0x0B9C}, {0x0B9E, 0x0B9F}, {0x0BA3, 0x0BA4},
+ {0x0BA8, 0x0BAA}, {0x0BAE, 0x0BB9}, {0x0BBE, 0x0BC2},
+ {0x0BC6, 0x0BC8}, {0x0BCA, 0x0BCD}, {0x0BD0, 0x0BD0},
+ {0x0BD7, 0x0BD7}, {0x0BE6, 0x0BFA}, {0x0C00, 0x0C0C},
+ {0x0C0E, 0x0C10}, {0x0C12, 0x0C28}, {0x0C2A, 0x0C39},
+ {0x0C3D, 0x0C44}, {0x0C46, 0x0C48}, {0x0C4A, 0x0C4D},
+ {0x0C55, 0x0C56}, {0x0C58, 0x0C5A}, {0x0C60, 0x0C63},
+ {0x0C66, 0x0C6F}, {0x0C77, 0x0C8C}, {0x0C8E, 0x0C90},
+ {0x0C92, 0x0CA8}, {0x0CAA, 0x0CB3}, {0x0CB5, 0x0CB9},
+ {0x0CBC, 0x0CC4}, {0x0CC6, 0x0CC8}, {0x0CCA, 0x0CCD},
+ {0x0CD5, 0x0CD6}, {0x0CDE, 0x0CDE}, {0x0CE0, 0x0CE3},
+ {0x0CE6, 0x0CEF}, {0x0CF1, 0x0CF2}, {0x0D00, 0x0D0C},
+ {0x0D0E, 0x0D10}, {0x0D12, 0x0D44}, {0x0D46, 0x0D48},
+ {0x0D4A, 0x0D4F}, {0x0D54, 0x0D63}, {0x0D66, 0x0D7F},
+ {0x0D81, 0x0D83}, {0x0D85, 0x0D96}, {0x0D9A, 0x0DB1},
+ {0x0DB3, 0x0DBB}, {0x0DBD, 0x0DBD}, {0x0DC0, 0x0DC6},
+ {0x0DCA, 0x0DCA}, {0x0DCF, 0x0DD4}, {0x0DD6, 0x0DD6},
+ {0x0DD8, 0x0DDF}, {0x0DE6, 0x0DEF}, {0x0DF2, 0x0DF4},
+ {0x0E01, 0x0E3A}, {0x0E3F, 0x0E5B}, {0x0E81, 0x0E82},
+ {0x0E84, 0x0E84}, {0x0E86, 0x0E8A}, {0x0E8C, 0x0EA3},
+ {0x0EA5, 0x0EA5}, {0x0EA7, 0x0EBD}, {0x0EC0, 0x0EC4},
+ {0x0EC6, 0x0EC6}, {0x0EC8, 0x0ECD}, {0x0ED0, 0x0ED9},
+ {0x0EDC, 0x0EDF}, {0x0F00, 0x0F47}, {0x0F49, 0x0F6C},
+ {0x0F71, 0x0F97}, {0x0F99, 0x0FBC}, {0x0FBE, 0x0FCC},
+ {0x0FCE, 0x0FDA}, {0x1000, 0x10C5}, {0x10C7, 0x10C7},
+ {0x10CD, 0x10CD}, {0x10D0, 0x10FF}, {0x1160, 0x1248},
+ {0x124A, 0x124D}, {0x1250, 0x1256}, {0x1258, 0x1258},
+ {0x125A, 0x125D}, {0x1260, 0x1288}, {0x128A, 0x128D},
+ {0x1290, 0x12B0}, {0x12B2, 0x12B5}, {0x12B8, 0x12BE},
+ {0x12C0, 0x12C0}, {0x12C2, 0x12C5}, {0x12C8, 0x12D6},
+ {0x12D8, 0x1310}, {0x1312, 0x1315}, {0x1318, 0x135A},
+ {0x135D, 0x137C}, {0x1380, 0x1399}, {0x13A0, 0x13F5},
+ {0x13F8, 0x13FD}, {0x1400, 0x169C}, {0x16A0, 0x16F8},
+ {0x1700, 0x170C}, {0x170E, 0x1714}, {0x1720, 0x1736},
+ {0x1740, 0x1753}, {0x1760, 0x176C}, {0x176E, 0x1770},
+ {0x1772, 0x1773}, {0x1780, 0x17DD}, {0x17E0, 0x17E9},
+ {0x17F0, 0x17F9}, {0x1800, 0x180E}, {0x1810, 0x1819},
+ {0x1820, 0x1878}, {0x1880, 0x18AA}, {0x18B0, 0x18F5},
+ {0x1900, 0x191E}, {0x1920, 0x192B}, {0x1930, 0x193B},
+ {0x1940, 0x1940}, {0x1944, 0x196D}, {0x1970, 0x1974},
+ {0x1980, 0x19AB}, {0x19B0, 0x19C9}, {0x19D0, 0x19DA},
+ {0x19DE, 0x1A1B}, {0x1A1E, 0x1A5E}, {0x1A60, 0x1A7C},
+ {0x1A7F, 0x1A89}, {0x1A90, 0x1A99}, {0x1AA0, 0x1AAD},
+ {0x1AB0, 0x1AC0}, {0x1B00, 0x1B4B}, {0x1B50, 0x1B7C},
+ {0x1B80, 0x1BF3}, {0x1BFC, 0x1C37}, {0x1C3B, 0x1C49},
+ {0x1C4D, 0x1C88}, {0x1C90, 0x1CBA}, {0x1CBD, 0x1CC7},
+ {0x1CD0, 0x1CFA}, {0x1D00, 0x1DF9}, {0x1DFB, 0x1F15},
+ {0x1F18, 0x1F1D}, {0x1F20, 0x1F45}, {0x1F48, 0x1F4D},
+ {0x1F50, 0x1F57}, {0x1F59, 0x1F59}, {0x1F5B, 0x1F5B},
+ {0x1F5D, 0x1F5D}, {0x1F5F, 0x1F7D}, {0x1F80, 0x1FB4},
+ {0x1FB6, 0x1FC4}, {0x1FC6, 0x1FD3}, {0x1FD6, 0x1FDB},
+ {0x1FDD, 0x1FEF}, {0x1FF2, 0x1FF4}, {0x1FF6, 0x1FFE},
+ {0x2000, 0x200F}, {0x2011, 0x2012}, {0x2017, 0x2017},
+ {0x201A, 0x201B}, {0x201E, 0x201F}, {0x2023, 0x2023},
+ {0x2028, 0x202F}, {0x2031, 0x2031}, {0x2034, 0x2034},
+ {0x2036, 0x203A}, {0x203C, 0x203D}, {0x203F, 0x2064},
+ {0x2066, 0x2071}, {0x2075, 0x207E}, {0x2080, 0x2080},
+ {0x2085, 0x208E}, {0x2090, 0x209C}, {0x20A0, 0x20A8},
+ {0x20AA, 0x20AB}, {0x20AD, 0x20BF}, {0x20D0, 0x20F0},
+ {0x2100, 0x2102}, {0x2104, 0x2104}, {0x2106, 0x2108},
+ {0x210A, 0x2112}, {0x2114, 0x2115}, {0x2117, 0x2120},
+ {0x2123, 0x2125}, {0x2127, 0x212A}, {0x212C, 0x2152},
+ {0x2155, 0x215A}, {0x215F, 0x215F}, {0x216C, 0x216F},
+ {0x217A, 0x2188}, {0x218A, 0x218B}, {0x219A, 0x21B7},
+ {0x21BA, 0x21D1}, {0x21D3, 0x21D3}, {0x21D5, 0x21E6},
+ {0x21E8, 0x21FF}, {0x2201, 0x2201}, {0x2204, 0x2206},
+ {0x2209, 0x220A}, {0x220C, 0x220E}, {0x2210, 0x2210},
+ {0x2212, 0x2214}, {0x2216, 0x2219}, {0x221B, 0x221C},
+ {0x2221, 0x2222}, {0x2224, 0x2224}, {0x2226, 0x2226},
+ {0x222D, 0x222D}, {0x222F, 0x2233}, {0x2238, 0x223B},
+ {0x223E, 0x2247}, {0x2249, 0x224B}, {0x224D, 0x2251},
+ {0x2253, 0x225F}, {0x2262, 0x2263}, {0x2268, 0x2269},
+ {0x226C, 0x226D}, {0x2270, 0x2281}, {0x2284, 0x2285},
+ {0x2288, 0x2294}, {0x2296, 0x2298}, {0x229A, 0x22A4},
+ {0x22A6, 0x22BE}, {0x22C0, 0x2311}, {0x2313, 0x2319},
+ {0x231C, 0x2328}, {0x232B, 0x23E8}, {0x23ED, 0x23EF},
+ {0x23F1, 0x23F2}, {0x23F4, 0x2426}, {0x2440, 0x244A},
+ {0x24EA, 0x24EA}, {0x254C, 0x254F}, {0x2574, 0x257F},
+ {0x2590, 0x2591}, {0x2596, 0x259F}, {0x25A2, 0x25A2},
+ {0x25AA, 0x25B1}, {0x25B4, 0x25B5}, {0x25B8, 0x25BB},
+ {0x25BE, 0x25BF}, {0x25C2, 0x25C5}, {0x25C9, 0x25CA},
+ {0x25CC, 0x25CD}, {0x25D2, 0x25E1}, {0x25E6, 0x25EE},
+ {0x25F0, 0x25FC}, {0x25FF, 0x2604}, {0x2607, 0x2608},
+ {0x260A, 0x260D}, {0x2610, 0x2613}, {0x2616, 0x261B},
+ {0x261D, 0x261D}, {0x261F, 0x263F}, {0x2641, 0x2641},
+ {0x2643, 0x2647}, {0x2654, 0x265F}, {0x2662, 0x2662},
+ {0x2666, 0x2666}, {0x266B, 0x266B}, {0x266E, 0x266E},
+ {0x2670, 0x267E}, {0x2680, 0x2692}, {0x2694, 0x269D},
+ {0x26A0, 0x26A0}, {0x26A2, 0x26A9}, {0x26AC, 0x26BC},
+ {0x26C0, 0x26C3}, {0x26E2, 0x26E2}, {0x26E4, 0x26E7},
+ {0x2700, 0x2704}, {0x2706, 0x2709}, {0x270C, 0x2727},
+ {0x2729, 0x273C}, {0x273E, 0x274B}, {0x274D, 0x274D},
+ {0x274F, 0x2752}, {0x2756, 0x2756}, {0x2758, 0x2775},
+ {0x2780, 0x2794}, {0x2798, 0x27AF}, {0x27B1, 0x27BE},
+ {0x27C0, 0x27E5}, {0x27EE, 0x2984}, {0x2987, 0x2B1A},
+ {0x2B1D, 0x2B4F}, {0x2B51, 0x2B54}, {0x2B5A, 0x2B73},
+ {0x2B76, 0x2B95}, {0x2B97, 0x2C2E}, {0x2C30, 0x2C5E},
+ {0x2C60, 0x2CF3}, {0x2CF9, 0x2D25}, {0x2D27, 0x2D27},
+ {0x2D2D, 0x2D2D}, {0x2D30, 0x2D67}, {0x2D6F, 0x2D70},
+ {0x2D7F, 0x2D96}, {0x2DA0, 0x2DA6}, {0x2DA8, 0x2DAE},
+ {0x2DB0, 0x2DB6}, {0x2DB8, 0x2DBE}, {0x2DC0, 0x2DC6},
+ {0x2DC8, 0x2DCE}, {0x2DD0, 0x2DD6}, {0x2DD8, 0x2DDE},
+ {0x2DE0, 0x2E52}, {0x303F, 0x303F}, {0x4DC0, 0x4DFF},
+ {0xA4D0, 0xA62B}, {0xA640, 0xA6F7}, {0xA700, 0xA7BF},
+ {0xA7C2, 0xA7CA}, {0xA7F5, 0xA82C}, {0xA830, 0xA839},
+ {0xA840, 0xA877}, {0xA880, 0xA8C5}, {0xA8CE, 0xA8D9},
+ {0xA8E0, 0xA953}, {0xA95F, 0xA95F}, {0xA980, 0xA9CD},
+ {0xA9CF, 0xA9D9}, {0xA9DE, 0xA9FE}, {0xAA00, 0xAA36},
+ {0xAA40, 0xAA4D}, {0xAA50, 0xAA59}, {0xAA5C, 0xAAC2},
+ {0xAADB, 0xAAF6}, {0xAB01, 0xAB06}, {0xAB09, 0xAB0E},
+ {0xAB11, 0xAB16}, {0xAB20, 0xAB26}, {0xAB28, 0xAB2E},
+ {0xAB30, 0xAB6B}, {0xAB70, 0xABED}, {0xABF0, 0xABF9},
+ {0xD7B0, 0xD7C6}, {0xD7CB, 0xD7FB}, {0xD800, 0xDFFF},
+ {0xFB00, 0xFB06}, {0xFB13, 0xFB17}, {0xFB1D, 0xFB36},
+ {0xFB38, 0xFB3C}, {0xFB3E, 0xFB3E}, {0xFB40, 0xFB41},
+ {0xFB43, 0xFB44}, {0xFB46, 0xFBC1}, {0xFBD3, 0xFD3F},
+ {0xFD50, 0xFD8F}, {0xFD92, 0xFDC7}, {0xFDF0, 0xFDFD},
+ {0xFE20, 0xFE2F}, {0xFE70, 0xFE74}, {0xFE76, 0xFEFC},
+ {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFC}, {0x10000, 0x1000B},
+ {0x1000D, 0x10026}, {0x10028, 0x1003A}, {0x1003C, 0x1003D},
+ {0x1003F, 0x1004D}, {0x10050, 0x1005D}, {0x10080, 0x100FA},
+ {0x10100, 0x10102}, {0x10107, 0x10133}, {0x10137, 0x1018E},
+ {0x10190, 0x1019C}, {0x101A0, 0x101A0}, {0x101D0, 0x101FD},
+ {0x10280, 0x1029C}, {0x102A0, 0x102D0}, {0x102E0, 0x102FB},
+ {0x10300, 0x10323}, {0x1032D, 0x1034A}, {0x10350, 0x1037A},
+ {0x10380, 0x1039D}, {0x1039F, 0x103C3}, {0x103C8, 0x103D5},
+ {0x10400, 0x1049D}, {0x104A0, 0x104A9}, {0x104B0, 0x104D3},
+ {0x104D8, 0x104FB}, {0x10500, 0x10527}, {0x10530, 0x10563},
+ {0x1056F, 0x1056F}, {0x10600, 0x10736}, {0x10740, 0x10755},
+ {0x10760, 0x10767}, {0x10800, 0x10805}, {0x10808, 0x10808},
+ {0x1080A, 0x10835}, {0x10837, 0x10838}, {0x1083C, 0x1083C},
+ {0x1083F, 0x10855}, {0x10857, 0x1089E}, {0x108A7, 0x108AF},
+ {0x108E0, 0x108F2}, {0x108F4, 0x108F5}, {0x108FB, 0x1091B},
+ {0x1091F, 0x10939}, {0x1093F, 0x1093F}, {0x10980, 0x109B7},
+ {0x109BC, 0x109CF}, {0x109D2, 0x10A03}, {0x10A05, 0x10A06},
+ {0x10A0C, 0x10A13}, {0x10A15, 0x10A17}, {0x10A19, 0x10A35},
+ {0x10A38, 0x10A3A}, {0x10A3F, 0x10A48}, {0x10A50, 0x10A58},
+ {0x10A60, 0x10A9F}, {0x10AC0, 0x10AE6}, {0x10AEB, 0x10AF6},
+ {0x10B00, 0x10B35}, {0x10B39, 0x10B55}, {0x10B58, 0x10B72},
+ {0x10B78, 0x10B91}, {0x10B99, 0x10B9C}, {0x10BA9, 0x10BAF},
+ {0x10C00, 0x10C48}, {0x10C80, 0x10CB2}, {0x10CC0, 0x10CF2},
+ {0x10CFA, 0x10D27}, {0x10D30, 0x10D39}, {0x10E60, 0x10E7E},
+ {0x10E80, 0x10EA9}, {0x10EAB, 0x10EAD}, {0x10EB0, 0x10EB1},
+ {0x10F00, 0x10F27}, {0x10F30, 0x10F59}, {0x10FB0, 0x10FCB},
+ {0x10FE0, 0x10FF6}, {0x11000, 0x1104D}, {0x11052, 0x1106F},
+ {0x1107F, 0x110C1}, {0x110CD, 0x110CD}, {0x110D0, 0x110E8},
+ {0x110F0, 0x110F9}, {0x11100, 0x11134}, {0x11136, 0x11147},
+ {0x11150, 0x11176}, {0x11180, 0x111DF}, {0x111E1, 0x111F4},
+ {0x11200, 0x11211}, {0x11213, 0x1123E}, {0x11280, 0x11286},
+ {0x11288, 0x11288}, {0x1128A, 0x1128D}, {0x1128F, 0x1129D},
+ {0x1129F, 0x112A9}, {0x112B0, 0x112EA}, {0x112F0, 0x112F9},
+ {0x11300, 0x11303}, {0x11305, 0x1130C}, {0x1130F, 0x11310},
+ {0x11313, 0x11328}, {0x1132A, 0x11330}, {0x11332, 0x11333},
+ {0x11335, 0x11339}, {0x1133B, 0x11344}, {0x11347, 0x11348},
+ {0x1134B, 0x1134D}, {0x11350, 0x11350}, {0x11357, 0x11357},
+ {0x1135D, 0x11363}, {0x11366, 0x1136C}, {0x11370, 0x11374},
+ {0x11400, 0x1145B}, {0x1145D, 0x11461}, {0x11480, 0x114C7},
+ {0x114D0, 0x114D9}, {0x11580, 0x115B5}, {0x115B8, 0x115DD},
+ {0x11600, 0x11644}, {0x11650, 0x11659}, {0x11660, 0x1166C},
+ {0x11680, 0x116B8}, {0x116C0, 0x116C9}, {0x11700, 0x1171A},
+ {0x1171D, 0x1172B}, {0x11730, 0x1173F}, {0x11800, 0x1183B},
+ {0x118A0, 0x118F2}, {0x118FF, 0x11906}, {0x11909, 0x11909},
+ {0x1190C, 0x11913}, {0x11915, 0x11916}, {0x11918, 0x11935},
+ {0x11937, 0x11938}, {0x1193B, 0x11946}, {0x11950, 0x11959},
+ {0x119A0, 0x119A7}, {0x119AA, 0x119D7}, {0x119DA, 0x119E4},
+ {0x11A00, 0x11A47}, {0x11A50, 0x11AA2}, {0x11AC0, 0x11AF8},
+ {0x11C00, 0x11C08}, {0x11C0A, 0x11C36}, {0x11C38, 0x11C45},
+ {0x11C50, 0x11C6C}, {0x11C70, 0x11C8F}, {0x11C92, 0x11CA7},
+ {0x11CA9, 0x11CB6}, {0x11D00, 0x11D06}, {0x11D08, 0x11D09},
+ {0x11D0B, 0x11D36}, {0x11D3A, 0x11D3A}, {0x11D3C, 0x11D3D},
+ {0x11D3F, 0x11D47}, {0x11D50, 0x11D59}, {0x11D60, 0x11D65},
+ {0x11D67, 0x11D68}, {0x11D6A, 0x11D8E}, {0x11D90, 0x11D91},
+ {0x11D93, 0x11D98}, {0x11DA0, 0x11DA9}, {0x11EE0, 0x11EF8},
+ {0x11FB0, 0x11FB0}, {0x11FC0, 0x11FF1}, {0x11FFF, 0x12399},
+ {0x12400, 0x1246E}, {0x12470, 0x12474}, {0x12480, 0x12543},
+ {0x13000, 0x1342E}, {0x13430, 0x13438}, {0x14400, 0x14646},
+ {0x16800, 0x16A38}, {0x16A40, 0x16A5E}, {0x16A60, 0x16A69},
+ {0x16A6E, 0x16A6F}, {0x16AD0, 0x16AED}, {0x16AF0, 0x16AF5},
+ {0x16B00, 0x16B45}, {0x16B50, 0x16B59}, {0x16B5B, 0x16B61},
+ {0x16B63, 0x16B77}, {0x16B7D, 0x16B8F}, {0x16E40, 0x16E9A},
+ {0x16F00, 0x16F4A}, {0x16F4F, 0x16F87}, {0x16F8F, 0x16F9F},
+ {0x1BC00, 0x1BC6A}, {0x1BC70, 0x1BC7C}, {0x1BC80, 0x1BC88},
+ {0x1BC90, 0x1BC99}, {0x1BC9C, 0x1BCA3}, {0x1D000, 0x1D0F5},
+ {0x1D100, 0x1D126}, {0x1D129, 0x1D1E8}, {0x1D200, 0x1D245},
+ {0x1D2E0, 0x1D2F3}, {0x1D300, 0x1D356}, {0x1D360, 0x1D378},
+ {0x1D400, 0x1D454}, {0x1D456, 0x1D49C}, {0x1D49E, 0x1D49F},
+ {0x1D4A2, 0x1D4A2}, {0x1D4A5, 0x1D4A6}, {0x1D4A9, 0x1D4AC},
+ {0x1D4AE, 0x1D4B9}, {0x1D4BB, 0x1D4BB}, {0x1D4BD, 0x1D4C3},
+ {0x1D4C5, 0x1D505}, {0x1D507, 0x1D50A}, {0x1D50D, 0x1D514},
+ {0x1D516, 0x1D51C}, {0x1D51E, 0x1D539}, {0x1D53B, 0x1D53E},
+ {0x1D540, 0x1D544}, {0x1D546, 0x1D546}, {0x1D54A, 0x1D550},
+ {0x1D552, 0x1D6A5}, {0x1D6A8, 0x1D7CB}, {0x1D7CE, 0x1DA8B},
+ {0x1DA9B, 0x1DA9F}, {0x1DAA1, 0x1DAAF}, {0x1E000, 0x1E006},
+ {0x1E008, 0x1E018}, {0x1E01B, 0x1E021}, {0x1E023, 0x1E024},
+ {0x1E026, 0x1E02A}, {0x1E100, 0x1E12C}, {0x1E130, 0x1E13D},
+ {0x1E140, 0x1E149}, {0x1E14E, 0x1E14F}, {0x1E2C0, 0x1E2F9},
+ {0x1E2FF, 0x1E2FF}, {0x1E800, 0x1E8C4}, {0x1E8C7, 0x1E8D6},
+ {0x1E900, 0x1E94B}, {0x1E950, 0x1E959}, {0x1E95E, 0x1E95F},
+ {0x1EC71, 0x1ECB4}, {0x1ED01, 0x1ED3D}, {0x1EE00, 0x1EE03},
+ {0x1EE05, 0x1EE1F}, {0x1EE21, 0x1EE22}, {0x1EE24, 0x1EE24},
+ {0x1EE27, 0x1EE27}, {0x1EE29, 0x1EE32}, {0x1EE34, 0x1EE37},
+ {0x1EE39, 0x1EE39}, {0x1EE3B, 0x1EE3B}, {0x1EE42, 0x1EE42},
+ {0x1EE47, 0x1EE47}, {0x1EE49, 0x1EE49}, {0x1EE4B, 0x1EE4B},
+ {0x1EE4D, 0x1EE4F}, {0x1EE51, 0x1EE52}, {0x1EE54, 0x1EE54},
+ {0x1EE57, 0x1EE57}, {0x1EE59, 0x1EE59}, {0x1EE5B, 0x1EE5B},
+ {0x1EE5D, 0x1EE5D}, {0x1EE5F, 0x1EE5F}, {0x1EE61, 0x1EE62},
+ {0x1EE64, 0x1EE64}, {0x1EE67, 0x1EE6A}, {0x1EE6C, 0x1EE72},
+ {0x1EE74, 0x1EE77}, {0x1EE79, 0x1EE7C}, {0x1EE7E, 0x1EE7E},
+ {0x1EE80, 0x1EE89}, {0x1EE8B, 0x1EE9B}, {0x1EEA1, 0x1EEA3},
+ {0x1EEA5, 0x1EEA9}, {0x1EEAB, 0x1EEBB}, {0x1EEF0, 0x1EEF1},
+ {0x1F000, 0x1F003}, {0x1F005, 0x1F02B}, {0x1F030, 0x1F093},
+ {0x1F0A0, 0x1F0AE}, {0x1F0B1, 0x1F0BF}, {0x1F0C1, 0x1F0CE},
+ {0x1F0D1, 0x1F0F5}, {0x1F10B, 0x1F10F}, {0x1F12E, 0x1F12F},
+ {0x1F16A, 0x1F16F}, {0x1F1AD, 0x1F1AD}, {0x1F1E6, 0x1F1FF},
+ {0x1F321, 0x1F32C}, {0x1F336, 0x1F336}, {0x1F37D, 0x1F37D},
+ {0x1F394, 0x1F39F}, {0x1F3CB, 0x1F3CE}, {0x1F3D4, 0x1F3DF},
+ {0x1F3F1, 0x1F3F3}, {0x1F3F5, 0x1F3F7}, {0x1F43F, 0x1F43F},
+ {0x1F441, 0x1F441}, {0x1F4FD, 0x1F4FE}, {0x1F53E, 0x1F54A},
+ {0x1F54F, 0x1F54F}, {0x1F568, 0x1F579}, {0x1F57B, 0x1F594},
+ {0x1F597, 0x1F5A3}, {0x1F5A5, 0x1F5FA}, {0x1F650, 0x1F67F},
+ {0x1F6C6, 0x1F6CB}, {0x1F6CD, 0x1F6CF}, {0x1F6D3, 0x1F6D4},
+ {0x1F6E0, 0x1F6EA}, {0x1F6F0, 0x1F6F3}, {0x1F700, 0x1F773},
+ {0x1F780, 0x1F7D8}, {0x1F800, 0x1F80B}, {0x1F810, 0x1F847},
+ {0x1F850, 0x1F859}, {0x1F860, 0x1F887}, {0x1F890, 0x1F8AD},
+ {0x1F8B0, 0x1F8B1}, {0x1F900, 0x1F90B}, {0x1F93B, 0x1F93B},
+ {0x1F946, 0x1F946}, {0x1FA00, 0x1FA53}, {0x1FA60, 0x1FA6D},
+ {0x1FB00, 0x1FB92}, {0x1FB94, 0x1FBCA}, {0x1FBF0, 0x1FBF9},
+ {0xE0001, 0xE0001}, {0xE0020, 0xE007F},
+}
+
+var emoji = table{
+ {0x203C, 0x203C}, {0x2049, 0x2049}, {0x2122, 0x2122},
+ {0x2139, 0x2139}, {0x2194, 0x2199}, {0x21A9, 0x21AA},
+ {0x231A, 0x231B}, {0x2328, 0x2328}, {0x2388, 0x2388},
+ {0x23CF, 0x23CF}, {0x23E9, 0x23F3}, {0x23F8, 0x23FA},
+ {0x24C2, 0x24C2}, {0x25AA, 0x25AB}, {0x25B6, 0x25B6},
+ {0x25C0, 0x25C0}, {0x25FB, 0x25FE}, {0x2600, 0x2605},
+ {0x2607, 0x2612}, {0x2614, 0x2685}, {0x2690, 0x2705},
+ {0x2708, 0x2712}, {0x2714, 0x2714}, {0x2716, 0x2716},
+ {0x271D, 0x271D}, {0x2721, 0x2721}, {0x2728, 0x2728},
+ {0x2733, 0x2734}, {0x2744, 0x2744}, {0x2747, 0x2747},
+ {0x274C, 0x274C}, {0x274E, 0x274E}, {0x2753, 0x2755},
+ {0x2757, 0x2757}, {0x2763, 0x2767}, {0x2795, 0x2797},
+ {0x27A1, 0x27A1}, {0x27B0, 0x27B0}, {0x27BF, 0x27BF},
+ {0x2934, 0x2935}, {0x2B05, 0x2B07}, {0x2B1B, 0x2B1C},
+ {0x2B50, 0x2B50}, {0x2B55, 0x2B55}, {0x3030, 0x3030},
+ {0x303D, 0x303D}, {0x3297, 0x3297}, {0x3299, 0x3299},
+ {0x1F000, 0x1F0FF}, {0x1F10D, 0x1F10F}, {0x1F12F, 0x1F12F},
+ {0x1F16C, 0x1F171}, {0x1F17E, 0x1F17F}, {0x1F18E, 0x1F18E},
+ {0x1F191, 0x1F19A}, {0x1F1AD, 0x1F1E5}, {0x1F201, 0x1F20F},
+ {0x1F21A, 0x1F21A}, {0x1F22F, 0x1F22F}, {0x1F232, 0x1F23A},
+ {0x1F23C, 0x1F23F}, {0x1F249, 0x1F3FA}, {0x1F400, 0x1F53D},
+ {0x1F546, 0x1F64F}, {0x1F680, 0x1F6FF}, {0x1F774, 0x1F77F},
+ {0x1F7D5, 0x1F7FF}, {0x1F80C, 0x1F80F}, {0x1F848, 0x1F84F},
+ {0x1F85A, 0x1F85F}, {0x1F888, 0x1F88F}, {0x1F8AE, 0x1F8FF},
+ {0x1F90C, 0x1F93A}, {0x1F93C, 0x1F945}, {0x1F947, 0x1FAFF},
+ {0x1FC00, 0x1FFFD},
+}
diff --git a/examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth_windows.go b/examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth_windows.go
new file mode 100644
index 000000000..d6a61777d
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mattn/go-runewidth/runewidth_windows.go
@@ -0,0 +1,28 @@
+// +build windows
+// +build !appengine
+
+package runewidth
+
+import (
+ "syscall"
+)
+
+var (
+ kernel32 = syscall.NewLazyDLL("kernel32")
+ procGetConsoleOutputCP = kernel32.NewProc("GetConsoleOutputCP")
+)
+
+// IsEastAsian return true if the current locale is CJK
+func IsEastAsian() bool {
+ r1, _, _ := procGetConsoleOutputCP.Call()
+ if r1 == 0 {
+ return false
+ }
+
+ switch int(r1) {
+ case 932, 51932, 936, 949, 950:
+ return true
+ }
+
+ return false
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/.gitignore b/examples/go-dashboard/src/github.com/mum4k/termdash/.gitignore
new file mode 100644
index 000000000..97e9bcbaa
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/.gitignore
@@ -0,0 +1,2 @@
+# Exclude MacOS attribute files.
+.DS_Store
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/.travis.yml b/examples/go-dashboard/src/github.com/mum4k/termdash/.travis.yml
new file mode 100644
index 000000000..7c8b739a0
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/.travis.yml
@@ -0,0 +1,17 @@
+language: go
+go:
+ - 1.14.x
+ - 1.15.x
+ - stable
+script:
+ - go get -t ./...
+ - go get -u golang.org/x/lint/golint
+ - go test ./...
+ - CGO_ENABLED=1 go test -race ./...
+ - go vet ./...
+ - diff -u <(echo -n) <(gofmt -d -s .)
+ - diff -u <(echo -n) <(./internal/scripts/autogen_licences.sh .)
+ - diff -u <(echo -n) <(golint ./...)
+env:
+ global:
+ - CGO_ENABLED=0
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/CHANGELOG.md b/examples/go-dashboard/src/github.com/mum4k/termdash/CHANGELOG.md
new file mode 100644
index 000000000..6889100b4
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/CHANGELOG.md
@@ -0,0 +1,361 @@
+# Changelog
+
+All notable changes to this project are documented here.
+
+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]
+
+## [0.12.2] - 31-Aug-2020
+
+### Fixed
+
+- advanced the CI Go versions up to Go 1.15.
+- fixed the build status badge to correctly point to travis-ci.com instead of
+ travis-ci.org.
+
+## [0.12.1] - 20-Jun-2020
+
+### Fixed
+
+- the `tcell` unit test can now pass in headless mode (when TERM="") which
+ happens under bazel.
+- switching coveralls integration to Github application.
+
+## [0.12.0] - 10-Apr-2020
+
+### Added
+
+- Migrating to [Go modules](https://blog.golang.org/using-go-modules).
+- Renamed directory `internal` to `private` so that external widget development
+ is possible. Noted in
+ [README.md](https://github.com/mum4k/termdash/blob/master/README.md) that packages in the
+ `private` directory don't have any API stability guarantee.
+
+## [0.11.0] - 7-Mar-2020
+
+#### Breaking API changes
+
+- Termdash now requires at least Go version 1.11.
+
+### Added
+
+- New [`tcell`](https://github.com/gdamore/tcell) based terminal implementation
+ which implements the `terminalapi.Terminal` interface.
+- tcell implementation supports two initialization `Option`s:
+ - `ColorMode` the terminal color output mode (defaults to 256 color mode)
+ - `ClearStyle` the foreground and background color style to use when clearing
+ the screen (defaults to the global ColorDefault for both foreground and
+ background)
+
+### Fixed
+
+- Improved test coverage of the `Gauge` widget.
+
+## [0.10.0] - 5-Jun-2019
+
+### Added
+
+- Added `time.Duration` based `ValueFormatter` for the `LineChart` Y-axis labels.
+- Added round and suffix `ValueFormatter` for the `LineChart` Y-axis labels.
+- Added decimal and suffix `ValueFormatter` for the `LineChart` Y-axis labels.
+- Added a `container.SplitOption` that allows fixed size container splits.
+- Added `grid` functions that allow fixed size rows and columns.
+
+### Changed
+
+- The `LineChart` can format the labels on the Y-axis with a `ValueFormatter`.
+- The `SegmentDisplay` can now display dots and colons ('.' and ':').
+- The `Donut` widget now guarantees spacing between the donut and its label.
+- The continuous build on Travis CI now builds with cgo explicitly disabled to
+ ensure both Termdash and its dependencies use pure Go.
+
+### Fixed
+
+- Lint issues found on the Go report card.
+- An internal library belonging to the `Text` widget was incorrectly passing
+ `math.MaxUint32` as an int argument.
+
+## [0.9.1] - 15-May-2019
+
+### Fixed
+
+- Termdash could deadlock when a `Button` or a `TextInput` was configured to
+ call the `Container.Update` method.
+
+## [0.9.0] - 28-Apr-2019
+
+### Added
+
+- The `TextInput` widget, an input field allowing interactive text input.
+- The `Donut` widget can now display an optional text label under the donut.
+
+### Changed
+
+- Widgets now get information whether their container is focused when Draw is
+ executed.
+- The SegmentDisplay widget now has a method that returns the observed character
+ capacity the last time Draw was called.
+- The grid.Builder API now allows users to specify options for intermediate
+ containers, i.e. containers that don't have widgets, but represent rows and
+ columns.
+- Line chart widget now allows `math.NaN` values to represent "no value" (values
+ that will not be rendered) in the values slice.
+
+#### Breaking API changes
+
+- The widgetapi.Widget.Draw method now accepts a second argument which provides
+ widgets with additional metadata. This affects all implemented widgets.
+- Termdash now requires at least Go version 1.10, which allows us to utilize
+ `math.Round` instead of our own implementation and `strings.Builder` instead
+ of `bytes.Buffer`.
+- Terminal shortcuts like `Ctrl-A` no longer come as two separate events,
+ Termdash now mirrors termbox-go and sends these as one event.
+
+## [0.8.0] - 30-Mar-2019
+
+### Added
+
+- New API for building layouts, a grid.Builder. Allows defining the layout
+ iteratively as repetitive Elements, Rows and Columns.
+- Containers now support margin around them and padding of their content.
+- Container now supports dynamic layout changes via the new Update method.
+
+### Changed
+
+- The Text widget now supports content wrapping on word boundaries.
+- The BarChart and SparkLine widgets now have a method that returns the
+ observed value capacity the last time Draw was called.
+- Moving widgetapi out of the internal directory to allow external users to
+ develop their own widgets.
+- Event delivery to widgets now has a stable defined order and happens when the
+ container is unlocked so that widgets can trigger dynamic layout changes.
+
+### Fixed
+
+- The termdash_test now correctly waits until all subscribers processed events,
+ not just received them.
+- Container focus tracker now correctly tracks focus changes in enlarged areas,
+ i.e. when the terminal size increased.
+- The BarChart, LineChart and SegmentDisplay widgets now protect against
+ external mutation of the values passed into them by copying the data they
+ receive.
+
+## [0.7.2] - 25-Feb-2019
+
+### Added
+
+- Test coverage for data only packages.
+
+### Changed
+
+- Refactoring packages that contained a mix of public and internal identifiers.
+
+#### Breaking API changes
+
+The following packages were refactored, no impact is expected as the removed
+identifiers shouldn't be used externally.
+
+- Functions align.Text and align.Rectangle were moved to a new
+ internal/alignfor package.
+- Types cell.Cell and cell.Buffer were moved into a new internal/canvas/buffer
+ package.
+
+## [0.7.1] - 24-Feb-2019
+
+### Fixed
+
+- Some of the packages that were moved into internal are required externally.
+ This release makes them available again.
+
+### Changed
+
+#### Breaking API changes
+
+- The draw.LineStyle enum was refactored into its own package
+ linestyle.LineStyle. Users will have to replace:
+
+ - draw.LineStyleNone -> linestyle.None
+ - draw.LineStyleLight -> linestyle.Light
+ - draw.LineStyleDouble -> linestyle.Double
+ - draw.LineStyleRound -> linestyle.Round
+
+## [0.7.0] - 24-Feb-2019
+
+### Added
+
+#### New widgets
+
+- The Button widget.
+
+#### Improvements to documentation
+
+- Clearly marked the public API surface by moving private packages into
+ internal directory.
+- Started a GitHub wiki for Termdash.
+
+#### Improvements to the LineChart widget
+
+- The LineChart widget can display X axis labels in vertical orientation.
+- The LineChart widget allows the user to specify a custom scale for the Y
+ axis.
+- The LineChart widget now has an option that disables scaling of the X axis.
+ Useful for applications that want to continuously feed data and make them
+ "roll" through the linechart.
+- The LineChart widget now has a method that returns the observed capacity of
+ the LineChart the last time Draw was called.
+- The LineChart widget now supports zoom of the content triggered by mouse
+ events.
+
+#### Improvements to the Text widget
+
+- The Text widget now has a Write option that atomically replaces the entire
+ text content.
+
+#### Improvements to the infrastructure
+
+- A function that draws text vertically.
+- A non-blocking event distribution system that can throttle repetitive events.
+- Generalized mouse button FSM for use in widgets that need to track mouse
+ button clicks.
+
+### Changed
+
+- Termbox is now initialized in 256 color mode by default.
+- The infrastructure now uses the non-blocking event distribution system to
+ distribute events to subscribers. Each widget is now an individual
+ subscriber.
+- The infrastructure now throttles event driven screen redraw rather than
+ redrawing for each input event.
+- Widgets can now specify the scope at which they want to receive keyboard and
+ mouse events.
+
+#### Breaking API changes
+
+##### High impact
+
+- The constructors of all the widgets now also return an error so that they
+ can validate the options. This is a breaking change for the following
+ widgets: BarChart, Gauge, LineChart, SparkLine, Text. The callers will have
+ to handle the returned error.
+
+##### Low impact
+
+- The container package no longer exports separate methods to receive Keyboard
+ and Mouse events which were replaced by a Subscribe method for the event
+ distribution system. This shouldn't affect users as the removed methods
+ aren't needed by container users.
+- The widgetapi.Options struct now uses an enum instead of a boolean when
+ widget specifies if it wants keyboard or mouse events. This only impacts
+ development of new widgets.
+
+### Fixed
+
+- The LineChart widget now correctly determines the Y axis scale when multiple
+ series are provided.
+- Lint issues in the codebase, and updated Travis configuration so that golint
+ is executed on every run.
+- Termdash now correctly starts in locales like zh_CN.UTF-8 where some of the
+ characters it uses internally can have ambiguous width.
+
+## [0.6.1] - 12-Feb-2019
+
+### Fixed
+
+- The LineChart widget now correctly places custom labels.
+
+## [0.6.0] - 07-Feb-2019
+
+### Added
+
+- The SegmentDisplay widget.
+- A CHANGELOG.
+- New line styles for borders.
+
+### Changed
+
+- Better recordings of the individual demos.
+
+### Fixed
+
+- The LineChart now has an option to change the behavior of the Y axis from
+ zero anchored to adaptive.
+- Lint errors reported on the Go report card.
+- Widgets now correctly handle a race when new user data are supplied between
+ calls to their Options() and Draw() methods.
+
+## [0.5.0] - 21-Jan-2019
+
+### Added
+
+- Draw primitives for drawing circles.
+- The Donut widget.
+
+### Fixed
+
+- Bugfixes in the braille canvas.
+- Lint errors reported on the Go report card.
+- Flaky behavior in termdash_test.
+
+## [0.4.0] - 15-Jan-2019
+
+### Added
+
+- 256 color support.
+- Variable size container splits.
+- A more complete demo of the functionality.
+
+### Changed
+
+- Updated documentation and README.
+
+## [0.3.0] - 13-Jan-2019
+
+### Added
+
+- Primitives for drawing lines.
+- Implementation of a Braille canvas.
+- The LineChart widget.
+
+## [0.2.0] - 02-Jul-2018
+
+### Added
+
+- The SparkLine widget.
+- The BarChart widget.
+- Manually triggered redraw.
+- Travis now checks for presence of licence headers.
+
+### Fixed
+
+- Fixing races in termdash_test.
+
+## 0.1.0 - 13-Jun-2018
+
+### Added
+
+- Documentation of the project and its goals.
+- Drawing infrastructure.
+- Testing infrastructure.
+- The Gauge widget.
+- The Text widget.
+
+[unreleased]: https://github.com/mum4k/termdash/compare/v0.12.2...devel
+[0.12.2]: https://github.com/mum4k/termdash/compare/v0.12.1...v0.12.2
+[0.12.1]: https://github.com/mum4k/termdash/compare/v0.12.0...v0.12.1
+[0.12.0]: https://github.com/mum4k/termdash/compare/v0.11.0...v0.12.0
+[0.11.0]: https://github.com/mum4k/termdash/compare/v0.10.0...v0.11.0
+[0.10.0]: https://github.com/mum4k/termdash/compare/v0.9.1...v0.10.0
+[0.9.1]: https://github.com/mum4k/termdash/compare/v0.9.0...v0.9.1
+[0.9.0]: https://github.com/mum4k/termdash/compare/v0.8.0...v0.9.0
+[0.8.0]: https://github.com/mum4k/termdash/compare/v0.7.2...v0.8.0
+[0.7.2]: https://github.com/mum4k/termdash/compare/v0.7.1...v0.7.2
+[0.7.1]: https://github.com/mum4k/termdash/compare/v0.7.0...v0.7.1
+[0.7.0]: https://github.com/mum4k/termdash/compare/v0.6.1...v0.7.0
+[0.6.1]: https://github.com/mum4k/termdash/compare/v0.6.0...v0.6.1
+[0.6.0]: https://github.com/mum4k/termdash/compare/v0.5.0...v0.6.0
+[0.5.0]: https://github.com/mum4k/termdash/compare/v0.4.0...v0.5.0
+[0.4.0]: https://github.com/mum4k/termdash/compare/v0.3.0...v0.4.0
+[0.3.0]: https://github.com/mum4k/termdash/compare/v0.2.0...v0.3.0
+[0.2.0]: https://github.com/mum4k/termdash/compare/v0.1.0...v0.2.0
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/CONTRIBUTING.md b/examples/go-dashboard/src/github.com/mum4k/termdash/CONTRIBUTING.md
new file mode 100644
index 000000000..9f2027288
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/CONTRIBUTING.md
@@ -0,0 +1,38 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Fork and merge into the "devel" branch
+
+All development in termdash repository must happen in the [devel
+branch](https://github.com/mum4k/termdash/tree/devel). The devel branch is
+merged into the master branch during release of each new version.
+
+When you fork the termdash repository, be sure to checkout the devel branch.
+When you are creating a pull request, be sure to pull back into the devel
+branch.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution;
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to <https://cla.developers.google.com/> to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
+
+## Community Guidelines
+
+This project follows [Google's Open Source Community
+Guidelines](https://opensource.google.com/conduct/).
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/LICENSE b/examples/go-dashboard/src/github.com/mum4k/termdash/LICENSE
new file mode 100644
index 000000000..261eeb9e9
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/README.md b/examples/go-dashboard/src/github.com/mum4k/termdash/README.md
new file mode 100644
index 000000000..8ecd1fd53
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/README.md
@@ -0,0 +1,215 @@
+[![Doc Status](https://godoc.org/github.com/mum4k/termdash?status.png)](https://godoc.org/github.com/mum4k/termdash)
+[![Build Status](https://travis-ci.com/mum4k/termdash.svg?branch=master)](https://travis-ci.com/mum4k/termdash)
+[![Sourcegraph](https://sourcegraph.com/github.com/mum4k/termdash/-/badge.svg)](https://sourcegraph.com/github.com/mum4k/termdash?badge)
+[![Coverage Status](https://coveralls.io/repos/github/mum4k/termdash/badge.svg?branch=master)](https://coveralls.io/github/mum4k/termdash?branch=master)
+[![Go Report Card](https://goreportcard.com/badge/github.com/mum4k/termdash)](https://goreportcard.com/report/github.com/mum4k/termdash)
+[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/mum4k/termdash/blob/master/LICENSE)
+[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)
+
+# [<img src="./doc/images/termdash.png" alt="termdashlogo" type="image/png" width="30%">](http://github.com/mum4k/termdash/wiki)
+
+Termdash is a cross-platform customizable terminal based dashboard.
+
+[<img src="./doc/images/termdashdemo_0_9_0.gif" alt="termdashdemo" type="image/gif">](termdashdemo/termdashdemo.go)
+
+The feature set is inspired by the
+[gizak/termui](http://github.com/gizak/termui) project, which in turn was
+inspired by
+[yaronn/blessed-contrib](http://github.com/yaronn/blessed-contrib).
+
+This rewrite focuses on code readability, maintainability and testability, see
+the [design goals](doc/design_goals.md). It aims to achieve the following
+[requirements](doc/requirements.md). See the [high-level design](doc/hld.md)
+for more details.
+
+# Public API and status
+
+The public API surface is documented in the
+[wiki](http://github.com/mum4k/termdash/wiki).
+
+Private packages can be identified by the presence of the **/private/**
+directory in their import path. Stability of the private packages isn't
+guaranteed and changes won't be backward compatible.
+
+There might still be breaking changes to the public API, at least until the
+project reaches version 1.0.0. Any breaking changes will be published in the
+[changelog](CHANGELOG.md).
+
+# Current feature set
+
+- Full support for terminal window resizing throughout the infrastructure.
+- Customizable layout, widget placement, borders, margins, padding, colors, etc.
+- Dynamic layout changes at runtime.
+- Binary tree and Grid forms of setting up the layout.
+- Focusable containers and widgets.
+- Processing of keyboard and mouse events.
+- Periodic and event driven screen redraw.
+- A library of widgets, see below.
+- UTF-8 for all text elements.
+- Drawing primitives (Go functions) for widget development with character and
+ sub-character resolution.
+
+# Installation
+
+To install this library, run the following:
+
+```go
+go get -u github.com/mum4k/termdash
+```
+
+# Usage
+
+The usage of most of these elements is demonstrated in
+[termdashdemo.go](termdashdemo/termdashdemo.go). To execute the demo:
+
+```go
+go run github.com/mum4k/termdash/termdashdemo/termdashdemo.go
+```
+
+# Documentation
+
+Please refer to the [Termdash wiki](http://github.com/mum4k/termdash/wiki) for
+all documentation and resources.
+
+# Implemented Widgets
+
+## The Button
+
+Allows users to interact with the application, each button press runs a callback function.
+Run the
+[buttondemo](widgets/button/buttondemo/buttondemo.go).
+
+```go
+go run github.com/mum4k/termdash/widgets/button/buttondemo/buttondemo.go
+```
+
+[<img src="./doc/images/buttondemo.gif" alt="buttondemo" type="image/gif" width="50%">](widgets/button/buttondemo/buttondemo.go)
+
+## The TextInput
+
+Allows users to interact with the application by entering, editing and
+submitting text data. Run the
+[textinputdemo](widgets/textinput/textinputdemo/textinputdemo.go).
+
+```go
+go run github.com/mum4k/termdash/widgets/textinput/textinputdemo/textinputdemo.go
+```
+
+[<img src="./doc/images/textinputdemo.gif" alt="textinputdemo" type="image/gif" width="80%">](widgets/textinput/textinputdemo/textinputdemo.go)
+
+## The Gauge
+
+Displays the progress of an operation. Run the
+[gaugedemo](widgets/gauge/gaugedemo/gaugedemo.go).
+
+```go
+go run github.com/mum4k/termdash/widgets/gauge/gaugedemo/gaugedemo.go
+```
+
+[<img src="./doc/images/gaugedemo.gif" alt="gaugedemo" type="image/gif">](widgets/gauge/gaugedemo/gaugedemo.go)
+
+## The Donut
+
+Visualizes progress of an operation as a partial or a complete donut. Run the
+[donutdemo](widgets/donut/donutdemo/donutdemo.go).
+
+```go
+go run github.com/mum4k/termdash/widgets/donut/donutdemo/donutdemo.go
+```
+
+[<img src="./doc/images/donutdemo.gif" alt="donutdemo" type="image/gif">](widgets/donut/donutdemo/donutdemo.go)
+
+## The Text
+
+Displays text content, supports trimming and scrolling of content. Run the
+[textdemo](widgets/text/textdemo/textdemo.go).
+
+```go
+go run github.com/mum4k/termdash/widgets/text/textdemo/textdemo.go
+```
+
+[<img src="./doc/images/textdemo.gif" alt="textdemo" type="image/gif">](widgets/text/textdemo/textdemo.go)
+
+## The SparkLine
+
+Draws a graph showing a series of values as vertical bars. The bars can have
+sub-cell height. Run the
+[sparklinedemo](widgets/sparkline/sparklinedemo/sparklinedemo.go).
+
+```go
+go run github.com/mum4k/termdash/widgets/sparkline/sparklinedemo/sparklinedemo.go
+```
+
+[<img src="./doc/images/sparklinedemo.gif" alt="sparklinedemo" type="image/gif" width="50%">](widgets/sparkline/sparklinedemo/sparklinedemo.go)
+
+## The BarChart
+
+Displays multiple bars showing relative ratios of values. Run the
+[barchartdemo](widgets/barchart/barchartdemo/barchartdemo.go).
+
+```go
+go run github.com/mum4k/termdash/widgets/barchart/barchartdemo/barchartdemo.go
+```
+
+[<img src="./doc/images/barchartdemo.gif" alt="barchartdemo" type="image/gif" width="50%">](widgets/barchart/barchartdemo/barchartdemo.go)
+
+## The LineChart
+
+Displays series of values on a line chart, supports zoom triggered by mouse
+events. Run the
+[linechartdemo](widgets/linechart/linechartdemo/linechartdemo.go).
+
+```go
+go run github.com/mum4k/termdash/widgets/linechart/linechartdemo/linechartdemo.go
+```
+
+[<img src="./doc/images/linechartdemo.gif" alt="linechartdemo" type="image/gif" width="70%">](widgets/linechart/linechartdemo/linechartdemo.go)
+
+## The SegmentDisplay
+
+Displays text by simulating a 16-segment display. Run the
+[segmentdisplaydemo](widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go).
+
+```go
+go run github.com/mum4k/termdash/widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go
+```
+
+[<img src="./doc/images/segmentdisplaydemo.gif" alt="segmentdisplaydemo" type="image/gif">](widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go)
+
+# Contributing
+
+If you are willing to contribute, improve the infrastructure or develop a
+widget, first of all Thank You! Your help is appreciated.
+
+Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines related
+to the Google's CLA, and code review requirements.
+
+As stated above the primary goal of this project is to develop readable, well
+designed code, the functionality and efficiency come second. This is achieved
+through detailed code reviews, design discussions and following of the [design
+guidelines](doc/design_guidelines.md). Please familiarize yourself with these
+before contributing.
+
+If you're developing a new widget, please see the [widget
+development](doc/widget_development.md) section.
+
+Termdash uses [this branching model](https://nvie.com/posts/a-successful-git-branching-model/). When you fork the repository, base your changes off the [devel](https://github.com/mum4k/termdash/tree/devel) branch and the pull request should merge it back to the devel branch. Commits to the master branch are limited to releases, major bug fixes and documentation updates.
+
+# Similar projects in Go
+
+- [clui](https://github.com/VladimirMarkelov/clui)
+- [gocui](https://github.com/jroimartin/gocui)
+- [gowid](https://github.com/gcla/gowid)
+- [termui](https://github.com/gizak/termui)
+- [tui-go](https://github.com/marcusolsson/tui-go)
+- [tview](https://github.com/rivo/tview)
+
+# Projects using Termdash
+
+- [datadash](https://github.com/keithknott26/datadash): Visualize streaming or tabular data inside the terminal.
+- [grafterm](https://github.com/slok/grafterm): Metrics dashboards visualization on the terminal.
+- [perfstat](https://github.com/flaviostutz/perfstat): Analyze and show tips about possible bottlenecks in Linux systems.
+
+# Disclaimer
+
+This is not an official Google product.
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/align/align.go b/examples/go-dashboard/src/github.com/mum4k/termdash/align/align.go
new file mode 100644
index 000000000..da4087f57
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/align/align.go
@@ -0,0 +1,70 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package align defines constants representing types of alignment.
+package align
+
+// Horizontal indicates the type of horizontal alignment.
+type Horizontal int
+
+// String implements fmt.Stringer()
+func (h Horizontal) String() string {
+ if n, ok := horizontalNames[h]; ok {
+ return n
+ }
+ return "HorizontalUnknown"
+}
+
+// horizontalNames maps Horizontal values to human readable names.
+var horizontalNames = map[Horizontal]string{
+ HorizontalLeft: "HorizontalLeft",
+ HorizontalCenter: "HorizontalCenter",
+ HorizontalRight: "HorizontalRight",
+}
+
+const (
+ // HorizontalLeft is left alignment along the horizontal axis.
+ HorizontalLeft Horizontal = iota
+ // HorizontalCenter is center alignment along the horizontal axis.
+ HorizontalCenter
+ // HorizontalRight is right alignment along the horizontal axis.
+ HorizontalRight
+)
+
+// Vertical indicates the type of vertical alignment.
+type Vertical int
+
+// String implements fmt.Stringer()
+func (v Vertical) String() string {
+ if n, ok := verticalNames[v]; ok {
+ return n
+ }
+ return "VerticalUnknown"
+}
+
+// verticalNames maps Vertical values to human readable names.
+var verticalNames = map[Vertical]string{
+ VerticalTop: "VerticalTop",
+ VerticalMiddle: "VerticalMiddle",
+ VerticalBottom: "VerticalBottom",
+}
+
+const (
+ // VerticalTop is top alignment along the vertical axis.
+ VerticalTop Vertical = iota
+ // VerticalMiddle is middle alignment along the vertical axis.
+ VerticalMiddle
+ // VerticalBottom is bottom alignment along the vertical axis.
+ VerticalBottom
+)
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/cell/cell.go b/examples/go-dashboard/src/github.com/mum4k/termdash/cell/cell.go
new file mode 100644
index 000000000..c3eb6df24
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/cell/cell.go
@@ -0,0 +1,64 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package cell implements cell options and attributes.
+package cell
+
+// Option is used to provide options for cells on a 2-D terminal.
+type Option interface {
+ // Set sets the provided option.
+ Set(*Options)
+}
+
+// Options stores the provided options.
+type Options struct {
+ FgColor Color
+ BgColor Color
+}
+
+// Set allows existing options to be passed as an option.
+func (o *Options) Set(other *Options) {
+ *other = *o
+}
+
+// NewOptions returns a new Options instance after applying the provided options.
+func NewOptions(opts ...Option) *Options {
+ o := &Options{}
+ for _, opt := range opts {
+ opt.Set(o)
+ }
+ return o
+}
+
+// option implements Option.
+type option func(*Options)
+
+// Set implements Option.set.
+func (co option) Set(opts *Options) {
+ co(opts)
+}
+
+// FgColor sets the foreground color of the cell.
+func FgColor(color Color) Option {
+ return option(func(co *Options) {
+ co.FgColor = color
+ })
+}
+
+// BgColor sets the background color of the cell.
+func BgColor(color Color) Option {
+ return option(func(co *Options) {
+ co.BgColor = color
+ })
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/cell/color.go b/examples/go-dashboard/src/github.com/mum4k/termdash/cell/color.go
new file mode 100644
index 000000000..94560b74c
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/cell/color.go
@@ -0,0 +1,106 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cell
+
+import (
+ "fmt"
+)
+
+// color.go defines constants for cell colors.
+
+// Color is the color of a cell.
+type Color int
+
+// String implements fmt.Stringer()
+func (cc Color) String() string {
+ if n, ok := colorNames[cc]; ok {
+ return n
+ }
+ return fmt.Sprintf("Color:%d", cc)
+}
+
+// colorNames maps Color values to human readable names.
+var colorNames = map[Color]string{
+ ColorDefault: "ColorDefault",
+ ColorBlack: "ColorBlack",
+ ColorRed: "ColorRed",
+ ColorGreen: "ColorGreen",
+ ColorYellow: "ColorYellow",
+ ColorBlue: "ColorBlue",
+ ColorMagenta: "ColorMagenta",
+ ColorCyan: "ColorCyan",
+ ColorWhite: "ColorWhite",
+}
+
+// The supported terminal colors.
+const (
+ ColorDefault Color = iota
+
+ // 8 "system" colors.
+ ColorBlack
+ ColorRed
+ ColorGreen
+ ColorYellow
+ ColorBlue
+ ColorMagenta
+ ColorCyan
+ ColorWhite
+)
+
+// ColorNumber sets a color using its number.
+// Make sure your terminal is set to a terminalapi.ColorMode that supports the
+// target color. The provided value must be in the range 0-255.
+// Larger or smaller values will be reset to the default color.
+//
+// For reference on these colors see the Xterm number in:
+// https://jonasjacek.github.io/colors/
+func ColorNumber(n int) Color {
+ if n < 0 || n > 255 {
+ return ColorDefault
+ }
+ return Color(n + 1) // Colors are off-by-one due to ColorDefault being zero.
+}
+
+// ColorRGB6 sets a color using the 6x6x6 terminal color.
+// Make sure your terminal is set to the terminalapi.ColorMode256 mode.
+// The provided values (r, g, b) must be in the range 0-5.
+// Larger or smaller values will be reset to the default color.
+//
+// For reference on these colors see:
+// https://superuser.com/questions/783656/whats-the-deal-with-terminal-colors
+func ColorRGB6(r, g, b int) Color {
+ for _, c := range []int{r, g, b} {
+ if c < 0 || c > 5 {
+ return ColorDefault
+ }
+ }
+ return Color(0x10 + 36*r + 6*g + b + 1) // Colors are off-by-one due to ColorDefault being zero.
+}
+
+// ColorRGB24 sets a color using the 24 bit web color scheme.
+// Make sure your terminal is set to the terminalapi.ColorMode256 mode.
+// The provided values (r, g, b) must be in the range 0-255.
+// Larger or smaller values will be reset to the default color.
+//
+// For reference on these colors see the RGB column in:
+// https://jonasjacek.github.io/colors/
+func ColorRGB24(r, g, b int) Color {
+ for _, c := range []int{r, g, b} {
+ if c < 0 || c > 255 {
+ return ColorDefault
+ }
+ }
+ return ColorRGB6(r/51, g/51, b/51)
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/container/container.go b/examples/go-dashboard/src/github.com/mum4k/termdash/container/container.go
new file mode 100644
index 000000000..54cef7856
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/container/container.go
@@ -0,0 +1,471 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/*
+Package container defines a type that wraps other containers or widgets.
+
+The container supports splitting container into sub containers, defining
+container styles and placing widgets. The container also creates and manages
+canvases assigned to the placed widgets.
+*/
+package container
+
+import (
+ "errors"
+ "fmt"
+ "image"
+ "sync"
+
+ "github.com/mum4k/termdash/linestyle"
+ "github.com/mum4k/termdash/private/alignfor"
+ "github.com/mum4k/termdash/private/area"
+ "github.com/mum4k/termdash/private/event"
+ "github.com/mum4k/termdash/terminal/terminalapi"
+ "github.com/mum4k/termdash/widgetapi"
+)
+
+// Container wraps either sub containers or widgets and positions them on the
+// terminal.
+// This is thread-safe.
+type Container struct {
+ // parent is the parent container, nil if this is the root container.
+ parent *Container
+ // The sub containers, if these aren't nil, the widget must be.
+ first *Container
+ second *Container
+
+ // term is the terminal this container is placed on.
+ // All containers in the tree share the same terminal.
+ term terminalapi.Terminal
+
+ // focusTracker tracks the active (focused) container.
+ // All containers in the tree share the same tracker.
+ focusTracker *focusTracker
+
+ // area is the area of the terminal this container has access to.
+ // Initialized the first time Draw is called.
+ area image.Rectangle
+
+ // opts are the options provided to the container.
+ opts *options
+
+ // clearNeeded indicates if the terminal needs to be cleared next time we
+ // are clearNeeded the container.
+ // This is required if the container was updated and thus the layout might
+ // have changed.
+ clearNeeded bool
+
+ // mu protects the container tree.
+ // All containers in the tree share the same lock.
+ mu *sync.Mutex
+}
+
+// String represents the container metadata in a human readable format.
+// Implements fmt.Stringer.
+func (c *Container) String() string {
+ return fmt.Sprintf("Container@%p{parent:%p, first:%p, second:%p, area:%+v}", c, c.parent, c.first, c.second, c.area)
+}
+
+// New returns a new root container that will use the provided terminal and
+// applies the provided options.
+func New(t terminalapi.Terminal, opts ...Option) (*Container, error) {
+ root := &Container{
+ term: t,
+ opts: newOptions( /* parent = */ nil),
+ mu: &sync.Mutex{},
+ }
+
+ // Initially the root is focused.
+ root.focusTracker = newFocusTracker(root)
+ if err := applyOptions(root, opts...); err != nil {
+ return nil, err
+ }
+ if err := validateOptions(root); err != nil {
+ return nil, err
+ }
+ return root, nil
+}
+
+// newChild creates a new child container of the given parent.
+func newChild(parent *Container, opts []Option) (*Container, error) {
+ child := &Container{
+ parent: parent,
+ term: parent.term,
+ focusTracker: parent.focusTracker,
+ opts: newOptions(parent.opts),
+ mu: parent.mu,
+ }
+ if err := applyOptions(child, opts...); err != nil {
+ return nil, err
+ }
+ return child, nil
+}
+
+// hasBorder determines if this container has a border.
+func (c *Container) hasBorder() bool {
+ return c.opts.border != linestyle.None
+}
+
+// hasWidget determines if this container has a widget.
+func (c *Container) hasWidget() bool {
+ return c.opts.widget != nil
+}
+
+// usable returns the usable area in this container.
+// This depends on whether the container has a border, etc.
+func (c *Container) usable() image.Rectangle {
+ if c.hasBorder() {
+ return area.ExcludeBorder(c.area)
+ }
+ return c.area
+}
+
+// widgetArea returns the area in the container that is available for the
+// widget's canvas. Takes the container border, widget's requested maximum size
+// and ratio and container's alignment into account.
+// Returns a zero area if the container has no widget.
+func (c *Container) widgetArea() (image.Rectangle, error) {
+ if !c.hasWidget() {
+ return image.ZR, nil
+ }
+
+ padded, err := c.opts.padding.apply(c.usable())
+ if err != nil {
+ return image.ZR, err
+ }
+ wOpts := c.opts.widget.Options()
+
+ adjusted := padded
+ if maxX := wOpts.MaximumSize.X; maxX > 0 && adjusted.Dx() > maxX {
+ adjusted.Max.X -= adjusted.Dx() - maxX
+ }
+ if maxY := wOpts.MaximumSize.Y; maxY > 0 && adjusted.Dy() > maxY {
+ adjusted.Max.Y -= adjusted.Dy() - maxY
+ }
+
+ if wOpts.Ratio.X > 0 && wOpts.Ratio.Y > 0 {
+ adjusted = area.WithRatio(adjusted, wOpts.Ratio)
+ }
+ aligned, err := alignfor.Rectangle(padded, adjusted, c.opts.hAlign, c.opts.vAlign)
+ if err != nil {
+ return image.ZR, err
+ }
+ return aligned, nil
+}
+
+// split splits the container's usable area into child areas.
+// Panics if the container isn't configured for a split.
+func (c *Container) split() (image.Rectangle, image.Rectangle, error) {
+ ar, err := c.opts.padding.apply(c.usable())
+ if err != nil {
+ return image.ZR, image.ZR, err
+ }
+ if c.opts.splitFixed > DefaultSplitFixed {
+ if c.opts.split == splitTypeVertical {
+ return area.VSplitCells(ar, c.opts.splitFixed)
+ }
+ return area.HSplitCells(ar, c.opts.splitFixed)
+ }
+
+ if c.opts.split == splitTypeVertical {
+ return area.VSplit(ar, c.opts.splitPercent)
+ }
+ return area.HSplit(ar, c.opts.splitPercent)
+}
+
+// createFirst creates and returns the first sub container of this container.
+func (c *Container) createFirst(opts []Option) error {
+ first, err := newChild(c, opts)
+ if err != nil {
+ return err
+ }
+ c.first = first
+ return nil
+}
+
+// createSecond creates and returns the second sub container of this container.
+func (c *Container) createSecond(opts []Option) error {
+ second, err := newChild(c, opts)
+ if err != nil {
+ return err
+ }
+ c.second = second
+ return nil
+}
+
+// Draw draws this container and all of its sub containers.
+func (c *Container) Draw() error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ if c.clearNeeded {
+ if err := c.term.Clear(); err != nil {
+ return fmt.Errorf("term.Clear => error: %v", err)
+ }
+ c.clearNeeded = false
+ }
+
+ // Update the area we are tracking for focus in case the terminal size
+ // changed.
+ ar, err := area.FromSize(c.term.Size())
+ if err != nil {
+ return err
+ }
+ c.focusTracker.updateArea(ar)
+ return drawTree(c)
+}
+
+// Update updates container with the specified id by setting the provided
+// options. This can be used to perform dynamic layout changes, i.e. anything
+// between replacing the widget in the container and completely changing the
+// layout and splits.
+// The argument id must match exactly one container with that was created with
+// matching ID() option. The argument id must not be an empty string.
+func (c *Container) Update(id string, opts ...Option) error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ target, err := findID(c, id)
+ if err != nil {
+ return err
+ }
+ c.clearNeeded = true
+
+ if err := applyOptions(target, opts...); err != nil {
+ return err
+ }
+ if err := validateOptions(c); err != nil {
+ return err
+ }
+
+ // The currently focused container might not be reachable anymore, because
+ // it was under the target. If that is so, move the focus up to the target.
+ if !c.focusTracker.reachableFrom(c) {
+ c.focusTracker.setActive(target)
+ }
+ return nil
+}
+
+// updateFocus processes the mouse event and determines if it changes the
+// focused container.
+// Caller must hold c.mu.
+func (c *Container) updateFocus(m *terminalapi.Mouse) {
+ target := pointCont(c, m.Position)
+ if target == nil { // Ignore mouse clicks where no containers are.
+ return
+ }
+ c.focusTracker.mouse(target, m)
+}
+
+// processEvent processes events delivered to the container.
+func (c *Container) processEvent(ev terminalapi.Event) error {
+ // This is done in two stages.
+ // 1) under lock we traverse the container and identify all targets
+ // (widgets) that should receive the event.
+ // 2) lock is released and events are delivered to the widgets. Widgets
+ // themselves are thread-safe. Lock must be releases when delivering,
+ // because some widgets might try to mutate the container when they
+ // receive the event, like dynamically change the layout.
+ c.mu.Lock()
+ sendFn, err := c.prepareEvTargets(ev)
+ c.mu.Unlock()
+ if err != nil {
+ return err
+ }
+ return sendFn()
+}
+
+// prepareEvTargets returns a closure, that when called delivers the event to
+// widgets that registered for it.
+// Also processes the event on behalf of the container (tracks keyboard focus).
+// Caller must hold c.mu.
+func (c *Container) prepareEvTargets(ev terminalapi.Event) (func() error, error) {
+ switch e := ev.(type) {
+ case *terminalapi.Mouse:
+ c.updateFocus(ev.(*terminalapi.Mouse))
+
+ targets, err := c.mouseEvTargets(e)
+ if err != nil {
+ return nil, err
+ }
+ return func() error {
+ for _, mt := range targets {
+ if err := mt.widget.Mouse(mt.ev); err != nil {
+ return err
+ }
+ }
+ return nil
+ }, nil
+
+ case *terminalapi.Keyboard:
+ targets := c.keyEvTargets()
+ return func() error {
+ for _, w := range targets {
+ if err := w.Keyboard(e); err != nil {
+ return err
+ }
+ }
+ return nil
+ }, nil
+
+ default:
+ return nil, fmt.Errorf("container received an unsupported event type %T", ev)
+ }
+}
+
+// keyEvTargets returns those widgets found in the container that should
+// receive this keyboard event.
+// Caller must hold c.mu.
+func (c *Container) keyEvTargets() []widgetapi.Widget {
+ var (
+ errStr string
+ widgets []widgetapi.Widget
+ )
+
+ // All the widgets that should receive this event.
+ // For now stable ordering (preOrder).
+ preOrder(c, &errStr, visitFunc(func(cur *Container) error {
+ if !cur.hasWidget() {
+ return nil
+ }
+
+ wOpt := cur.opts.widget.Options()
+ switch wOpt.WantKeyboard {
+ case widgetapi.KeyScopeNone:
+ // Widget doesn't want any keyboard events.
+ return nil
+
+ case widgetapi.KeyScopeFocused:
+ if cur.focusTracker.isActive(cur) {
+ widgets = append(widgets, cur.opts.widget)
+ }
+
+ case widgetapi.KeyScopeGlobal:
+ widgets = append(widgets, cur.opts.widget)
+ }
+ return nil
+ }))
+ return widgets
+}
+
+// mouseEvTarget contains a mouse event adjusted relative to the widget's area
+// and the widget that should receive it.
+type mouseEvTarget struct {
+ // widget is the widget that should receive the mouse event.
+ widget widgetapi.Widget
+ // ev is the adjusted mouse event.
+ ev *terminalapi.Mouse
+}
+
+// newMouseEvTarget returns a new newMouseEvTarget.
+func newMouseEvTarget(w widgetapi.Widget, wArea image.Rectangle, ev *terminalapi.Mouse) *mouseEvTarget {
+ return &mouseEvTarget{
+ widget: w,
+ ev: adjustMouseEv(ev, wArea),
+ }
+}
+
+// mouseEvTargets returns those widgets found in the container that should
+// receive this mouse event.
+// Caller must hold c.mu.
+func (c *Container) mouseEvTargets(m *terminalapi.Mouse) ([]*mouseEvTarget, error) {
+ var (
+ errStr string
+ widgets []*mouseEvTarget
+ )
+
+ // All the widgets that should receive this event.
+ // For now stable ordering (preOrder).
+ preOrder(c, &errStr, visitFunc(func(cur *Container) error {
+ if !cur.hasWidget() {
+ return nil
+ }
+
+ wOpts := cur.opts.widget.Options()
+ wa, err := cur.widgetArea()
+ if err != nil {
+ return err
+ }
+
+ switch wOpts.WantMouse {
+ case widgetapi.MouseScopeNone:
+ // Widget doesn't want any mouse events.
+ return nil
+
+ case widgetapi.MouseScopeWidget:
+ // Only if the event falls inside of the widget's canvas.
+ if m.Position.In(wa) {
+ widgets = append(widgets, newMouseEvTarget(cur.opts.widget, wa, m))
+ }
+
+ case widgetapi.MouseScopeContainer:
+ // Only if the event falls inside the widget's parent container.
+ if m.Position.In(cur.area) {
+ widgets = append(widgets, newMouseEvTarget(cur.opts.widget, wa, m))
+ }
+
+ case widgetapi.MouseScopeGlobal:
+ // Widget wants all mouse events.
+ widgets = append(widgets, newMouseEvTarget(cur.opts.widget, wa, m))
+ }
+ return nil
+ }))
+
+ if errStr != "" {
+ return nil, errors.New(errStr)
+ }
+ return widgets, nil
+}
+
+// Subscribe tells the container to subscribe itself and widgets to the
+// provided event distribution system.
+// This method is private to termdash, stability isn't guaranteed and changes
+// won't be backward compatible.
+func (c *Container) Subscribe(eds *event.DistributionSystem) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ // maxReps is the maximum number of repetitive events towards widgets
+ // before we throttle them.
+ const maxReps = 10
+
+ // Subscriber the container itself in order to track keyboard focus.
+ want := []terminalapi.Event{
+ &terminalapi.Keyboard{},
+ &terminalapi.Mouse{},
+ }
+ eds.Subscribe(want, func(ev terminalapi.Event) {
+ if err := c.processEvent(ev); err != nil {
+ eds.Event(terminalapi.NewErrorf("failed to process event %v: %v", ev, err))
+ }
+ }, event.MaxRepetitive(maxReps))
+}
+
+// adjustMouseEv adjusts the mouse event relative to the widget area.
+func adjustMouseEv(m *terminalapi.Mouse, wArea image.Rectangle) *terminalapi.Mouse {
+ // The sent mouse coordinate is relative to the widget canvas, i.e. zero
+ // based, even though the widget might not be in the top left corner on the
+ // terminal.
+ offset := wArea.Min
+ if m.Position.In(wArea) {
+ return &terminalapi.Mouse{
+ Position: m.Position.Sub(offset),
+ Button: m.Button,
+ }
+ }
+ return &terminalapi.Mouse{
+ Position: image.Point{-1, -1},
+ Button: m.Button,
+ }
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/container/draw.go b/examples/go-dashboard/src/github.com/mum4k/termdash/container/draw.go
new file mode 100644
index 000000000..d186b1272
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/container/draw.go
@@ -0,0 +1,175 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package container
+
+// draw.go contains logic to draw containers and the contained widgets.
+
+import (
+ "errors"
+ "fmt"
+ "image"
+
+ "github.com/mum4k/termdash/cell"
+ "github.com/mum4k/termdash/private/area"
+ "github.com/mum4k/termdash/private/canvas"
+ "github.com/mum4k/termdash/private/draw"
+ "github.com/mum4k/termdash/widgetapi"
+)
+
+// drawTree draws this container and all of its sub containers.
+func drawTree(c *Container) error {
+ var errStr string
+
+ root := rootCont(c)
+ size := root.term.Size()
+ ar, err := root.opts.margin.apply(image.Rect(0, 0, size.X, size.Y))
+ if err != nil {
+ return err
+ }
+ root.area = ar
+
+ preOrder(root, &errStr, visitFunc(func(c *Container) error {
+ first, second, err := c.split()
+ if err != nil {
+ return err
+ }
+ if c.first != nil {
+ ar, err := c.first.opts.margin.apply(first)
+ if err != nil {
+ return err
+ }
+ c.first.area = ar
+ }
+
+ if c.second != nil {
+ ar, err := c.second.opts.margin.apply(second)
+ if err != nil {
+ return err
+ }
+ c.second.area = ar
+ }
+ return drawCont(c)
+ }))
+ if errStr != "" {
+ return errors.New(errStr)
+ }
+ return nil
+}
+
+// drawBorder draws the border around the container if requested.
+func drawBorder(c *Container) error {
+ if !c.hasBorder() {
+ return nil
+ }
+
+ cvs, err := canvas.New(c.area)
+ if err != nil {
+ return err
+ }
+
+ ar, err := area.FromSize(cvs.Size())
+ if err != nil {
+ return err
+ }
+
+ var cOpts []cell.Option
+ if c.focusTracker.isActive(c) {
+ cOpts = append(cOpts, cell.FgColor(c.opts.inherited.focusedColor))
+ } else {
+ cOpts = append(cOpts, cell.FgColor(c.opts.inherited.borderColor))
+ }
+
+ if err := draw.Border(cvs, ar,
+ draw.BorderLineStyle(c.opts.border),
+ draw.BorderTitle(c.opts.borderTitle, draw.OverrunModeThreeDot, cOpts...),
+ draw.BorderTitleAlign(c.opts.borderTitleHAlign),
+ draw.BorderCellOpts(cOpts...),
+ ); err != nil {
+ return err
+ }
+ return cvs.Apply(c.term)
+}
+
+// drawWidget requests the widget to draw on the canvas.
+func drawWidget(c *Container) error {
+ widgetArea, err := c.widgetArea()
+ if err != nil {
+ return err
+ }
+ if widgetArea == image.ZR {
+ return nil
+ }
+
+ if !c.hasWidget() {
+ return nil
+ }
+
+ needSize := image.Point{1, 1}
+ wOpts := c.opts.widget.Options()
+ if wOpts.MinimumSize.X > 0 && wOpts.MinimumSize.Y > 0 {
+ needSize = wOpts.MinimumSize
+ }
+
+ if widgetArea.Dx() < needSize.X || widgetArea.Dy() < needSize.Y {
+ return drawResize(c, c.usable())
+ }
+
+ cvs, err := canvas.New(widgetArea)
+ if err != nil {
+ return err
+ }
+
+ meta := &widgetapi.Meta{
+ Focused: c.focusTracker.isActive(c),
+ }
+
+ if err := c.opts.widget.Draw(cvs, meta); err != nil {
+ return err
+ }
+ return cvs.Apply(c.term)
+}
+
+// drawResize draws an unicode character indicating that the size is too small to draw this container.
+// Does nothing if the size is smaller than one cell, leaving no space for the character.
+func drawResize(c *Container, area image.Rectangle) error {
+ if area.Dx() < 1 || area.Dy() < 1 {
+ return nil
+ }
+
+ cvs, err := canvas.New(area)
+ if err != nil {
+ return err
+ }
+ if err := draw.ResizeNeeded(cvs); err != nil {
+ return err
+ }
+ return cvs.Apply(c.term)
+}
+
+// drawCont draws the container and its widget.
+func drawCont(c *Container) error {
+ if us := c.usable(); us.Dx() <= 0 || us.Dy() <= 0 {
+ return drawResize(c, c.area)
+ }
+
+ if err := drawBorder(c); err != nil {
+ return fmt.Errorf("unable to draw container border: %v", err)
+ }
+
+ if err := drawWidget(c); err != nil {
+ return fmt.Errorf("unable to draw widget %T: %v", c.opts.widget, err)
+ }
+ return nil
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/container/focus.go b/examples/go-dashboard/src/github.com/mum4k/termdash/container/focus.go
new file mode 100644
index 000000000..4320eea73
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/container/focus.go
@@ -0,0 +1,116 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package container
+
+// focus.go contains code that tracks the focused container.
+
+import (
+ "image"
+
+ "github.com/mum4k/termdash/mouse"
+ "github.com/mum4k/termdash/private/button"
+ "github.com/mum4k/termdash/terminal/terminalapi"
+)
+
+// pointCont finds the top-most (on the screen) container whose area contains
+// the given point. Returns nil if none of the containers in the tree contain
+// this point.
+func pointCont(c *Container, p image.Point) *Container {
+ var (
+ errStr string
+ cont *Container
+ )
+ postOrder(rootCont(c), &errStr, visitFunc(func(c *Container) error {
+ if p.In(c.area) && cont == nil {
+ cont = c
+ }
+ return nil
+ }))
+ return cont
+}
+
+// focusTracker tracks the active (focused) container.
+// This is not thread-safe, the implementation assumes that the owner of
+// focusTracker performs locking.
+type focusTracker struct {
+ // container is the currently focused container.
+ container *Container
+
+ // candidate is the container that might become focused next. I.e. we got
+ // a mouse click and now waiting for a release or a timeout.
+ candidate *Container
+
+ // buttonFSM is a state machine tracking mouse clicks in containers and
+ // moving focus from one container to the next.
+ buttonFSM *button.FSM
+}
+
+// newFocusTracker returns a new focus tracker with focus set at the provided
+// container.
+func newFocusTracker(c *Container) *focusTracker {
+ return &focusTracker{
+ container: c,
+ // Mouse FSM tracking clicks inside the entire area for the root
+ // container.
+ buttonFSM: button.NewFSM(mouse.ButtonLeft, c.area),
+ }
+}
+
+// isActive determines if the provided container is the currently active container.
+func (ft *focusTracker) isActive(c *Container) bool {
+ return ft.container == c
+}
+
+// setActive sets the currently active container to the one provided.
+func (ft *focusTracker) setActive(c *Container) {
+ ft.container = c
+}
+
+// mouse identifies mouse events that change the focused container and track
+// the focused container in the tree.
+// The argument c is the container onto which the mouse event landed.
+func (ft *focusTracker) mouse(target *Container, m *terminalapi.Mouse) {
+ clicked, bs := ft.buttonFSM.Event(m)
+ switch {
+ case bs == button.Down:
+ ft.candidate = target
+ case bs == button.Up && clicked:
+ if target == ft.candidate {
+ ft.container = target
+ }
+ }
+}
+
+// updateArea updates the area that the focus tracker considers active for
+// mouse clicks.
+func (ft *focusTracker) updateArea(ar image.Rectangle) {
+ ft.buttonFSM.UpdateArea(ar)
+}
+
+// reachableFrom asserts whether the currently focused container is reachable
+// from the provided node in the tree.
+func (ft *focusTracker) reachableFrom(node *Container) bool {
+ var (
+ errStr string
+ reachable bool
+ )
+ preOrder(node, &errStr, visitFunc(func(c *Container) error {
+ if c == ft.container {
+ reachable = true
+ }
+ return nil
+ }))
+ return reachable
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/container/options.go b/examples/go-dashboard/src/github.com/mum4k/termdash/container/options.go
new file mode 100644
index 000000000..2d34af4db
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/container/options.go
@@ -0,0 +1,817 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package container
+
+// options.go defines container options.
+
+import (
+ "errors"
+ "fmt"
+ "image"
+
+ "github.com/mum4k/termdash/align"
+ "github.com/mum4k/termdash/cell"
+ "github.com/mum4k/termdash/linestyle"
+ "github.com/mum4k/termdash/private/area"
+ "github.com/mum4k/termdash/widgetapi"
+)
+
+// applyOptions applies the options to the container and validates them.
+func applyOptions(c *Container, opts ...Option) error {
+ for _, opt := range opts {
+ if err := opt.set(c); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// ensure all the container identifiers are either empty or unique.
+func validateIds(c *Container, seen map[string]bool) error {
+ if c.opts.id == "" {
+ return nil
+ } else if seen[c.opts.id] {
+ return fmt.Errorf("duplicate container ID %q", c.opts.id)
+ }
+ seen[c.opts.id] = true
+
+ return nil
+}
+
+// ensure all the container only have one split modifier.
+func validateSplits(c *Container) error {
+ if c.opts.splitFixed > DefaultSplitFixed && c.opts.splitPercent != DefaultSplitPercent {
+ return fmt.Errorf(
+ "only one of splitFixed `%v` and splitPercent `%v` is allowed to be set per container",
+ c.opts.splitFixed,
+ c.opts.splitPercent,
+ )
+ }
+
+ return nil
+}
+
+// validateOptions validates options set in the container tree.
+func validateOptions(c *Container) error {
+ var errStr string
+ seenID := map[string]bool{}
+ preOrder(c, &errStr, func(c *Container) error {
+ if err := validateIds(c, seenID); err != nil {
+ return err
+ }
+ if err := validateSplits(c); err != nil {
+ return err
+ }
+
+ return nil
+ })
+ if errStr != "" {
+ return errors.New(errStr)
+ }
+
+ return nil
+}
+
+// Option is used to provide options to a container.
+type Option interface {
+ // set sets the provided option.
+ set(*Container) error
+}
+
+// options stores the options provided to the container.
+type options struct {
+ // id is the identifier provided by the user.
+ id string
+
+ // inherited are options that are inherited by child containers.
+ inherited inherited
+
+ // split identifies how is this container split.
+ split splitType
+ splitPercent int
+ splitFixed int
+
+ // widget is the widget in the container.
+ // A container can have either two sub containers (left and right) or a
+ // widget. But not both.
+ widget widgetapi.Widget
+
+ // Alignment of the widget if present.
+ hAlign align.Horizontal
+ vAlign align.Vertical
+
+ // border is the border around the container.
+ border linestyle.LineStyle
+ borderTitle string
+ borderTitleHAlign align.Horizontal
+
+ // padding is a space reserved between the outer edge of the container and
+ // its content (the widget or other sub-containers).
+ padding padding
+
+ // margin is a space reserved on the outside of the container.
+ margin margin
+}
+
+// margin stores the configured margin for the container.
+// For each margin direction, only one of the percentage or cells is set.
+type margin struct {
+ topCells int
+ topPerc int
+ rightCells int
+ rightPerc int
+ bottomCells int
+ bottomPerc int
+ leftCells int
+ leftPerc int
+}
+
+// apply applies the configured margin to the area.
+func (p *margin) apply(ar image.Rectangle) (image.Rectangle, error) {
+ switch {
+ case p.topCells != 0 || p.rightCells != 0 || p.bottomCells != 0 || p.leftCells != 0:
+ return area.Shrink(ar, p.topCells, p.rightCells, p.bottomCells, p.leftCells)
+ case p.topPerc != 0 || p.rightPerc != 0 || p.bottomPerc != 0 || p.leftPerc != 0:
+ return area.ShrinkPercent(ar, p.topPerc, p.rightPerc, p.bottomPerc, p.leftPerc)
+ }
+ return ar, nil
+}
+
+// padding stores the configured padding for the container.
+// For each padding direction, only one of the percentage or cells is set.
+type padding struct {
+ topCells int
+ topPerc int
+ rightCells int
+ rightPerc int
+ bottomCells int
+ bottomPerc int
+ leftCells int
+ leftPerc int
+}
+
+// apply applies the configured padding to the area.
+func (p *padding) apply(ar image.Rectangle) (image.Rectangle, error) {
+ switch {
+ case p.topCells != 0 || p.rightCells != 0 || p.bottomCells != 0 || p.leftCells != 0:
+ return area.Shrink(ar, p.topCells, p.rightCells, p.bottomCells, p.leftCells)
+ case p.topPerc != 0 || p.rightPerc != 0 || p.bottomPerc != 0 || p.leftPerc != 0:
+ return area.ShrinkPercent(ar, p.topPerc, p.rightPerc, p.bottomPerc, p.leftPerc)
+ }
+ return ar, nil
+}
+
+// inherited contains options that are inherited by child containers.
+type inherited struct {
+ // borderColor is the color used for the border.
+ borderColor cell.Color
+ // focusedColor is the color used for the border when focused.
+ focusedColor cell.Color
+}
+
+// newOptions returns a new options instance with the default values.
+// Parent are the inherited options from the parent container or nil if these
+// options are for a container with no parent (the root).
+func newOptions(parent *options) *options {
+ opts := &options{
+ inherited: inherited{
+ focusedColor: cell.ColorYellow,
+ },
+ hAlign: align.HorizontalCenter,
+ vAlign: align.VerticalMiddle,
+ splitPercent: DefaultSplitPercent,
+ splitFixed: DefaultSplitFixed,
+ }
+ if parent != nil {
+ opts.inherited = parent.inherited
+ }
+ return opts
+}
+
+// option implements Option.
+type option func(*Container) error
+
+// set implements Option.set.
+func (o option) set(c *Container) error {
+ return o(c)
+}
+
+// SplitOption is used when splitting containers.
+type SplitOption interface {
+ // setSplit sets the provided split option.
+ setSplit(*options) error
+}
+
+// splitOption implements SplitOption.
+type splitOption func(*options) error
+
+// setSplit implements SplitOption.setSplit.
+func (so splitOption) setSplit(opts *options) error {
+ return so(opts)
+}
+
+// DefaultSplitPercent is the default value for the SplitPercent option.
+const DefaultSplitPercent = 50
+
+// DefaultSplitFixed is the default value for the SplitFixed option.
+const DefaultSplitFixed = -1
+
+// SplitPercent sets the relative size of the split as percentage of the available space.
+// When using SplitVertical, the provided size is applied to the new left
+// container, the new right container gets the reminder of the size.
+// When using SplitHorizontal, the provided size is applied to the new top
+// container, the new bottom container gets the reminder of the size.
+// The provided value must be a positive number in the range 0 < p < 100.
+// If not provided, defaults to DefaultSplitPercent.
+func SplitPercent(p int) SplitOption {
+ return splitOption(func(opts *options) error {
+ if min, max := 0, 100; p <= min || p >= max {
+ return fmt.Errorf("invalid split percentage %d, must be in range %d < p < %d", p, min, max)
+ }
+ opts.splitPercent = p
+ return nil
+ })
+}
+
+// SplitFixed sets the size of the first container to be a fixed value
+// and makes the second container take up the remaining space.
+// When using SplitVertical, the provided size is applied to the new left
+// container, the new right container gets the reminder of the size.
+// When using SplitHorizontal, the provided size is applied to the new top
+// container, the new bottom container gets the reminder of the size.
+// The provided value must be a positive number in the range 0 <= cells.
+// If SplitFixed() is not specified, it defaults to SplitPercent() and its given value.
+// Only one of SplitFixed() and SplitPercent() can be specified per container.
+func SplitFixed(cells int) SplitOption {
+ return splitOption(func(opts *options) error {
+ if cells < 0 {
+ return fmt.Errorf("invalid fixed value %d, must be in range %d <= cells", cells, 0)
+ }
+ opts.splitFixed = cells
+ return nil
+ })
+}
+
+// SplitVertical splits the container along the vertical axis into two sub
+// containers. The use of this option removes any widget placed at this
+// container, containers with sub containers cannot contain widgets.
+func SplitVertical(l LeftOption, r RightOption, opts ...SplitOption) Option {
+ return option(func(c *Container) error {
+ c.opts.split = splitTypeVertical
+ c.opts.widget = nil
+ for _, opt := range opts {
+ if err := opt.setSplit(c.opts); err != nil {
+ return err
+ }
+ }
+
+ if err := c.createFirst(l.lOpts()); err != nil {
+ return err
+ }
+ return c.createSecond(r.rOpts())
+ })
+}
+
+// SplitHorizontal splits the container along the horizontal axis into two sub
+// containers. The use of this option removes any widget placed at this
+// container, containers with sub containers cannot contain widgets.
+func SplitHorizontal(t TopOption, b BottomOption, opts ...SplitOption) Option {
+ return option(func(c *Container) error {
+ c.opts.split = splitTypeHorizontal
+ c.opts.widget = nil
+ for _, opt := range opts {
+ if err := opt.setSplit(c.opts); err != nil {
+ return err
+ }
+ }
+
+ if err := c.createFirst(t.tOpts()); err != nil {
+ return err
+ }
+
+ return c.createSecond(b.bOpts())
+ })
+}
+
+// ID sets an identifier for this container.
+// This ID can be later used to perform dynamic layout changes by passing new
+// options to this container. When provided, it must be a non-empty string that
+// is unique among all the containers.
+func ID(id string) Option {
+ return option(func(c *Container) error {
+ if id == "" {
+ return errors.New("the ID cannot be an empty string")
+ }
+ c.opts.id = id
+ return nil
+ })
+}
+
+// Clear clears this container.
+// If the container contains a widget, the widget is removed.
+// If the container had any sub containers or splits, they are removed.
+func Clear() Option {
+ return option(func(c *Container) error {
+ c.opts.widget = nil
+ c.first = nil
+ c.second = nil
+ return nil
+ })
+}
+
+// PlaceWidget places the provided widget into the container.
+// The use of this option removes any sub containers. Containers with sub
+// containers cannot have widgets.
+func PlaceWidget(w widgetapi.Widget) Option {
+ return option(func(c *Container) error {
+ c.opts.widget = w
+ c.first = nil
+ c.second = nil
+ return nil
+ })
+}
+
+// MarginTop sets reserved space outside of the container at its top.
+// The provided number is the absolute margin in cells and must be zero or a
+// positive integer. Only one of MarginTop or MarginTopPercent can be specified.
+func MarginTop(cells int) Option {
+ return option(func(c *Container) error {
+ if min := 0; cells < min {
+ return fmt.Errorf("invalid MarginTop(%d), must be in range %d <= value", cells, min)
+ }
+ if c.opts.margin.topPerc > 0 {
+ return fmt.Errorf("cannot specify both MarginTop(%d) and MarginTopPercent(%d)", cells, c.opts.margin.topPerc)
+ }
+ c.opts.margin.topCells = cells
+ return nil
+ })
+}
+
+// MarginRight sets reserved space outside of the container at its right.
+// The provided number is the absolute margin in cells and must be zero or a
+// positive integer. Only one of MarginRight or MarginRightPercent can be specified.
+func MarginRight(cells int) Option {
+ return option(func(c *Container) error {
+ if min := 0; cells < min {
+ return fmt.Errorf("invalid MarginRight(%d), must be in range %d <= value", cells, min)
+ }
+ if c.opts.margin.rightPerc > 0 {
+ return fmt.Errorf("cannot specify both MarginRight(%d) and MarginRightPercent(%d)", cells, c.opts.margin.rightPerc)
+ }
+ c.opts.margin.rightCells = cells
+ return nil
+ })
+}
+
+// MarginBottom sets reserved space outside of the container at its bottom.
+// The provided number is the absolute margin in cells and must be zero or a
+// positive integer. Only one of MarginBottom or MarginBottomPercent can be specified.
+func MarginBottom(cells int) Option {
+ return option(func(c *Container) error {
+ if min := 0; cells < min {
+ return fmt.Errorf("invalid MarginBottom(%d), must be in range %d <= value", cells, min)
+ }
+ if c.opts.margin.bottomPerc > 0 {
+ return fmt.Errorf("cannot specify both MarginBottom(%d) and MarginBottomPercent(%d)", cells, c.opts.margin.bottomPerc)
+ }
+ c.opts.margin.bottomCells = cells
+ return nil
+ })
+}
+
+// MarginLeft sets reserved space outside of the container at its left.
+// The provided number is the absolute margin in cells and must be zero or a
+// positive integer. Only one of MarginLeft or MarginLeftPercent can be specified.
+func MarginLeft(cells int) Option {
+ return option(func(c *Container) error {
+ if min := 0; cells < min {
+ return fmt.Errorf("invalid MarginLeft(%d), must be in range %d <= value", cells, min)
+ }
+ if c.opts.margin.leftPerc > 0 {
+ return fmt.Errorf("cannot specify both MarginLeft(%d) and MarginLeftPercent(%d)", cells, c.opts.margin.leftPerc)
+ }
+ c.opts.margin.leftCells = cells
+ return nil
+ })
+}
+
+// MarginTopPercent sets reserved space outside of the container at its top.
+// The provided number is a relative margin defined as percentage of the container's height.
+// Only one of MarginTop or MarginTopPercent can be specified.
+// The value must be in range 0 <= value <= 100.
+func MarginTopPercent(perc int) Option {
+ return option(func(c *Container) error {
+ if min, max := 0, 100; perc < min || perc > max {
+ return fmt.Errorf("invalid MarginTopPercent(%d), must be in range %d <= value <= %d", perc, min, max)
+ }
+ if c.opts.margin.topCells > 0 {
+ return fmt.Errorf("cannot specify both MarginTopPercent(%d) and MarginTop(%d)", perc, c.opts.margin.topCells)
+ }
+ c.opts.margin.topPerc = perc
+ return nil
+ })
+}
+
+// MarginRightPercent sets reserved space outside of the container at its right.
+// The provided number is a relative margin defined as percentage of the container's height.
+// Only one of MarginRight or MarginRightPercent can be specified.
+// The value must be in range 0 <= value <= 100.
+func MarginRightPercent(perc int) Option {
+ return option(func(c *Container) error {
+ if min, max := 0, 100; perc < min || perc > max {
+ return fmt.Errorf("invalid MarginRightPercent(%d), must be in range %d <= value <= %d", perc, min, max)
+ }
+ if c.opts.margin.rightCells > 0 {
+ return fmt.Errorf("cannot specify both MarginRightPercent(%d) and MarginRight(%d)", perc, c.opts.margin.rightCells)
+ }
+ c.opts.margin.rightPerc = perc
+ return nil
+ })
+}
+
+// MarginBottomPercent sets reserved space outside of the container at its bottom.
+// The provided number is a relative margin defined as percentage of the container's height.
+// Only one of MarginBottom or MarginBottomPercent can be specified.
+// The value must be in range 0 <= value <= 100.
+func MarginBottomPercent(perc int) Option {
+ return option(func(c *Container) error {
+ if min, max := 0, 100; perc < min || perc > max {
+ return fmt.Errorf("invalid MarginBottomPercent(%d), must be in range %d <= value <= %d", perc, min, max)
+ }
+ if c.opts.margin.bottomCells > 0 {
+ return fmt.Errorf("cannot specify both MarginBottomPercent(%d) and MarginBottom(%d)", perc, c.opts.margin.bottomCells)
+ }
+ c.opts.margin.bottomPerc = perc
+ return nil
+ })
+}
+
+// MarginLeftPercent sets reserved space outside of the container at its left.
+// The provided number is a relative margin defined as percentage of the container's height.
+// Only one of MarginLeft or MarginLeftPercent can be specified.
+// The value must be in range 0 <= value <= 100.
+func MarginLeftPercent(perc int) Option {
+ return option(func(c *Container) error {
+ if min, max := 0, 100; perc < min || perc > max {
+ return fmt.Errorf("invalid MarginLeftPercent(%d), must be in range %d <= value <= %d", perc, min, max)
+ }
+ if c.opts.margin.leftCells > 0 {
+ return fmt.Errorf("cannot specify both MarginLeftPercent(%d) and MarginLeft(%d)", perc, c.opts.margin.leftCells)
+ }
+ c.opts.margin.leftPerc = perc
+ return nil
+ })
+}
+
+// PaddingTop sets reserved space between container and the top side of its widget.
+// The widget's area size is decreased to accommodate the padding.
+// The provided number is the absolute padding in cells and must be zero or a
+// positive integer. Only one of PaddingTop or PaddingTopPercent can be specified.
+func PaddingTop(cells int) Option {
+ return option(func(c *Container) error {
+ if min := 0; cells < min {
+ return fmt.Errorf("invalid PaddingTop(%d), must be in range %d <= value", cells, min)
+ }
+ if c.opts.padding.topPerc > 0 {
+ return fmt.Errorf("cannot specify both PaddingTop(%d) and PaddingTopPercent(%d)", cells, c.opts.padding.topPerc)
+ }
+ c.opts.padding.topCells = cells
+ return nil
+ })
+}
+
+// PaddingRight sets reserved space between container and the right side of its widget.
+// The widget's area size is decreased to accommodate the padding.
+// The provided number is the absolute padding in cells and must be zero or a
+// positive integer. Only one of PaddingRight or PaddingRightPercent can be specified.
+func PaddingRight(cells int) Option {
+ return option(func(c *Container) error {
+ if min := 0; cells < min {
+ return fmt.Errorf("invalid PaddingRight(%d), must be in range %d <= value", cells, min)
+ }
+ if c.opts.padding.rightPerc > 0 {
+ return fmt.Errorf("cannot specify both PaddingRight(%d) and PaddingRightPercent(%d)", cells, c.opts.padding.rightPerc)
+ }
+ c.opts.padding.rightCells = cells
+ return nil
+ })
+}
+
+// PaddingBottom sets reserved space between container and the bottom side of its widget.
+// The widget's area size is decreased to accommodate the padding.
+// The provided number is the absolute padding in cells and must be zero or a
+// positive integer. Only one of PaddingBottom or PaddingBottomPercent can be specified.
+func PaddingBottom(cells int) Option {
+ return option(func(c *Container) error {
+ if min := 0; cells < min {
+ return fmt.Errorf("invalid PaddingBottom(%d), must be in range %d <= value", cells, min)
+ }
+ if c.opts.padding.bottomPerc > 0 {
+ return fmt.Errorf("cannot specify both PaddingBottom(%d) and PaddingBottomPercent(%d)", cells, c.opts.padding.bottomPerc)
+ }
+ c.opts.padding.bottomCells = cells
+ return nil
+ })
+}
+
+// PaddingLeft sets reserved space between container and the left side of its widget.
+// The widget's area size is decreased to accommodate the padding.
+// The provided number is the absolute padding in cells and must be zero or a
+// positive integer. Only one of PaddingLeft or PaddingLeftPercent can be specified.
+func PaddingLeft(cells int) Option {
+ return option(func(c *Container) error {
+ if min := 0; cells < min {
+ return fmt.Errorf("invalid PaddingLeft(%d), must be in range %d <= value", cells, min)
+ }
+ if c.opts.padding.leftPerc > 0 {
+ return fmt.Errorf("cannot specify both PaddingLeft(%d) and PaddingLeftPercent(%d)", cells, c.opts.padding.leftPerc)
+ }
+ c.opts.padding.leftCells = cells
+ return nil
+ })
+}
+
+// PaddingTopPercent sets reserved space between container and the top side of
+// its widget. The widget's area size is decreased to accommodate the padding.
+// The provided number is a relative padding defined as percentage of the
+// container's height. The value must be in range 0 <= value <= 100.
+// Only one of PaddingTop or PaddingTopPercent can be specified.
+func PaddingTopPercent(perc int) Option {
+ return option(func(c *Container) error {
+ if min, max := 0, 100; perc < min || perc > max {
+ return fmt.Errorf("invalid PaddingTopPercent(%d), must be in range %d <= value <= %d", perc, min, max)
+ }
+ if c.opts.padding.topCells > 0 {
+ return fmt.Errorf("cannot specify both PaddingTopPercent(%d) and PaddingTop(%d)", perc, c.opts.padding.topCells)
+ }
+ c.opts.padding.topPerc = perc
+ return nil
+ })
+}
+
+// PaddingRightPercent sets reserved space between container and the right side of
+// its widget. The widget's area size is decreased to accommodate the padding.
+// The provided number is a relative padding defined as percentage of the
+// container's width. The value must be in range 0 <= value <= 100.
+// Only one of PaddingRight or PaddingRightPercent can be specified.
+func PaddingRightPercent(perc int) Option {
+ return option(func(c *Container) error {
+ if min, max := 0, 100; perc < min || perc > max {
+ return fmt.Errorf("invalid PaddingRightPercent(%d), must be in range %d <= value <= %d", perc, min, max)
+ }
+ if c.opts.padding.rightCells > 0 {
+ return fmt.Errorf("cannot specify both PaddingRightPercent(%d) and PaddingRight(%d)", perc, c.opts.padding.rightCells)
+ }
+ c.opts.padding.rightPerc = perc
+ return nil
+ })
+}
+
+// PaddingBottomPercent sets reserved space between container and the bottom side of
+// its widget. The widget's area size is decreased to accommodate the padding.
+// The provided number is a relative padding defined as percentage of the
+// container's height. The value must be in range 0 <= value <= 100.
+// Only one of PaddingBottom or PaddingBottomPercent can be specified.
+func PaddingBottomPercent(perc int) Option {
+ return option(func(c *Container) error {
+ if min, max := 0, 100; perc < min || perc > max {
+ return fmt.Errorf("invalid PaddingBottomPercent(%d), must be in range %d <= value <= %d", perc, min, max)
+ }
+ if c.opts.padding.bottomCells > 0 {
+ return fmt.Errorf("cannot specify both PaddingBottomPercent(%d) and PaddingBottom(%d)", perc, c.opts.padding.bottomCells)
+ }
+ c.opts.padding.bottomPerc = perc
+ return nil
+ })
+}
+
+// PaddingLeftPercent sets reserved space between container and the left side of
+// its widget. The widget's area size is decreased to accommodate the padding.
+// The provided number is a relative padding defined as percentage of the
+// container's width. The value must be in range 0 <= value <= 100.
+// Only one of PaddingLeft or PaddingLeftPercent can be specified.
+func PaddingLeftPercent(perc int) Option {
+ return option(func(c *Container) error {
+ if min, max := 0, 100; perc < min || perc > max {
+ return fmt.Errorf("invalid PaddingLeftPercent(%d), must be in range %d <= value <= %d", perc, min, max)
+ }
+ if c.opts.padding.leftCells > 0 {
+ return fmt.Errorf("cannot specify both PaddingLeftPercent(%d) and PaddingLeft(%d)", perc, c.opts.padding.leftCells)
+ }
+ c.opts.padding.leftPerc = perc
+ return nil
+ })
+}
+
+// AlignHorizontal sets the horizontal alignment for the widget placed in the
+// container. Has no effect if the container contains no widget.
+// Defaults to alignment in the center.
+func AlignHorizontal(h align.Horizontal) Option {
+ return option(func(c *Container) error {
+ c.opts.hAlign = h
+ return nil
+ })
+}
+
+// AlignVertical sets the vertical alignment for the widget placed in the container.
+// Has no effect if the container contains no widget.
+// Defaults to alignment in the middle.
+func AlignVertical(v align.Vertical) Option {
+ return option(func(c *Container) error {
+ c.opts.vAlign = v
+ return nil
+ })
+}
+
+// Border configures the container to have a border of the specified style.
+func Border(ls linestyle.LineStyle) Option {
+ return option(func(c *Container) error {
+ c.opts.border = ls
+ return nil
+ })
+}
+
+// BorderTitle sets a text title within the border.
+func BorderTitle(title string) Option {
+ return option(func(c *Container) error {
+ c.opts.borderTitle = title
+ return nil
+ })
+}
+
+// BorderTitleAlignLeft aligns the border title on the left.
+func BorderTitleAlignLeft() Option {
+ return option(func(c *Container) error {
+ c.opts.borderTitleHAlign = align.HorizontalLeft
+ return nil
+ })
+}
+
+// BorderTitleAlignCenter aligns the border title in the center.
+func BorderTitleAlignCenter() Option {
+ return option(func(c *Container) error {
+ c.opts.borderTitleHAlign = align.HorizontalCenter
+ return nil
+ })
+}
+
+// BorderTitleAlignRight aligns the border title on the right.
+func BorderTitleAlignRight() Option {
+ return option(func(c *Container) error {
+ c.opts.borderTitleHAlign = align.HorizontalRight
+ return nil
+ })
+}
+
+// BorderColor sets the color of the border around the container.
+// This option is inherited to sub containers created by container splits.
+func BorderColor(color cell.Color) Option {
+ return option(func(c *Container) error {
+ c.opts.inherited.borderColor = color
+ return nil
+ })
+}
+
+// FocusedColor sets the color of the border around the container when it has
+// keyboard focus.
+// This option is inherited to sub containers created by container splits.
+func FocusedColor(color cell.Color) Option {
+ return option(func(c *Container) error {
+ c.opts.inherited.focusedColor = color
+ return nil
+ })
+}
+
+// splitType identifies how a container is split.
+type splitType int
+
+// String implements fmt.Stringer()
+func (st splitType) String() string {
+ if n, ok := splitTypeNames[st]; ok {
+ return n
+ }
+ return "splitTypeUnknown"
+}
+
+// splitTypeNames maps splitType values to human readable names.
+var splitTypeNames = map[splitType]string{
+ splitTypeVertical: "splitTypeVertical",
+ splitTypeHorizontal: "splitTypeHorizontal",
+}
+
+const (
+ splitTypeVertical splitType = iota
+ splitTypeHorizontal
+)
+
+// LeftOption is used to provide options to the left sub container after a
+// vertical split of the parent.
+type LeftOption interface {
+ // lOpts returns the options.
+ lOpts() []Option
+}
+
+// leftOption implements LeftOption.
+type leftOption func() []Option
+
+// lOpts implements LeftOption.lOpts.
+func (lo leftOption) lOpts() []Option {
+ if lo == nil {
+ return nil
+ }
+ return lo()
+}
+
+// Left applies options to the left sub container after a vertical split of the parent.
+func Left(opts ...Option) LeftOption {
+ return leftOption(func() []Option {
+ return opts
+ })
+}
+
+// RightOption is used to provide options to the right sub container after a
+// vertical split of the parent.
+type RightOption interface {
+ // rOpts returns the options.
+ rOpts() []Option
+}
+
+// rightOption implements RightOption.
+type rightOption func() []Option
+
+// rOpts implements RightOption.rOpts.
+func (lo rightOption) rOpts() []Option {
+ if lo == nil {
+ return nil
+ }
+ return lo()
+}
+
+// Right applies options to the right sub container after a vertical split of the parent.
+func Right(opts ...Option) RightOption {
+ return rightOption(func() []Option {
+ return opts
+ })
+}
+
+// TopOption is used to provide options to the top sub container after a
+// horizontal split of the parent.
+type TopOption interface {
+ // tOpts returns the options.
+ tOpts() []Option
+}
+
+// topOption implements TopOption.
+type topOption func() []Option
+
+// tOpts implements TopOption.tOpts.
+func (lo topOption) tOpts() []Option {
+ if lo == nil {
+ return nil
+ }
+ return lo()
+}
+
+// Top applies options to the top sub container after a horizontal split of the parent.
+func Top(opts ...Option) TopOption {
+ return topOption(func() []Option {
+ return opts
+ })
+}
+
+// BottomOption is used to provide options to the bottom sub container after a
+// horizontal split of the parent.
+type BottomOption interface {
+ // bOpts returns the options.
+ bOpts() []Option
+}
+
+// bottomOption implements BottomOption.
+type bottomOption func() []Option
+
+// bOpts implements BottomOption.bOpts.
+func (lo bottomOption) bOpts() []Option {
+ if lo == nil {
+ return nil
+ }
+ return lo()
+}
+
+// Bottom applies options to the bottom sub container after a horizontal split of the parent.
+func Bottom(opts ...Option) BottomOption {
+ return bottomOption(func() []Option {
+ return opts
+ })
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/container/traversal.go b/examples/go-dashboard/src/github.com/mum4k/termdash/container/traversal.go
new file mode 100644
index 000000000..f728b50ba
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/container/traversal.go
@@ -0,0 +1,86 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package container
+
+import (
+ "errors"
+ "fmt"
+)
+
+// traversal.go provides functions that navigate the container tree.
+
+// rootCont returns the root container.
+func rootCont(c *Container) *Container {
+ for p := c.parent; p != nil; p = c.parent {
+ c = p
+ }
+ return c
+}
+
+// visitFunc is executed during traversals when node is visited.
+// If the visit function returns an error, the traversal terminates and the
+// errStr is set to the text of the returned error.
+type visitFunc func(*Container) error
+
+// preOrder performs pre-order DFS traversal on the container tree.
+func preOrder(c *Container, errStr *string, visit visitFunc) {
+ if c == nil || *errStr != "" {
+ return
+ }
+
+ if err := visit(c); err != nil {
+ *errStr = err.Error()
+ return
+ }
+ preOrder(c.first, errStr, visit)
+ preOrder(c.second, errStr, visit)
+}
+
+// postOrder performs post-order DFS traversal on the container tree.
+func postOrder(c *Container, errStr *string, visit visitFunc) {
+ if c == nil || *errStr != "" {
+ return
+ }
+
+ postOrder(c.first, errStr, visit)
+ postOrder(c.second, errStr, visit)
+ if err := visit(c); err != nil {
+ *errStr = err.Error()
+ return
+ }
+}
+
+// findID finds container with the provided ID.
+// Returns an error of there is no container with the specified ID.
+func findID(root *Container, id string) (*Container, error) {
+ if id == "" {
+ return nil, errors.New("the container ID must not be empty")
+ }
+
+ var (
+ errStr string
+ cont *Container
+ )
+ preOrder(root, &errStr, visitFunc(func(c *Container) error {
+ if c.opts.id == id {
+ cont = c
+ }
+ return nil
+ }))
+ if cont == nil {
+ return nil, fmt.Errorf("cannot find container with ID %q", id)
+ }
+ return cont, nil
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/go.mod b/examples/go-dashboard/src/github.com/mum4k/termdash/go.mod
new file mode 100644
index 000000000..3a81b3235
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/go.mod
@@ -0,0 +1,10 @@
+module github.com/mum4k/termdash
+
+go 1.14
+
+require (
+ github.com/gdamore/tcell v1.3.0
+ github.com/kylelemons/godebug v1.1.0
+ github.com/mattn/go-runewidth v0.0.9
+ github.com/nsf/termbox-go v0.0.0-20200204031403-4d2b513ad8be
+)
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/keyboard/keyboard.go b/examples/go-dashboard/src/github.com/mum4k/termdash/keyboard/keyboard.go
new file mode 100644
index 000000000..3a852b326
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/keyboard/keyboard.go
@@ -0,0 +1,172 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package keyboard defines well known keyboard keys and shortcuts.
+package keyboard
+
+// Key represents a single button on the keyboard.
+// Printable characters are set to their ASCII/Unicode rune value.
+// Non-printable (control) characters are equal to one of the constants defined
+// below.
+type Key rune
+
+// String implements fmt.Stringer()
+func (b Key) String() string {
+ if n, ok := buttonNames[b]; ok {
+ return n
+ } else if b >= 0 {
+ return string(b)
+ }
+ return "KeyUnknown"
+}
+
+// buttonNames maps Key values to human readable names.
+var buttonNames = map[Key]string{
+ KeyF1: "KeyF1",
+ KeyF2: "KeyF2",
+ KeyF3: "KeyF3",
+ KeyF4: "KeyF4",
+ KeyF5: "KeyF5",
+ KeyF6: "KeyF6",
+ KeyF7: "KeyF7",
+ KeyF8: "KeyF8",
+ KeyF9: "KeyF9",
+ KeyF10: "KeyF10",
+ KeyF11: "KeyF11",
+ KeyF12: "KeyF12",
+ KeyInsert: "KeyInsert",
+ KeyDelete: "KeyDelete",
+ KeyHome: "KeyHome",
+ KeyEnd: "KeyEnd",
+ KeyPgUp: "KeyPgUp",
+ KeyPgDn: "KeyPgDn",
+ KeyArrowUp: "KeyArrowUp",
+ KeyArrowDown: "KeyArrowDown",
+ KeyArrowLeft: "KeyArrowLeft",
+ KeyArrowRight: "KeyArrowRight",
+ KeyCtrlTilde: "KeyCtrlTilde",
+ KeyCtrlA: "KeyCtrlA",
+ KeyCtrlB: "KeyCtrlB",
+ KeyCtrlC: "KeyCtrlC",
+ KeyCtrlD: "KeyCtrlD",
+ KeyCtrlE: "KeyCtrlE",
+ KeyCtrlF: "KeyCtrlF",
+ KeyCtrlG: "KeyCtrlG",
+ KeyBackspace: "KeyBackspace",
+ KeyTab: "KeyTab",
+ KeyCtrlJ: "KeyCtrlJ",
+ KeyCtrlK: "KeyCtrlK",
+ KeyCtrlL: "KeyCtrlL",
+ KeyEnter: "KeyEnter",
+ KeyCtrlN: "KeyCtrlN",
+ KeyCtrlO: "KeyCtrlO",
+ KeyCtrlP: "KeyCtrlP",
+ KeyCtrlQ: "KeyCtrlQ",
+ KeyCtrlR: "KeyCtrlR",
+ KeyCtrlS: "KeyCtrlS",
+ KeyCtrlT: "KeyCtrlT",
+ KeyCtrlU: "KeyCtrlU",
+ KeyCtrlV: "KeyCtrlV",
+ KeyCtrlW: "KeyCtrlW",
+ KeyCtrlX: "KeyCtrlX",
+ KeyCtrlY: "KeyCtrlY",
+ KeyCtrlZ: "KeyCtrlZ",
+ KeyEsc: "KeyEsc",
+ KeyCtrl4: "KeyCtrl4",
+ KeyCtrl5: "KeyCtrl5",
+ KeyCtrl6: "KeyCtrl6",
+ KeyCtrl7: "KeyCtrl7",
+ KeySpace: "KeySpace",
+ KeyBackspace2: "KeyBackspace2",
+}
+
+// Printable characters, but worth having constants for them.
+const (
+ KeySpace = ' '
+)
+
+// Negative values for non-printable characters.
+const (
+ KeyF1 Key = -(iota + 1)
+ KeyF2
+ KeyF3
+ KeyF4
+ KeyF5
+ KeyF6
+ KeyF7
+ KeyF8
+ KeyF9
+ KeyF10
+ KeyF11
+ KeyF12
+ KeyInsert
+ KeyDelete
+ KeyHome
+ KeyEnd
+ KeyPgUp
+ KeyPgDn
+ KeyArrowUp
+ KeyArrowDown
+ KeyArrowLeft
+ KeyArrowRight
+ KeyCtrlTilde
+ KeyCtrlA
+ KeyCtrlB
+ KeyCtrlC
+ KeyCtrlD
+ KeyCtrlE
+ KeyCtrlF
+ KeyCtrlG
+ KeyBackspace
+ KeyTab
+ KeyCtrlJ
+ KeyCtrlK
+ KeyCtrlL
+ KeyEnter
+ KeyCtrlN
+ KeyCtrlO
+ KeyCtrlP
+ KeyCtrlQ
+ KeyCtrlR
+ KeyCtrlS
+ KeyCtrlT
+ KeyCtrlU
+ KeyCtrlV
+ KeyCtrlW
+ KeyCtrlX
+ KeyCtrlY
+ KeyCtrlZ
+ KeyEsc
+ KeyCtrl4
+ KeyCtrl5
+ KeyCtrl6
+ KeyCtrl7
+ KeyBackspace2
+)
+
+// Keys declared as duplicates by termbox.
+const (
+ KeyCtrl2 Key = KeyCtrlTilde
+ KeyCtrlSpace Key = KeyCtrlTilde
+ KeyCtrlH Key = KeyBackspace
+ KeyCtrlI Key = KeyTab
+ KeyCtrlM Key = KeyEnter
+ KeyCtrlLsqBracket Key = KeyEsc
+ KeyCtrl3 Key = KeyEsc
+ KeyCtrlBackslash Key = KeyCtrl4
+ KeyCtrlRsqBracket Key = KeyCtrl5
+ KeyCtrlSlash Key = KeyCtrl7
+ KeyCtrlUnderscore Key = KeyCtrl7
+ KeyCtrl8 Key = KeyBackspace2
+)
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/linestyle/linestyle.go b/examples/go-dashboard/src/github.com/mum4k/termdash/linestyle/linestyle.go
new file mode 100644
index 000000000..c34fc3959
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/linestyle/linestyle.go
@@ -0,0 +1,51 @@
+// Copyright 2019 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package linestyle defines various line styles.
+package linestyle
+
+// LineStyle defines the supported line styles.
+type LineStyle int
+
+// String implements fmt.Stringer()
+func (ls LineStyle) String() string {
+ if n, ok := lineStyleNames[ls]; ok {
+ return n
+ }
+ return "LineStyleUnknown"
+}
+
+// lineStyleNames maps LineStyle values to human readable names.
+var lineStyleNames = map[LineStyle]string{
+ None: "LineStyleNone",
+ Light: "LineStyleLight",
+ Double: "LineStyleDouble",
+ Round: "LineStyleRound",
+}
+
+// Supported line styles.
+// See https://en.wikipedia.org/wiki/Box-drawing_character.
+const (
+ // None indicates that no line should be present.
+ None LineStyle = iota
+
+ // Light is line style using the '─' characters.
+ Light
+
+ // Double is line style using the '═' characters.
+ Double
+
+ // Round is line style using the rounded corners '╭' characters.
+ Round
+)
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/mouse/mouse.go b/examples/go-dashboard/src/github.com/mum4k/termdash/mouse/mouse.go
new file mode 100644
index 000000000..d21e1d310
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/mouse/mouse.go
@@ -0,0 +1,48 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package mouse defines known mouse buttons.
+package mouse
+
+// Button represents a mouse button.
+type Button int
+
+// String implements fmt.Stringer()
+func (b Button) String() string {
+ if n, ok := buttonNames[b]; ok {
+ return n
+ }
+ return "ButtonUnknown"
+}
+
+// buttonNames maps Button values to human readable names.
+var buttonNames = map[Button]string{
+ ButtonLeft: "ButtonLeft",
+ ButtonRight: "ButtonRight",
+ ButtonMiddle: "ButtonMiddle",
+ ButtonRelease: "ButtonRelease",
+ ButtonWheelUp: "ButtonWheelUp",
+ ButtonWheelDown: "ButtonWheelDown",
+}
+
+// Buttons recognized on the mouse.
+const (
+ buttonUnknown Button = iota
+ ButtonLeft
+ ButtonRight
+ ButtonMiddle
+ ButtonRelease
+ ButtonWheelUp
+ ButtonWheelDown
+)
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/alignfor/alignfor.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/alignfor/alignfor.go
new file mode 100644
index 000000000..93cbac844
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/alignfor/alignfor.go
@@ -0,0 +1,128 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package alignfor provides functions that align elements.
+package alignfor
+
+import (
+ "fmt"
+ "image"
+ "strings"
+
+ "github.com/mum4k/termdash/align"
+ "github.com/mum4k/termdash/private/runewidth"
+ "github.com/mum4k/termdash/private/wrap"
+)
+
+// hAlign aligns the given area in the rectangle horizontally.
+func hAlign(rect image.Rectangle, ar image.Rectangle, h align.Horizontal) (image.Rectangle, error) {
+ gap := rect.Dx() - ar.Dx()
+ switch h {
+ case align.HorizontalRight:
+ // Use gap from above.
+ case align.HorizontalCenter:
+ gap /= 2
+ case align.HorizontalLeft:
+ gap = 0
+ default:
+ return image.ZR, fmt.Errorf("unsupported horizontal alignment %v", h)
+ }
+
+ return image.Rect(
+ rect.Min.X+gap,
+ ar.Min.Y,
+ rect.Min.X+gap+ar.Dx(),
+ ar.Max.Y,
+ ), nil
+}
+
+// vAlign aligns the given area in the rectangle vertically.
+func vAlign(rect image.Rectangle, ar image.Rectangle, v align.Vertical) (image.Rectangle, error) {
+ gap := rect.Dy() - ar.Dy()
+ switch v {
+ case align.VerticalBottom:
+ // Use gap from above.
+ case align.VerticalMiddle:
+ gap /= 2
+ case align.VerticalTop:
+ gap = 0
+ default:
+ return image.ZR, fmt.Errorf("unsupported vertical alignment %v", v)
+ }
+
+ return image.Rect(
+ ar.Min.X,
+ rect.Min.Y+gap,
+ ar.Max.X,
+ rect.Min.Y+gap+ar.Dy(),
+ ), nil
+}
+
+// Rectangle aligns the area within the rectangle returning the
+// aligned area. The area must fall within the rectangle.
+func Rectangle(rect image.Rectangle, ar image.Rectangle, h align.Horizontal, v align.Vertical) (image.Rectangle, error) {
+ if !ar.In(rect) {
+ return image.ZR, fmt.Errorf("cannot align area %v inside rectangle %v, the area falls outside of the rectangle", ar, rect)
+ }
+
+ aligned, err := hAlign(rect, ar, h)
+ if err != nil {
+ return image.ZR, err
+ }
+ aligned, err = vAlign(rect, aligned, v)
+ if err != nil {
+ return image.ZR, err
+ }
+ return aligned, nil
+}
+
+// Text aligns the text within the given rectangle, returns the start point for the text.
+// For the purposes of the alignment this assumes that text will be trimmed if
+// it overruns the rectangle.
+// This only supports a single line of text, the text must not contain non-printable characters,
+// allows empty text.
+func Text(rect image.Rectangle, text string, h align.Horizontal, v align.Vertical) (image.Point, error) {
+ if strings.ContainsRune(text, '\n') {
+ return image.ZP, fmt.Errorf("the provided text contains a newline character: %q", text)
+ }
+
+ if text != "" {
+ if err := wrap.ValidText(text); err != nil {
+ return image.ZP, fmt.Errorf("the provided text contains non printable character(s): %s", err)
+ }
+ }
+
+ cells := runewidth.StringWidth(text)
+ var textLen int
+ if cells < rect.Dx() {
+ textLen = cells
+ } else {
+ textLen = rect.Dx()
+ }
+
+ textRect := image.Rect(
+ rect.Min.X,
+ rect.Min.Y,
+ // For the purposes of aligning the text, assume that it will be
+ // trimmed to the available space.
+ rect.Min.X+textLen,
+ rect.Min.Y+1,
+ )
+
+ aligned, err := Rectangle(rect, textRect, h, v)
+ if err != nil {
+ return image.ZP, err
+ }
+ return image.Point{aligned.Min.X, aligned.Min.Y}, nil
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/area/area.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/area/area.go
new file mode 100644
index 000000000..34b21a1b5
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/area/area.go
@@ -0,0 +1,258 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package area provides functions working with image areas.
+package area
+
+import (
+ "fmt"
+ "image"
+
+ "github.com/mum4k/termdash/private/numbers"
+)
+
+// Size returns the size of the provided area.
+func Size(area image.Rectangle) image.Point {
+ return image.Point{
+ area.Dx(),
+ area.Dy(),
+ }
+}
+
+// FromSize returns the corresponding area for the provided size.
+func FromSize(size image.Point) (image.Rectangle, error) {
+ if size.X < 0 || size.Y < 0 {
+ return image.Rectangle{}, fmt.Errorf("cannot convert zero or negative size to an area, got: %+v", size)
+ }
+ return image.Rect(0, 0, size.X, size.Y), nil
+}
+
+// HSplit returns two new areas created by splitting the provided area at the
+// specified percentage of its width. The percentage must be in the range
+// 0 <= heightPerc <= 100.
+// Can return zero size areas.
+func HSplit(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle, err error) {
+ if min, max := 0, 100; heightPerc < min || heightPerc > max {
+ return image.ZR, image.ZR, fmt.Errorf("invalid heightPerc %d, must be in range %d <= heightPerc <= %d", heightPerc, min, max)
+ }
+ height := area.Dy() * heightPerc / 100
+ top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Min.Y+height)
+ if top.Dy() == 0 {
+ top = image.ZR
+ }
+ bottom = image.Rect(area.Min.X, area.Min.Y+height, area.Max.X, area.Max.Y)
+ if bottom.Dy() == 0 {
+ bottom = image.ZR
+ }
+ return top, bottom, nil
+}
+
+// VSplit returns two new areas created by splitting the provided area at the
+// specified percentage of its width. The percentage must be in the range
+// 0 <= widthPerc <= 100.
+// Can return zero size areas.
+func VSplit(area image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle, err error) {
+ if min, max := 0, 100; widthPerc < min || widthPerc > max {
+ return image.ZR, image.ZR, fmt.Errorf("invalid widthPerc %d, must be in range %d <= widthPerc <= %d", widthPerc, min, max)
+ }
+ width := area.Dx() * widthPerc / 100
+ left = image.Rect(area.Min.X, area.Min.Y, area.Min.X+width, area.Max.Y)
+ if left.Dx() == 0 {
+ left = image.ZR
+ }
+ right = image.Rect(area.Min.X+width, area.Min.Y, area.Max.X, area.Max.Y)
+ if right.Dx() == 0 {
+ right = image.ZR
+ }
+ return left, right, nil
+}
+
+// VSplitCells returns two new areas created by splitting the provided area
+// after the specified amount of cells of its width. The number of cells must
+// be a zero or a positive integer. Providing a zero returns left=image.ZR,
+// right=area. Providing a number equal or larger to area's width returns
+// left=area, right=image.ZR.
+func VSplitCells(area image.Rectangle, cells int) (left image.Rectangle, right image.Rectangle, err error) {
+ if min := 0; cells < min {
+ return image.ZR, image.ZR, fmt.Errorf("invalid cells %d, must be a positive integer", cells)
+ }
+ if cells == 0 {
+ return image.ZR, area, nil
+ }
+
+ width := area.Dx()
+ if cells >= width {
+ return area, image.ZR, nil
+ }
+
+ left = image.Rect(area.Min.X, area.Min.Y, area.Min.X+cells, area.Max.Y)
+ right = image.Rect(area.Min.X+cells, area.Min.Y, area.Max.X, area.Max.Y)
+ return left, right, nil
+}
+
+// HSplitCells returns two new areas created by splitting the provided area
+// after the specified amount of cells of its height. The number of cells must
+// be a zero or a positive integer. Providing a zero returns top=image.ZR,
+// bottom=area. Providing a number equal or larger to area's height returns
+// top=area, bottom=image.ZR.
+func HSplitCells(area image.Rectangle, cells int) (top image.Rectangle, bottom image.Rectangle, err error) {
+ if min := 0; cells < min {
+ return image.ZR, image.ZR, fmt.Errorf("invalid cells %d, must be a positive integer", cells)
+ }
+ if cells == 0 {
+ return image.ZR, area, nil
+ }
+
+ height := area.Dy()
+ if cells >= height {
+ return area, image.ZR, nil
+ }
+
+ top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Min.Y+cells)
+ bottom = image.Rect(area.Min.X, area.Min.Y+cells, area.Max.X, area.Max.Y)
+ return top, bottom, nil
+}
+
+// ExcludeBorder returns a new area created by subtracting a border around the
+// provided area. Return the zero area if there isn't enough space to exclude
+// the border.
+func ExcludeBorder(area image.Rectangle) image.Rectangle {
+ // If the area dimensions are smaller than this, subtracting a point for the
+ // border on each of its sides results in a zero area.
+ const minDim = 2
+ if area.Dx() < minDim || area.Dy() < minDim {
+ return image.ZR
+ }
+ return image.Rect(
+ numbers.Abs(area.Min.X+1),
+ numbers.Abs(area.Min.Y+1),
+ numbers.Abs(area.Max.X-1),
+ numbers.Abs(area.Max.Y-1),
+ )
+}
+
+// WithRatio returns the largest area that has the requested ratio but is
+// either equal or smaller than the provided area. Returns zero area if the
+// area or the ratio are zero, or if there is no such area.
+func WithRatio(area image.Rectangle, ratio image.Point) image.Rectangle {
+ ratio = numbers.SimplifyRatio(ratio)
+ if area == image.ZR || ratio == image.ZP {
+ return image.ZR
+ }
+
+ wFact := area.Dx() / ratio.X
+ hFact := area.Dy() / ratio.Y
+
+ var fact int
+ if wFact < hFact {
+ fact = wFact
+ } else {
+ fact = hFact
+ }
+ return image.Rect(
+ area.Min.X,
+ area.Min.Y,
+ ratio.X*fact+area.Min.X,
+ ratio.Y*fact+area.Min.Y,
+ )
+}
+
+// Shrink returns a new area whose size is reduced by the specified amount of
+// cells. Can return a zero area if there is no space left in the area.
+// The values must be zero or positive integers.
+func Shrink(area image.Rectangle, topCells, rightCells, bottomCells, leftCells int) (image.Rectangle, error) {
+ for _, v := range []struct {
+ name string
+ value int
+ }{
+ {"topCells", topCells},
+ {"rightCells", rightCells},
+ {"bottomCells", bottomCells},
+ {"leftCells", leftCells},
+ } {
+ if min := 0; v.value < min {
+ return image.ZR, fmt.Errorf("invalid %s(%d), must be in range %d <= value", v.name, v.value, min)
+ }
+ }
+
+ shrunk := area
+ shrunk.Min.X, _ = numbers.MinMaxInts([]int{shrunk.Min.X + leftCells, shrunk.Max.X})
+ _, shrunk.Max.X = numbers.MinMaxInts([]int{shrunk.Max.X - rightCells, shrunk.Min.X})
+ shrunk.Min.Y, _ = numbers.MinMaxInts([]int{shrunk.Min.Y + topCells, shrunk.Max.Y})
+ _, shrunk.Max.Y = numbers.MinMaxInts([]int{shrunk.Max.Y - bottomCells, shrunk.Min.Y})
+
+ if shrunk.Dx() == 0 || shrunk.Dy() == 0 {
+ return image.ZR, nil
+ }
+ return shrunk, nil
+}
+
+// ShrinkPercent returns a new area whose size is reduced by percentage of its
+// width or height. Can return a zero area if there is no space left in the area.
+// The topPerc and bottomPerc indicate the percentage of area's height.
+// The rightPerc and leftPerc indicate the percentage of area's width.
+// The percentages must be in range 0 <= v <= 100.
+func ShrinkPercent(area image.Rectangle, topPerc, rightPerc, bottomPerc, leftPerc int) (image.Rectangle, error) {
+ for _, v := range []struct {
+ name string
+ value int
+ }{
+ {"topPerc", topPerc},
+ {"rightPerc", rightPerc},
+ {"bottomPerc", bottomPerc},
+ {"leftPerc", leftPerc},
+ } {
+ if min, max := 0, 100; v.value < min || v.value > max {
+ return image.ZR, fmt.Errorf("invalid %s(%d), must be in range %d <= value <= %d", v.name, v.value, min, max)
+ }
+ }
+
+ top := area.Dy() * topPerc / 100
+ bottom := area.Dy() * bottomPerc / 100
+ right := area.Dx() * rightPerc / 100
+ left := area.Dx() * leftPerc / 100
+ return Shrink(area, top, right, bottom, left)
+}
+
+// MoveUp returns a new area that is moved up by the specified amount of cells.
+// Returns an error if the move would result in negative Y coordinates.
+// The values must be zero or positive integers.
+func MoveUp(area image.Rectangle, cells int) (image.Rectangle, error) {
+ if min := 0; cells < min {
+ return image.ZR, fmt.Errorf("cannot move area %v up by %d cells, must be in range %d <= value", area, cells, min)
+ }
+
+ if area.Min.Y < cells {
+ return image.ZR, fmt.Errorf("cannot move area %v up by %d cells, would result in negative Y coordinate", area, cells)
+ }
+
+ moved := area
+ moved.Min.Y -= cells
+ moved.Max.Y -= cells
+ return moved, nil
+}
+
+// MoveDown returns a new area that is moved down by the specified amount of
+// cells.
+// The values must be zero or positive integers.
+func MoveDown(area image.Rectangle, cells int) (image.Rectangle, error) {
+ if min := 0; cells < min {
+ return image.ZR, fmt.Errorf("cannot move area %v down by %d cells, must be in range %d <= value", area, cells, min)
+ }
+
+ moved := area
+ moved.Min.Y += cells
+ moved.Max.Y += cells
+ return moved, nil
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/button/button.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/button/button.go
new file mode 100644
index 000000000..d4e0601b8
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/button/button.go
@@ -0,0 +1,135 @@
+// Copyright 2019 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package button implements a state machine that tracks mouse button clicks.
+package button
+
+import (
+ "image"
+
+ "github.com/mum4k/termdash/mouse"
+ "github.com/mum4k/termdash/terminal/terminalapi"
+)
+
+// State represents the state of the mouse button.
+type State int
+
+// String implements fmt.Stringer()
+func (s State) String() string {
+ if n, ok := stateNames[s]; ok {
+ return n
+ }
+ return "StateUnknown"
+}
+
+// stateNames maps State values to human readable names.
+var stateNames = map[State]string{
+ Up: "StateUp",
+ Down: "StateDown",
+}
+
+const (
+ // Up is the default idle state of the mouse button.
+ Up State = iota
+
+ // Down is a state where the mouse button is pressed down and held.
+ Down
+)
+
+// FSM implements a finite-state machine that tracks mouse clicks within an
+// area.
+//
+// Simplifies tracking of mouse button clicks, i.e. when the caller wants to
+// perform an action only if both the button press and release happen within
+// the specified area.
+//
+// This object is not thread-safe.
+type FSM struct {
+ // button is the mouse button whose state this FSM tracks.
+ button mouse.Button
+
+ // area is the area provided to NewFSM.
+ area image.Rectangle
+
+ // state is the current state of the FSM.
+ state stateFn
+}
+
+// NewFSM creates a new FSM instance that tracks the state of the specified
+// mouse button through button events that fall within the provided area.
+func NewFSM(button mouse.Button, area image.Rectangle) *FSM {
+ return &FSM{
+ button: button,
+ area: area,
+ state: wantPress,
+ }
+}
+
+// Event is used to forward mouse events to the state machine.
+// Only events related to the button specified on a call to NewFSM are
+// processed.
+//
+// Returns a bool indicating if an action guarded by the button should be
+// performed and the state of the button after the provided event.
+// The bool is true if the button click should take an effect, i.e. if the
+// FSM saw both the button click and its release.
+func (fsm *FSM) Event(m *terminalapi.Mouse) (bool, State) {
+ clicked, bs, next := fsm.state(fsm, m)
+ fsm.state = next
+ return clicked, bs
+}
+
+// UpdateArea informs FSM of an area change.
+// This method is idempotent.
+func (fsm *FSM) UpdateArea(area image.Rectangle) {
+ fsm.area = area
+}
+
+// stateFn is a single state in the state machine.
+// Returns bool indicating if a click happened, the state of the button and the
+// next state of the FSM.
+type stateFn func(fsm *FSM, m *terminalapi.Mouse) (bool, State, stateFn)
+
+// wantPress is the initial state, expecting a button press inside the area.
+func wantPress(fsm *FSM, m *terminalapi.Mouse) (bool, State, stateFn) {
+ if m.Button != fsm.button || !m.Position.In(fsm.area) {
+ return false, Up, wantPress
+ }
+ return false, Down, wantRelease
+}
+
+// wantRelease waits for a mouse button release in the same area as
+// the press.
+func wantRelease(fsm *FSM, m *terminalapi.Mouse) (bool, State, stateFn) {
+ switch m.Button {
+ case fsm.button:
+ if m.Position.In(fsm.area) {
+ // Remain in the same state, since termbox reports move of mouse with
+ // button held down as a series of clicks, one per position.
+ return false, Down, wantRelease
+ }
+ return false, Up, wantPress
+
+ case mouse.ButtonRelease:
+ if m.Position.In(fsm.area) {
+ // Seen both press and release, report a click.
+ return true, Up, wantPress
+ }
+ // Release the button even if the release event happened outside of the area.
+ return false, Up, wantPress
+
+ default:
+ return false, Up, wantPress
+ }
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/canvas/braille/braille.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/canvas/braille/braille.go
new file mode 100644
index 000000000..7cd902f87
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/canvas/braille/braille.go
@@ -0,0 +1,284 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/*
+Package braille provides a canvas that uses braille characters.
+
+This is inspired by https://github.com/asciimoo/drawille.
+
+The braille patterns documentation:
+http://www.alanwood.net/unicode/braille_patterns.html
+
+The use of braille characters gives additional points (higher resolution) on
+the canvas, each character cell now has eight pixels that can be set
+independently. Specifically each cell has the following pixels, the axes grow
+right and down.
+
+Each cell:
+
+ X→ 0 1 Y
+ ┌───┐ ↓
+ │● ●│ 0
+ │● ●│ 1
+ │● ●│ 2
+ │● ●│ 3
+ └───┘
+
+When using the braille canvas, the coordinates address the sub-cell points
+rather then cells themselves. However all points in the cell still share the
+same cell options.
+*/
+package braille
+
+import (
+ "fmt"
+ "image"
+
+ "github.com/mum4k/termdash/cell"
+ "github.com/mum4k/termdash/private/canvas"
+ "github.com/mum4k/termdash/terminal/terminalapi"
+)
+
+const (
+ // ColMult is the resolution multiplier for the width, i.e. two pixels per cell.
+ ColMult = 2
+
+ // RowMult is the resolution multiplier for the height, i.e. four pixels per cell.
+ RowMult = 4
+
+ // brailleCharOffset is the offset of the braille pattern unicode characters.
+ // From: http://www.alanwood.net/unicode/braille_patterns.html
+ brailleCharOffset = 0x2800
+
+ // brailleLastChar is the last braille pattern rune.
+ brailleLastChar = 0x28FF
+)
+
+// pixelRunes maps points addressing individual pixels in a cell into character
+// offset. I.e. the correct character to set pixel(0,0) is
+// brailleCharOffset|pixelRunes[image.Point{0,0}].
+var pixelRunes = map[image.Point]rune{
+ {0, 0}: 0x01, {1, 0}: 0x08,
+ {0, 1}: 0x02, {1, 1}: 0x10,
+ {0, 2}: 0x04, {1, 2}: 0x20,
+ {0, 3}: 0x40, {1, 3}: 0x80,
+}
+
+// Canvas is a canvas that uses the braille patterns. It is two times wider
+// and four times taller than a regular canvas that uses just plain characters,
+// since each cell now has 2x4 pixels that can be independently set.
+//
+// The braille canvas is an abstraction built on top of a regular character
+// canvas. After setting and toggling pixels on the braille canvas, it should
+// be copied to a regular character canvas or applied to a terminal which
+// results in setting of braille pattern characters.
+// See the examples for more details.
+//
+// The created braille canvas can be smaller and even misaligned relatively to
+// the regular character canvas or terminal, allowing the callers to create a
+// "view" of just a portion of the canvas or terminal.
+type Canvas struct {
+ // regular is the regular character canvas the braille canvas is based on.
+ regular *canvas.Canvas
+}
+
+// New returns a new braille canvas for the provided area.
+func New(ar image.Rectangle) (*Canvas, error) {
+ rc, err := canvas.New(ar)
+ if err != nil {
+ return nil, err
+ }
+ return &Canvas{
+ regular: rc,
+ }, nil
+}
+
+// Size returns the size of the braille canvas in pixels.
+func (c *Canvas) Size() image.Point {
+ s := c.regular.Size()
+ return image.Point{s.X * ColMult, s.Y * RowMult}
+}
+
+// CellArea returns the area of the underlying cell canvas in cells.
+func (c *Canvas) CellArea() image.Rectangle {
+ return c.regular.Area()
+}
+
+// Area returns the area of the braille canvas in pixels.
+// This will be zero-based area that is two times wider and four times taller
+// than the area used to create the braille canvas.
+func (c *Canvas) Area() image.Rectangle {
+ ar := c.regular.Area()
+ return image.Rect(0, 0, ar.Dx()*ColMult, ar.Dy()*RowMult)
+}
+
+// Clear clears all the content on the canvas.
+func (c *Canvas) Clear() error {
+ return c.regular.Clear()
+}
+
+// SetPixel turns on pixel at the specified point.
+// The provided cell options will be applied to the entire cell (all of its
+// pixels). This method is idempotent.
+func (c *Canvas) SetPixel(p image.Point, opts ...cell.Option) error {
+ cp, err := c.cellPoint(p)
+ if err != nil {
+ return err
+ }
+ cell, err := c.regular.Cell(cp)
+ if err != nil {
+ return err
+ }
+
+ var r rune
+ if isBraille(cell.Rune) {
+ // If the cell already has a braille pattern rune, we will be adding
+ // the pixel.
+ r = cell.Rune
+ } else {
+ r = brailleCharOffset
+ }
+
+ r |= pixelRunes[pixelPoint(p)]
+ if _, err := c.regular.SetCell(cp, r, opts...); err != nil {
+ return err
+ }
+ return nil
+}
+
+// ClearPixel turns off pixel at the specified point.
+// The provided cell options will be applied to the entire cell (all of its
+// pixels). This method is idempotent.
+func (c *Canvas) ClearPixel(p image.Point, opts ...cell.Option) error {
+ cp, err := c.cellPoint(p)
+ if err != nil {
+ return err
+ }
+ cell, err := c.regular.Cell(cp)
+ if err != nil {
+ return err
+ }
+
+ // Clear is idempotent.
+ if !isBraille(cell.Rune) || !pixelSet(cell.Rune, p) {
+ return nil
+ }
+
+ r := cell.Rune & ^pixelRunes[pixelPoint(p)]
+ if _, err := c.regular.SetCell(cp, r, opts...); err != nil {
+ return err
+ }
+ return nil
+}
+
+// TogglePixel toggles the state of the pixel at the specified point, i.e. it
+// either sets or clear it depending on its current state.
+// The provided cell options will be applied to the entire cell (all of its
+// pixels).
+func (c *Canvas) TogglePixel(p image.Point, opts ...cell.Option) error {
+ cp, err := c.cellPoint(p)
+ if err != nil {
+ return err
+ }
+ curCell, err := c.regular.Cell(cp)
+ if err != nil {
+ return err
+ }
+
+ if isBraille(curCell.Rune) && pixelSet(curCell.Rune, p) {
+ return c.ClearPixel(p, opts...)
+ }
+ return c.SetPixel(p, opts...)
+}
+
+// SetCellOpts sets options on the specified cell of the braille canvas without
+// modifying the content of the cell.
+// Sets the default cell options if no options are provided.
+// This method is idempotent.
+func (c *Canvas) SetCellOpts(cellPoint image.Point, opts ...cell.Option) error {
+ curCell, err := c.regular.Cell(cellPoint)
+ if err != nil {
+ return err
+ }
+
+ if len(opts) == 0 {
+ // Set the default options.
+ opts = []cell.Option{
+ cell.FgColor(cell.ColorDefault),
+ cell.BgColor(cell.ColorDefault),
+ }
+ }
+ if _, err := c.regular.SetCell(cellPoint, curCell.Rune, opts...); err != nil {
+ return err
+ }
+ return nil
+}
+
+// SetAreaCellOpts is like SetCellOpts, but sets the specified options on all
+// the cells within the provided area.
+func (c *Canvas) SetAreaCellOpts(cellArea image.Rectangle, opts ...cell.Option) error {
+ haveArea := c.regular.Area()
+ if !cellArea.In(haveArea) {
+ return fmt.Errorf("unable to set cell options in area %v, it must fit inside the available cell area is %v", cellArea, haveArea)
+ }
+ for col := cellArea.Min.X; col < cellArea.Max.X; col++ {
+ for row := cellArea.Min.Y; row < cellArea.Max.Y; row++ {
+ if err := c.SetCellOpts(image.Point{col, row}, opts...); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+// Apply applies the canvas to the corresponding area of the terminal.
+// Guarantees to stay within limits of the area the canvas was created with.
+func (c *Canvas) Apply(t terminalapi.Terminal) error {
+ return c.regular.Apply(t)
+}
+
+// CopyTo copies the content of this canvas onto the destination canvas.
+// This canvas can have an offset when compared to the destination canvas, i.e.
+// the area of this canvas doesn't have to be zero-based.
+func (c *Canvas) CopyTo(dst *canvas.Canvas) error {
+ return c.regular.CopyTo(dst)
+}
+
+// cellPoint determines the point (coordinate) of the character cell given
+// coordinates in pixels.
+func (c *Canvas) cellPoint(p image.Point) (image.Point, error) {
+ if p.X < 0 || p.Y < 0 {
+ return image.ZP, fmt.Errorf("pixels cannot have negative coordinates: %v", p)
+ }
+ cp := image.Point{p.X / ColMult, p.Y / RowMult}
+ if ar := c.regular.Area(); !cp.In(ar) {
+ return image.ZP, fmt.Errorf("pixel at%v would be in a character cell at%v which falls outside of the canvas area %v", p, cp, ar)
+ }
+ return cp, nil
+}
+
+// isBraille determines if the rune is a braille pattern rune.
+func isBraille(r rune) bool {
+ return r >= brailleCharOffset && r <= brailleLastChar
+}
+
+// pixelSet returns true if the provided rune has the specified pixel set.
+func pixelSet(r rune, p image.Point) bool {
+ return r&pixelRunes[pixelPoint(p)] > 0
+}
+
+// pixelPoint translates point within canvas to point within the target cell.
+func pixelPoint(p image.Point) image.Point {
+ return image.Point{p.X % ColMult, p.Y % RowMult}
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/canvas/buffer/buffer.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/canvas/buffer/buffer.go
new file mode 100644
index 000000000..5c21dd0ba
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/canvas/buffer/buffer.go
@@ -0,0 +1,188 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package buffer implements a 2-D buffer of cells.
+package buffer
+
+import (
+ "fmt"
+ "image"
+
+ "github.com/mum4k/termdash/cell"
+ "github.com/mum4k/termdash/private/area"
+ "github.com/mum4k/termdash/private/runewidth"
+)
+
+// NewCells breaks the provided text into cells and applies the options.
+func NewCells(text string, opts ...cell.Option) []*Cell {
+ var res []*Cell
+ for _, r := range text {
+ res = append(res, NewCell(r, opts...))
+ }
+ return res
+}
+
+// Cell represents a single cell on the terminal.
+type Cell struct {
+ // Rune is the rune stored in the cell.
+ Rune rune
+
+ // Opts are the cell options.
+ Opts *cell.Options
+}
+
+// String implements fmt.Stringer.
+func (c *Cell) String() string {
+ return fmt.Sprintf("{%q}", c.Rune)
+}
+
+// NewCell returns a new cell.
+func NewCell(r rune, opts ...cell.Option) *Cell {
+ return &Cell{
+ Rune: r,
+ Opts: cell.NewOptions(opts...),
+ }
+}
+
+// Copy returns a copy the cell.
+func (c *Cell) Copy() *Cell {
+ return &Cell{
+ Rune: c.Rune,
+ Opts: cell.NewOptions(c.Opts),
+ }
+}
+
+// Apply applies the provided options to the cell.
+func (c *Cell) Apply(opts ...cell.Option) {
+ for _, opt := range opts {
+ opt.Set(c.Opts)
+ }
+}
+
+// Buffer is a 2-D buffer of cells.
+// The axes increase right and down.
+// Uninitialized buffer is invalid, use New to create an instance.
+// Don't set cells directly, use the SetCell method instead which safely
+// handles limits and wide unicode characters.
+type Buffer [][]*Cell
+
+// New returns a new Buffer of the provided size.
+func New(size image.Point) (Buffer, error) {
+ if size.X <= 0 {
+ return nil, fmt.Errorf("invalid buffer width (size.X): %d, must be a positive number", size.X)
+ }
+ if size.Y <= 0 {
+ return nil, fmt.Errorf("invalid buffer height (size.Y): %d, must be a positive number", size.Y)
+ }
+
+ b := make([][]*Cell, size.X)
+ for col := range b {
+ b[col] = make([]*Cell, size.Y)
+ for row := range b[col] {
+ b[col][row] = NewCell(0)
+ }
+ }
+ return b, nil
+}
+
+// SetCell sets the rune of the specified cell in the buffer. Returns the
+// number of cells the rune occupies, wide runes can occupy multiple cells when
+// printed on the terminal. See http://www.unicode.org/reports/tr11/.
+// Use the options to specify which attributes to modify, if an attribute
+// option isn't specified, the attribute retains its previous value.
+func (b Buffer) SetCell(p image.Point, r rune, opts ...cell.Option) (int, error) {
+ partial, err := b.IsPartial(p)
+ if err != nil {
+ return -1, err
+ }
+ if partial {
+ return -1, fmt.Errorf("cannot set rune %q at point %v, it is a partial cell occupied by a wide rune in the previous cell", r, p)
+ }
+
+ remW, err := b.RemWidth(p)
+ if err != nil {
+ return -1, err
+ }
+ rw := runewidth.RuneWidth(r)
+ if rw == 0 {
+ // Even if the rune is invisible, like the zero-value rune, it still
+ // occupies at least the target cell.
+ rw = 1
+ }
+ if rw > remW {
+ return -1, fmt.Errorf("cannot set rune %q of width %d at point %v, only have %d remaining cells at this line", r, rw, p, remW)
+ }
+
+ c := b[p.X][p.Y]
+ c.Rune = r
+ c.Apply(opts...)
+ return rw, nil
+}
+
+// IsPartial returns true if the cell at the specified point holds a part of a
+// full width rune from a previous cell. See
+// http://www.unicode.org/reports/tr11/.
+func (b Buffer) IsPartial(p image.Point) (bool, error) {
+ size := b.Size()
+ ar, err := area.FromSize(size)
+ if err != nil {
+ return false, err
+ }
+
+ if !p.In(ar) {
+ return false, fmt.Errorf("point %v falls outside of the area %v occupied by the buffer", p, ar)
+ }
+
+ if p.X == 0 && p.Y == 0 {
+ return false, nil
+ }
+
+ prevP := image.Point{p.X - 1, p.Y}
+ if prevP.X < 0 {
+ prevP = image.Point{size.X - 1, p.Y - 1}
+ }
+
+ prevR := b[prevP.X][prevP.Y].Rune
+ switch rw := runewidth.RuneWidth(prevR); rw {
+ case 0, 1:
+ return false, nil
+ case 2:
+ return true, nil
+ default:
+ return false, fmt.Errorf("buffer cell %v contains rune %q which has an unsupported rune with %d", prevP, prevR, rw)
+ }
+}
+
+// RemWidth returns the remaining width (horizontal row of cells) available
+// from and inclusive of the specified point.
+func (b Buffer) RemWidth(p image.Point) (int, error) {
+ size := b.Size()
+ ar, err := area.FromSize(size)
+ if err != nil {
+ return -1, err
+ }
+
+ if !p.In(ar) {
+ return -1, fmt.Errorf("point %v falls outside of the area %v occupied by the buffer", p, ar)
+ }
+ return size.X - p.X, nil
+}
+
+// Size returns the size of the buffer.
+func (b Buffer) Size() image.Point {
+ return image.Point{
+ len(b),
+ len(b[0]),
+ }
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/canvas/canvas.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/canvas/canvas.go
new file mode 100644
index 000000000..65a1e6963
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/canvas/canvas.go
@@ -0,0 +1,247 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package canvas defines the canvas that the widgets draw on.
+package canvas
+
+import (
+ "fmt"
+ "image"
+
+ "github.com/mum4k/termdash/cell"
+ "github.com/mum4k/termdash/private/area"
+ "github.com/mum4k/termdash/private/canvas/buffer"
+ "github.com/mum4k/termdash/private/runewidth"
+ "github.com/mum4k/termdash/terminal/terminalapi"
+)
+
+// Canvas is where a widget draws its output for display on the terminal.
+type Canvas struct {
+ // area is the area the buffer was created for.
+ // Contains absolute coordinates on the target terminal, while the buffer
+ // contains relative zero-based coordinates for this canvas.
+ area image.Rectangle
+
+ // buffer is where the drawing happens.
+ buffer buffer.Buffer
+}
+
+// New returns a new Canvas with a buffer for the provided area.
+func New(ar image.Rectangle) (*Canvas, error) {
+ if ar.Min.X < 0 || ar.Min.Y < 0 || ar.Max.X < 0 || ar.Max.Y < 0 {
+ return nil, fmt.Errorf("area cannot start or end on the negative axis, got: %+v", ar)
+ }
+
+ b, err := buffer.New(area.Size(ar))
+ if err != nil {
+ return nil, err
+ }
+ return &Canvas{
+ area: ar,
+ buffer: b,
+ }, nil
+}
+
+// Size returns the size of the 2-D canvas.
+func (c *Canvas) Size() image.Point {
+ return c.buffer.Size()
+}
+
+// Area returns the area of the 2-D canvas.
+func (c *Canvas) Area() image.Rectangle {
+ s := c.buffer.Size()
+ return image.Rect(0, 0, s.X, s.Y)
+}
+
+// Clear clears all the content on the canvas.
+func (c *Canvas) Clear() error {
+ b, err := buffer.New(c.Size())
+ if err != nil {
+ return err
+ }
+ c.buffer = b
+ return nil
+}
+
+// SetCell sets the rune of the specified cell on the canvas. Returns the
+// number of cells the rune occupies, wide runes can occupy multiple cells when
+// printed on the terminal. See http://www.unicode.org/reports/tr11/.
+// Use the options to specify which attributes to modify, if an attribute
+// option isn't specified, the attribute retains its previous value.
+func (c *Canvas) SetCell(p image.Point, r rune, opts ...cell.Option) (int, error) {
+ return c.buffer.SetCell(p, r, opts...)
+}
+
+// Cell returns a copy of the specified cell.
+func (c *Canvas) Cell(p image.Point) (*buffer.Cell, error) {
+ ar, err := area.FromSize(c.Size())
+ if err != nil {
+ return nil, err
+ }
+ if !p.In(ar) {
+ return nil, fmt.Errorf("point %v falls outside of the area %v occupied by the canvas", p, ar)
+ }
+
+ return c.buffer[p.X][p.Y].Copy(), nil
+}
+
+// SetCellOpts sets options on the specified cell of the canvas without
+// modifying the content of the cell.
+// Sets the default cell options if no options are provided.
+// This method is idempotent.
+func (c *Canvas) SetCellOpts(p image.Point, opts ...cell.Option) error {
+ curCell, err := c.Cell(p)
+ if err != nil {
+ return err
+ }
+
+ if len(opts) == 0 {
+ // Set the default options.
+ opts = []cell.Option{
+ cell.FgColor(cell.ColorDefault),
+ cell.BgColor(cell.ColorDefault),
+ }
+ }
+ if _, err := c.SetCell(p, curCell.Rune, opts...); err != nil {
+ return err
+ }
+ return nil
+}
+
+// SetAreaCells is like SetCell, but sets the specified rune and options on all
+// the cells within the provided area.
+// This method is idempotent.
+func (c *Canvas) SetAreaCells(cellArea image.Rectangle, r rune, opts ...cell.Option) error {
+ haveArea := c.Area()
+ if !cellArea.In(haveArea) {
+ return fmt.Errorf("unable to set cell runes in area %v, it must fit inside the available cell area is %v", cellArea, haveArea)
+ }
+
+ rw := runewidth.RuneWidth(r)
+ for row := cellArea.Min.Y; row < cellArea.Max.Y; row++ {
+ for col := cellArea.Min.X; col < cellArea.Max.X; {
+ p := image.Point{col, row}
+ if col+rw > cellArea.Max.X {
+ break
+ }
+ cells, err := c.SetCell(p, r, opts...)
+ if err != nil {
+ return err
+ }
+ col += cells
+ }
+ }
+ return nil
+}
+
+// SetAreaCellOpts is like SetCellOpts, but sets the specified options on all
+// the cells within the provided area.
+func (c *Canvas) SetAreaCellOpts(cellArea image.Rectangle, opts ...cell.Option) error {
+ haveArea := c.Area()
+ if !cellArea.In(haveArea) {
+ return fmt.Errorf("unable to set cell options in area %v, it must fit inside the available cell area is %v", cellArea, haveArea)
+ }
+ for col := cellArea.Min.X; col < cellArea.Max.X; col++ {
+ for row := cellArea.Min.Y; row < cellArea.Max.Y; row++ {
+ if err := c.SetCellOpts(image.Point{col, row}, opts...); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+// setCellFunc is a function that sets cell content on a terminal or a canvas.
+type setCellFunc func(image.Point, rune, ...cell.Option) error
+
+// copyTo is the internal implementation of code that copies the content of a
+// canvas. If a non zero offset is provided, all the copied points are offset by
+// this amount.
+// The dstSetCell function is called for every point in this canvas when
+// copying it to the destination.
+func (c *Canvas) copyTo(offset image.Point, dstSetCell setCellFunc) error {
+ for col := range c.buffer {
+ for row := range c.buffer[col] {
+ partial, err := c.buffer.IsPartial(image.Point{col, row})
+ if err != nil {
+ return err
+ }
+ if partial {
+ // Skip over partial cells, i.e. cells that follow a cell
+ // containing a full-width rune. A full-width rune takes only
+ // one cell in the buffer, but two on the terminal.
+ // See http://www.unicode.org/reports/tr11/.
+ continue
+ }
+ cell := c.buffer[col][row]
+ p := image.Point{col, row}.Add(offset)
+ if err := dstSetCell(p, cell.Rune, cell.Opts); err != nil {
+ return fmt.Errorf("setCellFunc%v => error: %v", p, err)
+ }
+ }
+ }
+ return nil
+}
+
+// Apply applies the canvas to the corresponding area of the terminal.
+// Guarantees to stay within limits of the area the canvas was created with.
+func (c *Canvas) Apply(t terminalapi.Terminal) error {
+ termArea, err := area.FromSize(t.Size())
+ if err != nil {
+ return err
+ }
+
+ bufArea, err := area.FromSize(c.buffer.Size())
+ if err != nil {
+ return err
+ }
+
+ if !bufArea.In(termArea) {
+ return fmt.Errorf("the canvas area %+v doesn't fit onto the terminal %+v", bufArea, termArea)
+ }
+
+ // The image.Point{0, 0} of this canvas isn't always exactly at
+ // image.Point{0, 0} on the terminal.
+ // Depends on area assigned by the container.
+ offset := c.area.Min
+ return c.copyTo(offset, t.SetCell)
+}
+
+// CopyTo copies the content of this canvas onto the destination canvas.
+// This canvas can have an offset when compared to the destination canvas, i.e.
+// the area of this canvas doesn't have to be zero-based.
+func (c *Canvas) CopyTo(dst *Canvas) error {
+ if !c.area.In(dst.Area()) {
+ return fmt.Errorf("the canvas area %v doesn't fit or lie inside the destination canvas area %v", c.area, dst.Area())
+ }
+
+ fn := setCellFunc(func(p image.Point, r rune, opts ...cell.Option) error {
+ if _, err := dst.SetCell(p, r, opts...); err != nil {
+ return fmt.Errorf("dst.SetCell => %v", err)
+ }
+ return nil
+ })
+
+ // Neither of the two canvases (source and destination) have to be zero
+ // based. Canvas is not zero based if it is positioned elsewhere, i.e.
+ // providing a smaller view of another canvas.
+ // E.g. a widget can assign a smaller portion of its canvas to a component
+ // in order to restrict drawing of this component to a smaller area. To do
+ // this it can create a sub-canvas. This sub-canvas can have a specific
+ // starting position other than image.Point{0, 0} relative to the parent
+ // canvas. Copying this sub-canvas back onto the parent accounts for this
+ // offset.
+ offset := c.area.Min
+ return c.copyTo(offset, fn)
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/border.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/border.go
new file mode 100644
index 000000000..a19ec096c
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/border.go
@@ -0,0 +1,182 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package draw
+
+// border.go contains code that draws borders.
+
+import (
+ "fmt"
+ "image"
+
+ "github.com/mum4k/termdash/align"
+ "github.com/mum4k/termdash/cell"
+ "github.com/mum4k/termdash/linestyle"
+ "github.com/mum4k/termdash/private/alignfor"
+ "github.com/mum4k/termdash/private/canvas"
+)
+
+// BorderOption is used to provide options to Border().
+type BorderOption interface {
+ // set sets the provided option.
+ set(*borderOptions)
+}
+
+// borderOptions stores the provided options.
+type borderOptions struct {
+ cellOpts []cell.Option
+ lineStyle linestyle.LineStyle
+ title string
+ titleOM OverrunMode
+ titleCellOpts []cell.Option
+ titleHAlign align.Horizontal
+}
+
+// borderOption implements BorderOption.
+type borderOption func(bOpts *borderOptions)
+
+// set implements BorderOption.set.
+func (bo borderOption) set(bOpts *borderOptions) {
+ bo(bOpts)
+}
+
+// DefaultBorderLineStyle is the default value for the BorderLineStyle option.
+const DefaultBorderLineStyle = linestyle.Light
+
+// BorderLineStyle sets the style of the line used to draw the border.
+func BorderLineStyle(ls linestyle.LineStyle) BorderOption {
+ return borderOption(func(bOpts *borderOptions) {
+ bOpts.lineStyle = ls
+ })
+}
+
+// BorderCellOpts sets options on the cells that create the border.
+func BorderCellOpts(opts ...cell.Option) BorderOption {
+ return borderOption(func(bOpts *borderOptions) {
+ bOpts.cellOpts = opts
+ })
+}
+
+// BorderTitle sets a title for the border.
+func BorderTitle(title string, overrun OverrunMode, opts ...cell.Option) BorderOption {
+ return borderOption(func(bOpts *borderOptions) {
+ bOpts.title = title
+ bOpts.titleOM = overrun
+ bOpts.titleCellOpts = opts
+ })
+}
+
+// BorderTitleAlign configures the horizontal alignment for the title.
+func BorderTitleAlign(h align.Horizontal) BorderOption {
+ return borderOption(func(bOpts *borderOptions) {
+ bOpts.titleHAlign = h
+ })
+}
+
+// borderChar returns the correct border character from the parts for the use
+// at the specified point of the border. Returns -1 if no character should be at
+// this point.
+func borderChar(p image.Point, border image.Rectangle, parts map[linePart]rune) rune {
+ switch {
+ case p.X == border.Min.X && p.Y == border.Min.Y:
+ return parts[topLeftCorner]
+ case p.X == border.Max.X-1 && p.Y == border.Min.Y:
+ return parts[topRightCorner]
+ case p.X == border.Min.X && p.Y == border.Max.Y-1:
+ return parts[bottomLeftCorner]
+ case p.X == border.Max.X-1 && p.Y == border.Max.Y-1:
+ return parts[bottomRightCorner]
+ case p.X == border.Min.X || p.X == border.Max.X-1:
+ return parts[vLine]
+ case p.Y == border.Min.Y || p.Y == border.Max.Y-1:
+ return parts[hLine]
+ }
+ return -1
+}
+
+// drawTitle draws a text title at the top of the border.
+func drawTitle(c *canvas.Canvas, border image.Rectangle, opt *borderOptions) error {
+ // Don't attempt to draw the title if there isn't space for at least one rune.
+ // The title must not overwrite any of the corner runes on the border so we
+ // need the following minimum width.
+ const minForTitle = 3
+ if border.Dx() < minForTitle {
+ return nil
+ }
+
+ available := image.Rect(
+ border.Min.X+1, // One space for the top left corner char.
+ border.Min.Y,
+ border.Max.X-1, // One space for the top right corner char.
+ border.Min.Y+1,
+ )
+ start, err := alignfor.Text(available, opt.title, opt.titleHAlign, align.VerticalTop)
+ if err != nil {
+ return err
+ }
+
+ return Text(
+ c, opt.title, start,
+ TextCellOpts(opt.titleCellOpts...),
+ TextOverrunMode(opt.titleOM),
+ TextMaxX(available.Max.X),
+ )
+}
+
+// Border draws a border on the canvas.
+func Border(c *canvas.Canvas, border image.Rectangle, opts ...BorderOption) error {
+ if ar := c.Area(); !border.In(ar) {
+ return fmt.Errorf("the requested border %+v falls outside of the provided canvas %+v", border, ar)
+ }
+
+ const minSize = 2
+ if border.Dx() < minSize || border.Dy() < minSize {
+ return fmt.Errorf("the smallest supported border is %dx%d, got: %dx%d", minSize, minSize, border.Dx(), border.Dy())
+ }
+
+ opt := &borderOptions{
+ lineStyle: DefaultBorderLineStyle,
+ }
+ for _, o := range opts {
+ o.set(opt)
+ }
+
+ parts, err := lineParts(opt.lineStyle)
+ if err != nil {
+ return err
+ }
+
+ for col := border.Min.X; col < border.Max.X; col++ {
+ for row := border.Min.Y; row < border.Max.Y; row++ {
+ p := image.Point{col, row}
+ r := borderChar(p, border, parts)
+ if r == -1 {
+ continue
+ }
+
+ cells, err := c.SetCell(p, r, opt.cellOpts...)
+ if err != nil {
+ return err
+ }
+ if cells != 1 {
+ panic(fmt.Sprintf("invalid border rune %q, this rune occupies %d cells, border implementation only supports half-width runes that occupy exactly one cell", r, cells))
+ }
+ }
+ }
+
+ if opt.title != "" {
+ return drawTitle(c, border, opt)
+ }
+ return nil
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/braille_circle.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/braille_circle.go
new file mode 100644
index 000000000..d2b3b86bc
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/braille_circle.go
@@ -0,0 +1,263 @@
+// Copyright 2019 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package draw
+
+// braille_circle.go contains code that draws circles on a braille canvas.
+
+import (
+ "fmt"
+ "image"
+
+ "github.com/mum4k/termdash/cell"
+ "github.com/mum4k/termdash/private/canvas/braille"
+ "github.com/mum4k/termdash/private/numbers/trig"
+)
+
+// BrailleCircleOption is used to provide options to BrailleCircle.
+type BrailleCircleOption interface {
+ // set sets the provided option.
+ set(*brailleCircleOptions)
+}
+
+// brailleCircleOptions stores the provided options.
+type brailleCircleOptions struct {
+ cellOpts []cell.Option
+ filled bool
+ pixelChange braillePixelChange
+
+ arcOnly bool
+ startDegree int
+ endDegree int
+}
+
+// newBrailleCircleOptions returns a new brailleCircleOptions instance.
+func newBrailleCircleOptions() *brailleCircleOptions {
+ return &brailleCircleOptions{
+ pixelChange: braillePixelChangeSet,
+ }
+}
+
+// validate validates the provided options.
+func (opts *brailleCircleOptions) validate() error {
+ if !opts.arcOnly {
+ return nil
+ }
+
+ if opts.startDegree == opts.endDegree {
+ return fmt.Errorf("invalid degree range, start %d and end %d cannot be equal", opts.startDegree, opts.endDegree)
+ }
+ return nil
+}
+
+// brailleCircleOption implements BrailleCircleOption.
+type brailleCircleOption func(*brailleCircleOptions)
+
+// set implements BrailleCircleOption.set.
+func (o brailleCircleOption) set(opts *brailleCircleOptions) {
+ o(opts)
+}
+
+// BrailleCircleCellOpts sets options on the cells that contain the circle.
+// Cell options on a braille canvas can only be set on the entire cell, not per
+// pixel.
+func BrailleCircleCellOpts(cOpts ...cell.Option) BrailleCircleOption {
+ return brailleCircleOption(func(opts *brailleCircleOptions) {
+ opts.cellOpts = cOpts
+ })
+}
+
+// BrailleCircleFilled indicates that the drawn circle should be filled.
+func BrailleCircleFilled() BrailleCircleOption {
+ return brailleCircleOption(func(opts *brailleCircleOptions) {
+ opts.filled = true
+ })
+}
+
+// BrailleCircleArcOnly indicates that only a portion of the circle should be drawn.
+// The arc will be between the two provided angles in degrees.
+// Each angle must be in range 0 <= angle <= 360. Start and end must not be equal.
+// The zero angle is on the X axis, angles grow counter-clockwise.
+func BrailleCircleArcOnly(startDegree, endDegree int) BrailleCircleOption {
+ return brailleCircleOption(func(opts *brailleCircleOptions) {
+ opts.arcOnly = true
+ opts.startDegree = startDegree
+ opts.endDegree = endDegree
+
+ })
+}
+
+// BrailleCircleClearPixels changes the behavior of BrailleCircle, so that it
+// clears the pixels belonging to the circle instead of setting them.
+// Useful in order to "erase" a circle from the canvas as opposed to drawing one.
+func BrailleCircleClearPixels() BrailleCircleOption {
+ return brailleCircleOption(func(opts *brailleCircleOptions) {
+ opts.pixelChange = braillePixelChangeClear
+ })
+}
+
+// BrailleCircle draws an approximated circle with the specified mid point and radius.
+// The mid point must be a valid pixel within the canvas.
+// All the points that form the circle must fit into the canvas.
+// The smallest valid radius is two.
+func BrailleCircle(bc *braille.Canvas, mid image.Point, radius int, opts ...BrailleCircleOption) error {
+ if ar := bc.Area(); !mid.In(ar) {
+ return fmt.Errorf("unable to draw circle with mid point %v which is outside of the braille canvas area %v", mid, ar)
+ }
+ if min := 2; radius < min {
+ return fmt.Errorf("unable to draw circle with radius %d, must be in range %d <= radius", radius, min)
+ }
+
+ opt := newBrailleCircleOptions()
+ for _, o := range opts {
+ o.set(opt)
+ }
+
+ if err := opt.validate(); err != nil {
+ return err
+ }
+
+ points := circlePoints(mid, radius)
+ if opt.arcOnly {
+ f, err := trig.FilterByAngle(points, mid, opt.startDegree, opt.endDegree)
+ if err != nil {
+ return err
+ }
+ points = f
+ if opt.filled && (opt.startDegree != 0 || opt.endDegree != 360) {
+ points = append(points, openingPoints(mid, radius, opt)...)
+ }
+ }
+ if err := drawPoints(bc, points, opt); err != nil {
+ return fmt.Errorf("failed to draw circle with mid:%v, radius:%d, start:%d degrees, end:%d degrees: %v", mid, radius, opt.startDegree, opt.endDegree, err)
+ }
+ if opt.filled {
+ return fillCircle(bc, points, mid, radius, opt)
+ }
+ return nil
+}
+
+// drawPoints draws the points onto the canvas.
+func drawPoints(bc *braille.Canvas, points []image.Point, opt *brailleCircleOptions) error {
+ for _, p := range points {
+ switch opt.pixelChange {
+ case braillePixelChangeSet:
+ if err := bc.SetPixel(p, opt.cellOpts...); err != nil {
+ return fmt.Errorf("SetPixel => %v", err)
+ }
+ case braillePixelChangeClear:
+ if err := bc.ClearPixel(p, opt.cellOpts...); err != nil {
+ return fmt.Errorf("ClearPixel => %v", err)
+ }
+
+ }
+ }
+ return nil
+}
+
+// fillCircle fills a circle that consists of the provided point and has the
+// mid point and radius.
+func fillCircle(bc *braille.Canvas, points []image.Point, mid image.Point, radius int, opt *brailleCircleOptions) error {
+ lineOpts := []BrailleLineOption{
+ BrailleLineCellOpts(opt.cellOpts...),
+ }
+ fillOpts := []BrailleFillOption{
+ BrailleFillCellOpts(opt.cellOpts...),
+ }
+ if opt.pixelChange == braillePixelChangeClear {
+ lineOpts = append(lineOpts, BrailleLineClearPixels())
+ fillOpts = append(fillOpts, BrailleFillClearPixels())
+ }
+
+ // Determine a fill point that should be inside of the circle sector.
+ midA, err := trig.RangeMid(opt.startDegree, opt.endDegree)
+ if err != nil {
+ return err
+ }
+ fp := trig.CirclePointAtAngle(midA, mid, radius-1)
+
+ // Ensure the fill point falls inside the circle.
+ // If drawing a partial circle, it must also fall within points belonging
+ // to the opening.
+ // This might not be true if drawing a partial circle and the arc is very
+ // small.
+ shape := points
+ if opt.arcOnly {
+ startP := trig.CirclePointAtAngle(opt.startDegree, mid, radius-1)
+ endP := trig.CirclePointAtAngle(opt.endDegree, mid, radius-1)
+ shape = append(shape, startP, endP)
+ }
+ if trig.PointIsIn(fp, shape) {
+ if err := BrailleFill(bc, fp, points, fillOpts...); err != nil {
+ return err
+ }
+ if err := BrailleLine(bc, mid, fp, lineOpts...); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// openingPoints returns points on the lines from the mid point to the circle
+// opening when drawing an incomplete circle.
+func openingPoints(mid image.Point, radius int, opt *brailleCircleOptions) []image.Point {
+ var points []image.Point
+ startP := trig.CirclePointAtAngle(opt.startDegree, mid, radius)
+ endP := trig.CirclePointAtAngle(opt.endDegree, mid, radius)
+ points = append(points, brailleLinePoints(mid, startP)...)
+ points = append(points, brailleLinePoints(mid, endP)...)
+ return points
+}
+
+// circlePoints returns a list of points that represent a circle with
+// the specified mid point and radius.
+func circlePoints(mid image.Point, radius int) []image.Point {
+ var points []image.Point
+
+ // Bresenham algorithm.
+ // https://en.wikipedia.org/wiki/Midpoint_circle_algorithm
+ x := radius
+ y := 0
+ dx := 1
+ dy := 1
+ diff := dx - (radius << 1) // Cheap multiplication by two.
+
+ for x >= y {
+ points = append(
+ points,
+ image.Point{mid.X + x, mid.Y + y},
+ image.Point{mid.X + y, mid.Y + x},
+ image.Point{mid.X - y, mid.Y + x},
+ image.Point{mid.X - x, mid.Y + y},
+ image.Point{mid.X - x, mid.Y - y},
+ image.Point{mid.X - y, mid.Y - x},
+ image.Point{mid.X + y, mid.Y - x},
+ image.Point{mid.X + x, mid.Y - y},
+ )
+
+ if diff <= 0 {
+ y++
+ diff += dy
+ dy += 2
+ }
+
+ if diff > 0 {
+ x--
+ dx += 2
+ diff += dx - (radius << 1)
+ }
+
+ }
+ return points
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/braille_fill.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/braille_fill.go
new file mode 100644
index 000000000..8bb311f1c
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/braille_fill.go
@@ -0,0 +1,160 @@
+// Copyright 2019 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package draw
+
+// braille_fill.go implements the flood-fill algorithm for filling shapes on the braille canvas.
+
+import (
+ "fmt"
+ "image"
+
+ "github.com/mum4k/termdash/cell"
+ "github.com/mum4k/termdash/private/canvas/braille"
+)
+
+// BrailleFillOption is used to provide options to BrailleFill.
+type BrailleFillOption interface {
+ // set sets the provided option.
+ set(*brailleFillOptions)
+}
+
+// brailleFillOptions stores the provided options.
+type brailleFillOptions struct {
+ cellOpts []cell.Option
+ pixelChange braillePixelChange
+}
+
+// newBrailleFillOptions returns a new brailleFillOptions instance.
+func newBrailleFillOptions() *brailleFillOptions {
+ return &brailleFillOptions{
+ pixelChange: braillePixelChangeSet,
+ }
+}
+
+// brailleFillOption implements BrailleFillOption.
+type brailleFillOption func(*brailleFillOptions)
+
+// set implements BrailleFillOption.set.
+func (o brailleFillOption) set(opts *brailleFillOptions) {
+ o(opts)
+}
+
+// BrailleFillCellOpts sets options on the cells that are set as part of
+// filling shapes.
+// Cell options on a braille canvas can only be set on the entire cell, not per
+// pixel.
+func BrailleFillCellOpts(cOpts ...cell.Option) BrailleFillOption {
+ return brailleFillOption(func(opts *brailleFillOptions) {
+ opts.cellOpts = cOpts
+ })
+}
+
+// BrailleFillClearPixels changes the behavior of BrailleFill, so that it
+// clears the pixels instead of setting them.
+// Useful in order to "erase" the filled area as opposed to drawing one.
+func BrailleFillClearPixels() BrailleFillOption {
+ return brailleFillOption(func(opts *brailleFillOptions) {
+ opts.pixelChange = braillePixelChangeClear
+ })
+}
+
+// BrailleFill fills the braille canvas starting at the specified point.
+// The function will not fill or cross over any points in the defined border.
+// The start point must be in the canvas.
+func BrailleFill(bc *braille.Canvas, start image.Point, border []image.Point, opts ...BrailleFillOption) error {
+ if ar := bc.Area(); !start.In(ar) {
+ return fmt.Errorf("unable to start filling canvas at point %v which is outside of the braille canvas area %v", start, ar)
+ }
+
+ opt := newBrailleFillOptions()
+ for _, o := range opts {
+ o.set(opt)
+ }
+
+ b := map[image.Point]struct{}{}
+ for _, p := range border {
+ b[p] = struct{}{}
+ }
+
+ v := newVisitable(bc.Area(), b)
+ visitor := func(p image.Point) error {
+ switch opt.pixelChange {
+ case braillePixelChangeSet:
+ return bc.SetPixel(p, opt.cellOpts...)
+ case braillePixelChangeClear:
+ return bc.ClearPixel(p, opt.cellOpts...)
+ }
+ return nil
+ }
+ return brailleDFS(v, start, visitor)
+}
+
+// visitable represents an area that can be visited.
+// It tracks nodes that are already visited.
+type visitable struct {
+ area image.Rectangle
+ visited map[image.Point]struct{}
+}
+
+// newVisitable returns a new visitable object initialized for the provided
+// area and already visited nodes.
+func newVisitable(ar image.Rectangle, visited map[image.Point]struct{}) *visitable {
+ if visited == nil {
+ visited = map[image.Point]struct{}{}
+ }
+ return &visitable{
+ area: ar,
+ visited: visited,
+ }
+}
+
+// neighborsAt returns all valid neighbors for the specified point.
+func (v *visitable) neighborsAt(p image.Point) []image.Point {
+ var res []image.Point
+ for _, neigh := range []image.Point{
+ {p.X - 1, p.Y}, // left
+ {p.X + 1, p.Y}, // right
+ {p.X, p.Y - 1}, // up
+ {p.X, p.Y + 1}, // down
+ } {
+ if !neigh.In(v.area) {
+ continue
+ }
+ if _, ok := v.visited[neigh]; ok {
+ continue
+ }
+ v.visited[neigh] = struct{}{}
+ res = append(res, neigh)
+ }
+ return res
+}
+
+// brailleDFS visits every point in the area and runs the visitor function.
+func brailleDFS(v *visitable, p image.Point, visitFn func(image.Point) error) error {
+ neigh := v.neighborsAt(p)
+ if len(neigh) == 0 {
+ return nil
+ }
+
+ for _, n := range neigh {
+ if err := visitFn(n); err != nil {
+ return err
+ }
+ if err := brailleDFS(v, n, visitFn); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/braille_line.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/braille_line.go
new file mode 100644
index 000000000..c9f412321
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/braille_line.go
@@ -0,0 +1,204 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package draw
+
+// braille_line.go contains code that draws lines on a braille canvas.
+
+import (
+ "fmt"
+ "image"
+
+ "github.com/mum4k/termdash/cell"
+ "github.com/mum4k/termdash/private/canvas/braille"
+ "github.com/mum4k/termdash/private/numbers"
+)
+
+// braillePixelChange represents an action on a pixel on the braille canvas.
+type braillePixelChange int
+
+// String implements fmt.Stringer()
+func (bpc braillePixelChange) String() string {
+ if n, ok := braillePixelChangeNames[bpc]; ok {
+ return n
+ }
+ return "braillePixelChangeUnknown"
+}
+
+// braillePixelChangeNames maps braillePixelChange values to human readable names.
+var braillePixelChangeNames = map[braillePixelChange]string{
+ braillePixelChangeSet: "braillePixelChangeSet",
+ braillePixelChangeClear: "braillePixelChangeClear",
+}
+
+const (
+ braillePixelChangeUnknown braillePixelChange = iota
+
+ braillePixelChangeSet
+ braillePixelChangeClear
+)
+
+// BrailleLineOption is used to provide options to BrailleLine().
+type BrailleLineOption interface {
+ // set sets the provided option.
+ set(*brailleLineOptions)
+}
+
+// brailleLineOptions stores the provided options.
+type brailleLineOptions struct {
+ cellOpts []cell.Option
+ pixelChange braillePixelChange
+}
+
+// newBrailleLineOptions returns a new brailleLineOptions instance.
+func newBrailleLineOptions() *brailleLineOptions {
+ return &brailleLineOptions{
+ pixelChange: braillePixelChangeSet,
+ }
+}
+
+// brailleLineOption implements BrailleLineOption.
+type brailleLineOption func(*brailleLineOptions)
+
+// set implements BrailleLineOption.set.
+func (o brailleLineOption) set(opts *brailleLineOptions) {
+ o(opts)
+}
+
+// BrailleLineCellOpts sets options on the cells that contain the line.
+// Cell options on a braille canvas can only be set on the entire cell, not per
+// pixel.
+func BrailleLineCellOpts(cOpts ...cell.Option) BrailleLineOption {
+ return brailleLineOption(func(opts *brailleLineOptions) {
+ opts.cellOpts = cOpts
+ })
+}
+
+// BrailleLineClearPixels changes the behavior of BrailleLine, so that it
+// clears the pixels belonging to the line instead of setting them.
+// Useful in order to "erase" a line from the canvas as opposed to drawing one.
+func BrailleLineClearPixels() BrailleLineOption {
+ return brailleLineOption(func(opts *brailleLineOptions) {
+ opts.pixelChange = braillePixelChangeClear
+ })
+}
+
+// BrailleLine draws an approximated line segment on the braille canvas between
+// the two provided points.
+// Both start and end must be valid points within the canvas. Start and end can
+// be the same point in which case only one pixel will be set on the braille
+// canvas.
+// The start or end coordinates must not be negative.
+func BrailleLine(bc *braille.Canvas, start, end image.Point, opts ...BrailleLineOption) error {
+ if start.X < 0 || start.Y < 0 {
+ return fmt.Errorf("the start coordinates cannot be negative, got: %v", start)
+ }
+ if end.X < 0 || end.Y < 0 {
+ return fmt.Errorf("the end coordinates cannot be negative, got: %v", end)
+ }
+
+ opt := newBrailleLineOptions()
+ for _, o := range opts {
+ o.set(opt)
+ }
+
+ points := brailleLinePoints(start, end)
+ for _, p := range points {
+ switch opt.pixelChange {
+ case braillePixelChangeSet:
+ if err := bc.SetPixel(p, opt.cellOpts...); err != nil {
+ return fmt.Errorf("bc.SetPixel(%v) => %v", p, err)
+ }
+ case braillePixelChangeClear:
+ if err := bc.ClearPixel(p, opt.cellOpts...); err != nil {
+ return fmt.Errorf("bc.ClearPixel(%v) => %v", p, err)
+ }
+ }
+ }
+ return nil
+}
+
+// brailleLinePoints returns the points to set when drawing the line.
+func brailleLinePoints(start, end image.Point) []image.Point {
+ // Implements Bresenham's line algorithm.
+ // https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
+
+ vertProj := numbers.Abs(end.Y - start.Y)
+ horizProj := numbers.Abs(end.X - start.X)
+ if vertProj < horizProj {
+ if start.X > end.X {
+ return lineLow(end.X, end.Y, start.X, start.Y)
+ }
+ return lineLow(start.X, start.Y, end.X, end.Y)
+ }
+ if start.Y > end.Y {
+ return lineHigh(end.X, end.Y, start.X, start.Y)
+ }
+ return lineHigh(start.X, start.Y, end.X, end.Y)
+}
+
+// lineLow returns points that create a line whose horizontal projection
+// (end.X - start.X) is longer than its vertical projection
+// (end.Y - start.Y).
+func lineLow(x0, y0, x1, y1 int) []image.Point {
+ deltaX := x1 - x0
+ deltaY := y1 - y0
+
+ stepY := 1
+ if deltaY < 0 {
+ stepY = -1
+ deltaY = -deltaY
+ }
+
+ var res []image.Point
+ diff := 2*deltaY - deltaX
+ y := y0
+ for x := x0; x <= x1; x++ {
+ res = append(res, image.Point{x, y})
+ if diff > 0 {
+ y += stepY
+ diff -= 2 * deltaX
+ }
+ diff += 2 * deltaY
+ }
+ return res
+}
+
+// lineHigh returns points that createa line whose vertical projection
+// (end.Y - start.Y) is longer than its horizontal projection
+// (end.X - start.X).
+func lineHigh(x0, y0, x1, y1 int) []image.Point {
+ deltaX := x1 - x0
+ deltaY := y1 - y0
+
+ stepX := 1
+ if deltaX < 0 {
+ stepX = -1
+ deltaX = -deltaX
+ }
+
+ var res []image.Point
+ diff := 2*deltaX - deltaY
+ x := x0
+ for y := y0; y <= y1; y++ {
+ res = append(res, image.Point{x, y})
+
+ if diff > 0 {
+ x += stepX
+ diff -= 2 * deltaY
+ }
+ diff += 2 * deltaX
+ }
+ return res
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/draw.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/draw.go
new file mode 100644
index 000000000..37c01bf7e
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/draw.go
@@ -0,0 +1,17 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package draw provides functions that draw lines, shapes, etc on 2-D terminal
+// like canvases.
+package draw
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/hv_line.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/hv_line.go
new file mode 100644
index 000000000..35318f42d
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/hv_line.go
@@ -0,0 +1,207 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package draw
+
+// hv_line.go contains code that draws horizontal and vertical lines.
+
+import (
+ "fmt"
+ "image"
+
+ "github.com/mum4k/termdash/cell"
+ "github.com/mum4k/termdash/linestyle"
+ "github.com/mum4k/termdash/private/canvas"
+)
+
+// HVLineOption is used to provide options to HVLine().
+type HVLineOption interface {
+ // set sets the provided option.
+ set(*hVLineOptions)
+}
+
+// hVLineOptions stores the provided options.
+type hVLineOptions struct {
+ cellOpts []cell.Option
+ lineStyle linestyle.LineStyle
+}
+
+// newHVLineOptions returns a new hVLineOptions instance.
+func newHVLineOptions() *hVLineOptions {
+ return &hVLineOptions{
+ lineStyle: DefaultLineStyle,
+ }
+}
+
+// hVLineOption implements HVLineOption.
+type hVLineOption func(*hVLineOptions)
+
+// set implements HVLineOption.set.
+func (o hVLineOption) set(opts *hVLineOptions) {
+ o(opts)
+}
+
+// DefaultLineStyle is the default value for the HVLineStyle option.
+const DefaultLineStyle = linestyle.Light
+
+// HVLineStyle sets the style of the line.
+// Defaults to DefaultLineStyle.
+func HVLineStyle(ls linestyle.LineStyle) HVLineOption {
+ return hVLineOption(func(opts *hVLineOptions) {
+ opts.lineStyle = ls
+ })
+}
+
+// HVLineCellOpts sets options on the cells that contain the line.
+func HVLineCellOpts(cOpts ...cell.Option) HVLineOption {
+ return hVLineOption(func(opts *hVLineOptions) {
+ opts.cellOpts = cOpts
+ })
+}
+
+// HVLine represents one horizontal or vertical line.
+type HVLine struct {
+ // Start is the cell where the line starts.
+ Start image.Point
+ // End is the cell where the line ends.
+ End image.Point
+}
+
+// HVLines draws horizontal or vertical lines. Handles drawing of the correct
+// characters for locations where any two lines cross (e.g. a corner, a T shape
+// or a cross). Each line must be at least two cells long. Both start and end
+// must be on the same horizontal (same X coordinate) or same vertical (same Y
+// coordinate) line.
+func HVLines(c *canvas.Canvas, lines []HVLine, opts ...HVLineOption) error {
+ opt := newHVLineOptions()
+ for _, o := range opts {
+ o.set(opt)
+ }
+
+ g := newHVLineGraph()
+ for _, l := range lines {
+ line, err := newHVLine(c, l.Start, l.End, opt)
+ if err != nil {
+ return err
+ }
+ g.addLine(line)
+
+ switch {
+ case line.horizontal():
+ for curX := line.start.X; ; curX++ {
+ cur := image.Point{curX, line.start.Y}
+ if _, err := c.SetCell(cur, line.mainPart, opt.cellOpts...); err != nil {
+ return err
+ }
+
+ if curX == line.end.X {
+ break
+ }
+ }
+
+ case line.vertical():
+ for curY := line.start.Y; ; curY++ {
+ cur := image.Point{line.start.X, curY}
+ if _, err := c.SetCell(cur, line.mainPart, opt.cellOpts...); err != nil {
+ return err
+ }
+
+ if curY == line.end.Y {
+ break
+ }
+ }
+ }
+ }
+
+ for _, n := range g.multiEdgeNodes() {
+ r, err := n.rune(opt.lineStyle)
+ if err != nil {
+ return err
+ }
+ if _, err := c.SetCell(n.p, r, opt.cellOpts...); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// hVLine represents a line that will be drawn on the canvas.
+type hVLine struct {
+ // start is the starting point of the line.
+ start image.Point
+
+ // end is the ending point of the line.
+ end image.Point
+
+ // mainPart is either parts[vLine] or parts[hLine] depending on whether
+ // this is horizontal or vertical line.
+ mainPart rune
+
+ // opts are the options provided in a call to HVLine().
+ opts *hVLineOptions
+}
+
+// newHVLine creates a new hVLine instance.
+// Swaps start and end if necessary, so that horizontal drawing is always left
+// to right and vertical is always top down.
+func newHVLine(c *canvas.Canvas, start, end image.Point, opts *hVLineOptions) (*hVLine, error) {
+ if ar := c.Area(); !start.In(ar) || !end.In(ar) {
+ return nil, fmt.Errorf("both the start%v and the end%v must be in the canvas area: %v", start, end, ar)
+ }
+
+ parts, err := lineParts(opts.lineStyle)
+ if err != nil {
+ return nil, err
+ }
+
+ var mainPart rune
+ switch {
+ case start.X != end.X && start.Y != end.Y:
+ return nil, fmt.Errorf("can only draw horizontal (same X coordinates) or vertical (same Y coordinates), got start:%v end:%v", start, end)
+
+ case start.X == end.X && start.Y == end.Y:
+ return nil, fmt.Errorf("the line must at least one cell long, got start%v, end%v", start, end)
+
+ case start.X == end.X:
+ mainPart = parts[vLine]
+ if start.Y > end.Y {
+ start, end = end, start
+ }
+
+ case start.Y == end.Y:
+ mainPart = parts[hLine]
+ if start.X > end.X {
+ start, end = end, start
+ }
+
+ }
+
+ return &hVLine{
+ start: start,
+ end: end,
+ mainPart: mainPart,
+ opts: opts,
+ }, nil
+}
+
+// horizontal determines if this is a horizontal line.
+func (hvl *hVLine) horizontal() bool {
+ return hvl.mainPart == lineStyleChars[hvl.opts.lineStyle][hLine]
+}
+
+// vertical determines if this is a vertical line.
+func (hvl *hVLine) vertical() bool {
+ return hvl.mainPart == lineStyleChars[hvl.opts.lineStyle][vLine]
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/hv_line_graph.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/hv_line_graph.go
new file mode 100644
index 000000000..ccbc72a57
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/hv_line_graph.go
@@ -0,0 +1,206 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package draw
+
+// hv_line_graph.go helps to keep track of locations where lines cross.
+
+import (
+ "fmt"
+ "image"
+
+ "github.com/mum4k/termdash/linestyle"
+)
+
+// hVLineEdge is an edge between two points on the graph.
+type hVLineEdge struct {
+ // from is the starting node of this edge.
+ // From is guaranteed to be less than to.
+ from image.Point
+
+ // to is the ending point of this edge.
+ to image.Point
+}
+
+// newHVLineEdge returns a new edge between the two points.
+func newHVLineEdge(from, to image.Point) hVLineEdge {
+ return hVLineEdge{
+ from: from,
+ to: to,
+ }
+}
+
+// hVLineNode represents one node in the graph.
+// I.e. one cell.
+type hVLineNode struct {
+ // p is the point where this node is.
+ p image.Point
+
+ // edges are the edges between this node and the surrounding nodes.
+ // The code only supports horizontal and vertical lines so there can only
+ // ever be edges to nodes on these planes.
+ edges map[hVLineEdge]bool
+}
+
+// newHVLineNode creates a new newHVLineNode.
+func newHVLineNode(p image.Point) *hVLineNode {
+ return &hVLineNode{
+ p: p,
+ edges: map[hVLineEdge]bool{},
+ }
+}
+
+// hasDown determines if this node has an edge to the one below it.
+func (n *hVLineNode) hasDown() bool {
+ target := newHVLineEdge(n.p, image.Point{n.p.X, n.p.Y + 1})
+ _, ok := n.edges[target]
+ return ok
+}
+
+// hasUp determines if this node has an edge to the one above it.
+func (n *hVLineNode) hasUp() bool {
+ target := newHVLineEdge(image.Point{n.p.X, n.p.Y - 1}, n.p)
+ _, ok := n.edges[target]
+ return ok
+}
+
+// hasLeft determines if this node has an edge to the next node on the left.
+func (n *hVLineNode) hasLeft() bool {
+ target := newHVLineEdge(image.Point{n.p.X - 1, n.p.Y}, n.p)
+ _, ok := n.edges[target]
+ return ok
+}
+
+// hasRight determines if this node has an edge to the next node on the right.
+func (n *hVLineNode) hasRight() bool {
+ target := newHVLineEdge(n.p, image.Point{n.p.X + 1, n.p.Y})
+ _, ok := n.edges[target]
+ return ok
+}
+
+// rune, given the selected line style returns the correct line character to
+// represent this node.
+// Only handles nodes with two or more edges, as returned by multiEdgeNodes().
+func (n *hVLineNode) rune(ls linestyle.LineStyle) (rune, error) {
+ parts, err := lineParts(ls)
+ if err != nil {
+ return -1, err
+ }
+
+ switch len(n.edges) {
+ case 2:
+ switch {
+ case n.hasLeft() && n.hasRight():
+ return parts[hLine], nil
+ case n.hasUp() && n.hasDown():
+ return parts[vLine], nil
+ case n.hasDown() && n.hasRight():
+ return parts[topLeftCorner], nil
+ case n.hasDown() && n.hasLeft():
+ return parts[topRightCorner], nil
+ case n.hasUp() && n.hasRight():
+ return parts[bottomLeftCorner], nil
+ case n.hasUp() && n.hasLeft():
+ return parts[bottomRightCorner], nil
+ default:
+ return -1, fmt.Errorf("unexpected two edges in node representing point %v: %v", n.p, n.edges)
+ }
+
+ case 3:
+ switch {
+ case n.hasUp() && n.hasLeft() && n.hasRight():
+ return parts[hAndUp], nil
+ case n.hasDown() && n.hasLeft() && n.hasRight():
+ return parts[hAndDown], nil
+ case n.hasUp() && n.hasDown() && n.hasRight():
+ return parts[vAndRight], nil
+ case n.hasUp() && n.hasDown() && n.hasLeft():
+ return parts[vAndLeft], nil
+
+ default:
+ return -1, fmt.Errorf("unexpected three edges in node representing point %v: %v", n.p, n.edges)
+ }
+
+ case 4:
+ return parts[vAndH], nil
+ default:
+ return -1, fmt.Errorf("unexpected number of edges(%d) in node representing point %v", len(n.edges), n.p)
+ }
+}
+
+// hVLineGraph represents lines on the canvas as a bidirectional graph of
+// nodes. Helps to determine the characters that should be used where multiple
+// lines cross.
+type hVLineGraph struct {
+ nodes map[image.Point]*hVLineNode
+}
+
+// newHVLineGraph creates a new hVLineGraph.
+func newHVLineGraph() *hVLineGraph {
+ return &hVLineGraph{
+ nodes: make(map[image.Point]*hVLineNode),
+ }
+}
+
+// getOrCreateNode gets an existing or creates a new node for the point.
+func (g *hVLineGraph) getOrCreateNode(p image.Point) *hVLineNode {
+ if n, ok := g.nodes[p]; ok {
+ return n
+ }
+ n := newHVLineNode(p)
+ g.nodes[p] = n
+ return n
+}
+
+// addLine adds a line to the graph.
+// This adds edges between all the points on the line.
+func (g *hVLineGraph) addLine(line *hVLine) {
+ switch {
+ case line.horizontal():
+ for curX := line.start.X; curX < line.end.X; curX++ {
+ from := image.Point{curX, line.start.Y}
+ to := image.Point{curX + 1, line.start.Y}
+ n1 := g.getOrCreateNode(from)
+ n2 := g.getOrCreateNode(to)
+ edge := newHVLineEdge(from, to)
+ n1.edges[edge] = true
+ n2.edges[edge] = true
+ }
+
+ case line.vertical():
+ for curY := line.start.Y; curY < line.end.Y; curY++ {
+ from := image.Point{line.start.X, curY}
+ to := image.Point{line.start.X, curY + 1}
+ n1 := g.getOrCreateNode(from)
+ n2 := g.getOrCreateNode(to)
+ edge := newHVLineEdge(from, to)
+ n1.edges[edge] = true
+ n2.edges[edge] = true
+ }
+ }
+}
+
+// multiEdgeNodes returns all nodes that have more than one edge. These are
+// the nodes where we might need to use different line characters to represent
+// the crossing of multiple lines.
+func (g *hVLineGraph) multiEdgeNodes() []*hVLineNode {
+ var nodes []*hVLineNode
+ for _, n := range g.nodes {
+ if len(n.edges) <= 1 {
+ continue
+ }
+ nodes = append(nodes, n)
+ }
+ return nodes
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/line_style.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/line_style.go
new file mode 100644
index 000000000..41f1df4ee
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/line_style.go
@@ -0,0 +1,129 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package draw
+
+import (
+ "fmt"
+
+ "github.com/mum4k/termdash/linestyle"
+ "github.com/mum4k/termdash/private/runewidth"
+)
+
+// line_style.go contains the Unicode characters used for drawing lines of
+// different styles.
+
+// lineStyleChars maps the line styles to the corresponding component characters.
+// Source: http://en.wikipedia.org/wiki/Box-drawing_character.
+var lineStyleChars = map[linestyle.LineStyle]map[linePart]rune{
+ linestyle.Light: {
+ hLine: '─',
+ vLine: '│',
+ topLeftCorner: '┌',
+ topRightCorner: '┐',
+ bottomLeftCorner: '└',
+ bottomRightCorner: '┘',
+ hAndUp: '┴',
+ hAndDown: '┬',
+ vAndLeft: '┤',
+ vAndRight: '├',
+ vAndH: '┼',
+ },
+ linestyle.Double: {
+ hLine: '═',
+ vLine: '║',
+ topLeftCorner: '╔',
+ topRightCorner: '╗',
+ bottomLeftCorner: '╚',
+ bottomRightCorner: '╝',
+ hAndUp: '╩',
+ hAndDown: '╦',
+ vAndLeft: '╣',
+ vAndRight: '╠',
+ vAndH: '╬',
+ },
+ linestyle.Round: {
+ hLine: '─',
+ vLine: '│',
+ topLeftCorner: '╭',
+ topRightCorner: '╮',
+ bottomLeftCorner: '╰',
+ bottomRightCorner: '╯',
+ hAndUp: '┴',
+ hAndDown: '┬',
+ vAndLeft: '┤',
+ vAndRight: '├',
+ vAndH: '┼',
+ },
+}
+
+// init verifies that all line parts are half-width runes (occupy only one
+// cell).
+func init() {
+ for ls, parts := range lineStyleChars {
+ for part, r := range parts {
+ if got := runewidth.RuneWidth(r); got > 1 {
+ panic(fmt.Errorf("line style %v line part %v is a rune %c with width %v, all parts must be half-width runes (width of one)", ls, part, r, got))
+ }
+ }
+ }
+}
+
+// lineParts returns the line component characters for the provided line style.
+func lineParts(ls linestyle.LineStyle) (map[linePart]rune, error) {
+ parts, ok := lineStyleChars[ls]
+ if !ok {
+ return nil, fmt.Errorf("unsupported line style %d", ls)
+ }
+ return parts, nil
+}
+
+// linePart identifies individual line parts.
+type linePart int
+
+// String implements fmt.Stringer()
+func (lp linePart) String() string {
+ if n, ok := linePartNames[lp]; ok {
+ return n
+ }
+ return "linePartUnknown"
+}
+
+// linePartNames maps linePart values to human readable names.
+var linePartNames = map[linePart]string{
+ vLine: "linePartVLine",
+ topLeftCorner: "linePartTopLeftCorner",
+ topRightCorner: "linePartTopRightCorner",
+ bottomLeftCorner: "linePartBottomLeftCorner",
+ bottomRightCorner: "linePartBottomRightCorner",
+ hAndUp: "linePartHAndUp",
+ hAndDown: "linePartHAndDown",
+ vAndLeft: "linePartVAndLeft",
+ vAndRight: "linePartVAndRight",
+ vAndH: "linePartVAndH",
+}
+
+const (
+ hLine linePart = iota
+ vLine
+ topLeftCorner
+ topRightCorner
+ bottomLeftCorner
+ bottomRightCorner
+ hAndUp
+ hAndDown
+ vAndLeft
+ vAndRight
+ vAndH
+)
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/rectangle.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/rectangle.go
new file mode 100644
index 000000000..cd96ff715
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/rectangle.go
@@ -0,0 +1,93 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package draw
+
+// rectangle.go draws a rectangle.
+
+import (
+ "fmt"
+ "image"
+
+ "github.com/mum4k/termdash/cell"
+ "github.com/mum4k/termdash/private/canvas"
+)
+
+// RectangleOption is used to provide options to the Rectangle function.
+type RectangleOption interface {
+ // set sets the provided option.
+ set(*rectOptions)
+}
+
+// rectOptions stores the provided options.
+type rectOptions struct {
+ cellOpts []cell.Option
+ char rune
+}
+
+// rectOption implements RectangleOption.
+type rectOption func(rOpts *rectOptions)
+
+// set implements RectangleOption.set.
+func (ro rectOption) set(rOpts *rectOptions) {
+ ro(rOpts)
+}
+
+// RectCellOpts sets options on the cells that create the rectangle.
+func RectCellOpts(opts ...cell.Option) RectangleOption {
+ return rectOption(func(rOpts *rectOptions) {
+ rOpts.cellOpts = append(rOpts.cellOpts, opts...)
+ })
+}
+
+// DefaultRectChar is the default value for the RectChar option.
+const DefaultRectChar = ' '
+
+// RectChar sets the character used in each of the cells of the rectangle.
+func RectChar(c rune) RectangleOption {
+ return rectOption(func(rOpts *rectOptions) {
+ rOpts.char = c
+ })
+}
+
+// Rectangle draws a filled rectangle on the canvas.
+func Rectangle(c *canvas.Canvas, r image.Rectangle, opts ...RectangleOption) error {
+ opt := &rectOptions{
+ char: DefaultRectChar,
+ }
+ for _, o := range opts {
+ o.set(opt)
+ }
+
+ if ar := c.Area(); !r.In(ar) {
+ return fmt.Errorf("the requested rectangle %v doesn't fit the canvas area %v", r, ar)
+ }
+
+ if r.Dx() < 1 || r.Dy() < 1 {
+ return fmt.Errorf("the rectangle must be at least 1x1 cell, got %v", r)
+ }
+
+ for col := r.Min.X; col < r.Max.X; col++ {
+ for row := r.Min.Y; row < r.Max.Y; row++ {
+ cells, err := c.SetCell(image.Point{col, row}, opt.char, opt.cellOpts...)
+ if err != nil {
+ return err
+ }
+ if cells != 1 {
+ return fmt.Errorf("invalid rectangle character %q, this character occupies %d cells, the implementation only supports half-width runes that occupy exactly one cell", opt.char, cells)
+ }
+ }
+ }
+ return nil
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/text.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/text.go
new file mode 100644
index 000000000..17c4954a0
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/text.go
@@ -0,0 +1,195 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package draw
+
+// text.go contains code that prints UTF-8 encoded strings on the canvas.
+
+import (
+ "fmt"
+ "image"
+ "strings"
+
+ "github.com/mum4k/termdash/cell"
+ "github.com/mum4k/termdash/private/canvas"
+ "github.com/mum4k/termdash/private/runewidth"
+)
+
+// OverrunMode represents
+type OverrunMode int
+
+// String implements fmt.Stringer()
+func (om OverrunMode) String() string {
+ if n, ok := overrunModeNames[om]; ok {
+ return n
+ }
+ return "OverrunModeUnknown"
+}
+
+// overrunModeNames maps OverrunMode values to human readable names.
+var overrunModeNames = map[OverrunMode]string{
+ OverrunModeStrict: "OverrunModeStrict",
+ OverrunModeTrim: "OverrunModeTrim",
+ OverrunModeThreeDot: "OverrunModeThreeDot",
+}
+
+const (
+ // OverrunModeStrict verifies that the drawn value fits the canvas and
+ // returns an error if it doesn't.
+ OverrunModeStrict OverrunMode = iota
+
+ // OverrunModeTrim trims the part of the text that doesn't fit.
+ OverrunModeTrim
+
+ // OverrunModeThreeDot trims the text and places the horizontal ellipsis
+ // '…' character at the end.
+ OverrunModeThreeDot
+)
+
+// TextOption is used to provide options to Text().
+type TextOption interface {
+ // set sets the provided option.
+ set(*textOptions)
+}
+
+// textOptions stores the provided options.
+type textOptions struct {
+ cellOpts []cell.Option
+ maxX int
+ overrunMode OverrunMode
+}
+
+// textOption implements TextOption.
+type textOption func(*textOptions)
+
+// set implements TextOption.set.
+func (to textOption) set(tOpts *textOptions) {
+ to(tOpts)
+}
+
+// TextCellOpts sets options on the cells that contain the text.
+func TextCellOpts(opts ...cell.Option) TextOption {
+ return textOption(func(tOpts *textOptions) {
+ tOpts.cellOpts = opts
+ })
+}
+
+// TextMaxX sets a limit on the X coordinate (column) of the drawn text.
+// The X coordinate of all cells used by the text must be within
+// start.X <= X < TextMaxX.
+// If not provided, the width of the canvas is used as TextMaxX.
+func TextMaxX(x int) TextOption {
+ return textOption(func(tOpts *textOptions) {
+ tOpts.maxX = x
+ })
+}
+
+// TextOverrunMode indicates what to do with text that overruns the TextMaxX()
+// or the width of the canvas if TextMaxX() isn't specified.
+// Defaults to OverrunModeStrict.
+func TextOverrunMode(om OverrunMode) TextOption {
+ return textOption(func(tOpts *textOptions) {
+ tOpts.overrunMode = om
+ })
+}
+
+// TrimText trims the provided text so that it fits the specified amount of cells.
+func TrimText(text string, maxCells int, om OverrunMode) (string, error) {
+ if maxCells < 1 {
+ return "", fmt.Errorf("maxCells(%d) cannot be less than one", maxCells)
+ }
+
+ textCells := runewidth.StringWidth(text)
+ if textCells <= maxCells {
+ // Nothing to do if the text fits.
+ return text, nil
+ }
+
+ switch om {
+ case OverrunModeStrict:
+ return "", fmt.Errorf("the requested text %q takes %d cells to draw, space is available for only %d cells and overrun mode is %v", text, textCells, maxCells, om)
+ case OverrunModeTrim, OverrunModeThreeDot:
+ default:
+ return "", fmt.Errorf("unsupported overrun mode %d", om)
+ }
+
+ var b strings.Builder
+ cur := 0
+ for _, r := range text {
+ rw := runewidth.RuneWidth(r)
+ if cur+rw >= maxCells {
+ switch {
+ case om == OverrunModeTrim:
+ // Only write the rune if it still fits, i.e. don't cut
+ // full-width runes in half.
+ if cur+rw == maxCells {
+ b.WriteRune(r)
+ }
+ case om == OverrunModeThreeDot:
+ b.WriteRune('…')
+ }
+ break
+ }
+
+ b.WriteRune(r)
+ cur += rw
+ }
+ return b.String(), nil
+}
+
+// Text prints the provided text on the canvas starting at the provided point.
+func Text(c *canvas.Canvas, text string, start image.Point, opts ...TextOption) error {
+ ar := c.Area()
+ if !start.In(ar) {
+ return fmt.Errorf("the requested start point %v falls outside of the provided canvas %v", start, ar)
+ }
+
+ opt := &textOptions{}
+ for _, o := range opts {
+ o.set(opt)
+ }
+
+ if opt.maxX < 0 || opt.maxX > ar.Max.X {
+ return fmt.Errorf("invalid TextMaxX(%v), must be a positive number that is <= canvas.width %v", opt.maxX, ar.Dx())
+ }
+
+ var wantMaxX int
+ if opt.maxX == 0 {
+ wantMaxX = ar.Max.X
+ } else {
+ wantMaxX = opt.maxX
+ }
+
+ maxCells := wantMaxX - start.X
+ trimmed, err := TrimText(text, maxCells, opt.overrunMode)
+ if err != nil {
+ return err
+ }
+
+ cur := start
+ for _, r := range trimmed {
+ cells, err := c.SetCell(cur, r, opt.cellOpts...)
+ if err != nil {
+ return err
+ }
+ cur = image.Point{cur.X + cells, cur.Y}
+ }
+ return nil
+}
+
+// ResizeNeeded draws an unicode character indicating that the canvas size is
+// too small to draw meaningful content.
+func ResizeNeeded(cvs *canvas.Canvas) error {
+ return Text(cvs, "⇄", image.Point{0, 0})
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/vertical_text.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/vertical_text.go
new file mode 100644
index 000000000..44aadc9e5
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/draw/vertical_text.go
@@ -0,0 +1,120 @@
+// Copyright 2019 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package draw
+
+// vertical_text.go contains code that prints UTF-8 encoded strings on the
+// canvas in vertical columns instead of lines.
+
+import (
+ "fmt"
+ "image"
+
+ "github.com/mum4k/termdash/cell"
+ "github.com/mum4k/termdash/private/canvas"
+)
+
+// VerticalTextOption is used to provide options to Text().
+type VerticalTextOption interface {
+ // set sets the provided option.
+ set(*verticalTextOptions)
+}
+
+// verticalTextOptions stores the provided options.
+type verticalTextOptions struct {
+ cellOpts []cell.Option
+ maxY int
+ overrunMode OverrunMode
+}
+
+// verticalTextOption implements VerticalTextOption.
+type verticalTextOption func(*verticalTextOptions)
+
+// set implements VerticalTextOption.set.
+func (vto verticalTextOption) set(vtOpts *verticalTextOptions) {
+ vto(vtOpts)
+}
+
+// VerticalTextCellOpts sets options on the cells that contain the text.
+func VerticalTextCellOpts(opts ...cell.Option) VerticalTextOption {
+ return verticalTextOption(func(vtOpts *verticalTextOptions) {
+ vtOpts.cellOpts = opts
+ })
+}
+
+// VerticalTextMaxY sets a limit on the Y coordinate (row) of the drawn text.
+// The Y coordinate of all cells used by the vertical text must be within
+// start.Y <= Y < VerticalTextMaxY.
+// If not provided, the height of the canvas is used as VerticalTextMaxY.
+func VerticalTextMaxY(y int) VerticalTextOption {
+ return verticalTextOption(func(vtOpts *verticalTextOptions) {
+ vtOpts.maxY = y
+ })
+}
+
+// VerticalTextOverrunMode indicates what to do with text that overruns the
+// VerticalTextMaxY() or the width of the canvas if VerticalTextMaxY() isn't
+// specified.
+// Defaults to OverrunModeStrict.
+func VerticalTextOverrunMode(om OverrunMode) VerticalTextOption {
+ return verticalTextOption(func(vtOpts *verticalTextOptions) {
+ vtOpts.overrunMode = om
+ })
+}
+
+// VerticalText prints the provided text on the canvas starting at the provided point.
+// The text is printed in a vertical orientation, i.e:
+// H
+// e
+// l
+// l
+// o
+func VerticalText(c *canvas.Canvas, text string, start image.Point, opts ...VerticalTextOption) error {
+ ar := c.Area()
+ if !start.In(ar) {
+ return fmt.Errorf("the requested start point %v falls outside of the provided canvas %v", start, ar)
+ }
+
+ opt := &verticalTextOptions{}
+ for _, o := range opts {
+ o.set(opt)
+ }
+
+ if opt.maxY < 0 || opt.maxY > ar.Max.Y {
+ return fmt.Errorf("invalid VerticalTextMaxY(%v), must be a positive number that is <= canvas.width %v", opt.maxY, ar.Dy())
+ }
+
+ var wantMaxY int
+ if opt.maxY == 0 {
+ wantMaxY = ar.Max.Y
+ } else {
+ wantMaxY = opt.maxY
+ }
+
+ maxCells := wantMaxY - start.Y
+ trimmed, err := TrimText(text, maxCells, opt.overrunMode)
+ if err != nil {
+ return err
+ }
+
+ cur := start
+ for _, r := range trimmed {
+ cells, err := c.SetCell(cur, r, opt.cellOpts...)
+ if err != nil {
+ return err
+ }
+ cur = image.Point{cur.X, cur.Y + cells}
+ }
+ return nil
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/event/event.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/event/event.go
new file mode 100644
index 000000000..e9ef18dbb
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/event/event.go
@@ -0,0 +1,260 @@
+// Copyright 2019 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package event provides a non-blocking event distribution and subscription
+// system.
+package event
+
+import (
+ "context"
+ "reflect"
+ "sync"
+
+ "github.com/mum4k/termdash/private/event/eventqueue"
+ "github.com/mum4k/termdash/terminal/terminalapi"
+)
+
+// Callback is a function provided by an event subscriber.
+// It gets called with each event that passed the subscription filter.
+// Implementations must be thread-safe, events come from a separate goroutine.
+// Implementation should be light-weight, otherwise a slow-processing
+// subscriber can build a long tail of events.
+type Callback func(terminalapi.Event)
+
+// queue is a queue of terminal events.
+type queue interface {
+ Push(e terminalapi.Event)
+ Pull(ctx context.Context) terminalapi.Event
+ Close()
+}
+
+// subscriber represents a single subscriber.
+type subscriber struct {
+ // cb is the callback the subscriber receives events on.
+ cb Callback
+
+ // filter filters events towards the subscriber.
+ // An empty filter receives all events.
+ filter map[reflect.Type]bool
+
+ // queue is a queue of events towards the subscriber.
+ queue queue
+
+ // cancel when called terminates the goroutine that forwards events towards
+ // this subscriber.
+ cancel context.CancelFunc
+
+ // processes is the number of events that were fully processed, i.e.
+ // delivered to the callback.
+ processed int
+
+ // mu protects busy.
+ mu sync.Mutex
+}
+
+// newSubscriber creates a new event subscriber.
+func newSubscriber(filter []terminalapi.Event, cb Callback, opts *subscribeOptions) *subscriber {
+ f := map[reflect.Type]bool{}
+ for _, ev := range filter {
+ f[reflect.TypeOf(ev)] = true
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ var q queue
+ if opts.throttle {
+ q = eventqueue.NewThrottled(opts.maxRep)
+ } else {
+ q = eventqueue.New()
+ }
+
+ s := &subscriber{
+ cb: cb,
+ filter: f,
+ queue: q,
+ cancel: cancel,
+ }
+
+ // Terminates when stop() is called.
+ go s.run(ctx)
+ return s
+}
+
+// callback sends the event to the callback.
+func (s *subscriber) callback(ev terminalapi.Event) {
+ s.cb(ev)
+
+ func() {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.processed++
+ }()
+}
+
+// run periodically forwards events towards the subscriber.
+// Terminates when the context expires.
+func (s *subscriber) run(ctx context.Context) {
+ for {
+ ev := s.queue.Pull(ctx)
+ if ev != nil {
+ s.callback(ev)
+ }
+
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ }
+ }
+}
+
+// event forwards an event to the subscriber.
+func (s *subscriber) event(ev terminalapi.Event) {
+ if len(s.filter) == 0 {
+ s.queue.Push(ev)
+ }
+
+ t := reflect.TypeOf(ev)
+ if s.filter[t] {
+ s.queue.Push(ev)
+ }
+}
+
+// processedEvents returns the number of events processed by this subscriber.
+func (s *subscriber) processedEvents() int {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ return s.processed
+}
+
+// stop stops the event subscriber.
+func (s *subscriber) stop() {
+ s.cancel()
+ s.queue.Close()
+}
+
+// DistributionSystem distributes events to subscribers.
+//
+// Subscribers can request filtering of events they get based on event type or
+// subscribe to all events.
+//
+// The distribution system maintains a queue towards each subscriber, making
+// sure that a single slow subscriber only slows itself down, rather than the
+// entire application.
+//
+// This object is thread-safe.
+type DistributionSystem struct {
+ // subscribers subscribe to events.
+ // maps subscriber id to subscriber.
+ subscribers map[int]*subscriber
+
+ // nextID is id for the next subscriber.
+ nextID int
+
+ // mu protects the distribution system.
+ mu sync.Mutex
+}
+
+// NewDistributionSystem creates a new event distribution system.
+func NewDistributionSystem() *DistributionSystem {
+ return &DistributionSystem{
+ subscribers: map[int]*subscriber{},
+ }
+}
+
+// Event should be called with events coming from the terminal.
+// The distribution system will distribute these to all the subscribers.
+func (eds *DistributionSystem) Event(ev terminalapi.Event) {
+ eds.mu.Lock()
+ defer eds.mu.Unlock()
+
+ for _, sub := range eds.subscribers {
+ sub.event(ev)
+ }
+}
+
+// StopFunc when called unsubscribes the subscriber from all events and
+// releases resources tied to the subscriber.
+type StopFunc func()
+
+// SubscribeOption is used to provide options to Subscribe.
+type SubscribeOption interface {
+ // set sets the provided option.
+ set(*subscribeOptions)
+}
+
+// subscribeOptions stores the provided options.
+type subscribeOptions struct {
+ throttle bool
+ maxRep int
+}
+
+// subscribeOption implements Option.
+type subscribeOption func(*subscribeOptions)
+
+// set implements SubscribeOption.set.
+func (o subscribeOption) set(sOpts *subscribeOptions) {
+ o(sOpts)
+}
+
+// MaxRepetitive when provided, instructs the system to drop repetitive
+// events instead of delivering them.
+// The argument maxRep indicates the maximum number of repetitive events to
+// enqueue towards the subscriber.
+func MaxRepetitive(maxRep int) SubscribeOption {
+ return subscribeOption(func(sOpts *subscribeOptions) {
+ sOpts.throttle = true
+ sOpts.maxRep = maxRep
+ })
+}
+
+// Subscribe subscribes to events according to the filter.
+// An empty filter indicates that the subscriber wishes to receive events of
+// all kinds. If the filter is non-empty, only events of the provided type will
+// be sent to the subscriber.
+// Returns a function that allows the subscriber to unsubscribe.
+func (eds *DistributionSystem) Subscribe(filter []terminalapi.Event, cb Callback, opts ...SubscribeOption) StopFunc {
+ eds.mu.Lock()
+ defer eds.mu.Unlock()
+
+ opt := &subscribeOptions{}
+ for _, o := range opts {
+ o.set(opt)
+ }
+
+ id := eds.nextID
+ eds.nextID++
+ sub := newSubscriber(filter, cb, opt)
+ eds.subscribers[id] = sub
+
+ return func() {
+ eds.mu.Lock()
+ defer eds.mu.Unlock()
+
+ sub.stop()
+ delete(eds.subscribers, id)
+ }
+}
+
+// Processed returns the number of events that were fully processed, i.e.
+// delivered to all the subscribers and their callbacks returned.
+func (eds *DistributionSystem) Processed() int {
+ eds.mu.Lock()
+ defer eds.mu.Unlock()
+
+ var res int
+ for _, sub := range eds.subscribers {
+ res += sub.processedEvents()
+ }
+ return res
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/event/eventqueue/eventqueue.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/event/eventqueue/eventqueue.go
new file mode 100644
index 000000000..eb22d4f2c
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/event/eventqueue/eventqueue.go
@@ -0,0 +1,231 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package eventqueue provides an unboud FIFO queue of events.
+package eventqueue
+
+import (
+ "context"
+ "reflect"
+ "sync"
+ "time"
+
+ "github.com/mum4k/termdash/terminal/terminalapi"
+)
+
+// node is a single data item on the queue.
+type node struct {
+ prev *node
+ next *node
+ event terminalapi.Event
+}
+
+// Unbound is an unbound FIFO queue of terminal events.
+// Unbound must not be copied, pass it by reference only.
+// This implementation is thread-safe.
+type Unbound struct {
+ first *node
+ last *node
+ // mu protects first and last.
+ mu sync.Mutex
+
+ // cond is used to notify any callers waiting on a call to Pull().
+ cond *sync.Cond
+
+ // condMU protects cond.
+ condMU sync.RWMutex
+
+ // done is closed when the queue isn't needed anymore.
+ done chan struct{}
+}
+
+// New returns a new Unbound queue of terminal events.
+// Call Close() when done with the queue.
+func New() *Unbound {
+ u := &Unbound{
+ done: make(chan (struct{})),
+ }
+ u.cond = sync.NewCond(&u.condMU)
+ go u.wake() // Stops when Close() is called.
+ return u
+}
+
+// wake periodically wakes up all goroutines waiting at Pull() so they can
+// check if the context expired.
+func (u *Unbound) wake() {
+ const spinTime = 250 * time.Millisecond
+ t := time.NewTicker(spinTime)
+ defer t.Stop()
+ for {
+ select {
+ case <-t.C:
+ u.cond.Broadcast()
+ case <-u.done:
+ return
+ }
+ }
+}
+
+// Empty determines if the queue is empty.
+func (u *Unbound) Empty() bool {
+ u.mu.Lock()
+ defer u.mu.Unlock()
+ return u.empty()
+}
+
+// empty determines if the queue is empty.
+func (u *Unbound) empty() bool {
+ return u.first == nil
+}
+
+// Push pushes an event onto the queue.
+func (u *Unbound) Push(e terminalapi.Event) {
+ u.mu.Lock()
+ defer u.mu.Unlock()
+ u.push(e)
+}
+
+// push is the implementation of Push.
+// Caller must hold u.mu.
+func (u *Unbound) push(e terminalapi.Event) {
+ n := &node{
+ event: e,
+ }
+ if u.empty() {
+ u.first = n
+ u.last = n
+ } else {
+ prev := u.last
+ u.last.next = n
+ u.last = n
+ u.last.prev = prev
+ }
+ u.cond.Signal()
+}
+
+// Pop pops an event from the queue. Returns nil if the queue is empty.
+func (u *Unbound) Pop() terminalapi.Event {
+ u.mu.Lock()
+ defer u.mu.Unlock()
+
+ if u.empty() {
+ return nil
+ }
+
+ n := u.first
+ u.first = u.first.next
+
+ if u.empty() {
+ u.last = nil
+ }
+ return n.event
+}
+
+// Pull is like Pop(), but blocks until an item is available or the context
+// expires. Returns a nil event if the context expired.
+func (u *Unbound) Pull(ctx context.Context) terminalapi.Event {
+ if e := u.Pop(); e != nil {
+ return e
+ }
+
+ u.cond.L.Lock()
+ defer u.cond.L.Unlock()
+ for {
+ select {
+ case <-ctx.Done():
+ return nil
+ default:
+ }
+
+ if e := u.Pop(); e != nil {
+ return e
+ }
+ u.cond.Wait()
+ }
+}
+
+// Close should be called when the queue isn't needed anymore.
+func (u *Unbound) Close() {
+ close(u.done)
+}
+
+// Throttled is an unbound and throttled FIFO queue of terminal events.
+// Throttled must not be copied, pass it by reference only.
+// This implementation is thread-safe.
+type Throttled struct {
+ queue *Unbound
+ max int
+}
+
+// NewThrottled returns a new Throttled queue of terminal events.
+//
+// This queue scans the queue content on each Push call and won't Push the
+// event if there already is a continuous chain of exactly the same events
+// en queued. The argument maxRep specifies the maximum number of repetitive
+// events.
+//
+// Call Close() when done with the queue.
+func NewThrottled(maxRep int) *Throttled {
+ t := &Throttled{
+ queue: New(),
+ max: maxRep,
+ }
+ return t
+}
+
+// Empty determines if the queue is empty.
+func (t *Throttled) Empty() bool {
+ return t.queue.empty()
+}
+
+// Push pushes an event onto the queue.
+func (t *Throttled) Push(e terminalapi.Event) {
+ t.queue.mu.Lock()
+ defer t.queue.mu.Unlock()
+
+ if t.queue.empty() {
+ t.queue.push(e)
+ return
+ }
+
+ var same int
+ for n := t.queue.last; n != nil; n = n.prev {
+ if reflect.DeepEqual(e, n.event) {
+ same++
+ } else {
+ break
+ }
+
+ if same > t.max {
+ return // Drop the repetitive event.
+ }
+ }
+ t.queue.push(e)
+}
+
+// Pop pops an event from the queue. Returns nil if the queue is empty.
+func (t *Throttled) Pop() terminalapi.Event {
+ return t.queue.Pop()
+}
+
+// Pull is like Pop(), but blocks until an item is available or the context
+// expires. Returns a nil event if the context expired.
+func (t *Throttled) Pull(ctx context.Context) terminalapi.Event {
+ return t.queue.Pull(ctx)
+}
+
+// Close should be called when the queue isn't needed anymore.
+func (t *Throttled) Close() {
+ close(t.queue.done)
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/numbers/numbers.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/numbers/numbers.go
new file mode 100644
index 000000000..e91620f77
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/numbers/numbers.go
@@ -0,0 +1,222 @@
+// Copyright 2019 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package numbers implements various numerical functions.
+package numbers
+
+import (
+ "image"
+ "math"
+)
+
+// RoundToNonZeroPlaces rounds the float up, so that it has at least the provided
+// number of non-zero decimal places.
+// Returns the rounded float and the number of leading decimal places that
+// are zero. Returns the original float when places is zero. Negative places
+// are treated as positive, so that -2 == 2.
+func RoundToNonZeroPlaces(f float64, places int) (float64, int) {
+ if f == 0 {
+ return 0, 0
+ }
+
+ decOnly := zeroBeforeDecimal(f)
+ if decOnly == 0 {
+ return f, 0
+ }
+ nzMult := multToNonZero(decOnly)
+ if places == 0 {
+ return f, multToPlaces(nzMult)
+ }
+ plMult := placesToMult(places)
+
+ m := float64(nzMult * plMult)
+ return math.Ceil(f*m) / m, multToPlaces(nzMult)
+}
+
+// multToNonZero returns multiplier for the float, so that the first decimal
+// place is non-zero. The float must not be zero.
+func multToNonZero(f float64) int {
+ v := f
+ if v < 0 {
+ v *= -1
+ }
+
+ mult := 1
+ for v < 0.1 {
+ v *= 10
+ mult *= 10
+ }
+ return mult
+}
+
+// placesToMult translates the number of decimal places to a multiple of 10.
+func placesToMult(places int) int {
+ if places < 0 {
+ places *= -1
+ }
+
+ mult := 1
+ for i := 0; i < places; i++ {
+ mult *= 10
+ }
+ return mult
+}
+
+// multToPlaces translates the multiple of 10 to a number of decimal places.
+func multToPlaces(mult int) int {
+ places := 0
+ for mult > 1 {
+ mult /= 10
+ places++
+ }
+ return places
+}
+
+// zeroBeforeDecimal modifies the float so that it only has zero value before
+// the decimal point.
+func zeroBeforeDecimal(f float64) float64 {
+ var sign float64 = 1
+ if f < 0 {
+ f *= -1
+ sign = -1
+ }
+
+ floor := math.Floor(f)
+ return (f - floor) * sign
+}
+
+// MinMax returns the smallest and the largest value among the provided values.
+// Returns (0, 0) if there are no values.
+// Ignores NaN values. Allowing NaN values could lead to a corner case where all
+// values can be NaN, in this case the function will return NaN as min and max.
+func MinMax(values []float64) (min, max float64) {
+ if len(values) == 0 {
+ return 0, 0
+ }
+ min = math.MaxFloat64
+ max = -1 * math.MaxFloat64
+ allNaN := true
+ for _, v := range values {
+ if math.IsNaN(v) {
+ continue
+ }
+ allNaN = false
+
+ if v < min {
+ min = v
+ }
+ if v > max {
+ max = v
+ }
+ }
+
+ if allNaN {
+ return math.NaN(), math.NaN()
+ }
+
+ return min, max
+}
+
+// MinMaxInts returns the smallest and the largest int value among the provided
+// values. Returns (0, 0) if there are no values.
+func MinMaxInts(values []int) (min, max int) {
+ if len(values) == 0 {
+ return 0, 0
+ }
+ min = math.MaxInt32
+ max = -1 * math.MaxInt32
+
+ for _, v := range values {
+ if v < min {
+ min = v
+ }
+ if v > max {
+ max = v
+ }
+ }
+ return min, max
+}
+
+// DegreesToRadians converts degrees to the equivalent in radians.
+func DegreesToRadians(degrees int) float64 {
+ if degrees > 360 {
+ degrees %= 360
+ }
+ return (float64(degrees) / 180) * math.Pi
+}
+
+// RadiansToDegrees converts radians to the equivalent in degrees.
+func RadiansToDegrees(radians float64) int {
+ d := int(math.Round(radians * 180 / math.Pi))
+ if d < 0 {
+ d += 360
+ }
+ return d
+}
+
+// Abs returns the absolute value of x.
+func Abs(x int) int {
+ if x < 0 {
+ return -x
+ }
+ return x
+}
+
+// findGCF finds the greatest common factor of two integers.
+func findGCF(a, b int) int {
+ if a == 0 || b == 0 {
+ return 0
+ }
+ a = Abs(a)
+ b = Abs(b)
+
+ // https://en.wikipedia.org/wiki/Euclidean_algorithm
+ for {
+ rem := a % b
+ a = b
+ b = rem
+
+ if b == 0 {
+ break
+ }
+ }
+ return a
+}
+
+// SimplifyRatio simplifies the given ratio.
+func SimplifyRatio(ratio image.Point) image.Point {
+ gcf := findGCF(ratio.X, ratio.Y)
+ if gcf == 0 {
+ return image.ZP
+ }
+ return image.Point{
+ X: ratio.X / gcf,
+ Y: ratio.Y / gcf,
+ }
+}
+
+// SplitByRatio splits the provided number by the specified ratio.
+func SplitByRatio(n int, ratio image.Point) image.Point {
+ sr := SimplifyRatio(ratio)
+ if sr.Eq(image.ZP) {
+ return image.ZP
+ }
+ fn := float64(n)
+ sum := float64(sr.X + sr.Y)
+ fact := fn / sum
+ return image.Point{
+ int(math.Round(fact * float64(sr.X))),
+ int(math.Round(fact * float64(sr.Y))),
+ }
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/numbers/trig/trig.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/numbers/trig/trig.go
new file mode 100644
index 000000000..16d179d02
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/numbers/trig/trig.go
@@ -0,0 +1,224 @@
+// Copyright 2019 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package trig implements various trigonometrical calculations.
+package trig
+
+import (
+ "fmt"
+ "image"
+ "math"
+ "sort"
+
+ "github.com/mum4k/termdash/private/numbers"
+)
+
+// CirclePointAtAngle given an angle in degrees and a circle midpoint and
+// radius, calculates coordinates of a point on the circle at that angle.
+// Angles are zero at the X axis and grow counter-clockwise.
+func CirclePointAtAngle(degrees int, mid image.Point, radius int) image.Point {
+ angle := numbers.DegreesToRadians(degrees)
+ r := float64(radius)
+ x := mid.X + int(math.Round(r*math.Cos(angle)))
+ // Y coordinates grow down on the canvas.
+ y := mid.Y - int(math.Round(r*math.Sin(angle)))
+ return image.Point{x, y}
+}
+
+// CircleAngleAtPoint given a point on a circle and its midpoint,
+// calculates the angle in degrees.
+// Angles are zero at the X axis and grow counter-clockwise.
+func CircleAngleAtPoint(point, mid image.Point) int {
+ adj := float64(point.X - mid.X)
+ opp := float64(mid.Y - point.Y)
+ if opp != 0 {
+ angle := math.Atan2(opp, adj)
+ return numbers.RadiansToDegrees(angle)
+ } else if adj >= 0 {
+ return 0
+ } else {
+ return 180
+ }
+}
+
+// PointIsIn asserts whether the provided point is inside of a shape outlined
+// with the provided points.
+// Does not verify that the shape is closed or complete, it merely counts the
+// number of intersections with the shape on one row.
+func PointIsIn(p image.Point, points []image.Point) bool {
+ maxX := p.X
+ set := map[image.Point]struct{}{}
+ for _, sp := range points {
+ set[sp] = struct{}{}
+ if sp.X > maxX {
+ maxX = sp.X
+ }
+ }
+
+ if _, ok := set[p]; ok {
+ // Not inside if it is on the shape.
+ return false
+ }
+
+ byY := map[int][]int{} // maps y->x
+ for p := range set {
+ byY[p.Y] = append(byY[p.Y], p.X)
+ }
+ for y := range byY {
+ sort.Ints(byY[y])
+ }
+
+ set = map[image.Point]struct{}{}
+ for y, xses := range byY {
+ set[image.Point{xses[0], y}] = struct{}{}
+ if len(xses) == 1 {
+ continue
+ }
+
+ for i := 1; i < len(xses); i++ {
+ if xses[i] != xses[i-1]+1 {
+ set[image.Point{xses[i], y}] = struct{}{}
+ }
+ }
+ }
+
+ crosses := 0
+ for x := p.X; x <= maxX; x++ {
+ if _, ok := set[image.Point{x, p.Y}]; ok {
+ crosses++
+ }
+ }
+ return crosses%2 != 0
+}
+
+const (
+ // MinAngle is the smallest valid angle in degrees.
+ MinAngle = 0
+ // MaxAngle is the largest valid angle in degrees.
+ MaxAngle = 360
+)
+
+// angleRange represents a range of angles in degrees.
+// The range includes all angles such that start <= angle <= end.
+type angleRange struct {
+ // start is the start if the range.
+ // This is always less or equal to the end.
+ start int
+
+ // end is the end of the range.
+ end int
+}
+
+// contains asserts whether the specified angle is in the range.
+func (ar *angleRange) contains(angle int) bool {
+ return angle >= ar.start && angle <= ar.end
+}
+
+// normalizeRange normalizes the start and end angles in degrees into ranges of
+// angles. Useful for cases where the 0/360 point falls within the range.
+// E.g:
+// 0,25 => angleRange{0, 26}
+// 0,360 => angleRange{0, 361}
+// 359,20 => angleRange{359, 361}, angleRange{0, 21}
+func normalizeRange(start, end int) ([]*angleRange, error) {
+ if start < MinAngle || start > MaxAngle {
+ return nil, fmt.Errorf("invalid start angle:%d, must be in range %d <= start <= %d", start, MinAngle, MaxAngle)
+ }
+ if end < MinAngle || end > MaxAngle {
+ return nil, fmt.Errorf("invalid end angle:%d, must be in range %d <= end <= %d", end, MinAngle, MaxAngle)
+ }
+
+ if start == MaxAngle && end == 0 {
+ start, end = end, start
+ }
+
+ if start <= end {
+ return []*angleRange{
+ {start, end},
+ }, nil
+ }
+
+ // The range is crossing the 0/360 degree point.
+ // Break it into multiple ranges.
+ return []*angleRange{
+ {start, MaxAngle},
+ {0, end},
+ }, nil
+}
+
+// RangeSize returns the size of the degree range.
+// E.g:
+// 0,25 => 25
+// 359,1 => 2
+func RangeSize(start, end int) (int, error) {
+ ranges, err := normalizeRange(start, end)
+ if err != nil {
+ return 0, err
+ }
+ if len(ranges) == 1 {
+ return end - start, nil
+ }
+ return MaxAngle - start + end, nil
+}
+
+// RangeMid returns an angle that lies in the middle between start and end.
+// E.g:
+// 0,10 => 5
+// 350,10 => 0
+func RangeMid(start, end int) (int, error) {
+ ranges, err := normalizeRange(start, end)
+ if err != nil {
+ return 0, err
+ }
+ if len(ranges) == 1 {
+ return start + ((end - start) / 2), nil
+ }
+
+ length := MaxAngle - start + end
+ want := length / 2
+ res := start + want
+ return res % MaxAngle, nil
+}
+
+// FilterByAngle filters the provided points, returning only those that fall
+// within the starting and the ending angle on a circle with the provided mid
+// point.
+func FilterByAngle(points []image.Point, mid image.Point, start, end int) ([]image.Point, error) {
+ var res []image.Point
+ ranges, err := normalizeRange(start, end)
+ if err != nil {
+ return nil, err
+ }
+ if mid.X < 0 || mid.Y < 0 {
+ return nil, fmt.Errorf("the mid point %v cannot have negative coordinates", mid)
+ }
+
+ for _, p := range points {
+ angle := CircleAngleAtPoint(p, mid)
+
+ // Edge case, this might mean 0 or 360.
+ // Decide based on where we are starting.
+ if angle == 0 && start > 0 {
+ angle = MaxAngle
+ }
+
+ for _, r := range ranges {
+ if r.contains(angle) {
+ res = append(res, p)
+ break
+ }
+ }
+ }
+ return res, nil
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/runewidth/runewidth.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/runewidth/runewidth.go
new file mode 100644
index 000000000..4f2f63a8f
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/runewidth/runewidth.go
@@ -0,0 +1,98 @@
+// Copyright 2019 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package runewidth is a wrapper over github.com/mattn/go-runewidth which
+// gives different treatment to certain runes with ambiguous width.
+package runewidth
+
+import runewidth "github.com/mattn/go-runewidth"
+
+// RuneWidth returns the number of cells needed to draw r.
+// Background in http://www.unicode.org/reports/tr11/.
+//
+// Treats runes used internally by termdash as single-cell (half-width) runes
+// regardless of the locale. I.e. runes that are used to draw lines, boxes,
+// indicate resize or text trimming was needed and runes used by the braille
+// canvas.
+//
+// This should be safe, since even in locales where these runes have ambiguous
+// width, we still place all the character content around them so they should
+// have be half-width.
+func RuneWidth(r rune) int {
+ if inTable(r, exceptions) {
+ return 1
+ }
+ return runewidth.RuneWidth(r)
+}
+
+// StringWidth is like RuneWidth, but returns the number of cells occupied by
+// all the runes in the string.
+func StringWidth(s string) int {
+ var width int
+ for _, r := range []rune(s) {
+ width += RuneWidth(r)
+ }
+ return width
+}
+
+// inTable determines if the rune falls within the table.
+// Copied from github.com/mattn/go-runewidth/blob/master/runewidth.go.
+func inTable(r rune, t table) bool {
+ // func (t table) IncludesRune(r rune) bool {
+ if r < t[0].first {
+ return false
+ }
+
+ bot := 0
+ top := len(t) - 1
+ for top >= bot {
+ mid := (bot + top) >> 1
+
+ switch {
+ case t[mid].last < r:
+ bot = mid + 1
+ case t[mid].first > r:
+ top = mid - 1
+ default:
+ return true
+ }
+ }
+
+ return false
+}
+
+type interval struct {
+ first rune
+ last rune
+}
+
+type table []interval
+
+// exceptions runes defined here are always considered to be half-width even if
+// they might be ambiguous in some contexts.
+var exceptions = table{
+ // Characters used by termdash to indicate text trim or scroll.
+ {0x2026, 0x2026},
+ {0x21c4, 0x21c4},
+ {0x21e7, 0x21e7},
+ {0x21e9, 0x21e9},
+
+ // Box drawing, used as line-styles.
+ // https://en.wikipedia.org/wiki/Box-drawing_character
+ {0x2500, 0x257F},
+
+ // Block elements used as sparks.
+ // https://en.wikipedia.org/wiki/Box-drawing_character
+ {0x2580, 0x258F},
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/private/wrap/wrap.go b/examples/go-dashboard/src/github.com/mum4k/termdash/private/wrap/wrap.go
new file mode 100644
index 000000000..5ee78a7a5
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/private/wrap/wrap.go
@@ -0,0 +1,409 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package wrap implements line wrapping at character or word boundaries.
+package wrap
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+ "unicode"
+
+ "github.com/mum4k/termdash/private/canvas/buffer"
+ "github.com/mum4k/termdash/private/runewidth"
+)
+
+// Mode sets the wrapping mode.
+type Mode int
+
+// String implements fmt.Stringer()
+func (m Mode) String() string {
+ if n, ok := modeNames[m]; ok {
+ return n
+ }
+ return "ModeUnknown"
+}
+
+// modeNames maps Mode values to human readable names.
+var modeNames = map[Mode]string{
+ Never: "WrapModeNever",
+ AtRunes: "WrapModeAtRunes",
+ AtWords: "WrapModeAtWords",
+}
+
+const (
+ // Never is the default wrapping mode, which disables line wrapping.
+ Never Mode = iota
+
+ // AtRunes is a wrapping mode where if the width of the text crosses the
+ // width of the canvas, wrapping is performed at rune boundaries.
+ AtRunes
+
+ // AtWords is a wrapping mode where if the width of the text crosses the
+ // width of the canvas, wrapping is performed at word boundaries. The
+ // wrapping still switches back to the AtRunes mode for any words that are
+ // longer than the width.
+ AtWords
+)
+
+// ValidText validates the provided text for wrapping.
+// The text must not be empty, contain any control or
+// space characters other than '\n' and ' '.
+func ValidText(text string) error {
+ if text == "" {
+ return errors.New("the text cannot be empty")
+ }
+
+ for _, c := range text {
+ if c == ' ' || c == '\n' { // Allowed space and control runes.
+ continue
+ }
+ if unicode.IsControl(c) {
+ return fmt.Errorf("the provided text %q cannot contain control characters, found: %q", text, c)
+ }
+ if unicode.IsSpace(c) {
+ return fmt.Errorf("the provided text %q cannot contain space character %q", text, c)
+ }
+ }
+ return nil
+}
+
+// ValidCells validates the provided cells for wrapping.
+// The text in the cells must follow the same rules as described for ValidText.
+func ValidCells(cells []*buffer.Cell) error {
+ var b strings.Builder
+ for _, c := range cells {
+ b.WriteRune(c.Rune)
+ }
+ return ValidText(b.String())
+}
+
+// Cells returns the cells wrapped into individual lines according to the
+// specified width and wrapping mode.
+//
+// This function consumes any cells that contain newline characters and uses
+// them to start new lines.
+//
+// If the mode is AtWords, this function also drops cells with leading space
+// character before a word at which the wrap occurs.
+func Cells(cells []*buffer.Cell, width int, m Mode) ([][]*buffer.Cell, error) {
+ if err := ValidCells(cells); err != nil {
+ return nil, err
+ }
+ switch m {
+ case Never:
+ case AtRunes:
+ case AtWords:
+ default:
+ return nil, fmt.Errorf("unsupported wrapping mode %v(%d)", m, m)
+ }
+ if width <= 0 {
+ return nil, nil
+ }
+
+ cs := newCellScanner(cells, width, m)
+ for state := scanCellRunes; state != nil; state = state(cs) {
+ }
+ return cs.lines, nil
+}
+
+// cellScannerState is a state in the FSM that scans the input text and identifies
+// newlines.
+type cellScannerState func(*cellScanner) cellScannerState
+
+// cellScanner tracks the progress of scanning the input cells when finding
+// lines.
+type cellScanner struct {
+ // cells are the cells being scanned.
+ cells []*buffer.Cell
+
+ // nextIdx is the index of the cell that will be returned by next.
+ nextIdx int
+
+ // wordStartIdx stores the starting index of the current word.
+ // A starting position of a word includes any leading space characters.
+ // E.g.: hello world
+ // ^
+ // lastWordIdx
+ wordStartIdx int
+ // wordEndIdx stores the ending index of the current word.
+ // The word consists of all indexes that are
+ // wordStartIdx <= idx < wordEndIdx.
+ // A word also includes any punctuation after it.
+ wordEndIdx int
+
+ // width is the width of the canvas the text will be drawn on.
+ width int
+
+ // posX tracks the horizontal position of the current cell on the canvas.
+ posX int
+
+ // mode is the wrapping mode.
+ mode Mode
+
+ // atRunesInWord overrides the mode back to AtRunes.
+ atRunesInWord bool
+
+ // lines are the identified lines.
+ lines [][]*buffer.Cell
+
+ // line is the current line.
+ line []*buffer.Cell
+}
+
+// newCellScanner returns a scanner of the provided cells.
+func newCellScanner(cells []*buffer.Cell, width int, m Mode) *cellScanner {
+ return &cellScanner{
+ cells: cells,
+ width: width,
+ mode: m,
+ }
+}
+
+// next returns the next cell and advances the scanner.
+// Returns nil when there are no more cells to scan.
+func (cs *cellScanner) next() *buffer.Cell {
+ c := cs.peek()
+ if c != nil {
+ cs.nextIdx++
+ }
+ return c
+}
+
+// peek returns the next cell without advancing the scanner's position.
+// Returns nil when there are no more cells to peek at.
+func (cs *cellScanner) peek() *buffer.Cell {
+ if cs.nextIdx >= len(cs.cells) {
+ return nil
+ }
+ return cs.cells[cs.nextIdx]
+}
+
+// peekPrev returns the previous cell without changing the scanner's position.
+// Returns nil if the scanner is at the first cell.
+func (cs *cellScanner) peekPrev() *buffer.Cell {
+ if cs.nextIdx == 0 {
+ return nil
+ }
+ return cs.cells[cs.nextIdx-1]
+}
+
+// wordCells returns all the cells that belong to the current word.
+func (cs *cellScanner) wordCells() []*buffer.Cell {
+ return cs.cells[cs.wordStartIdx:cs.wordEndIdx]
+}
+
+// wordWidth returns the width of the current word in cells when printed on the
+// terminal.
+func (cs *cellScanner) wordWidth() int {
+ var b strings.Builder
+ for _, wc := range cs.wordCells() {
+ b.WriteRune(wc.Rune)
+ }
+ return runewidth.StringWidth(b.String())
+}
+
+// isWordStart determines if the scanner is at the beginning of a word.
+func (cs *cellScanner) isWordStart() bool {
+ if cs.mode != AtWords {
+ return false
+ }
+
+ current := cs.peekPrev()
+ next := cs.peek()
+ if current == nil || next == nil {
+ return false
+ }
+
+ switch nr := next.Rune; {
+ case nr == '\n':
+ case nr == ' ':
+ default:
+ return true
+ }
+ return false
+}
+
+// scanCellRunes scans the cells a rune at a time.
+func scanCellRunes(cs *cellScanner) cellScannerState {
+ for {
+ cell := cs.next()
+ if cell == nil {
+ return scanEOF
+ }
+
+ r := cell.Rune
+ if r == '\n' {
+ return newLineForLineBreak
+ }
+
+ if cs.mode == Never {
+ return runeToCurrentLine
+ }
+
+ if cs.atRunesInWord && !isWordCell(cell) {
+ cs.atRunesInWord = false
+ }
+
+ if !cs.atRunesInWord && cs.isWordStart() {
+ return markWordStart
+ }
+
+ if runeWrapNeeded(r, cs.posX, cs.width) {
+ return newLineForAtRunes
+ }
+
+ return runeToCurrentLine
+ }
+}
+
+// runeToCurrentLine scans a single cell rune onto the current line.
+func runeToCurrentLine(cs *cellScanner) cellScannerState {
+ cell := cs.peekPrev()
+ // Move horizontally within the line for each scanned cell.
+ cs.posX += runewidth.RuneWidth(cell.Rune)
+
+ // Copy the cell into the current line.
+ cs.line = append(cs.line, cell)
+ return scanCellRunes
+}
+
+// newLineForLineBreak processes a newline character cell.
+func newLineForLineBreak(cs *cellScanner) cellScannerState {
+ cs.lines = append(cs.lines, cs.line)
+ cs.posX = 0
+ cs.line = nil
+ return scanCellRunes
+}
+
+// newLineForAtRunes processes a line wrap at rune boundaries due to canvas width.
+func newLineForAtRunes(cs *cellScanner) cellScannerState {
+ // The character on which we wrapped will be printed and is the start of
+ // new line.
+ cs.lines = append(cs.lines, cs.line)
+ cs.posX = runewidth.RuneWidth(cs.peekPrev().Rune)
+ cs.line = []*buffer.Cell{cs.peekPrev()}
+ return scanCellRunes
+}
+
+// scanEOF terminates the scanning.
+func scanEOF(cs *cellScanner) cellScannerState {
+ // Need to add the current line if it isn't empty, or if the previous rune
+ // was a newline.
+ // Newlines aren't copied onto the lines so just checking for emptiness
+ // isn't enough. We still want to include trailing empty newlines if
+ // they are in the input text.
+ if len(cs.line) > 0 || cs.peekPrev().Rune == '\n' {
+ cs.lines = append(cs.lines, cs.line)
+ }
+ return nil
+}
+
+// markWordStart stores the starting position of the current word.
+func markWordStart(cs *cellScanner) cellScannerState {
+ cs.wordStartIdx = cs.nextIdx - 1
+ cs.wordEndIdx = cs.nextIdx
+ return scanWord
+}
+
+// scanWord scans the entire word until it finds its end.
+func scanWord(cs *cellScanner) cellScannerState {
+ for {
+ if isWordCell(cs.peek()) {
+ cs.next()
+ cs.wordEndIdx++
+ continue
+ }
+ return wordToCurrentLine
+ }
+}
+
+// wordToCurrentLine decides how to place the word into the output.
+func wordToCurrentLine(cs *cellScanner) cellScannerState {
+ wordCells := cs.wordCells()
+ wordWidth := cs.wordWidth()
+
+ if cs.posX+wordWidth <= cs.width {
+ // Place the word onto the current line.
+ cs.posX += wordWidth
+ cs.line = append(cs.line, wordCells...)
+ return scanCellRunes
+ }
+ return wrapWord
+}
+
+// wrapWord wraps the word onto the next line or lines.
+func wrapWord(cs *cellScanner) cellScannerState {
+ // Edge-case - the word starts the line and immediately doesn't fit.
+ if cs.posX > 0 {
+ cs.lines = append(cs.lines, cs.line)
+ cs.posX = 0
+ cs.line = nil
+ }
+
+ for i, wc := range cs.wordCells() {
+ if i == 0 && wc.Rune == ' ' {
+ // Skip the leading space when word wrapping.
+ continue
+ }
+
+ if !runeWrapNeeded(wc.Rune, cs.posX, cs.width) {
+ cs.posX += runewidth.RuneWidth(wc.Rune)
+ cs.line = append(cs.line, wc)
+ continue
+ }
+
+ // Replace the last placed rune with a dash indicating we wrapped the
+ // word. Only do this for half-width runes.
+ lastIdx := len(cs.line) - 1
+ last := cs.line[lastIdx]
+ lastRW := runewidth.RuneWidth(last.Rune)
+ if cs.width > 1 && lastRW == 1 {
+ cs.line[lastIdx] = buffer.NewCell('-', last.Opts)
+ // Reset the scanner's position back to start scanning at the first
+ // rune of this word that wasn't placed.
+ cs.nextIdx = cs.wordStartIdx + i - 1
+ } else {
+ // Edge-case width is one, no space to put the dash rune.
+ cs.nextIdx = cs.wordStartIdx + i
+ }
+ cs.atRunesInWord = true
+ return scanCellRunes
+ }
+
+ cs.nextIdx = cs.wordEndIdx
+ return scanCellRunes
+}
+
+// isWordCell determines if the cell contains a rune that belongs to a word.
+func isWordCell(c *buffer.Cell) bool {
+ if c == nil {
+ return false
+ }
+ switch r := c.Rune; {
+ case r == '\n':
+ case r == ' ':
+ default:
+ return true
+ }
+ return false
+}
+
+// runeWrapNeeded returns true if wrapping is needed for the rune at the horizontal
+// position on the canvas that has the specified width.
+func runeWrapNeeded(r rune, posX, width int) bool {
+ rw := runewidth.RuneWidth(r)
+ return posX > width-rw
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/termdash.go b/examples/go-dashboard/src/github.com/mum4k/termdash/termdash.go
new file mode 100644
index 000000000..8103ee717
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/termdash.go
@@ -0,0 +1,362 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/*
+Package termdash implements a terminal based dashboard.
+
+While running, the terminal dashboard performs the following:
+ - Periodic redrawing of the canvas and all the widgets.
+ - Event based redrawing of the widgets (i.e. on Keyboard or Mouse events).
+ - Forwards input events to widgets and optional subscribers.
+ - Handles terminal resize events.
+*/
+package termdash
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/mum4k/termdash/container"
+ "github.com/mum4k/termdash/private/event"
+ "github.com/mum4k/termdash/terminal/terminalapi"
+)
+
+// DefaultRedrawInterval is the default for the RedrawInterval option.
+const DefaultRedrawInterval = 250 * time.Millisecond
+
+// Option is used to provide options.
+type Option interface {
+ // set sets the provided option.
+ set(td *termdash)
+}
+
+// option implements Option.
+type option func(td *termdash)
+
+// set implements Option.set.
+func (o option) set(td *termdash) {
+ o(td)
+}
+
+// RedrawInterval sets how often termdash redraws the container and all the widgets.
+// Defaults to DefaultRedrawInterval. Use the controller to disable the
+// periodic redraw.
+func RedrawInterval(t time.Duration) Option {
+ return option(func(td *termdash) {
+ td.redrawInterval = t
+ })
+}
+
+// ErrorHandler is used to provide a function that will be called with all
+// errors that occur while the dashboard is running. If not provided, any
+// errors panic the application.
+// The provided function must be thread-safe.
+func ErrorHandler(f func(error)) Option {
+ return option(func(td *termdash) {
+ td.errorHandler = f
+ })
+}
+
+// KeyboardSubscriber registers a subscriber for Keyboard events. Each
+// keyboard event is forwarded to the container and the registered subscriber.
+// The provided function must be thread-safe.
+func KeyboardSubscriber(f func(*terminalapi.Keyboard)) Option {
+ return option(func(td *termdash) {
+ td.keyboardSubscriber = f
+ })
+}
+
+// MouseSubscriber registers a subscriber for Mouse events. Each mouse event
+// is forwarded to the container and the registered subscriber.
+// The provided function must be thread-safe.
+func MouseSubscriber(f func(*terminalapi.Mouse)) Option {
+ return option(func(td *termdash) {
+ td.mouseSubscriber = f
+ })
+}
+
+// withEDS indicates that termdash should run with the provided event
+// distribution system instead of creating one.
+// Useful for tests.
+func withEDS(eds *event.DistributionSystem) Option {
+ return option(func(td *termdash) {
+ td.eds = eds
+ })
+}
+
+// Run runs the terminal dashboard with the provided container on the terminal.
+// Redraws the terminal periodically. If you prefer a manual redraw, use the
+// Controller instead.
+// Blocks until the context expires.
+func Run(ctx context.Context, t terminalapi.Terminal, c *container.Container, opts ...Option) error {
+ td := newTermdash(t, c, opts...)
+
+ err := td.start(ctx)
+ // Only return the status (error or nil) after the termdash event
+ // processing goroutine actually exits.
+ td.stop()
+ return err
+}
+
+// Controller controls a termdash instance.
+// The controller instance is only valid until Close() is called.
+// The controller is not thread-safe.
+type Controller struct {
+ td *termdash
+ cancel context.CancelFunc
+}
+
+// NewController initializes termdash and returns an instance of the controller.
+// Periodic redrawing is disabled when using the controller, the RedrawInterval
+// option is ignored.
+// Close the controller when it isn't needed anymore.
+func NewController(t terminalapi.Terminal, c *container.Container, opts ...Option) (*Controller, error) {
+ ctx, cancel := context.WithCancel(context.Background())
+ ctrl := &Controller{
+ td: newTermdash(t, c, opts...),
+ cancel: cancel,
+ }
+
+ // stops when Close() is called.
+ go ctrl.td.processEvents(ctx)
+ if err := ctrl.td.periodicRedraw(); err != nil {
+ return nil, err
+ }
+ return ctrl, nil
+}
+
+// Redraw triggers redraw of the terminal.
+func (c *Controller) Redraw() error {
+ if c.td == nil {
+ return errors.New("the termdash instance is no longer running, this controller is now invalid")
+ }
+
+ c.td.mu.Lock()
+ defer c.td.mu.Unlock()
+ return c.td.redraw()
+}
+
+// Close closes the Controller and its termdash instance.
+func (c *Controller) Close() {
+ c.cancel()
+ c.td.stop()
+ c.td = nil
+}
+
+// termdash is a terminal based dashboard.
+// This object is thread-safe.
+type termdash struct {
+ // term is the terminal the dashboard runs on.
+ term terminalapi.Terminal
+
+ // container maintains terminal splits and places widgets.
+ container *container.Container
+
+ // eds distributes input events to subscribers.
+ eds *event.DistributionSystem
+
+ // closeCh gets closed when Stop() is called, which tells the event
+ // collecting goroutine to exit.
+ closeCh chan struct{}
+ // exitCh gets closed when the event collecting goroutine actually exits.
+ exitCh chan struct{}
+
+ // clearNeeded indicates if the terminal needs to be cleared next time
+ // we're drawing it. Terminal needs to be cleared if its sized changed.
+ clearNeeded bool
+
+ // mu protects termdash.
+ mu sync.Mutex
+
+ // Options.
+ redrawInterval time.Duration
+ errorHandler func(error)
+ mouseSubscriber func(*terminalapi.Mouse)
+ keyboardSubscriber func(*terminalapi.Keyboard)
+}
+
+// newTermdash creates a new termdash.
+func newTermdash(t terminalapi.Terminal, c *container.Container, opts ...Option) *termdash {
+ td := &termdash{
+ term: t,
+ container: c,
+ eds: event.NewDistributionSystem(),
+ closeCh: make(chan struct{}),
+ exitCh: make(chan struct{}),
+ redrawInterval: DefaultRedrawInterval,
+ }
+
+ for _, opt := range opts {
+ opt.set(td)
+ }
+ td.subscribers()
+ c.Subscribe(td.eds)
+ return td
+}
+
+// subscribers subscribes event receivers that live in this package to EDS.
+func (td *termdash) subscribers() {
+ // Handler for all errors that occur during input event processing.
+ td.eds.Subscribe([]terminalapi.Event{terminalapi.NewError("")}, func(ev terminalapi.Event) {
+ td.handleError(ev.(*terminalapi.Error).Error())
+ })
+
+ // Handles terminal resize events.
+ td.eds.Subscribe([]terminalapi.Event{&terminalapi.Resize{}}, func(terminalapi.Event) {
+ td.setClearNeeded()
+ })
+
+ // Redraws the screen on Keyboard and Mouse events.
+ // These events very likely change the content of the widgets (e.g. zooming
+ // a LineChart) so a redraw is needed to make that visible.
+ td.eds.Subscribe([]terminalapi.Event{
+ &terminalapi.Keyboard{},
+ &terminalapi.Mouse{},
+ }, func(terminalapi.Event) {
+ td.evRedraw()
+ }, event.MaxRepetitive(0)) // No repetitive events that cause terminal redraw.
+
+ // Keyboard and Mouse subscribers specified via options.
+ if td.keyboardSubscriber != nil {
+ td.eds.Subscribe([]terminalapi.Event{&terminalapi.Keyboard{}}, func(ev terminalapi.Event) {
+ td.keyboardSubscriber(ev.(*terminalapi.Keyboard))
+ })
+ }
+ if td.mouseSubscriber != nil {
+ td.eds.Subscribe([]terminalapi.Event{&terminalapi.Mouse{}}, func(ev terminalapi.Event) {
+ td.mouseSubscriber(ev.(*terminalapi.Mouse))
+ })
+ }
+}
+
+// handleError forwards the error to the error handler if one was
+// provided or panics.
+func (td *termdash) handleError(err error) {
+ if td.errorHandler != nil {
+ td.errorHandler(err)
+ } else {
+ panic(err)
+ }
+}
+
+// setClearNeeded flags that the terminal needs to be cleared next time we're
+// drawing it.
+func (td *termdash) setClearNeeded() {
+ td.mu.Lock()
+ defer td.mu.Unlock()
+ td.clearNeeded = true
+}
+
+// redraw redraws the container and its widgets.
+// The caller must hold td.mu.
+func (td *termdash) redraw() error {
+ if td.clearNeeded {
+ if err := td.term.Clear(); err != nil {
+ return fmt.Errorf("term.Clear => error: %v", err)
+ }
+ td.clearNeeded = false
+ }
+
+ if err := td.container.Draw(); err != nil {
+ return fmt.Errorf("container.Draw => error: %v", err)
+ }
+
+ if err := td.term.Flush(); err != nil {
+ return fmt.Errorf("term.Flush => error: %v", err)
+ }
+ return nil
+}
+
+// evRedraw redraws the container and its widgets.
+func (td *termdash) evRedraw() error {
+ td.mu.Lock()
+ defer td.mu.Unlock()
+
+ // Don't redraw immediately, give widgets that are performing enough time
+ // to update.
+ // We don't want to actually synchronize until all widgets update, we are
+ // purposefully leaving slow widgets behind.
+ time.Sleep(25 * time.Millisecond)
+ return td.redraw()
+}
+
+// periodicRedraw is called once each RedrawInterval.
+func (td *termdash) periodicRedraw() error {
+ td.mu.Lock()
+ defer td.mu.Unlock()
+ return td.redraw()
+}
+
+// processEvents processes terminal input events.
+// This is the body of the event collecting goroutine.
+func (td *termdash) processEvents(ctx context.Context) {
+ defer close(td.exitCh)
+
+ for {
+ ev := td.term.Event(ctx)
+ if ev != nil {
+ td.eds.Event(ev)
+ }
+
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ }
+ }
+}
+
+// start starts the terminal dashboard. Blocks until the context expires or
+// until stop() is called.
+func (td *termdash) start(ctx context.Context) error {
+ // Redraw once to initialize the container sizes.
+ if err := td.periodicRedraw(); err != nil {
+ close(td.exitCh)
+ return err
+ }
+
+ redrawTimer := time.NewTicker(td.redrawInterval)
+ defer redrawTimer.Stop()
+
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ // stops when stop() is called or the context expires.
+ go td.processEvents(ctx)
+
+ for {
+ select {
+ case <-redrawTimer.C:
+ if err := td.periodicRedraw(); err != nil {
+ return err
+ }
+
+ case <-ctx.Done():
+ return nil
+
+ case <-td.closeCh:
+ return nil
+ }
+ }
+}
+
+// stop tells the event collecting goroutine to stop.
+// Blocks until it exits.
+func (td *termdash) stop() {
+ close(td.closeCh)
+ <-td.exitCh
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/termbox/cell_options.go b/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/termbox/cell_options.go
new file mode 100644
index 000000000..41ee76013
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/termbox/cell_options.go
@@ -0,0 +1,37 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package termbox
+
+// cell_options.go converts termdash cell options to the termbox format.
+
+import (
+ "github.com/mum4k/termdash/cell"
+ tbx "github.com/nsf/termbox-go"
+)
+
+// cellColor converts termdash cell color to the termbox format.
+func cellColor(c cell.Color) tbx.Attribute {
+ return tbx.Attribute(c)
+}
+
+// cellOptsToFg converts the cell options to the termbox foreground attribute.
+func cellOptsToFg(opts *cell.Options) tbx.Attribute {
+ return cellColor(opts.FgColor)
+}
+
+// cellOptsToBg converts the cell options to the termbox background attribute.
+func cellOptsToBg(opts *cell.Options) tbx.Attribute {
+ return cellColor(opts.BgColor)
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/termbox/color_mode.go b/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/termbox/color_mode.go
new file mode 100644
index 000000000..793f2a966
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/termbox/color_mode.go
@@ -0,0 +1,38 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package termbox
+
+import (
+ "fmt"
+
+ "github.com/mum4k/termdash/terminal/terminalapi"
+ tbx "github.com/nsf/termbox-go"
+)
+
+// colorMode converts termdash color modes to the termbox format.
+func colorMode(cm terminalapi.ColorMode) (tbx.OutputMode, error) {
+ switch cm {
+ case terminalapi.ColorModeNormal:
+ return tbx.OutputNormal, nil
+ case terminalapi.ColorMode256:
+ return tbx.Output256, nil
+ case terminalapi.ColorMode216:
+ return tbx.Output216, nil
+ case terminalapi.ColorModeGrayscale:
+ return tbx.OutputGrayscale, nil
+ default:
+ return -1, fmt.Errorf("don't know how to convert color mode %v to the termbox format", cm)
+ }
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/termbox/event.go b/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/termbox/event.go
new file mode 100644
index 000000000..c26d88c18
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/termbox/event.go
@@ -0,0 +1,179 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package termbox
+
+// event.go converts termbox events to the termdash format.
+
+import (
+ "image"
+
+ "github.com/mum4k/termdash/keyboard"
+ "github.com/mum4k/termdash/mouse"
+ "github.com/mum4k/termdash/terminal/terminalapi"
+ tbx "github.com/nsf/termbox-go"
+)
+
+// tbxToTd maps termbox key values to the termdash format.
+var tbxToTd = map[tbx.Key]keyboard.Key{
+ tbx.KeySpace: keyboard.KeySpace,
+ tbx.KeyF1: keyboard.KeyF1,
+ tbx.KeyF2: keyboard.KeyF2,
+ tbx.KeyF3: keyboard.KeyF3,
+ tbx.KeyF4: keyboard.KeyF4,
+ tbx.KeyF5: keyboard.KeyF5,
+ tbx.KeyF6: keyboard.KeyF6,
+ tbx.KeyF7: keyboard.KeyF7,
+ tbx.KeyF8: keyboard.KeyF8,
+ tbx.KeyF9: keyboard.KeyF9,
+ tbx.KeyF10: keyboard.KeyF10,
+ tbx.KeyF11: keyboard.KeyF11,
+ tbx.KeyF12: keyboard.KeyF12,
+ tbx.KeyInsert: keyboard.KeyInsert,
+ tbx.KeyDelete: keyboard.KeyDelete,
+ tbx.KeyHome: keyboard.KeyHome,
+ tbx.KeyEnd: keyboard.KeyEnd,
+ tbx.KeyPgup: keyboard.KeyPgUp,
+ tbx.KeyPgdn: keyboard.KeyPgDn,
+ tbx.KeyArrowUp: keyboard.KeyArrowUp,
+ tbx.KeyArrowDown: keyboard.KeyArrowDown,
+ tbx.KeyArrowLeft: keyboard.KeyArrowLeft,
+ tbx.KeyArrowRight: keyboard.KeyArrowRight,
+ tbx.KeyCtrlTilde: keyboard.KeyCtrlTilde,
+ tbx.KeyCtrlA: keyboard.KeyCtrlA,
+ tbx.KeyCtrlB: keyboard.KeyCtrlB,
+ tbx.KeyCtrlC: keyboard.KeyCtrlC,
+ tbx.KeyCtrlD: keyboard.KeyCtrlD,
+ tbx.KeyCtrlE: keyboard.KeyCtrlE,
+ tbx.KeyCtrlF: keyboard.KeyCtrlF,
+ tbx.KeyCtrlG: keyboard.KeyCtrlG,
+ tbx.KeyBackspace: keyboard.KeyBackspace,
+ tbx.KeyTab: keyboard.KeyTab,
+ tbx.KeyCtrlJ: keyboard.KeyCtrlJ,
+ tbx.KeyCtrlK: keyboard.KeyCtrlK,
+ tbx.KeyCtrlL: keyboard.KeyCtrlL,
+ tbx.KeyEnter: keyboard.KeyEnter,
+ tbx.KeyCtrlN: keyboard.KeyCtrlN,
+ tbx.KeyCtrlO: keyboard.KeyCtrlO,
+ tbx.KeyCtrlP: keyboard.KeyCtrlP,
+ tbx.KeyCtrlQ: keyboard.KeyCtrlQ,
+ tbx.KeyCtrlR: keyboard.KeyCtrlR,
+ tbx.KeyCtrlS: keyboard.KeyCtrlS,
+ tbx.KeyCtrlT: keyboard.KeyCtrlT,
+ tbx.KeyCtrlU: keyboard.KeyCtrlU,
+ tbx.KeyCtrlV: keyboard.KeyCtrlV,
+ tbx.KeyCtrlW: keyboard.KeyCtrlW,
+ tbx.KeyCtrlX: keyboard.KeyCtrlX,
+ tbx.KeyCtrlY: keyboard.KeyCtrlY,
+ tbx.KeyCtrlZ: keyboard.KeyCtrlZ,
+ tbx.KeyEsc: keyboard.KeyEsc,
+ tbx.KeyCtrl4: keyboard.KeyCtrl4,
+ tbx.KeyCtrl5: keyboard.KeyCtrl5,
+ tbx.KeyCtrl6: keyboard.KeyCtrl6,
+ tbx.KeyCtrl7: keyboard.KeyCtrl7,
+ tbx.KeyBackspace2: keyboard.KeyBackspace2,
+}
+
+// convKey converts a termbox keyboard event to the termdash format.
+func convKey(tbxEv tbx.Event) terminalapi.Event {
+ if tbxEv.Key != 0 && tbxEv.Ch != 0 {
+ return terminalapi.NewErrorf("the key event contain both a key(%v) and a character(%v)", tbxEv.Key, tbxEv.Ch)
+ }
+
+ if tbxEv.Ch != 0 {
+ return &terminalapi.Keyboard{
+ Key: keyboard.Key(tbxEv.Ch),
+ }
+ }
+
+ k, ok := tbxToTd[tbxEv.Key]
+ if !ok {
+ return terminalapi.NewErrorf("unknown keyboard key '%v' in a keyboard event", k)
+ }
+ return &terminalapi.Keyboard{
+ Key: k,
+ }
+}
+
+// convMouse converts a termbox mouse event to the termdash format.
+func convMouse(tbxEv tbx.Event) terminalapi.Event {
+ var button mouse.Button
+
+ switch k := tbxEv.Key; k {
+ case tbx.MouseLeft:
+ button = mouse.ButtonLeft
+ case tbx.MouseMiddle:
+ button = mouse.ButtonMiddle
+ case tbx.MouseRight:
+ button = mouse.ButtonRight
+ case tbx.MouseRelease:
+ button = mouse.ButtonRelease
+ case tbx.MouseWheelUp:
+ button = mouse.ButtonWheelUp
+ case tbx.MouseWheelDown:
+ button = mouse.ButtonWheelDown
+ default:
+ return terminalapi.NewErrorf("unknown mouse key %v in a mouse event", k)
+ }
+
+ return &terminalapi.Mouse{
+ Position: image.Point{tbxEv.MouseX, tbxEv.MouseY},
+ Button: button,
+ }
+}
+
+// convResize converts a termbox resize event to the termdash format.
+func convResize(tbxEv tbx.Event) terminalapi.Event {
+ size := image.Point{tbxEv.Width, tbxEv.Height}
+ if size.X < 0 || size.Y < 0 {
+ return terminalapi.NewErrorf("terminal resized to negative size: %v", size)
+ }
+ return &terminalapi.Resize{
+ Size: size,
+ }
+}
+
+// toTermdashEvents converts a termbox event to the termdash event format.
+func toTermdashEvents(tbxEv tbx.Event) []terminalapi.Event {
+ switch t := tbxEv.Type; t {
+ case tbx.EventInterrupt:
+ return []terminalapi.Event{
+ terminalapi.NewError("event type EventInterrupt isn't supported"),
+ }
+ case tbx.EventRaw:
+ return []terminalapi.Event{
+ terminalapi.NewError("event type EventRaw isn't supported"),
+ }
+ case tbx.EventNone:
+ return []terminalapi.Event{
+ terminalapi.NewError("event type EventNone isn't supported"),
+ }
+ case tbx.EventError:
+ return []terminalapi.Event{
+ terminalapi.NewErrorf("input error occurred: %v", tbxEv.Err),
+ }
+ case tbx.EventResize:
+ return []terminalapi.Event{convResize(tbxEv)}
+ case tbx.EventMouse:
+ return []terminalapi.Event{convMouse(tbxEv)}
+ case tbx.EventKey:
+ return []terminalapi.Event{
+ convKey(tbxEv),
+ }
+ default:
+ return []terminalapi.Event{
+ terminalapi.NewErrorf("unknown termbox event type: %v", t),
+ }
+ }
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/termbox/termbox.go b/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/termbox/termbox.go
new file mode 100644
index 000000000..4329e4686
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/termbox/termbox.go
@@ -0,0 +1,164 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package termbox implements terminal using the nsf/termbox-go library.
+package termbox
+
+import (
+ "context"
+ "image"
+
+ "github.com/mum4k/termdash/cell"
+ "github.com/mum4k/termdash/private/event/eventqueue"
+ "github.com/mum4k/termdash/terminal/terminalapi"
+ tbx "github.com/nsf/termbox-go"
+)
+
+// Option is used to provide options.
+type Option interface {
+ // set sets the provided option.
+ set(*Terminal)
+}
+
+// option implements Option.
+type option func(*Terminal)
+
+// set implements Option.set.
+func (o option) set(t *Terminal) {
+ o(t)
+}
+
+// DefaultColorMode is the default value for the ColorMode option.
+const DefaultColorMode = terminalapi.ColorMode256
+
+// ColorMode sets the terminal color mode.
+// Defaults to DefaultColorMode.
+func ColorMode(cm terminalapi.ColorMode) Option {
+ return option(func(t *Terminal) {
+ t.colorMode = cm
+ })
+}
+
+// Terminal provides input and output to a real terminal. Wraps the
+// nsf/termbox-go terminal implementation. This object is not thread-safe.
+// Implements terminalapi.Terminal.
+type Terminal struct {
+ // events is a queue of input events.
+ events *eventqueue.Unbound
+
+ // done gets closed when Close() is called.
+ done chan struct{}
+
+ // Options.
+ colorMode terminalapi.ColorMode
+}
+
+// newTerminal creates the terminal and applies the options.
+func newTerminal(opts ...Option) *Terminal {
+ t := &Terminal{
+ events: eventqueue.New(),
+ done: make(chan struct{}),
+ colorMode: DefaultColorMode,
+ }
+ for _, opt := range opts {
+ opt.set(t)
+ }
+ return t
+}
+
+// New returns a new termbox based Terminal.
+// Call Close() when the terminal isn't required anymore.
+func New(opts ...Option) (*Terminal, error) {
+ if err := tbx.Init(); err != nil {
+ return nil, err
+ }
+ tbx.SetInputMode(tbx.InputEsc | tbx.InputMouse)
+
+ t := newTerminal(opts...)
+ om, err := colorMode(t.colorMode)
+ if err != nil {
+ return nil, err
+ }
+ tbx.SetOutputMode(om)
+
+ go t.pollEvents() // Stops when Close() is called.
+ return t, nil
+}
+
+// Size implements terminalapi.Terminal.Size.
+func (t *Terminal) Size() image.Point {
+ w, h := tbx.Size()
+ return image.Point{w, h}
+}
+
+// Clear implements terminalapi.Terminal.Clear.
+func (t *Terminal) Clear(opts ...cell.Option) error {
+ o := cell.NewOptions(opts...)
+ return tbx.Clear(cellOptsToFg(o), cellOptsToBg(o))
+}
+
+// Flush implements terminalapi.Terminal.Flush.
+func (t *Terminal) Flush() error {
+ return tbx.Flush()
+}
+
+// SetCursor implements terminalapi.Terminal.SetCursor.
+func (t *Terminal) SetCursor(p image.Point) {
+ tbx.SetCursor(p.X, p.Y)
+}
+
+// HideCursor implements terminalapi.Terminal.HideCursor.
+func (t *Terminal) HideCursor() {
+ tbx.HideCursor()
+}
+
+// SetCell implements terminalapi.Terminal.SetCell.
+func (t *Terminal) SetCell(p image.Point, r rune, opts ...cell.Option) error {
+ o := cell.NewOptions(opts...)
+ tbx.SetCell(p.X, p.Y, r, cellOptsToFg(o), cellOptsToBg(o))
+ return nil
+}
+
+// pollEvents polls and enqueues the input events.
+func (t *Terminal) pollEvents() {
+ for {
+ select {
+ case <-t.done:
+ return
+ default:
+ }
+
+ events := toTermdashEvents(tbx.PollEvent())
+ for _, ev := range events {
+ t.events.Push(ev)
+ }
+ }
+}
+
+// Event implements terminalapi.Terminal.Event.
+func (t *Terminal) Event(ctx context.Context) terminalapi.Event {
+ ev := t.events.Pull(ctx)
+ if ev == nil {
+ return nil
+ }
+ return ev
+}
+
+// Close closes the terminal, should be called when the terminal isn't required
+// anymore to return the screen to a sane state.
+// Implements terminalapi.Terminal.Close.
+func (t *Terminal) Close() {
+ close(t.done)
+ tbx.Close()
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/terminalapi/color_mode.go b/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/terminalapi/color_mode.go
new file mode 100644
index 000000000..28c93277f
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/terminalapi/color_mode.go
@@ -0,0 +1,60 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package terminalapi
+
+// color_mode.go defines the terminal color modes.
+
+// ColorMode represents a color mode of a terminal.
+type ColorMode int
+
+// String implements fmt.Stringer()
+func (cm ColorMode) String() string {
+ if n, ok := colorModeNames[cm]; ok {
+ return n
+ }
+ return "ColorModeUnknown"
+}
+
+// colorModeNames maps ColorMode values to human readable names.
+var colorModeNames = map[ColorMode]string{
+ ColorModeNormal: "ColorModeNormal",
+ ColorMode256: "ColorMode256",
+ ColorMode216: "ColorMode216",
+ ColorModeGrayscale: "ColorModeGrayscale",
+}
+
+// Supported color modes.
+const (
+ // ColorModeNormal supports 8 "system" colors.
+ // These are defined as constants in the cell package.
+ ColorModeNormal ColorMode = iota
+
+ // ColorMode256 enables using any of the 256 terminal colors.
+ // 0-7: the 8 "system" colors accessible in ColorModeNormal.
+ // 8-15: the 8 "bright system" colors.
+ // 16-231: the 216 different terminal colors.
+ // 232-255: the 24 different shades of grey.
+ ColorMode256
+
+ // ColorMode216 supports only the third range of the ColorMode256, i.e the
+ // 216 different terminal colors. However in this mode the colors are zero
+ // based, so the caller doesn't need to provide an offset.
+ ColorMode216
+
+ // ColorModeGrayscale supports only the fourth range of the ColorMode256,
+ // i.e the 24 different shades of grey. However in this mode the colors are
+ // zero based, so the caller doesn't need to provide an offset.
+ ColorModeGrayscale
+)
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/terminalapi/event.go b/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/terminalapi/event.go
new file mode 100644
index 000000000..a543e8433
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/terminalapi/event.go
@@ -0,0 +1,106 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package terminalapi
+
+import (
+ "errors"
+ "fmt"
+ "image"
+
+ "github.com/mum4k/termdash/keyboard"
+ "github.com/mum4k/termdash/mouse"
+)
+
+// event.go defines events that can be received through the terminal API.
+
+// Event represents an input event.
+type Event interface {
+ isEvent()
+}
+
+// Keyboard is the event used when a key is pressed.
+// Implements terminalapi.Event.
+type Keyboard struct {
+ // Key is the pressed key.
+ Key keyboard.Key
+}
+
+func (*Keyboard) isEvent() {}
+
+// String implements fmt.Stringer.
+func (k Keyboard) String() string {
+ return fmt.Sprintf("Keyboard{Key: %v}", k.Key)
+}
+
+// Resize is the event used when the terminal was resized.
+// Implements terminalapi.Event.
+type Resize struct {
+ // Size is the new size of the terminal.
+ Size image.Point
+}
+
+func (*Resize) isEvent() {}
+
+// String implements fmt.Stringer.
+func (r Resize) String() string {
+ return fmt.Sprintf("Resize{Size: %v}", r.Size)
+}
+
+// Mouse is the event used when the mouse is moved or a mouse button is
+// pressed.
+// Implements terminalapi.Event.
+type Mouse struct {
+ // Position of the mouse on the terminal.
+ Position image.Point
+ // Button identifies the pressed button if any.
+ Button mouse.Button
+}
+
+func (*Mouse) isEvent() {}
+
+// String implements fmt.Stringer.
+func (m Mouse) String() string {
+ return fmt.Sprintf("Mouse{Position: %v, Button: %v}", m.Position, m.Button)
+}
+
+// Error is an event indicating an error while processing input.
+type Error string
+
+// NewError returns a new Error event.
+func NewError(e string) *Error {
+ err := Error(e)
+ return &err
+}
+
+// NewErrorf returns a new Error event, arguments are similar to fmt.Sprintf.
+func NewErrorf(format string, args ...interface{}) *Error {
+ err := Error(fmt.Sprintf(format, args...))
+ return &err
+}
+
+func (*Error) isEvent() {}
+
+// Error returns the error that occurred.
+func (e *Error) Error() error {
+ if e == nil || *e == "" {
+ return nil
+ }
+ return errors.New(string(*e))
+}
+
+// String implements fmt.Stringer.
+func (e Error) String() string {
+ return string(e)
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/terminalapi/terminalapi.go b/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/terminalapi/terminalapi.go
new file mode 100644
index 000000000..831abc169
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/terminal/terminalapi/terminalapi.go
@@ -0,0 +1,56 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package terminalapi defines the API of all terminal implementations.
+package terminalapi
+
+import (
+ "context"
+ "image"
+
+ "github.com/mum4k/termdash/cell"
+)
+
+// Terminal abstracts an implementation of a 2-D terminal.
+// A terminal consists of a number of cells.
+type Terminal interface {
+ // Size returns the terminal width and height in cells.
+ Size() image.Point
+
+ // Clear clears the content of the internal back buffer, resetting all
+ // cells to their default content and attributes. Sets the provided options
+ // on all the cell.
+ Clear(opts ...cell.Option) error
+ // Flush flushes the internal back buffer to the terminal.
+ Flush() error
+
+ // SetCursor sets the position of the cursor.
+ SetCursor(p image.Point)
+ // HideCursos hides the cursor.
+ HideCursor()
+
+ // SetCell sets the value of the specified cell to the provided rune.
+ // Use the options to specify which attributes to modify, if an attribute
+ // option isn't specified, the attribute retains its previous value.
+ SetCell(p image.Point, r rune, opts ...cell.Option) error
+
+ // Event waits for the next event and returns it.
+ // This call blocks until the next event or cancellation of the context.
+ // Returns nil when the context gets canceled.
+ Event(ctx context.Context) Event
+
+ // Close closes the underlying terminal implementation and should be called when
+ // the terminal isn't required anymore to return the screen to a sane state.
+ Close()
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/widgetapi/widgetapi.go b/examples/go-dashboard/src/github.com/mum4k/termdash/widgetapi/widgetapi.go
new file mode 100644
index 000000000..ee27136a1
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/widgetapi/widgetapi.go
@@ -0,0 +1,185 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package widgetapi defines the API of a widget on the dashboard.
+package widgetapi
+
+import (
+ "image"
+
+ "github.com/mum4k/termdash/private/canvas"
+ "github.com/mum4k/termdash/terminal/terminalapi"
+)
+
+// KeyScope indicates the scope at which the widget wants to receive keyboard
+// events.
+type KeyScope int
+
+// String implements fmt.Stringer()
+func (ks KeyScope) String() string {
+ if n, ok := keyScopeNames[ks]; ok {
+ return n
+ }
+ return "KeyScopeUnknown"
+}
+
+// keyScopeNames maps KeyScope values to human readable names.
+var keyScopeNames = map[KeyScope]string{
+ KeyScopeNone: "KeyScopeNone",
+ KeyScopeFocused: "KeyScopeFocused",
+ KeyScopeGlobal: "KeyScopeGlobal",
+}
+
+const (
+ // KeyScopeNone is used when the widget doesn't want to receive any
+ // keyboard events.
+ KeyScopeNone KeyScope = iota
+
+ // KeyScopeFocused is used when the widget wants to only receive keyboard
+ // events when its container is focused.
+ KeyScopeFocused
+
+ // KeyScopeGlobal is used when the widget wants to receive all keyboard
+ // events regardless of which container is focused.
+ KeyScopeGlobal
+)
+
+// MouseScope indicates the scope at which the widget wants to receive mouse
+// events.
+type MouseScope int
+
+// String implements fmt.Stringer()
+func (ms MouseScope) String() string {
+ if n, ok := mouseScopeNames[ms]; ok {
+ return n
+ }
+ return "MouseScopeUnknown"
+}
+
+// mouseScopeNames maps MouseScope values to human readable names.
+var mouseScopeNames = map[MouseScope]string{
+ MouseScopeNone: "MouseScopeNone",
+ MouseScopeWidget: "MouseScopeWidget",
+ MouseScopeContainer: "MouseScopeContainer",
+ MouseScopeGlobal: "MouseScopeGlobal",
+}
+
+const (
+ // MouseScopeNone is used when the widget doesn't want to receive any mouse
+ // events.
+ MouseScopeNone MouseScope = iota
+
+ // MouseScopeWidget is used when the widget only wants mouse events that
+ // fall onto its canvas.
+ // The position of these widgets is always relative to widget's canvas.
+ MouseScopeWidget
+
+ // MouseScopeContainer is used when the widget only wants mouse events that
+ // fall onto its container. The area size of a container is always larger
+ // or equal to the one of the widget's canvas. So a widget selecting
+ // MouseScopeContainer will either receive the same or larger amount of
+ // events as compared to MouseScopeWidget.
+ // The position of mouse events that fall outside of widget's canvas is
+ // reset to image.Point{-1, -1}.
+ // The widgets are allowed to process the button event.
+ MouseScopeContainer
+
+ // MouseScopeGlobal is used when the widget wants to receive all mouse
+ // events regardless on where on the terminal they land.
+ // The position of mouse events that fall outside of widget's canvas is
+ // reset to image.Point{-1, -1} and must not be used by the widgets.
+ // The widgets are allowed to process the button event.
+ MouseScopeGlobal
+)
+
+// Options contains registration options for a widget.
+// This is how the widget indicates its needs to the infrastructure.
+type Options struct {
+ // Ratio allows a widget to request a canvas whose size will always have
+ // the specified ratio of width:height (Ratio.X:Ratio.Y).
+ // The zero value i.e. image.Point{0, 0} indicates that the widget accepts
+ // canvas of any ratio.
+ Ratio image.Point
+
+ // MinimumSize allows a widget to specify the smallest allowed canvas size.
+ // If the terminal size and/or splits cause the assigned canvas to be
+ // smaller than this, the widget will be skipped. I.e. The Draw() method
+ // won't be called until a resize above the specified minimum.
+ MinimumSize image.Point
+
+ // MaximumSize allows a widget to specify the largest allowed canvas size.
+ // If the terminal size and/or splits cause the assigned canvas to be larger
+ // than this, the widget will only receive a canvas of this size within its
+ // container. Setting any of the two coordinates to zero indicates
+ // unlimited.
+ MaximumSize image.Point
+
+ // WantKeyboard allows a widget to request keyboard events and specify
+ // their desired scope. If set to KeyScopeNone, no keyboard events are
+ // forwarded to the widget.
+ WantKeyboard KeyScope
+
+ // WantMouse allows a widget to request mouse events and specify their
+ // desired scope. If set to MouseScopeNone, no mouse events are forwarded
+ // to the widget.
+ // Note that the widget is only able to see the position of the mouse event
+ // if it falls onto its canvas. See the documentation next to individual
+ // MouseScope values for details.
+ WantMouse MouseScope
+}
+
+// Meta provide additional metadata to widgets.
+type Meta struct {
+ // Focused asserts whether the widget's container is focused.
+ Focused bool
+}
+
+// Widget is a single widget on the dashboard.
+// Implementations must be thread safe.
+type Widget interface {
+ // When the infrastructure calls Draw(), the widget must block on the call
+ // until it finishes drawing onto the provided canvas. When given the
+ // canvas, the widget must first determine its size by calling
+ // Canvas.Size(), then limit all its drawing to this area.
+ //
+ // The widget must not assume that the size of the canvas or its content
+ // remains the same between calls.
+ //
+ // The argument meta is guaranteed to be valid (i.e. non-nil).
+ Draw(cvs *canvas.Canvas, meta *Meta) error
+
+ // Keyboard is called when the widget is focused on the dashboard and a key
+ // shortcut the widget registered for was pressed. Only called if the widget
+ // registered for keyboard events.
+ Keyboard(k *terminalapi.Keyboard) error
+
+ // Mouse is called when the widget is focused on the dashboard and a mouse
+ // event happens on its canvas. Only called if the widget registered for mouse
+ // events.
+ Mouse(m *terminalapi.Mouse) error
+
+ // Options returns registration options for the widget.
+ // This is how the widget indicates to the infrastructure whether it is
+ // interested in keyboard or mouse shortcuts, what is its minimum canvas
+ // size, etc.
+ //
+ // Most widgets will return statically compiled options (minimum and
+ // maximum size, etc.). If the returned options depend on the runtime state
+ // of the widget (e.g. the user data provided to the widget), the widget
+ // must protect against a case where the infrastructure calls the Draw
+ // method with a canvas that doesn't meet the requested options. This is
+ // because the data in the widget might change between calls to Options and
+ // Draw.
+ Options() Options
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/widgets/text/line_trim.go b/examples/go-dashboard/src/github.com/mum4k/termdash/widgets/text/line_trim.go
new file mode 100644
index 000000000..6ca8c83aa
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/widgets/text/line_trim.go
@@ -0,0 +1,117 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package text
+
+import (
+ "fmt"
+ "image"
+
+ "github.com/mum4k/termdash/private/canvas"
+ "github.com/mum4k/termdash/private/runewidth"
+ "github.com/mum4k/termdash/private/wrap"
+)
+
+// line_trim.go contains code that trims lines that are too long.
+
+type trimResult struct {
+ // trimmed is set to true if the current and the following runes on this
+ // line are trimmed.
+ trimmed bool
+
+ // curPoint is the updated current point the drawing should continue on.
+ curPoint image.Point
+}
+
+// drawTrimChar draws the horizontal ellipsis '…' character as the last
+// character in the canvas on the specified line.
+func drawTrimChar(cvs *canvas.Canvas, line int) error {
+ lastPoint := image.Point{cvs.Area().Dx() - 1, line}
+ // If the penultimate cell contains a full-width rune, we need to clear it
+ // first. Otherwise the trim char would cover just half of it.
+ if width := cvs.Area().Dx(); width > 1 {
+ penUlt := image.Point{width - 2, line}
+ prev, err := cvs.Cell(penUlt)
+ if err != nil {
+ return err
+ }
+
+ if runewidth.RuneWidth(prev.Rune) == 2 {
+ if _, err := cvs.SetCell(penUlt, 0); err != nil {
+ return err
+ }
+ }
+ }
+
+ cells, err := cvs.SetCell(lastPoint, '…')
+ if err != nil {
+ return err
+ }
+ if cells != 1 {
+ panic(fmt.Errorf("invalid trim character, it occupies %d cells, the implementation only supports scroll markers that occupy exactly one cell", cells))
+ }
+ return nil
+}
+
+// lineTrim determines if the current line needs to be trimmed. The cvs is the
+// canvas assigned to the widget, the curPoint is the current point the widget
+// is going to place the curRune at. If line trimming is needed, this function
+// replaces the last character with the horizontal ellipsis '…' character.
+func lineTrim(cvs *canvas.Canvas, curPoint image.Point, curRune rune, opts *options) (*trimResult, error) {
+ if opts.wrapMode == wrap.AtRunes {
+ // Don't trim if the widget is configured to wrap lines.
+ return &trimResult{
+ trimmed: false,
+ curPoint: curPoint,
+ }, nil
+ }
+
+ // Newline characters are never trimmed, they start the next line.
+ if curRune == '\n' {
+ return &trimResult{
+ trimmed: false,
+ curPoint: curPoint,
+ }, nil
+ }
+
+ width := cvs.Area().Dx()
+ rw := runewidth.RuneWidth(curRune)
+ switch {
+ case rw == 1:
+ if curPoint.X == width {
+ if err := drawTrimChar(cvs, curPoint.Y); err != nil {
+ return nil, err
+ }
+ }
+
+ case rw == 2:
+ if curPoint.X == width || curPoint.X == width-1 {
+ if err := drawTrimChar(cvs, curPoint.Y); err != nil {
+ return nil, err
+ }
+ }
+
+ default:
+ return nil, fmt.Errorf("unable to decide line trimming at position %v for rune %q which has an unsupported width %d", curPoint, curRune, rw)
+ }
+
+ trimmed := curPoint.X > width-rw
+ if trimmed {
+ curPoint = image.Point{curPoint.X + rw, curPoint.Y}
+ }
+ return &trimResult{
+ trimmed: trimmed,
+ curPoint: curPoint,
+ }, nil
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/widgets/text/options.go b/examples/go-dashboard/src/github.com/mum4k/termdash/widgets/text/options.go
new file mode 100644
index 000000000..b91cec85e
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/widgets/text/options.go
@@ -0,0 +1,156 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package text
+
+import (
+ "fmt"
+
+ "github.com/mum4k/termdash/keyboard"
+ "github.com/mum4k/termdash/mouse"
+ "github.com/mum4k/termdash/private/wrap"
+)
+
+// options.go contains configurable options for Text.
+
+// Option is used to provide options to New().
+type Option interface {
+ // set sets the provided option.
+ set(*options)
+}
+
+// options stores the provided options.
+type options struct {
+ wrapMode wrap.Mode
+ rollContent bool
+ disableScrolling bool
+ mouseUpButton mouse.Button
+ mouseDownButton mouse.Button
+ keyUp keyboard.Key
+ keyDown keyboard.Key
+ keyPgUp keyboard.Key
+ keyPgDown keyboard.Key
+}
+
+// newOptions returns a new options instance.
+func newOptions(opts ...Option) *options {
+ opt := &options{
+ mouseUpButton: DefaultScrollMouseButtonUp,
+ mouseDownButton: DefaultScrollMouseButtonDown,
+ keyUp: DefaultScrollKeyUp,
+ keyDown: DefaultScrollKeyDown,
+ keyPgUp: DefaultScrollKeyPageUp,
+ keyPgDown: DefaultScrollKeyPageDown,
+ }
+ for _, o := range opts {
+ o.set(opt)
+ }
+ return opt
+}
+
+// validate validates the provided options.
+func (o *options) validate() error {
+ keys := map[keyboard.Key]bool{
+ o.keyUp: true,
+ o.keyDown: true,
+ o.keyPgUp: true,
+ o.keyPgDown: true,
+ }
+ if len(keys) != 4 {
+ return fmt.Errorf("invalid ScrollKeys(up:%v, down:%v, pageUp:%v, pageDown:%v), the keys must be unique", o.keyUp, o.keyDown, o.keyPgUp, o.keyPgDown)
+ }
+ if o.mouseUpButton == o.mouseDownButton {
+ return fmt.Errorf("invalid ScrollMouseButtons(up:%v, down:%v), the buttons must be unique", o.mouseUpButton, o.mouseDownButton)
+ }
+ return nil
+}
+
+// option implements Option.
+type option func(*options)
+
+// set implements Option.set.
+func (o option) set(opts *options) {
+ o(opts)
+}
+
+// WrapAtWords configures the text widget so that it automatically wraps lines
+// that are longer than the width of the widget at word boundaries. If not
+// provided, long lines are trimmed instead.
+func WrapAtWords() Option {
+ return option(func(opts *options) {
+ opts.wrapMode = wrap.AtWords
+ })
+}
+
+// WrapAtRunes configures the text widget so that it automatically wraps lines
+// that are longer than the width of the widget at rune boundaries. If not
+// provided, long lines are trimmed instead.
+func WrapAtRunes() Option {
+ return option(func(opts *options) {
+ opts.wrapMode = wrap.AtRunes
+ })
+}
+
+// RollContent configures the text widget so that it rolls the text content up
+// if more text than the size of the container is added. If not provided, the
+// content is trimmed instead.
+func RollContent() Option {
+ return option(func(opts *options) {
+ opts.rollContent = true
+ })
+}
+
+// DisableScrolling disables the scrolling of the content using keyboard and
+// mouse.
+func DisableScrolling() Option {
+ return option(func(opts *options) {
+ opts.disableScrolling = true
+ })
+}
+
+// The default mouse buttons for content scrolling.
+const (
+ DefaultScrollMouseButtonUp = mouse.ButtonWheelUp
+ DefaultScrollMouseButtonDown = mouse.ButtonWheelDown
+)
+
+// ScrollMouseButtons configures the mouse buttons that scroll the content.
+// The provided buttons must be unique, e.g. the same button cannot be both up
+// and down.
+func ScrollMouseButtons(up, down mouse.Button) Option {
+ return option(func(opts *options) {
+ opts.mouseUpButton = up
+ opts.mouseDownButton = down
+ })
+}
+
+// The default keys for content scrolling.
+const (
+ DefaultScrollKeyUp = keyboard.KeyArrowUp
+ DefaultScrollKeyDown = keyboard.KeyArrowDown
+ DefaultScrollKeyPageUp = keyboard.KeyPgUp
+ DefaultScrollKeyPageDown = keyboard.KeyPgDn
+)
+
+// ScrollKeys configures the keyboard keys that scroll the content.
+// The provided keys must be unique, e.g. the same key cannot be both up and
+// down.
+func ScrollKeys(up, down, pageUp, pageDown keyboard.Key) Option {
+ return option(func(opts *options) {
+ opts.keyUp = up
+ opts.keyDown = down
+ opts.keyPgUp = pageUp
+ opts.keyPgDown = pageDown
+ })
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/widgets/text/scroll.go b/examples/go-dashboard/src/github.com/mum4k/termdash/widgets/text/scroll.go
new file mode 100644
index 000000000..f63092fdb
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/widgets/text/scroll.go
@@ -0,0 +1,165 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package text
+
+// scroll.go contains code that tracks the current scrolling position.
+
+import "math"
+
+// scrollTracker tracks the current scrolling position for the Text widget.
+//
+// The text widget displays the contained text buffer as lines of text that fit
+// the widget's canvas. The main goal of this object is to inform the text
+// widget which should be the first drawn line from the buffer. This depends on
+// two things, the scrolling position based on user inputs and whether the text
+// widget is configured to roll the content up as new text is added by the
+// client.
+//
+// The rolling Vs. scrolling state is tracked in an FSM implemented in this
+// file.
+//
+// The client can scroll the content by either a keyboard or a mouse event. The
+// widget receives these events concurrently with requests to redraw the
+// content, so this objects keeps a track of all the scrolling events that
+// happened since the last redraw and consumes them when calculating which is
+// the first drawn line on the next redraw event.
+//
+// This is not thread safe.
+type scrollTracker struct {
+ // scroll stores user requests to scroll up (negative) or down (positive).
+ // E.g. -1 means up by one line and 2 means down by two lines.
+ scroll int
+
+ // scrollPage stores user requests to scroll up (negative) or down
+ // (positive) by a page of content. E.g. -1 means up by one page and 2
+ // means down by two pages.
+ scrollPage int
+
+ // first tracks the first line that will be printed.
+ first int
+
+ // state is the state of the scrolling FSM.
+ state rollState
+}
+
+// newScrollTracker returns a new scroll tracker.
+func newScrollTracker(opts *options) *scrollTracker {
+ if opts.rollContent {
+ return &scrollTracker{state: rollToEnd}
+ }
+ return &scrollTracker{state: rollingDisabled}
+}
+
+// upOneLine processes a user request to scroll up by one line.
+func (st *scrollTracker) upOneLine() {
+ st.scroll--
+}
+
+// downOneLine processes a user request to scroll down by one line.
+func (st *scrollTracker) downOneLine() {
+ st.scroll++
+}
+
+// upOnePage processes a user request to scroll up by one page.
+func (st *scrollTracker) upOnePage() {
+ st.scrollPage--
+}
+
+// downOnePage processes a user request to scroll down by one page.
+func (st *scrollTracker) downOnePage() {
+ st.scrollPage++
+}
+
+// doScroll processes any outstanding scroll requests and calculates the
+// resulting first line.
+func (st *scrollTracker) doScroll(lines, height int) int {
+ first := st.first + st.scroll + st.scrollPage*height
+ st.scroll = 0
+ st.scrollPage = 0
+ return normalizeScroll(first, lines, height)
+}
+
+// firstLine returns the number of the first line that should be drawn on a
+// canvas of the specified height if there is the provided number of lines of
+// text.
+func (st *scrollTracker) firstLine(lines, height int) int {
+ // Execute the scrolling FSM.
+ st.state = st.state(st, lines, height)
+ return st.first
+}
+
+// rollState is a state in the scrolling FSM.
+type rollState func(st *scrollTracker, lines, height int) rollState
+
+// rollingDisabled is a state where content rolling was disabled by the
+// configuration of the Text widget.
+func rollingDisabled(st *scrollTracker, lines, height int) rollState {
+ st.first = st.doScroll(lines, height)
+ return rollingDisabled
+}
+
+// rollToEnd is a state in which the last line of the content is always
+// visible. When new content arrives, it is rolled upwards.
+func rollToEnd(st *scrollTracker, lines, height int) rollState {
+ // If the user didn't scroll, just roll the content so that the last line
+ // is visible.
+ if st.scroll == 0 && st.scrollPage == 0 {
+ st.first = normalizeScroll(math.MaxInt32, lines, height)
+ return rollToEnd
+ }
+
+ st.first = st.doScroll(lines, height)
+ if lastLineVisible(st.first, lines, height) {
+ return rollToEnd
+ }
+ return rollingPaused
+}
+
+// rollingPaused is a state in which the user scrolled up and made the last
+// line scroll out of the view, so the content rolling is paused.
+func rollingPaused(st *scrollTracker, lines, height int) rollState {
+ st.first = st.doScroll(lines, height)
+ if lastLineVisible(st.first, lines, height) {
+ return rollToEnd
+ }
+ return rollingPaused
+}
+
+// lastLineVisible returns true if the last text line from within the buffer of
+// the text widget is visible on the canvas when drawing of the text starts
+// from the specified start line, there is the provided total amount of lines
+// and the canvas has the height.
+func lastLineVisible(start, lines, height int) bool {
+ return lines-start <= height
+}
+
+// normalizeScroll returns normalized position of the first line that should be
+// drawn when drawing the specified number of lines on a canvas with the
+// provided height.
+func normalizeScroll(first, lines, height int) int {
+ if first < 0 || lines <= 0 || height <= 0 {
+ return 0
+ }
+
+ if lines <= height {
+ return 0 // Scrolling not necessary if the content fits.
+ }
+
+ max := lines - height
+ if first > max {
+ return max
+ }
+ return first
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/widgets/text/text.go b/examples/go-dashboard/src/github.com/mum4k/termdash/widgets/text/text.go
new file mode 100644
index 000000000..0712cbcbe
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/widgets/text/text.go
@@ -0,0 +1,286 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package text contains a widget that displays textual data.
+package text
+
+import (
+ "fmt"
+ "image"
+ "sync"
+
+ "github.com/mum4k/termdash/private/canvas"
+ "github.com/mum4k/termdash/private/canvas/buffer"
+ "github.com/mum4k/termdash/private/wrap"
+ "github.com/mum4k/termdash/terminal/terminalapi"
+ "github.com/mum4k/termdash/widgetapi"
+)
+
+// Text displays a block of text.
+//
+// Each line of the text is either trimmed or wrapped according to the provided
+// options. The entire text content is either trimmed or rolled up through the
+// canvas according to the provided options.
+//
+// By default the widget supports scrolling of content with either the keyboard
+// or mouse. See the options for the default keys and mouse buttons.
+//
+// Implements widgetapi.Widget. This object is thread-safe.
+type Text struct {
+ // content is the text content that will be displayed in the widget as
+ // provided by the caller (i.e. not wrapped or pre-processed).
+ content []*buffer.Cell
+ // wrapped is the content wrapped to the current width of the canvas.
+ wrapped [][]*buffer.Cell
+
+ // scroll tracks scrolling the position.
+ scroll *scrollTracker
+
+ // lastWidth stores the width of the last canvas the widget drew on.
+ // Used to determine if the previous line wrapping was invalidated.
+ lastWidth int
+ // contentChanged indicates if the text content of the widget changed since
+ // the last drawing. Used to determine if the previous line wrapping was
+ // invalidated.
+ contentChanged bool
+
+ // mu protects the Text widget.
+ mu sync.Mutex
+
+ // opts are the provided options.
+ opts *options
+}
+
+// New returns a new text widget.
+func New(opts ...Option) (*Text, error) {
+ opt := newOptions(opts...)
+ if err := opt.validate(); err != nil {
+ return nil, err
+ }
+ return &Text{
+ scroll: newScrollTracker(opt),
+ opts: opt,
+ }, nil
+}
+
+// Reset resets the widget back to empty content.
+func (t *Text) Reset() {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ t.reset()
+}
+
+// reset implements Reset, caller must hold t.mu.
+func (t *Text) reset() {
+ t.content = nil
+ t.wrapped = nil
+ t.scroll = newScrollTracker(t.opts)
+ t.lastWidth = 0
+ t.contentChanged = true
+}
+
+// Write writes text for the widget to display. Multiple calls append
+// additional text. The text contain cannot control characters
+// (unicode.IsControl) or space character (unicode.IsSpace) other than:
+// ' ', '\n'
+// Any newline ('\n') characters are interpreted as newlines when displaying
+// the text.
+func (t *Text) Write(text string, wOpts ...WriteOption) error {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ if err := wrap.ValidText(text); err != nil {
+ return err
+ }
+
+ opts := newWriteOptions(wOpts...)
+ if opts.replace {
+ t.reset()
+ }
+ for _, r := range text {
+ t.content = append(t.content, buffer.NewCell(r, opts.cellOpts))
+ }
+ t.contentChanged = true
+ return nil
+}
+
+// minLinesForMarkers are the minimum amount of lines required on the canvas in
+// order to draw the scroll markers ('⇧' and '⇩').
+const minLinesForMarkers = 3
+
+// drawScrollUp draws the scroll up marker on the first line if there is more
+// text "above" the canvas due to the scrolling position. Returns true if the
+// marker was drawn.
+func (t *Text) drawScrollUp(cvs *canvas.Canvas, cur image.Point, fromLine int) (bool, error) {
+ height := cvs.Area().Dy()
+ if cur.Y == 0 && height >= minLinesForMarkers && fromLine > 0 {
+ cells, err := cvs.SetCell(cur, '⇧')
+ if err != nil {
+ return false, err
+ }
+ if cells != 1 {
+ panic(fmt.Errorf("invalid scroll up marker, it occupies %d cells, the implementation only supports scroll markers that occupy exactly one cell", cells))
+ }
+ return true, nil
+ }
+ return false, nil
+}
+
+// drawScrollDown draws the scroll down marker on the last line if there is
+// more text "below" the canvas due to the scrolling position. Returns true if
+// the marker was drawn.
+func (t *Text) drawScrollDown(cvs *canvas.Canvas, cur image.Point, fromLine int) (bool, error) {
+ height := cvs.Area().Dy()
+ lines := len(t.wrapped)
+ if cur.Y == height-1 && height >= minLinesForMarkers && height < lines-fromLine {
+ cells, err := cvs.SetCell(cur, '⇩')
+ if err != nil {
+ return false, err
+ }
+ if cells != 1 {
+ panic(fmt.Errorf("invalid scroll down marker, it occupies %d cells, the implementation only supports scroll markers that occupy exactly one cell", cells))
+ }
+ return true, nil
+ }
+ return false, nil
+}
+
+// draw draws the text context on the canvas starting at the specified line.
+func (t *Text) draw(cvs *canvas.Canvas) error {
+ var cur image.Point // Tracks the current drawing position on the canvas.
+ height := cvs.Area().Dy()
+ fromLine := t.scroll.firstLine(len(t.wrapped), height)
+
+ for _, line := range t.wrapped[fromLine:] {
+ // Scroll up marker.
+ scrlUp, err := t.drawScrollUp(cvs, cur, fromLine)
+ if err != nil {
+ return err
+ }
+ if scrlUp {
+ cur = image.Point{0, cur.Y + 1} // Move to the next line.
+ // Skip one line of text, the marker replaced it.
+ continue
+ }
+
+ // Scroll down marker.
+ scrlDown, err := t.drawScrollDown(cvs, cur, fromLine)
+ if err != nil {
+ return err
+ }
+ if scrlDown || cur.Y >= height {
+ break // Skip all lines falling after (under) the canvas.
+ }
+
+ for _, cell := range line {
+ tr, err := lineTrim(cvs, cur, cell.Rune, t.opts)
+ if err != nil {
+ return err
+ }
+ cur = tr.curPoint
+ if tr.trimmed {
+ break // Skip over any characters trimmed on the current line.
+ }
+
+ cells, err := cvs.SetCell(cur, cell.Rune, cell.Opts)
+ if err != nil {
+ return err
+ }
+ cur = image.Point{cur.X + cells, cur.Y} // Move within the same line.
+ }
+ cur = image.Point{0, cur.Y + 1} // Move to the next line.
+ }
+ return nil
+}
+
+// Draw draws the text onto the canvas.
+// Implements widgetapi.Widget.Draw.
+func (t *Text) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ width := cvs.Area().Dx()
+ if len(t.content) > 0 && (t.contentChanged || t.lastWidth != width) {
+ // The previous text preprocessing (line wrapping) is invalidated when
+ // new text is added or the width of the canvas changed.
+ wr, err := wrap.Cells(t.content, width, t.opts.wrapMode)
+ if err != nil {
+ return err
+ }
+ t.wrapped = wr
+ }
+ t.lastWidth = width
+
+ if len(t.wrapped) == 0 {
+ return nil // Nothing to draw if there's no text.
+ }
+
+ if err := t.draw(cvs); err != nil {
+ return err
+ }
+ t.contentChanged = false
+ return nil
+}
+
+// Keyboard implements widgetapi.Widget.Keyboard.
+func (t *Text) Keyboard(k *terminalapi.Keyboard) error {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ switch {
+ case k.Key == t.opts.keyUp:
+ t.scroll.upOneLine()
+ case k.Key == t.opts.keyDown:
+ t.scroll.downOneLine()
+ case k.Key == t.opts.keyPgUp:
+ t.scroll.upOnePage()
+ case k.Key == t.opts.keyPgDown:
+ t.scroll.downOnePage()
+ }
+ return nil
+}
+
+// Mouse implements widgetapi.Widget.Mouse.
+func (t *Text) Mouse(m *terminalapi.Mouse) error {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ switch b := m.Button; {
+ case b == t.opts.mouseUpButton:
+ t.scroll.upOneLine()
+ case b == t.opts.mouseDownButton:
+ t.scroll.downOneLine()
+ }
+ return nil
+}
+
+// Options of the widget
+func (t *Text) Options() widgetapi.Options {
+ var ks widgetapi.KeyScope
+ var ms widgetapi.MouseScope
+ if t.opts.disableScrolling {
+ ks = widgetapi.KeyScopeNone
+ ms = widgetapi.MouseScopeNone
+ } else {
+ ks = widgetapi.KeyScopeFocused
+ ms = widgetapi.MouseScopeWidget
+ }
+
+ return widgetapi.Options{
+ // At least one line with at least one full-width rune.
+ MinimumSize: image.Point{1, 1},
+ WantMouse: ms,
+ WantKeyboard: ks,
+ }
+}
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/widgets/text/write_options.go b/examples/go-dashboard/src/github.com/mum4k/termdash/widgets/text/write_options.go
new file mode 100644
index 000000000..ddb5c40bc
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/widgets/text/write_options.go
@@ -0,0 +1,67 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package text
+
+// write_options.go contains options used when writing content to the Text widget.
+
+import (
+ "github.com/mum4k/termdash/cell"
+)
+
+// WriteOption is used to provide options to Write().
+type WriteOption interface {
+ // set sets the provided option.
+ set(*writeOptions)
+}
+
+// writeOptions stores the provided options.
+type writeOptions struct {
+ cellOpts *cell.Options
+ replace bool
+}
+
+// newWriteOptions returns new writeOptions instance.
+func newWriteOptions(wOpts ...WriteOption) *writeOptions {
+ wo := &writeOptions{
+ cellOpts: cell.NewOptions(),
+ }
+ for _, o := range wOpts {
+ o.set(wo)
+ }
+ return wo
+}
+
+// writeOption implements WriteOption.
+type writeOption func(*writeOptions)
+
+// set implements WriteOption.set.
+func (wo writeOption) set(wOpts *writeOptions) {
+ wo(wOpts)
+}
+
+// WriteCellOpts sets options on the cells that contain the text.
+func WriteCellOpts(opts ...cell.Option) WriteOption {
+ return writeOption(func(wOpts *writeOptions) {
+ wOpts.cellOpts = cell.NewOptions(opts...)
+ })
+}
+
+// WriteReplace instructs the text widget to replace the entire text content on
+// this write instead of appending.
+func WriteReplace() WriteOption {
+ return writeOption(func(wOpts *writeOptions) {
+ wOpts.replace = true
+ })
+}
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/AUTHORS b/examples/go-dashboard/src/github.com/nsf/termbox-go/AUTHORS
new file mode 100644
index 000000000..fe26fb0fb
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/AUTHORS
@@ -0,0 +1,4 @@
+# Please keep this file sorted.
+
+Georg Reinke <guelfey@googlemail.com>
+nsf <no.smile.face@gmail.com>
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/LICENSE b/examples/go-dashboard/src/github.com/nsf/termbox-go/LICENSE
new file mode 100644
index 000000000..d9bc068ce
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/LICENSE
@@ -0,0 +1,19 @@
+Copyright (C) 2012 termbox-go authors
+
+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.
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/README.md b/examples/go-dashboard/src/github.com/nsf/termbox-go/README.md
new file mode 100644
index 000000000..a54d21878
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/README.md
@@ -0,0 +1,51 @@
+[![GoDoc](https://godoc.org/github.com/nsf/termbox-go?status.svg)](http://godoc.org/github.com/nsf/termbox-go)
+
+## IMPORTANT
+
+This library is somewhat not maintained anymore. But I'm glad that it did what I wanted the most. It moved people away from "ncurses" mindset and these days we see both re-implementations of termbox API in various languages and even possibly better libs with similar API design. If you're looking for a Go lib that provides terminal-based user interface facilities, I've heard that https://github.com/gdamore/tcell is good (never used it myself). Also for more complicated interfaces and/or computer games I recommend you to consider using HTML-based UI. Having said that, termbox still somewhat works. In fact I'm writing this line of text right now in godit (which is a text editor written using termbox-go). So, be aware. Good luck and have a nice day.
+
+## Termbox
+Termbox is a library that provides a minimalistic API which allows the programmer to write text-based user interfaces. The library is crossplatform and has both terminal-based implementations on *nix operating systems and a winapi console based implementation for windows operating systems. The basic idea is an abstraction of the greatest common subset of features available on all major terminals and other terminal-like APIs in a minimalistic fashion. Small API means it is easy to implement, test, maintain and learn it, that's what makes the termbox a distinct library in its area.
+
+### Installation
+Install and update this go package with `go get -u github.com/nsf/termbox-go`
+
+### Examples
+For examples of what can be done take a look at demos in the _demos directory. You can try them with go run: `go run _demos/keyboard.go`
+
+There are also some interesting projects using termbox-go:
+ - [godit](https://github.com/nsf/godit) is an emacsish lightweight text editor written using termbox.
+ - [gotetris](https://github.com/jjinux/gotetris) is an implementation of Tetris.
+ - [sokoban-go](https://github.com/rn2dy/sokoban-go) is an implementation of sokoban game.
+ - [hecate](https://github.com/evanmiller/hecate) is a hex editor designed by Satan.
+ - [httopd](https://github.com/verdverm/httopd) is top for httpd logs.
+ - [mop](https://github.com/mop-tracker/mop) is stock market tracker for hackers.
+ - [termui](https://github.com/gizak/termui) is a terminal dashboard.
+ - [termdash](https://github.com/mum4k/termdash) is a terminal dashboard.
+ - [termloop](https://github.com/JoelOtter/termloop) is a terminal game engine.
+ - [xterm-color-chart](https://github.com/kutuluk/xterm-color-chart) is a XTerm 256 color chart.
+ - [gocui](https://github.com/jroimartin/gocui) is a minimalist Go library aimed at creating console user interfaces.
+ - [dry](https://github.com/moncho/dry) is an interactive cli to manage Docker containers.
+ - [pxl](https://github.com/ichinaski/pxl) displays images in the terminal.
+ - [snake-game](https://github.com/DyegoCosta/snake-game) is an implementation of the Snake game.
+ - [gone](https://github.com/guillaumebreton/gone) is a CLI pomodoro® timer.
+ - [Spoof.go](https://github.com/sabey/spoofgo) controllable movement spoofing from the cli
+ - [lf](https://github.com/gokcehan/lf) is a terminal file manager
+ - [rat](https://github.com/ericfreese/rat) lets you compose shell commands to build terminal applications.
+ - [httplab](https://github.com/gchaincl/httplab) An interactive web server.
+ - [tetris](https://github.com/MichaelS11/tetris) Go Tetris with AI option
+ - [wot](https://github.com/kyu-suke/wot) Wait time during command is completed.
+ - [2048-go](https://github.com/1984weed/2048-go) is 2048 in Go
+ - [jv](https://github.com/maxzender/jv) helps you view JSON on the command-line.
+ - [pinger](https://github.com/hirose31/pinger) helps you to monitor numerous hosts using ICMP ECHO_REQUEST.
+ - [vixl44](https://github.com/sebashwa/vixl44) lets you create pixel art inside your terminal using vim movements
+ - [zterm](https://github.com/varunrau/zterm) is a typing game inspired by http://zty.pe/
+ - [gotypist](https://github.com/pb-/gotypist) is a fun touch-typing tutor following Steve Yegge's method.
+ - [cointop](https://github.com/miguelmota/cointop) is an interactive terminal based UI application for tracking cryptocurrencies.
+ - [pexpo](https://github.com/nnao45/pexpo) is a terminal sending ping tool written in Go.
+ - [jid](https://github.com/simeji/jid) is an interactive JSON drill down tool using filtering queries like jq.
+ - [nonograminGo](https://github.com/N0RM4L15T/nonograminGo) is a nonogram(aka. picross) in Go
+ - [tower-of-go](https://github.com/kjirou/tower-of-go) is a tiny maze game that runs on the terminal.
+
+### API reference
+[godoc.org/github.com/nsf/termbox-go](http://godoc.org/github.com/nsf/termbox-go)
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/api.go b/examples/go-dashboard/src/github.com/nsf/termbox-go/api.go
new file mode 100644
index 000000000..3adfdc635
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/api.go
@@ -0,0 +1,500 @@
+// +build !windows
+
+package termbox
+
+import (
+ "fmt"
+ "os"
+ "os/signal"
+ "runtime"
+ "syscall"
+ "time"
+
+ "github.com/mattn/go-runewidth"
+)
+
+// public API
+
+// Initializes termbox library. This function should be called before any other functions.
+// After successful initialization, the library must be finalized using 'Close' function.
+//
+// Example usage:
+// err := termbox.Init()
+// if err != nil {
+// panic(err)
+// }
+// defer termbox.Close()
+func Init() error {
+ var err error
+
+ if runtime.GOOS == "openbsd" || runtime.GOOS == "freebsd" {
+ out, err = os.OpenFile("/dev/tty", os.O_RDWR, 0)
+ if err != nil {
+ return err
+ }
+ in = int(out.Fd())
+ } else {
+ out, err = os.OpenFile("/dev/tty", os.O_WRONLY, 0)
+ if err != nil {
+ return err
+ }
+ in, err = syscall.Open("/dev/tty", syscall.O_RDONLY, 0)
+ if err != nil {
+ return err
+ }
+ }
+
+ err = setup_term()
+ if err != nil {
+ return fmt.Errorf("termbox: error while reading terminfo data: %v", err)
+ }
+
+ signal.Notify(sigwinch, syscall.SIGWINCH)
+ signal.Notify(sigio, syscall.SIGIO)
+
+ _, err = fcntl(in, syscall.F_SETFL, syscall.O_ASYNC|syscall.O_NONBLOCK)
+ if err != nil {
+ return err
+ }
+ _, err = fcntl(in, syscall.F_SETOWN, syscall.Getpid())
+ if runtime.GOOS != "darwin" && err != nil {
+ return err
+ }
+ err = tcgetattr(out.Fd(), &orig_tios)
+ if err != nil {
+ return err
+ }
+
+ tios := orig_tios
+ tios.Iflag &^= syscall_IGNBRK | syscall_BRKINT | syscall_PARMRK |
+ syscall_ISTRIP | syscall_INLCR | syscall_IGNCR |
+ syscall_ICRNL | syscall_IXON
+ tios.Lflag &^= syscall_ECHO | syscall_ECHONL | syscall_ICANON |
+ syscall_ISIG | syscall_IEXTEN
+ tios.Cflag &^= syscall_CSIZE | syscall_PARENB
+ tios.Cflag |= syscall_CS8
+ tios.Cc[syscall_VMIN] = 1
+ tios.Cc[syscall_VTIME] = 0
+
+ err = tcsetattr(out.Fd(), &tios)
+ if err != nil {
+ return err
+ }
+
+ out.WriteString(funcs[t_enter_ca])
+ out.WriteString(funcs[t_enter_keypad])
+ out.WriteString(funcs[t_hide_cursor])
+ out.WriteString(funcs[t_clear_screen])
+
+ termw, termh = get_term_size(out.Fd())
+ back_buffer.init(termw, termh)
+ front_buffer.init(termw, termh)
+ back_buffer.clear()
+ front_buffer.clear()
+
+ go func() {
+ buf := make([]byte, 128)
+ for {
+ select {
+ case <-sigio:
+ for {
+ n, err := syscall.Read(in, buf)
+ if err == syscall.EAGAIN || err == syscall.EWOULDBLOCK {
+ break
+ }
+ select {
+ case input_comm <- input_event{buf[:n], err}:
+ ie := <-input_comm
+ buf = ie.data[:128]
+ case <-quit:
+ return
+ }
+ }
+ case <-quit:
+ return
+ }
+ }
+ }()
+
+ IsInit = true
+ return nil
+}
+
+// Interrupt an in-progress call to PollEvent by causing it to return
+// EventInterrupt. Note that this function will block until the PollEvent
+// function has successfully been interrupted.
+func Interrupt() {
+ interrupt_comm <- struct{}{}
+}
+
+// Finalizes termbox library, should be called after successful initialization
+// when termbox's functionality isn't required anymore.
+func Close() {
+ quit <- 1
+ out.WriteString(funcs[t_show_cursor])
+ out.WriteString(funcs[t_sgr0])
+ out.WriteString(funcs[t_clear_screen])
+ out.WriteString(funcs[t_exit_ca])
+ out.WriteString(funcs[t_exit_keypad])
+ out.WriteString(funcs[t_exit_mouse])
+ tcsetattr(out.Fd(), &orig_tios)
+
+ out.Close()
+ syscall.Close(in)
+
+ // reset the state, so that on next Init() it will work again
+ termw = 0
+ termh = 0
+ input_mode = InputEsc
+ out = nil
+ in = 0
+ lastfg = attr_invalid
+ lastbg = attr_invalid
+ lastx = coord_invalid
+ lasty = coord_invalid
+ cursor_x = cursor_hidden
+ cursor_y = cursor_hidden
+ foreground = ColorDefault
+ background = ColorDefault
+ IsInit = false
+}
+
+// Synchronizes the internal back buffer with the terminal.
+func Flush() error {
+ // invalidate cursor position
+ lastx = coord_invalid
+ lasty = coord_invalid
+
+ update_size_maybe()
+
+ for y := 0; y < front_buffer.height; y++ {
+ line_offset := y * front_buffer.width
+ for x := 0; x < front_buffer.width; {
+ cell_offset := line_offset + x
+ back := &back_buffer.cells[cell_offset]
+ front := &front_buffer.cells[cell_offset]
+ if back.Ch < ' ' {
+ back.Ch = ' '
+ }
+ w := runewidth.RuneWidth(back.Ch)
+ if w == 0 || w == 2 && runewidth.IsAmbiguousWidth(back.Ch) {
+ w = 1
+ }
+ if *back == *front {
+ x += w
+ continue
+ }
+ *front = *back
+ send_attr(back.Fg, back.Bg)
+
+ if w == 2 && x == front_buffer.width-1 {
+ // there's not enough space for 2-cells rune,
+ // let's just put a space in there
+ send_char(x, y, ' ')
+ } else {
+ send_char(x, y, back.Ch)
+ if w == 2 {
+ next := cell_offset + 1
+ front_buffer.cells[next] = Cell{
+ Ch: 0,
+ Fg: back.Fg,
+ Bg: back.Bg,
+ }
+ }
+ }
+ x += w
+ }
+ }
+ if !is_cursor_hidden(cursor_x, cursor_y) {
+ write_cursor(cursor_x, cursor_y)
+ }
+ return flush()
+}
+
+// Sets the position of the cursor. See also HideCursor().
+func SetCursor(x, y int) {
+ if is_cursor_hidden(cursor_x, cursor_y) && !is_cursor_hidden(x, y) {
+ outbuf.WriteString(funcs[t_show_cursor])
+ }
+
+ if !is_cursor_hidden(cursor_x, cursor_y) && is_cursor_hidden(x, y) {
+ outbuf.WriteString(funcs[t_hide_cursor])
+ }
+
+ cursor_x, cursor_y = x, y
+ if !is_cursor_hidden(cursor_x, cursor_y) {
+ write_cursor(cursor_x, cursor_y)
+ }
+}
+
+// The shortcut for SetCursor(-1, -1).
+func HideCursor() {
+ SetCursor(cursor_hidden, cursor_hidden)
+}
+
+// Changes cell's parameters in the internal back buffer at the specified
+// position.
+func SetCell(x, y int, ch rune, fg, bg Attribute) {
+ if x < 0 || x >= back_buffer.width {
+ return
+ }
+ if y < 0 || y >= back_buffer.height {
+ return
+ }
+
+ back_buffer.cells[y*back_buffer.width+x] = Cell{ch, fg, bg}
+}
+
+// Returns a slice into the termbox's back buffer. You can get its dimensions
+// using 'Size' function. The slice remains valid as long as no 'Clear' or
+// 'Flush' function calls were made after call to this function.
+func CellBuffer() []Cell {
+ return back_buffer.cells
+}
+
+// After getting a raw event from PollRawEvent function call, you can parse it
+// again into an ordinary one using termbox logic. That is parse an event as
+// termbox would do it. Returned event in addition to usual Event struct fields
+// sets N field to the amount of bytes used within 'data' slice. If the length
+// of 'data' slice is zero or event cannot be parsed for some other reason, the
+// function will return a special event type: EventNone.
+//
+// IMPORTANT: EventNone may contain a non-zero N, which means you should skip
+// these bytes, because termbox cannot recognize them.
+//
+// NOTE: This API is experimental and may change in future.
+func ParseEvent(data []byte) Event {
+ event := Event{Type: EventKey}
+ status := extract_event(data, &event, false)
+ if status != event_extracted {
+ return Event{Type: EventNone, N: event.N}
+ }
+ return event
+}
+
+// Wait for an event and return it. This is a blocking function call. Instead
+// of EventKey and EventMouse it returns EventRaw events. Raw event is written
+// into `data` slice and Event's N field is set to the amount of bytes written.
+// The minimum required length of the 'data' slice is 1. This requirement may
+// vary on different platforms.
+//
+// NOTE: This API is experimental and may change in future.
+func PollRawEvent(data []byte) Event {
+ if len(data) == 0 {
+ panic("len(data) >= 1 is a requirement")
+ }
+
+ var event Event
+ if extract_raw_event(data, &event) {
+ return event
+ }
+
+ for {
+ select {
+ case ev := <-input_comm:
+ if ev.err != nil {
+ return Event{Type: EventError, Err: ev.err}
+ }
+
+ inbuf = append(inbuf, ev.data...)
+ input_comm <- ev
+ if extract_raw_event(data, &event) {
+ return event
+ }
+ case <-interrupt_comm:
+ event.Type = EventInterrupt
+ return event
+
+ case <-sigwinch:
+ event.Type = EventResize
+ event.Width, event.Height = get_term_size(out.Fd())
+ return event
+ }
+ }
+}
+
+// Wait for an event and return it. This is a blocking function call.
+func PollEvent() Event {
+ // Constant governing macOS specific behavior. See https://github.com/nsf/termbox-go/issues/132
+ // This is an arbitrary delay which hopefully will be enough time for any lagging
+ // partial escape sequences to come through.
+ const esc_wait_delay = 100 * time.Millisecond
+
+ var event Event
+ var esc_wait_timer *time.Timer
+ var esc_timeout <-chan time.Time
+
+ // try to extract event from input buffer, return on success
+ event.Type = EventKey
+ status := extract_event(inbuf, &event, true)
+ if event.N != 0 {
+ copy(inbuf, inbuf[event.N:])
+ inbuf = inbuf[:len(inbuf)-event.N]
+ }
+ if status == event_extracted {
+ return event
+ } else if status == esc_wait {
+ esc_wait_timer = time.NewTimer(esc_wait_delay)
+ esc_timeout = esc_wait_timer.C
+ }
+
+ for {
+ select {
+ case ev := <-input_comm:
+ if esc_wait_timer != nil {
+ if !esc_wait_timer.Stop() {
+ <-esc_wait_timer.C
+ }
+ esc_wait_timer = nil
+ }
+
+ if ev.err != nil {
+ return Event{Type: EventError, Err: ev.err}
+ }
+
+ inbuf = append(inbuf, ev.data...)
+ input_comm <- ev
+ status := extract_event(inbuf, &event, true)
+ if event.N != 0 {
+ copy(inbuf, inbuf[event.N:])
+ inbuf = inbuf[:len(inbuf)-event.N]
+ }
+ if status == event_extracted {
+ return event
+ } else if status == esc_wait {
+ esc_wait_timer = time.NewTimer(esc_wait_delay)
+ esc_timeout = esc_wait_timer.C
+ }
+ case <-esc_timeout:
+ esc_wait_timer = nil
+
+ status := extract_event(inbuf, &event, false)
+ if event.N != 0 {
+ copy(inbuf, inbuf[event.N:])
+ inbuf = inbuf[:len(inbuf)-event.N]
+ }
+ if status == event_extracted {
+ return event
+ }
+ case <-interrupt_comm:
+ event.Type = EventInterrupt
+ return event
+
+ case <-sigwinch:
+ event.Type = EventResize
+ event.Width, event.Height = get_term_size(out.Fd())
+ return event
+ }
+ }
+}
+
+// Returns the size of the internal back buffer (which is mostly the same as
+// terminal's window size in characters). But it doesn't always match the size
+// of the terminal window, after the terminal size has changed, the internal
+// back buffer will get in sync only after Clear or Flush function calls.
+func Size() (width int, height int) {
+ return termw, termh
+}
+
+// Clears the internal back buffer.
+func Clear(fg, bg Attribute) error {
+ foreground, background = fg, bg
+ err := update_size_maybe()
+ back_buffer.clear()
+ return err
+}
+
+// Sets termbox input mode. Termbox has two input modes:
+//
+// 1. Esc input mode. When ESC sequence is in the buffer and it doesn't match
+// any known sequence. ESC means KeyEsc. This is the default input mode.
+//
+// 2. Alt input mode. When ESC sequence is in the buffer and it doesn't match
+// any known sequence. ESC enables ModAlt modifier for the next keyboard event.
+//
+// Both input modes can be OR'ed with Mouse mode. Setting Mouse mode bit up will
+// enable mouse button press/release and drag events.
+//
+// If 'mode' is InputCurrent, returns the current input mode. See also Input*
+// constants.
+func SetInputMode(mode InputMode) InputMode {
+ if mode == InputCurrent {
+ return input_mode
+ }
+ if mode&(InputEsc|InputAlt) == 0 {
+ mode |= InputEsc
+ }
+ if mode&(InputEsc|InputAlt) == InputEsc|InputAlt {
+ mode &^= InputAlt
+ }
+ if mode&InputMouse != 0 {
+ out.WriteString(funcs[t_enter_mouse])
+ } else {
+ out.WriteString(funcs[t_exit_mouse])
+ }
+
+ input_mode = mode
+ return input_mode
+}
+
+// Sets the termbox output mode. Termbox has four output options:
+//
+// 1. OutputNormal => [1..8]
+// This mode provides 8 different colors:
+// black, red, green, yellow, blue, magenta, cyan, white
+// Shortcut: ColorBlack, ColorRed, ...
+// Attributes: AttrBold, AttrUnderline, AttrReverse
+//
+// Example usage:
+// SetCell(x, y, '@', ColorBlack | AttrBold, ColorRed);
+//
+// 2. Output256 => [1..256]
+// In this mode you can leverage the 256 terminal mode:
+// 0x01 - 0x08: the 8 colors as in OutputNormal
+// 0x09 - 0x10: Color* | AttrBold
+// 0x11 - 0xe8: 216 different colors
+// 0xe9 - 0x1ff: 24 different shades of grey
+//
+// Example usage:
+// SetCell(x, y, '@', 184, 240);
+// SetCell(x, y, '@', 0xb8, 0xf0);
+//
+// 3. Output216 => [1..216]
+// This mode supports the 3rd range of the 256 mode only.
+// But you don't need to provide an offset.
+//
+// 4. OutputGrayscale => [1..26]
+// This mode supports the 4th range of the 256 mode
+// and black and white colors from 3th range of the 256 mode
+// But you don't need to provide an offset.
+//
+// In all modes, 0x00 represents the default color.
+//
+// `go run _demos/output.go` to see its impact on your terminal.
+//
+// If 'mode' is OutputCurrent, it returns the current output mode.
+//
+// Note that this may return a different OutputMode than the one requested,
+// as the requested mode may not be available on the target platform.
+func SetOutputMode(mode OutputMode) OutputMode {
+ if mode == OutputCurrent {
+ return output_mode
+ }
+
+ output_mode = mode
+ return output_mode
+}
+
+// Sync comes handy when something causes desync between termbox's understanding
+// of a terminal buffer and the reality. Such as a third party process. Sync
+// forces a complete resync between the termbox and a terminal, it may not be
+// visually pretty though.
+func Sync() error {
+ front_buffer.clear()
+ err := send_clear()
+ if err != nil {
+ return err
+ }
+
+ return Flush()
+}
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/api_common.go b/examples/go-dashboard/src/github.com/nsf/termbox-go/api_common.go
new file mode 100644
index 000000000..5ca1371a5
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/api_common.go
@@ -0,0 +1,187 @@
+// termbox is a library for creating cross-platform text-based interfaces
+package termbox
+
+// public API, common OS agnostic part
+
+type (
+ InputMode int
+ OutputMode int
+ EventType uint8
+ Modifier uint8
+ Key uint16
+ Attribute uint16
+)
+
+// This type represents a termbox event. The 'Mod', 'Key' and 'Ch' fields are
+// valid if 'Type' is EventKey. The 'Width' and 'Height' fields are valid if
+// 'Type' is EventResize. The 'Err' field is valid if 'Type' is EventError.
+type Event struct {
+ Type EventType // one of Event* constants
+ Mod Modifier // one of Mod* constants or 0
+ Key Key // one of Key* constants, invalid if 'Ch' is not 0
+ Ch rune // a unicode character
+ Width int // width of the screen
+ Height int // height of the screen
+ Err error // error in case if input failed
+ MouseX int // x coord of mouse
+ MouseY int // y coord of mouse
+ N int // number of bytes written when getting a raw event
+}
+
+// A cell, single conceptual entity on the screen. The screen is basically a 2d
+// array of cells. 'Ch' is a unicode character, 'Fg' and 'Bg' are foreground
+// and background attributes respectively.
+type Cell struct {
+ Ch rune
+ Fg Attribute
+ Bg Attribute
+}
+
+// To know if termbox has been initialized or not
+var (
+ IsInit bool = false
+)
+
+// Key constants, see Event.Key field.
+const (
+ KeyF1 Key = 0xFFFF - iota
+ KeyF2
+ KeyF3
+ KeyF4
+ KeyF5
+ KeyF6
+ KeyF7
+ KeyF8
+ KeyF9
+ KeyF10
+ KeyF11
+ KeyF12
+ KeyInsert
+ KeyDelete
+ KeyHome
+ KeyEnd
+ KeyPgup
+ KeyPgdn
+ KeyArrowUp
+ KeyArrowDown
+ KeyArrowLeft
+ KeyArrowRight
+ key_min // see terminfo
+ MouseLeft
+ MouseMiddle
+ MouseRight
+ MouseRelease
+ MouseWheelUp
+ MouseWheelDown
+)
+
+const (
+ KeyCtrlTilde Key = 0x00
+ KeyCtrl2 Key = 0x00
+ KeyCtrlSpace Key = 0x00
+ KeyCtrlA Key = 0x01
+ KeyCtrlB Key = 0x02
+ KeyCtrlC Key = 0x03
+ KeyCtrlD Key = 0x04
+ KeyCtrlE Key = 0x05
+ KeyCtrlF Key = 0x06
+ KeyCtrlG Key = 0x07
+ KeyBackspace Key = 0x08
+ KeyCtrlH Key = 0x08
+ KeyTab Key = 0x09
+ KeyCtrlI Key = 0x09
+ KeyCtrlJ Key = 0x0A
+ KeyCtrlK Key = 0x0B
+ KeyCtrlL Key = 0x0C
+ KeyEnter Key = 0x0D
+ KeyCtrlM Key = 0x0D
+ KeyCtrlN Key = 0x0E
+ KeyCtrlO Key = 0x0F
+ KeyCtrlP Key = 0x10
+ KeyCtrlQ Key = 0x11
+ KeyCtrlR Key = 0x12
+ KeyCtrlS Key = 0x13
+ KeyCtrlT Key = 0x14
+ KeyCtrlU Key = 0x15
+ KeyCtrlV Key = 0x16
+ KeyCtrlW Key = 0x17
+ KeyCtrlX Key = 0x18
+ KeyCtrlY Key = 0x19
+ KeyCtrlZ Key = 0x1A
+ KeyEsc Key = 0x1B
+ KeyCtrlLsqBracket Key = 0x1B
+ KeyCtrl3 Key = 0x1B
+ KeyCtrl4 Key = 0x1C
+ KeyCtrlBackslash Key = 0x1C
+ KeyCtrl5 Key = 0x1D
+ KeyCtrlRsqBracket Key = 0x1D
+ KeyCtrl6 Key = 0x1E
+ KeyCtrl7 Key = 0x1F
+ KeyCtrlSlash Key = 0x1F
+ KeyCtrlUnderscore Key = 0x1F
+ KeySpace Key = 0x20
+ KeyBackspace2 Key = 0x7F
+ KeyCtrl8 Key = 0x7F
+)
+
+// Alt modifier constant, see Event.Mod field and SetInputMode function.
+const (
+ ModAlt Modifier = 1 << iota
+ ModMotion
+)
+
+// Cell colors, you can combine a color with multiple attributes using bitwise
+// OR ('|').
+const (
+ ColorDefault Attribute = iota
+ ColorBlack
+ ColorRed
+ ColorGreen
+ ColorYellow
+ ColorBlue
+ ColorMagenta
+ ColorCyan
+ ColorWhite
+)
+
+// Cell attributes, it is possible to use multiple attributes by combining them
+// using bitwise OR ('|'). Although, colors cannot be combined. But you can
+// combine attributes and a single color.
+//
+// It's worth mentioning that some platforms don't support certain attributes.
+// For example windows console doesn't support AttrUnderline. And on some
+// terminals applying AttrBold to background may result in blinking text. Use
+// them with caution and test your code on various terminals.
+const (
+ AttrBold Attribute = 1 << (iota + 9)
+ AttrUnderline
+ AttrReverse
+)
+
+// Input mode. See SetInputMode function.
+const (
+ InputEsc InputMode = 1 << iota
+ InputAlt
+ InputMouse
+ InputCurrent InputMode = 0
+)
+
+// Output mode. See SetOutputMode function.
+const (
+ OutputCurrent OutputMode = iota
+ OutputNormal
+ Output256
+ Output216
+ OutputGrayscale
+)
+
+// Event type. See Event.Type field.
+const (
+ EventKey EventType = iota
+ EventResize
+ EventMouse
+ EventError
+ EventInterrupt
+ EventRaw
+ EventNone
+)
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/api_windows.go b/examples/go-dashboard/src/github.com/nsf/termbox-go/api_windows.go
new file mode 100644
index 000000000..373e6c76c
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/api_windows.go
@@ -0,0 +1,257 @@
+package termbox
+
+import (
+ "syscall"
+
+ "github.com/mattn/go-runewidth"
+)
+
+// public API
+
+// Initializes termbox library. This function should be called before any other functions.
+// After successful initialization, the library must be finalized using 'Close' function.
+//
+// Example usage:
+// err := termbox.Init()
+// if err != nil {
+// panic(err)
+// }
+// defer termbox.Close()
+func Init() error {
+ var err error
+
+ interrupt, err = create_event()
+ if err != nil {
+ return err
+ }
+
+ in, err = syscall.Open("CONIN$", syscall.O_RDWR, 0)
+ if err != nil {
+ return err
+ }
+ out, err = syscall.Open("CONOUT$", syscall.O_RDWR, 0)
+ if err != nil {
+ return err
+ }
+
+ err = get_console_mode(in, &orig_mode)
+ if err != nil {
+ return err
+ }
+
+ err = set_console_mode(in, enable_window_input)
+ if err != nil {
+ return err
+ }
+
+ orig_size, orig_window = get_term_size(out)
+ win_size := get_win_size(out)
+
+ err = set_console_screen_buffer_size(out, win_size)
+ if err != nil {
+ return err
+ }
+
+ err = fix_win_size(out, win_size)
+ if err != nil {
+ return err
+ }
+
+ err = get_console_cursor_info(out, &orig_cursor_info)
+ if err != nil {
+ return err
+ }
+
+ show_cursor(false)
+ term_size, _ = get_term_size(out)
+ back_buffer.init(int(term_size.x), int(term_size.y))
+ front_buffer.init(int(term_size.x), int(term_size.y))
+ back_buffer.clear()
+ front_buffer.clear()
+ clear()
+
+ diffbuf = make([]diff_msg, 0, 32)
+
+ go input_event_producer()
+ IsInit = true
+ return nil
+}
+
+// Finalizes termbox library, should be called after successful initialization
+// when termbox's functionality isn't required anymore.
+func Close() {
+ // we ignore errors here, because we can't really do anything about them
+ Clear(0, 0)
+ Flush()
+
+ // stop event producer
+ cancel_comm <- true
+ set_event(interrupt)
+ select {
+ case <-input_comm:
+ default:
+ }
+ <-cancel_done_comm
+
+ set_console_screen_buffer_size(out, orig_size)
+ set_console_window_info(out, &orig_window)
+ set_console_cursor_info(out, &orig_cursor_info)
+ set_console_cursor_position(out, coord{})
+ set_console_mode(in, orig_mode)
+ syscall.Close(in)
+ syscall.Close(out)
+ syscall.Close(interrupt)
+ IsInit = false
+}
+
+// Interrupt an in-progress call to PollEvent by causing it to return
+// EventInterrupt. Note that this function will block until the PollEvent
+// function has successfully been interrupted.
+func Interrupt() {
+ interrupt_comm <- struct{}{}
+}
+
+// Synchronizes the internal back buffer with the terminal.
+func Flush() error {
+ update_size_maybe()
+ prepare_diff_messages()
+ for _, diff := range diffbuf {
+ chars := []char_info{}
+ for _, char := range diff.chars {
+ chars = append(chars, char)
+ if runewidth.RuneWidth(rune(char.char)) > 1 {
+ chars = append(chars, char_info{
+ char: ' ',
+ attr: char.attr,
+ })
+ }
+ }
+ r := small_rect{
+ left: 0,
+ top: diff.pos,
+ right: term_size.x - 1,
+ bottom: diff.pos + diff.lines - 1,
+ }
+ write_console_output(out, chars, r)
+ }
+ if !is_cursor_hidden(cursor_x, cursor_y) {
+ move_cursor(cursor_x, cursor_y)
+ }
+ return nil
+}
+
+// Sets the position of the cursor. See also HideCursor().
+func SetCursor(x, y int) {
+ if is_cursor_hidden(cursor_x, cursor_y) && !is_cursor_hidden(x, y) {
+ show_cursor(true)
+ }
+
+ if !is_cursor_hidden(cursor_x, cursor_y) && is_cursor_hidden(x, y) {
+ show_cursor(false)
+ }
+
+ cursor_x, cursor_y = x, y
+ if !is_cursor_hidden(cursor_x, cursor_y) {
+ move_cursor(cursor_x, cursor_y)
+ }
+}
+
+// The shortcut for SetCursor(-1, -1).
+func HideCursor() {
+ SetCursor(cursor_hidden, cursor_hidden)
+}
+
+// Changes cell's parameters in the internal back buffer at the specified
+// position.
+func SetCell(x, y int, ch rune, fg, bg Attribute) {
+ if x < 0 || x >= back_buffer.width {
+ return
+ }
+ if y < 0 || y >= back_buffer.height {
+ return
+ }
+
+ back_buffer.cells[y*back_buffer.width+x] = Cell{ch, fg, bg}
+}
+
+// Returns a slice into the termbox's back buffer. You can get its dimensions
+// using 'Size' function. The slice remains valid as long as no 'Clear' or
+// 'Flush' function calls were made after call to this function.
+func CellBuffer() []Cell {
+ return back_buffer.cells
+}
+
+// Wait for an event and return it. This is a blocking function call.
+func PollEvent() Event {
+ select {
+ case ev := <-input_comm:
+ return ev
+ case <-interrupt_comm:
+ return Event{Type: EventInterrupt}
+ }
+}
+
+// Returns the size of the internal back buffer (which is mostly the same as
+// console's window size in characters). But it doesn't always match the size
+// of the console window, after the console size has changed, the internal back
+// buffer will get in sync only after Clear or Flush function calls.
+func Size() (int, int) {
+ return int(term_size.x), int(term_size.y)
+}
+
+// Clears the internal back buffer.
+func Clear(fg, bg Attribute) error {
+ foreground, background = fg, bg
+ update_size_maybe()
+ back_buffer.clear()
+ return nil
+}
+
+// Sets termbox input mode. Termbox has two input modes:
+//
+// 1. Esc input mode. When ESC sequence is in the buffer and it doesn't match
+// any known sequence. ESC means KeyEsc. This is the default input mode.
+//
+// 2. Alt input mode. When ESC sequence is in the buffer and it doesn't match
+// any known sequence. ESC enables ModAlt modifier for the next keyboard event.
+//
+// Both input modes can be OR'ed with Mouse mode. Setting Mouse mode bit up will
+// enable mouse button press/release and drag events.
+//
+// If 'mode' is InputCurrent, returns the current input mode. See also Input*
+// constants.
+func SetInputMode(mode InputMode) InputMode {
+ if mode == InputCurrent {
+ return input_mode
+ }
+ if mode&InputMouse != 0 {
+ err := set_console_mode(in, enable_window_input|enable_mouse_input|enable_extended_flags)
+ if err != nil {
+ panic(err)
+ }
+ } else {
+ err := set_console_mode(in, enable_window_input)
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ input_mode = mode
+ return input_mode
+}
+
+// Sets the termbox output mode.
+//
+// Windows console does not support extra colour modes,
+// so this will always set and return OutputNormal.
+func SetOutputMode(mode OutputMode) OutputMode {
+ return OutputNormal
+}
+
+// Sync comes handy when something causes desync between termbox's understanding
+// of a terminal buffer and the reality. Such as a third party process. Sync
+// forces a complete resync between the termbox and a terminal, it may not be
+// visually pretty though. At the moment on Windows it does nothing.
+func Sync() error {
+ return nil
+}
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/collect_terminfo.py b/examples/go-dashboard/src/github.com/nsf/termbox-go/collect_terminfo.py
new file mode 100644
index 000000000..5e50975e6
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/collect_terminfo.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python
+
+import sys, os, subprocess
+
+def escaped(s):
+ return repr(s)[1:-1]
+
+def tput(term, name):
+ try:
+ return subprocess.check_output(['tput', '-T%s' % term, name]).decode()
+ except subprocess.CalledProcessError as e:
+ return e.output.decode()
+
+
+def w(s):
+ if s == None:
+ return
+ sys.stdout.write(s)
+
+terminals = {
+ 'xterm' : 'xterm',
+ 'rxvt-256color' : 'rxvt_256color',
+ 'rxvt-unicode' : 'rxvt_unicode',
+ 'linux' : 'linux',
+ 'Eterm' : 'eterm',
+ 'screen' : 'screen'
+}
+
+keys = [
+ "F1", "kf1",
+ "F2", "kf2",
+ "F3", "kf3",
+ "F4", "kf4",
+ "F5", "kf5",
+ "F6", "kf6",
+ "F7", "kf7",
+ "F8", "kf8",
+ "F9", "kf9",
+ "F10", "kf10",
+ "F11", "kf11",
+ "F12", "kf12",
+ "INSERT", "kich1",
+ "DELETE", "kdch1",
+ "HOME", "khome",
+ "END", "kend",
+ "PGUP", "kpp",
+ "PGDN", "knp",
+ "KEY_UP", "kcuu1",
+ "KEY_DOWN", "kcud1",
+ "KEY_LEFT", "kcub1",
+ "KEY_RIGHT", "kcuf1"
+]
+
+funcs = [
+ "T_ENTER_CA", "smcup",
+ "T_EXIT_CA", "rmcup",
+ "T_SHOW_CURSOR", "cnorm",
+ "T_HIDE_CURSOR", "civis",
+ "T_CLEAR_SCREEN", "clear",
+ "T_SGR0", "sgr0",
+ "T_UNDERLINE", "smul",
+ "T_BOLD", "bold",
+ "T_BLINK", "blink",
+ "T_REVERSE", "rev",
+ "T_ENTER_KEYPAD", "smkx",
+ "T_EXIT_KEYPAD", "rmkx"
+]
+
+def iter_pairs(iterable):
+ iterable = iter(iterable)
+ while True:
+ yield (next(iterable), next(iterable))
+
+def do_term(term, nick):
+ w("// %s\n" % term)
+ w("var %s_keys = []string{\n\t" % nick)
+ for k, v in iter_pairs(keys):
+ w('"')
+ w(escaped(tput(term, v)))
+ w('",')
+ w("\n}\n")
+ w("var %s_funcs = []string{\n\t" % nick)
+ for k,v in iter_pairs(funcs):
+ w('"')
+ if v == "sgr":
+ w("\\033[3%d;4%dm")
+ elif v == "cup":
+ w("\\033[%d;%dH")
+ else:
+ w(escaped(tput(term, v)))
+ w('", ')
+ w("\n}\n\n")
+
+def do_terms(d):
+ w("var terms = []struct {\n")
+ w("\tname string\n")
+ w("\tkeys []string\n")
+ w("\tfuncs []string\n")
+ w("}{\n")
+ for k, v in d.items():
+ w('\t{"%s", %s_keys, %s_funcs},\n' % (k, v, v))
+ w("}\n\n")
+
+w("// +build !windows\n\npackage termbox\n\n")
+
+for k,v in terminals.items():
+ do_term(k, v)
+
+do_terms(terminals)
+
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/escwait.go b/examples/go-dashboard/src/github.com/nsf/termbox-go/escwait.go
new file mode 100644
index 000000000..b7bbb891f
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/escwait.go
@@ -0,0 +1,11 @@
+// +build !darwin
+
+package termbox
+
+// On all systems other than macOS, disable behavior which will wait before
+// deciding that the escape key was pressed, to account for partially send
+// escape sequences, especially with regard to lengthy mouse sequences.
+// See https://github.com/nsf/termbox-go/issues/132
+func enable_wait_for_escape_sequence() bool {
+ return false
+}
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/escwait_darwin.go b/examples/go-dashboard/src/github.com/nsf/termbox-go/escwait_darwin.go
new file mode 100644
index 000000000..dde69b6cb
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/escwait_darwin.go
@@ -0,0 +1,9 @@
+package termbox
+
+// On macOS, enable behavior which will wait before deciding that the escape
+// key was pressed, to account for partially send escape sequences, especially
+// with regard to lengthy mouse sequences.
+// See https://github.com/nsf/termbox-go/issues/132
+func enable_wait_for_escape_sequence() bool {
+ return true
+}
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_darwin.go b/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_darwin.go
new file mode 100644
index 000000000..25b78f7ab
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_darwin.go
@@ -0,0 +1,41 @@
+// Created by cgo -godefs - DO NOT EDIT
+// cgo -godefs syscalls.go
+
+// +build !amd64
+
+package termbox
+
+type syscall_Termios struct {
+ Iflag uint32
+ Oflag uint32
+ Cflag uint32
+ Lflag uint32
+ Cc [20]uint8
+ Ispeed uint32
+ Ospeed uint32
+}
+
+const (
+ syscall_IGNBRK = 0x1
+ syscall_BRKINT = 0x2
+ syscall_PARMRK = 0x8
+ syscall_ISTRIP = 0x20
+ syscall_INLCR = 0x40
+ syscall_IGNCR = 0x80
+ syscall_ICRNL = 0x100
+ syscall_IXON = 0x200
+ syscall_OPOST = 0x1
+ syscall_ECHO = 0x8
+ syscall_ECHONL = 0x10
+ syscall_ICANON = 0x100
+ syscall_ISIG = 0x80
+ syscall_IEXTEN = 0x400
+ syscall_CSIZE = 0x300
+ syscall_PARENB = 0x1000
+ syscall_CS8 = 0x300
+ syscall_VMIN = 0x10
+ syscall_VTIME = 0x11
+
+ syscall_TCGETS = 0x402c7413
+ syscall_TCSETS = 0x802c7414
+)
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_darwin_amd64.go b/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_darwin_amd64.go
new file mode 100644
index 000000000..11f25be79
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_darwin_amd64.go
@@ -0,0 +1,40 @@
+// Created by cgo -godefs - DO NOT EDIT
+// cgo -godefs syscalls.go
+
+package termbox
+
+type syscall_Termios struct {
+ Iflag uint64
+ Oflag uint64
+ Cflag uint64
+ Lflag uint64
+ Cc [20]uint8
+ Pad_cgo_0 [4]byte
+ Ispeed uint64
+ Ospeed uint64
+}
+
+const (
+ syscall_IGNBRK = 0x1
+ syscall_BRKINT = 0x2
+ syscall_PARMRK = 0x8
+ syscall_ISTRIP = 0x20
+ syscall_INLCR = 0x40
+ syscall_IGNCR = 0x80
+ syscall_ICRNL = 0x100
+ syscall_IXON = 0x200
+ syscall_OPOST = 0x1
+ syscall_ECHO = 0x8
+ syscall_ECHONL = 0x10
+ syscall_ICANON = 0x100
+ syscall_ISIG = 0x80
+ syscall_IEXTEN = 0x400
+ syscall_CSIZE = 0x300
+ syscall_PARENB = 0x1000
+ syscall_CS8 = 0x300
+ syscall_VMIN = 0x10
+ syscall_VTIME = 0x11
+
+ syscall_TCGETS = 0x40487413
+ syscall_TCSETS = 0x80487414
+)
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_dragonfly.go b/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_dragonfly.go
new file mode 100644
index 000000000..e03624ebc
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_dragonfly.go
@@ -0,0 +1,39 @@
+// Created by cgo -godefs - DO NOT EDIT
+// cgo -godefs syscalls.go
+
+package termbox
+
+type syscall_Termios struct {
+ Iflag uint32
+ Oflag uint32
+ Cflag uint32
+ Lflag uint32
+ Cc [20]uint8
+ Ispeed uint32
+ Ospeed uint32
+}
+
+const (
+ syscall_IGNBRK = 0x1
+ syscall_BRKINT = 0x2
+ syscall_PARMRK = 0x8
+ syscall_ISTRIP = 0x20
+ syscall_INLCR = 0x40
+ syscall_IGNCR = 0x80
+ syscall_ICRNL = 0x100
+ syscall_IXON = 0x200
+ syscall_OPOST = 0x1
+ syscall_ECHO = 0x8
+ syscall_ECHONL = 0x10
+ syscall_ICANON = 0x100
+ syscall_ISIG = 0x80
+ syscall_IEXTEN = 0x400
+ syscall_CSIZE = 0x300
+ syscall_PARENB = 0x1000
+ syscall_CS8 = 0x300
+ syscall_VMIN = 0x10
+ syscall_VTIME = 0x11
+
+ syscall_TCGETS = 0x402c7413
+ syscall_TCSETS = 0x802c7414
+)
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_freebsd.go b/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_freebsd.go
new file mode 100644
index 000000000..e03624ebc
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_freebsd.go
@@ -0,0 +1,39 @@
+// Created by cgo -godefs - DO NOT EDIT
+// cgo -godefs syscalls.go
+
+package termbox
+
+type syscall_Termios struct {
+ Iflag uint32
+ Oflag uint32
+ Cflag uint32
+ Lflag uint32
+ Cc [20]uint8
+ Ispeed uint32
+ Ospeed uint32
+}
+
+const (
+ syscall_IGNBRK = 0x1
+ syscall_BRKINT = 0x2
+ syscall_PARMRK = 0x8
+ syscall_ISTRIP = 0x20
+ syscall_INLCR = 0x40
+ syscall_IGNCR = 0x80
+ syscall_ICRNL = 0x100
+ syscall_IXON = 0x200
+ syscall_OPOST = 0x1
+ syscall_ECHO = 0x8
+ syscall_ECHONL = 0x10
+ syscall_ICANON = 0x100
+ syscall_ISIG = 0x80
+ syscall_IEXTEN = 0x400
+ syscall_CSIZE = 0x300
+ syscall_PARENB = 0x1000
+ syscall_CS8 = 0x300
+ syscall_VMIN = 0x10
+ syscall_VTIME = 0x11
+
+ syscall_TCGETS = 0x402c7413
+ syscall_TCSETS = 0x802c7414
+)
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_linux.go b/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_linux.go
new file mode 100644
index 000000000..b88960de6
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_linux.go
@@ -0,0 +1,33 @@
+// Created by cgo -godefs - DO NOT EDIT
+// cgo -godefs syscalls.go
+
+package termbox
+
+import "syscall"
+
+type syscall_Termios syscall.Termios
+
+const (
+ syscall_IGNBRK = syscall.IGNBRK
+ syscall_BRKINT = syscall.BRKINT
+ syscall_PARMRK = syscall.PARMRK
+ syscall_ISTRIP = syscall.ISTRIP
+ syscall_INLCR = syscall.INLCR
+ syscall_IGNCR = syscall.IGNCR
+ syscall_ICRNL = syscall.ICRNL
+ syscall_IXON = syscall.IXON
+ syscall_OPOST = syscall.OPOST
+ syscall_ECHO = syscall.ECHO
+ syscall_ECHONL = syscall.ECHONL
+ syscall_ICANON = syscall.ICANON
+ syscall_ISIG = syscall.ISIG
+ syscall_IEXTEN = syscall.IEXTEN
+ syscall_CSIZE = syscall.CSIZE
+ syscall_PARENB = syscall.PARENB
+ syscall_CS8 = syscall.CS8
+ syscall_VMIN = syscall.VMIN
+ syscall_VTIME = syscall.VTIME
+
+ syscall_TCGETS = syscall.TCGETS
+ syscall_TCSETS = syscall.TCSETS
+)
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_netbsd.go b/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_netbsd.go
new file mode 100644
index 000000000..49a3355b9
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_netbsd.go
@@ -0,0 +1,39 @@
+// Created by cgo -godefs - DO NOT EDIT
+// cgo -godefs syscalls.go
+
+package termbox
+
+type syscall_Termios struct {
+ Iflag uint32
+ Oflag uint32
+ Cflag uint32
+ Lflag uint32
+ Cc [20]uint8
+ Ispeed int32
+ Ospeed int32
+}
+
+const (
+ syscall_IGNBRK = 0x1
+ syscall_BRKINT = 0x2
+ syscall_PARMRK = 0x8
+ syscall_ISTRIP = 0x20
+ syscall_INLCR = 0x40
+ syscall_IGNCR = 0x80
+ syscall_ICRNL = 0x100
+ syscall_IXON = 0x200
+ syscall_OPOST = 0x1
+ syscall_ECHO = 0x8
+ syscall_ECHONL = 0x10
+ syscall_ICANON = 0x100
+ syscall_ISIG = 0x80
+ syscall_IEXTEN = 0x400
+ syscall_CSIZE = 0x300
+ syscall_PARENB = 0x1000
+ syscall_CS8 = 0x300
+ syscall_VMIN = 0x10
+ syscall_VTIME = 0x11
+
+ syscall_TCGETS = 0x402c7413
+ syscall_TCSETS = 0x802c7414
+)
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_openbsd.go b/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_openbsd.go
new file mode 100644
index 000000000..49a3355b9
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_openbsd.go
@@ -0,0 +1,39 @@
+// Created by cgo -godefs - DO NOT EDIT
+// cgo -godefs syscalls.go
+
+package termbox
+
+type syscall_Termios struct {
+ Iflag uint32
+ Oflag uint32
+ Cflag uint32
+ Lflag uint32
+ Cc [20]uint8
+ Ispeed int32
+ Ospeed int32
+}
+
+const (
+ syscall_IGNBRK = 0x1
+ syscall_BRKINT = 0x2
+ syscall_PARMRK = 0x8
+ syscall_ISTRIP = 0x20
+ syscall_INLCR = 0x40
+ syscall_IGNCR = 0x80
+ syscall_ICRNL = 0x100
+ syscall_IXON = 0x200
+ syscall_OPOST = 0x1
+ syscall_ECHO = 0x8
+ syscall_ECHONL = 0x10
+ syscall_ICANON = 0x100
+ syscall_ISIG = 0x80
+ syscall_IEXTEN = 0x400
+ syscall_CSIZE = 0x300
+ syscall_PARENB = 0x1000
+ syscall_CS8 = 0x300
+ syscall_VMIN = 0x10
+ syscall_VTIME = 0x11
+
+ syscall_TCGETS = 0x402c7413
+ syscall_TCSETS = 0x802c7414
+)
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_windows.go b/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_windows.go
new file mode 100644
index 000000000..472d002a5
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/syscalls_windows.go
@@ -0,0 +1,61 @@
+// Created by cgo -godefs - DO NOT EDIT
+// cgo -godefs -- -DUNICODE syscalls.go
+
+package termbox
+
+const (
+ foreground_blue = 0x1
+ foreground_green = 0x2
+ foreground_red = 0x4
+ foreground_intensity = 0x8
+ background_blue = 0x10
+ background_green = 0x20
+ background_red = 0x40
+ background_intensity = 0x80
+ std_input_handle = -0xa
+ std_output_handle = -0xb
+ key_event = 0x1
+ mouse_event = 0x2
+ window_buffer_size_event = 0x4
+ enable_window_input = 0x8
+ enable_mouse_input = 0x10
+ enable_extended_flags = 0x80
+
+ vk_f1 = 0x70
+ vk_f2 = 0x71
+ vk_f3 = 0x72
+ vk_f4 = 0x73
+ vk_f5 = 0x74
+ vk_f6 = 0x75
+ vk_f7 = 0x76
+ vk_f8 = 0x77
+ vk_f9 = 0x78
+ vk_f10 = 0x79
+ vk_f11 = 0x7a
+ vk_f12 = 0x7b
+ vk_insert = 0x2d
+ vk_delete = 0x2e
+ vk_home = 0x24
+ vk_end = 0x23
+ vk_pgup = 0x21
+ vk_pgdn = 0x22
+ vk_arrow_up = 0x26
+ vk_arrow_down = 0x28
+ vk_arrow_left = 0x25
+ vk_arrow_right = 0x27
+ vk_backspace = 0x8
+ vk_tab = 0x9
+ vk_enter = 0xd
+ vk_esc = 0x1b
+ vk_space = 0x20
+
+ left_alt_pressed = 0x2
+ left_ctrl_pressed = 0x8
+ right_alt_pressed = 0x1
+ right_ctrl_pressed = 0x4
+ shift_pressed = 0x10
+
+ generic_read = 0x80000000
+ generic_write = 0x40000000
+ console_textmode_buffer = 0x1
+)
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/termbox.go b/examples/go-dashboard/src/github.com/nsf/termbox-go/termbox.go
new file mode 100644
index 000000000..fbe4c3de9
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/termbox.go
@@ -0,0 +1,529 @@
+// +build !windows
+
+package termbox
+
+import "unicode/utf8"
+import "bytes"
+import "syscall"
+import "unsafe"
+import "strings"
+import "strconv"
+import "os"
+import "io"
+
+// private API
+
+const (
+ t_enter_ca = iota
+ t_exit_ca
+ t_show_cursor
+ t_hide_cursor
+ t_clear_screen
+ t_sgr0
+ t_underline
+ t_bold
+ t_blink
+ t_reverse
+ t_enter_keypad
+ t_exit_keypad
+ t_enter_mouse
+ t_exit_mouse
+ t_max_funcs
+)
+
+const (
+ coord_invalid = -2
+ attr_invalid = Attribute(0xFFFF)
+)
+
+type input_event struct {
+ data []byte
+ err error
+}
+
+type extract_event_res int
+
+const (
+ event_not_extracted extract_event_res = iota
+ event_extracted
+ esc_wait
+)
+
+var (
+ // term specific sequences
+ keys []string
+ funcs []string
+
+ // termbox inner state
+ orig_tios syscall_Termios
+ back_buffer cellbuf
+ front_buffer cellbuf
+ termw int
+ termh int
+ input_mode = InputEsc
+ output_mode = OutputNormal
+ out *os.File
+ in int
+ lastfg = attr_invalid
+ lastbg = attr_invalid
+ lastx = coord_invalid
+ lasty = coord_invalid
+ cursor_x = cursor_hidden
+ cursor_y = cursor_hidden
+ foreground = ColorDefault
+ background = ColorDefault
+ inbuf = make([]byte, 0, 64)
+ outbuf bytes.Buffer
+ sigwinch = make(chan os.Signal, 1)
+ sigio = make(chan os.Signal, 1)
+ quit = make(chan int)
+ input_comm = make(chan input_event)
+ interrupt_comm = make(chan struct{})
+ intbuf = make([]byte, 0, 16)
+
+ // grayscale indexes
+ grayscale = []Attribute{
+ 0, 17, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244,
+ 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 232,
+ }
+)
+
+func write_cursor(x, y int) {
+ outbuf.WriteString("\033[")
+ outbuf.Write(strconv.AppendUint(intbuf, uint64(y+1), 10))
+ outbuf.WriteString(";")
+ outbuf.Write(strconv.AppendUint(intbuf, uint64(x+1), 10))
+ outbuf.WriteString("H")
+}
+
+func write_sgr_fg(a Attribute) {
+ switch output_mode {
+ case Output256, Output216, OutputGrayscale:
+ outbuf.WriteString("\033[38;5;")
+ outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10))
+ outbuf.WriteString("m")
+ default:
+ outbuf.WriteString("\033[3")
+ outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10))
+ outbuf.WriteString("m")
+ }
+}
+
+func write_sgr_bg(a Attribute) {
+ switch output_mode {
+ case Output256, Output216, OutputGrayscale:
+ outbuf.WriteString("\033[48;5;")
+ outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10))
+ outbuf.WriteString("m")
+ default:
+ outbuf.WriteString("\033[4")
+ outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10))
+ outbuf.WriteString("m")
+ }
+}
+
+func write_sgr(fg, bg Attribute) {
+ switch output_mode {
+ case Output256, Output216, OutputGrayscale:
+ outbuf.WriteString("\033[38;5;")
+ outbuf.Write(strconv.AppendUint(intbuf, uint64(fg-1), 10))
+ outbuf.WriteString("m")
+ outbuf.WriteString("\033[48;5;")
+ outbuf.Write(strconv.AppendUint(intbuf, uint64(bg-1), 10))
+ outbuf.WriteString("m")
+ default:
+ outbuf.WriteString("\033[3")
+ outbuf.Write(strconv.AppendUint(intbuf, uint64(fg-1), 10))
+ outbuf.WriteString(";4")
+ outbuf.Write(strconv.AppendUint(intbuf, uint64(bg-1), 10))
+ outbuf.WriteString("m")
+ }
+}
+
+type winsize struct {
+ rows uint16
+ cols uint16
+ xpixels uint16
+ ypixels uint16
+}
+
+func get_term_size(fd uintptr) (int, int) {
+ var sz winsize
+ _, _, _ = syscall.Syscall(syscall.SYS_IOCTL,
+ fd, uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&sz)))
+ return int(sz.cols), int(sz.rows)
+}
+
+func send_attr(fg, bg Attribute) {
+ if fg == lastfg && bg == lastbg {
+ return
+ }
+
+ outbuf.WriteString(funcs[t_sgr0])
+
+ var fgcol, bgcol Attribute
+
+ switch output_mode {
+ case Output256:
+ fgcol = fg & 0x1FF
+ bgcol = bg & 0x1FF
+ case Output216:
+ fgcol = fg & 0xFF
+ bgcol = bg & 0xFF
+ if fgcol > 216 {
+ fgcol = ColorDefault
+ }
+ if bgcol > 216 {
+ bgcol = ColorDefault
+ }
+ if fgcol != ColorDefault {
+ fgcol += 0x10
+ }
+ if bgcol != ColorDefault {
+ bgcol += 0x10
+ }
+ case OutputGrayscale:
+ fgcol = fg & 0x1F
+ bgcol = bg & 0x1F
+ if fgcol > 26 {
+ fgcol = ColorDefault
+ }
+ if bgcol > 26 {
+ bgcol = ColorDefault
+ }
+ if fgcol != ColorDefault {
+ fgcol = grayscale[fgcol]
+ }
+ if bgcol != ColorDefault {
+ bgcol = grayscale[bgcol]
+ }
+ default:
+ fgcol = fg & 0x0F
+ bgcol = bg & 0x0F
+ }
+
+ if fgcol != ColorDefault {
+ if bgcol != ColorDefault {
+ write_sgr(fgcol, bgcol)
+ } else {
+ write_sgr_fg(fgcol)
+ }
+ } else if bgcol != ColorDefault {
+ write_sgr_bg(bgcol)
+ }
+
+ if fg&AttrBold != 0 {
+ outbuf.WriteString(funcs[t_bold])
+ }
+ if bg&AttrBold != 0 {
+ outbuf.WriteString(funcs[t_blink])
+ }
+ if fg&AttrUnderline != 0 {
+ outbuf.WriteString(funcs[t_underline])
+ }
+ if fg&AttrReverse|bg&AttrReverse != 0 {
+ outbuf.WriteString(funcs[t_reverse])
+ }
+
+ lastfg, lastbg = fg, bg
+}
+
+func send_char(x, y int, ch rune) {
+ var buf [8]byte
+ n := utf8.EncodeRune(buf[:], ch)
+ if x-1 != lastx || y != lasty {
+ write_cursor(x, y)
+ }
+ lastx, lasty = x, y
+ outbuf.Write(buf[:n])
+}
+
+func flush() error {
+ _, err := io.Copy(out, &outbuf)
+ outbuf.Reset()
+ return err
+}
+
+func send_clear() error {
+ send_attr(foreground, background)
+ outbuf.WriteString(funcs[t_clear_screen])
+ if !is_cursor_hidden(cursor_x, cursor_y) {
+ write_cursor(cursor_x, cursor_y)
+ }
+
+ // we need to invalidate cursor position too and these two vars are
+ // used only for simple cursor positioning optimization, cursor
+ // actually may be in the correct place, but we simply discard
+ // optimization once and it gives us simple solution for the case when
+ // cursor moved
+ lastx = coord_invalid
+ lasty = coord_invalid
+
+ return flush()
+}
+
+func update_size_maybe() error {
+ w, h := get_term_size(out.Fd())
+ if w != termw || h != termh {
+ termw, termh = w, h
+ back_buffer.resize(termw, termh)
+ front_buffer.resize(termw, termh)
+ front_buffer.clear()
+ return send_clear()
+ }
+ return nil
+}
+
+func tcsetattr(fd uintptr, termios *syscall_Termios) error {
+ r, _, e := syscall.Syscall(syscall.SYS_IOCTL,
+ fd, uintptr(syscall_TCSETS), uintptr(unsafe.Pointer(termios)))
+ if r != 0 {
+ return os.NewSyscallError("SYS_IOCTL", e)
+ }
+ return nil
+}
+
+func tcgetattr(fd uintptr, termios *syscall_Termios) error {
+ r, _, e := syscall.Syscall(syscall.SYS_IOCTL,
+ fd, uintptr(syscall_TCGETS), uintptr(unsafe.Pointer(termios)))
+ if r != 0 {
+ return os.NewSyscallError("SYS_IOCTL", e)
+ }
+ return nil
+}
+
+func parse_mouse_event(event *Event, buf string) (int, bool) {
+ if strings.HasPrefix(buf, "\033[M") && len(buf) >= 6 {
+ // X10 mouse encoding, the simplest one
+ // \033 [ M Cb Cx Cy
+ b := buf[3] - 32
+ switch b & 3 {
+ case 0:
+ if b&64 != 0 {
+ event.Key = MouseWheelUp
+ } else {
+ event.Key = MouseLeft
+ }
+ case 1:
+ if b&64 != 0 {
+ event.Key = MouseWheelDown
+ } else {
+ event.Key = MouseMiddle
+ }
+ case 2:
+ event.Key = MouseRight
+ case 3:
+ event.Key = MouseRelease
+ default:
+ return 6, false
+ }
+ event.Type = EventMouse // KeyEvent by default
+ if b&32 != 0 {
+ event.Mod |= ModMotion
+ }
+
+ // the coord is 1,1 for upper left
+ event.MouseX = int(buf[4]) - 1 - 32
+ event.MouseY = int(buf[5]) - 1 - 32
+ return 6, true
+ } else if strings.HasPrefix(buf, "\033[<") || strings.HasPrefix(buf, "\033[") {
+ // xterm 1006 extended mode or urxvt 1015 extended mode
+ // xterm: \033 [ < Cb ; Cx ; Cy (M or m)
+ // urxvt: \033 [ Cb ; Cx ; Cy M
+
+ // find the first M or m, that's where we stop
+ mi := strings.IndexAny(buf, "Mm")
+ if mi == -1 {
+ return 0, false
+ }
+
+ // whether it's a capital M or not
+ isM := buf[mi] == 'M'
+
+ // whether it's urxvt or not
+ isU := false
+
+ // buf[2] is safe here, because having M or m found means we have at
+ // least 3 bytes in a string
+ if buf[2] == '<' {
+ buf = buf[3:mi]
+ } else {
+ isU = true
+ buf = buf[2:mi]
+ }
+
+ s1 := strings.Index(buf, ";")
+ s2 := strings.LastIndex(buf, ";")
+ // not found or only one ';'
+ if s1 == -1 || s2 == -1 || s1 == s2 {
+ return 0, false
+ }
+
+ n1, err := strconv.ParseInt(buf[0:s1], 10, 64)
+ if err != nil {
+ return 0, false
+ }
+ n2, err := strconv.ParseInt(buf[s1+1:s2], 10, 64)
+ if err != nil {
+ return 0, false
+ }
+ n3, err := strconv.ParseInt(buf[s2+1:], 10, 64)
+ if err != nil {
+ return 0, false
+ }
+
+ // on urxvt, first number is encoded exactly as in X10, but we need to
+ // make it zero-based, on xterm it is zero-based already
+ if isU {
+ n1 -= 32
+ }
+ switch n1 & 3 {
+ case 0:
+ if n1&64 != 0 {
+ event.Key = MouseWheelUp
+ } else {
+ event.Key = MouseLeft
+ }
+ case 1:
+ if n1&64 != 0 {
+ event.Key = MouseWheelDown
+ } else {
+ event.Key = MouseMiddle
+ }
+ case 2:
+ event.Key = MouseRight
+ case 3:
+ event.Key = MouseRelease
+ default:
+ return mi + 1, false
+ }
+ if !isM {
+ // on xterm mouse release is signaled by lowercase m
+ event.Key = MouseRelease
+ }
+
+ event.Type = EventMouse // KeyEvent by default
+ if n1&32 != 0 {
+ event.Mod |= ModMotion
+ }
+
+ event.MouseX = int(n2) - 1
+ event.MouseY = int(n3) - 1
+ return mi + 1, true
+ }
+
+ return 0, false
+}
+
+func parse_escape_sequence(event *Event, buf []byte) (int, bool) {
+ bufstr := string(buf)
+ for i, key := range keys {
+ if strings.HasPrefix(bufstr, key) {
+ event.Ch = 0
+ event.Key = Key(0xFFFF - i)
+ return len(key), true
+ }
+ }
+
+ // if none of the keys match, let's try mouse sequences
+ return parse_mouse_event(event, bufstr)
+}
+
+func extract_raw_event(data []byte, event *Event) bool {
+ if len(inbuf) == 0 {
+ return false
+ }
+
+ n := len(data)
+ if n == 0 {
+ return false
+ }
+
+ n = copy(data, inbuf)
+ copy(inbuf, inbuf[n:])
+ inbuf = inbuf[:len(inbuf)-n]
+
+ event.N = n
+ event.Type = EventRaw
+ return true
+}
+
+func extract_event(inbuf []byte, event *Event, allow_esc_wait bool) extract_event_res {
+ if len(inbuf) == 0 {
+ event.N = 0
+ return event_not_extracted
+ }
+
+ if inbuf[0] == '\033' {
+ // possible escape sequence
+ if n, ok := parse_escape_sequence(event, inbuf); n != 0 {
+ event.N = n
+ if ok {
+ return event_extracted
+ } else {
+ return event_not_extracted
+ }
+ }
+
+ // possible partially read escape sequence; trigger a wait if appropriate
+ if enable_wait_for_escape_sequence() && allow_esc_wait {
+ event.N = 0
+ return esc_wait
+ }
+
+ // it's not escape sequence, then it's Alt or Esc, check input_mode
+ switch {
+ case input_mode&InputEsc != 0:
+ // if we're in escape mode, fill Esc event, pop buffer, return success
+ event.Ch = 0
+ event.Key = KeyEsc
+ event.Mod = 0
+ event.N = 1
+ return event_extracted
+ case input_mode&InputAlt != 0:
+ // if we're in alt mode, set Alt modifier to event and redo parsing
+ event.Mod = ModAlt
+ status := extract_event(inbuf[1:], event, false)
+ if status == event_extracted {
+ event.N++
+ } else {
+ event.N = 0
+ }
+ return status
+ default:
+ panic("unreachable")
+ }
+ }
+
+ // if we're here, this is not an escape sequence and not an alt sequence
+ // so, it's a FUNCTIONAL KEY or a UNICODE character
+
+ // first of all check if it's a functional key
+ if Key(inbuf[0]) <= KeySpace || Key(inbuf[0]) == KeyBackspace2 {
+ // fill event, pop buffer, return success
+ event.Ch = 0
+ event.Key = Key(inbuf[0])
+ event.N = 1
+ return event_extracted
+ }
+
+ // the only possible option is utf8 rune
+ if r, n := utf8.DecodeRune(inbuf); r != utf8.RuneError {
+ event.Ch = r
+ event.Key = 0
+ event.N = n
+ return event_extracted
+ }
+
+ return event_not_extracted
+}
+
+func fcntl(fd int, cmd int, arg int) (val int, err error) {
+ r, _, e := syscall.Syscall(syscall.SYS_FCNTL, uintptr(fd), uintptr(cmd),
+ uintptr(arg))
+ val = int(r)
+ if e != 0 {
+ err = e
+ }
+ return
+}
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/termbox_common.go b/examples/go-dashboard/src/github.com/nsf/termbox-go/termbox_common.go
new file mode 100644
index 000000000..c3355cc25
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/termbox_common.go
@@ -0,0 +1,59 @@
+package termbox
+
+// private API, common OS agnostic part
+
+type cellbuf struct {
+ width int
+ height int
+ cells []Cell
+}
+
+func (this *cellbuf) init(width, height int) {
+ this.width = width
+ this.height = height
+ this.cells = make([]Cell, width*height)
+}
+
+func (this *cellbuf) resize(width, height int) {
+ if this.width == width && this.height == height {
+ return
+ }
+
+ oldw := this.width
+ oldh := this.height
+ oldcells := this.cells
+
+ this.init(width, height)
+ this.clear()
+
+ minw, minh := oldw, oldh
+
+ if width < minw {
+ minw = width
+ }
+ if height < minh {
+ minh = height
+ }
+
+ for i := 0; i < minh; i++ {
+ srco, dsto := i*oldw, i*width
+ src := oldcells[srco : srco+minw]
+ dst := this.cells[dsto : dsto+minw]
+ copy(dst, src)
+ }
+}
+
+func (this *cellbuf) clear() {
+ for i := range this.cells {
+ c := &this.cells[i]
+ c.Ch = ' '
+ c.Fg = foreground
+ c.Bg = background
+ }
+}
+
+const cursor_hidden = -1
+
+func is_cursor_hidden(x, y int) bool {
+ return x == cursor_hidden || y == cursor_hidden
+}
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/termbox_windows.go b/examples/go-dashboard/src/github.com/nsf/termbox-go/termbox_windows.go
new file mode 100644
index 000000000..d46eb043e
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/termbox_windows.go
@@ -0,0 +1,952 @@
+package termbox
+
+import "math"
+import "syscall"
+import "unsafe"
+import "unicode/utf16"
+import "github.com/mattn/go-runewidth"
+
+type (
+ wchar uint16
+ short int16
+ dword uint32
+ word uint16
+ char_info struct {
+ char wchar
+ attr word
+ }
+ coord struct {
+ x short
+ y short
+ }
+ small_rect struct {
+ left short
+ top short
+ right short
+ bottom short
+ }
+ console_screen_buffer_info struct {
+ size coord
+ cursor_position coord
+ attributes word
+ window small_rect
+ maximum_window_size coord
+ }
+ console_cursor_info struct {
+ size dword
+ visible int32
+ }
+ input_record struct {
+ event_type word
+ _ [2]byte
+ event [16]byte
+ }
+ key_event_record struct {
+ key_down int32
+ repeat_count word
+ virtual_key_code word
+ virtual_scan_code word
+ unicode_char wchar
+ control_key_state dword
+ }
+ window_buffer_size_record struct {
+ size coord
+ }
+ mouse_event_record struct {
+ mouse_pos coord
+ button_state dword
+ control_key_state dword
+ event_flags dword
+ }
+ console_font_info struct {
+ font uint32
+ font_size coord
+ }
+)
+
+const (
+ mouse_lmb = 0x1
+ mouse_rmb = 0x2
+ mouse_mmb = 0x4 | 0x8 | 0x10
+ SM_CXMIN = 28
+ SM_CYMIN = 29
+)
+
+func (this coord) uintptr() uintptr {
+ return uintptr(*(*int32)(unsafe.Pointer(&this)))
+}
+
+func (this *small_rect) uintptr() uintptr {
+ return uintptr(unsafe.Pointer(this))
+}
+
+var kernel32 = syscall.NewLazyDLL("kernel32.dll")
+var moduser32 = syscall.NewLazyDLL("user32.dll")
+var is_cjk = runewidth.IsEastAsian()
+
+var (
+ proc_set_console_active_screen_buffer = kernel32.NewProc("SetConsoleActiveScreenBuffer")
+ proc_set_console_screen_buffer_size = kernel32.NewProc("SetConsoleScreenBufferSize")
+ proc_set_console_window_info = kernel32.NewProc("SetConsoleWindowInfo")
+ proc_create_console_screen_buffer = kernel32.NewProc("CreateConsoleScreenBuffer")
+ proc_get_console_screen_buffer_info = kernel32.NewProc("GetConsoleScreenBufferInfo")
+ proc_write_console_output = kernel32.NewProc("WriteConsoleOutputW")
+ proc_write_console_output_character = kernel32.NewProc("WriteConsoleOutputCharacterW")
+ proc_write_console_output_attribute = kernel32.NewProc("WriteConsoleOutputAttribute")
+ proc_set_console_cursor_info = kernel32.NewProc("SetConsoleCursorInfo")
+ proc_set_console_cursor_position = kernel32.NewProc("SetConsoleCursorPosition")
+ proc_get_console_cursor_info = kernel32.NewProc("GetConsoleCursorInfo")
+ proc_read_console_input = kernel32.NewProc("ReadConsoleInputW")
+ proc_get_console_mode = kernel32.NewProc("GetConsoleMode")
+ proc_set_console_mode = kernel32.NewProc("SetConsoleMode")
+ proc_fill_console_output_character = kernel32.NewProc("FillConsoleOutputCharacterW")
+ proc_fill_console_output_attribute = kernel32.NewProc("FillConsoleOutputAttribute")
+ proc_create_event = kernel32.NewProc("CreateEventW")
+ proc_wait_for_multiple_objects = kernel32.NewProc("WaitForMultipleObjects")
+ proc_set_event = kernel32.NewProc("SetEvent")
+ proc_get_current_console_font = kernel32.NewProc("GetCurrentConsoleFont")
+ get_system_metrics = moduser32.NewProc("GetSystemMetrics")
+)
+
+func set_console_active_screen_buffer(h syscall.Handle) (err error) {
+ r0, _, e1 := syscall.Syscall(proc_set_console_active_screen_buffer.Addr(),
+ 1, uintptr(h), 0, 0)
+ if int(r0) == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func set_console_screen_buffer_size(h syscall.Handle, size coord) (err error) {
+ r0, _, e1 := syscall.Syscall(proc_set_console_screen_buffer_size.Addr(),
+ 2, uintptr(h), size.uintptr(), 0)
+ if int(r0) == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func set_console_window_info(h syscall.Handle, window *small_rect) (err error) {
+ var absolute uint32
+ absolute = 1
+ r0, _, e1 := syscall.Syscall(proc_set_console_window_info.Addr(),
+ 3, uintptr(h), uintptr(absolute), window.uintptr())
+ if int(r0) == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func create_console_screen_buffer() (h syscall.Handle, err error) {
+ r0, _, e1 := syscall.Syscall6(proc_create_console_screen_buffer.Addr(),
+ 5, uintptr(generic_read|generic_write), 0, 0, console_textmode_buffer, 0, 0)
+ if int(r0) == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return syscall.Handle(r0), err
+}
+
+func get_console_screen_buffer_info(h syscall.Handle, info *console_screen_buffer_info) (err error) {
+ r0, _, e1 := syscall.Syscall(proc_get_console_screen_buffer_info.Addr(),
+ 2, uintptr(h), uintptr(unsafe.Pointer(info)), 0)
+ if int(r0) == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func write_console_output(h syscall.Handle, chars []char_info, dst small_rect) (err error) {
+ tmp_coord = coord{dst.right - dst.left + 1, dst.bottom - dst.top + 1}
+ tmp_rect = dst
+ r0, _, e1 := syscall.Syscall6(proc_write_console_output.Addr(),
+ 5, uintptr(h), uintptr(unsafe.Pointer(&chars[0])), tmp_coord.uintptr(),
+ tmp_coord0.uintptr(), uintptr(unsafe.Pointer(&tmp_rect)), 0)
+ if int(r0) == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func write_console_output_character(h syscall.Handle, chars []wchar, pos coord) (err error) {
+ r0, _, e1 := syscall.Syscall6(proc_write_console_output_character.Addr(),
+ 5, uintptr(h), uintptr(unsafe.Pointer(&chars[0])), uintptr(len(chars)),
+ pos.uintptr(), uintptr(unsafe.Pointer(&tmp_arg)), 0)
+ if int(r0) == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func write_console_output_attribute(h syscall.Handle, attrs []word, pos coord) (err error) {
+ r0, _, e1 := syscall.Syscall6(proc_write_console_output_attribute.Addr(),
+ 5, uintptr(h), uintptr(unsafe.Pointer(&attrs[0])), uintptr(len(attrs)),
+ pos.uintptr(), uintptr(unsafe.Pointer(&tmp_arg)), 0)
+ if int(r0) == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func set_console_cursor_info(h syscall.Handle, info *console_cursor_info) (err error) {
+ r0, _, e1 := syscall.Syscall(proc_set_console_cursor_info.Addr(),
+ 2, uintptr(h), uintptr(unsafe.Pointer(info)), 0)
+ if int(r0) == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func get_console_cursor_info(h syscall.Handle, info *console_cursor_info) (err error) {
+ r0, _, e1 := syscall.Syscall(proc_get_console_cursor_info.Addr(),
+ 2, uintptr(h), uintptr(unsafe.Pointer(info)), 0)
+ if int(r0) == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func set_console_cursor_position(h syscall.Handle, pos coord) (err error) {
+ r0, _, e1 := syscall.Syscall(proc_set_console_cursor_position.Addr(),
+ 2, uintptr(h), pos.uintptr(), 0)
+ if int(r0) == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func read_console_input(h syscall.Handle, record *input_record) (err error) {
+ r0, _, e1 := syscall.Syscall6(proc_read_console_input.Addr(),
+ 4, uintptr(h), uintptr(unsafe.Pointer(record)), 1, uintptr(unsafe.Pointer(&tmp_arg)), 0, 0)
+ if int(r0) == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func get_console_mode(h syscall.Handle, mode *dword) (err error) {
+ r0, _, e1 := syscall.Syscall(proc_get_console_mode.Addr(),
+ 2, uintptr(h), uintptr(unsafe.Pointer(mode)), 0)
+ if int(r0) == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func set_console_mode(h syscall.Handle, mode dword) (err error) {
+ r0, _, e1 := syscall.Syscall(proc_set_console_mode.Addr(),
+ 2, uintptr(h), uintptr(mode), 0)
+ if int(r0) == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func fill_console_output_character(h syscall.Handle, char wchar, n int) (err error) {
+ tmp_coord = coord{0, 0}
+ r0, _, e1 := syscall.Syscall6(proc_fill_console_output_character.Addr(),
+ 5, uintptr(h), uintptr(char), uintptr(n), tmp_coord.uintptr(),
+ uintptr(unsafe.Pointer(&tmp_arg)), 0)
+ if int(r0) == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func fill_console_output_attribute(h syscall.Handle, attr word, n int) (err error) {
+ tmp_coord = coord{0, 0}
+ r0, _, e1 := syscall.Syscall6(proc_fill_console_output_attribute.Addr(),
+ 5, uintptr(h), uintptr(attr), uintptr(n), tmp_coord.uintptr(),
+ uintptr(unsafe.Pointer(&tmp_arg)), 0)
+ if int(r0) == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func create_event() (out syscall.Handle, err error) {
+ r0, _, e1 := syscall.Syscall6(proc_create_event.Addr(),
+ 4, 0, 0, 0, 0, 0, 0)
+ if int(r0) == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return syscall.Handle(r0), err
+}
+
+func wait_for_multiple_objects(objects []syscall.Handle) (err error) {
+ r0, _, e1 := syscall.Syscall6(proc_wait_for_multiple_objects.Addr(),
+ 4, uintptr(len(objects)), uintptr(unsafe.Pointer(&objects[0])),
+ 0, 0xFFFFFFFF, 0, 0)
+ if uint32(r0) == 0xFFFFFFFF {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func set_event(ev syscall.Handle) (err error) {
+ r0, _, e1 := syscall.Syscall(proc_set_event.Addr(),
+ 1, uintptr(ev), 0, 0)
+ if int(r0) == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func get_current_console_font(h syscall.Handle, info *console_font_info) (err error) {
+ r0, _, e1 := syscall.Syscall(proc_get_current_console_font.Addr(),
+ 3, uintptr(h), 0, uintptr(unsafe.Pointer(info)))
+ if int(r0) == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+type diff_msg struct {
+ pos short
+ lines short
+ chars []char_info
+}
+
+type input_event struct {
+ event Event
+ err error
+}
+
+var (
+ orig_cursor_info console_cursor_info
+ orig_size coord
+ orig_window small_rect
+ orig_mode dword
+ orig_screen syscall.Handle
+ back_buffer cellbuf
+ front_buffer cellbuf
+ term_size coord
+ input_mode = InputEsc
+ cursor_x = cursor_hidden
+ cursor_y = cursor_hidden
+ foreground = ColorDefault
+ background = ColorDefault
+ in syscall.Handle
+ out syscall.Handle
+ interrupt syscall.Handle
+ charbuf []char_info
+ diffbuf []diff_msg
+ beg_x = -1
+ beg_y = -1
+ beg_i = -1
+ input_comm = make(chan Event)
+ interrupt_comm = make(chan struct{})
+ cancel_comm = make(chan bool, 1)
+ cancel_done_comm = make(chan bool)
+ alt_mode_esc = false
+
+ // these ones just to prevent heap allocs at all costs
+ tmp_info console_screen_buffer_info
+ tmp_arg dword
+ tmp_coord0 = coord{0, 0}
+ tmp_coord = coord{0, 0}
+ tmp_rect = small_rect{0, 0, 0, 0}
+ tmp_finfo console_font_info
+)
+
+func get_cursor_position(out syscall.Handle) coord {
+ err := get_console_screen_buffer_info(out, &tmp_info)
+ if err != nil {
+ panic(err)
+ }
+ return tmp_info.cursor_position
+}
+
+func get_term_size(out syscall.Handle) (coord, small_rect) {
+ err := get_console_screen_buffer_info(out, &tmp_info)
+ if err != nil {
+ panic(err)
+ }
+ return tmp_info.size, tmp_info.window
+}
+
+func get_win_min_size(out syscall.Handle) coord {
+ x, _, err := get_system_metrics.Call(SM_CXMIN)
+ y, _, err := get_system_metrics.Call(SM_CYMIN)
+
+ if x == 0 || y == 0 {
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ err1 := get_current_console_font(out, &tmp_finfo)
+ if err1 != nil {
+ panic(err1)
+ }
+
+ return coord{
+ x: short(math.Ceil(float64(x) / float64(tmp_finfo.font_size.x))),
+ y: short(math.Ceil(float64(y) / float64(tmp_finfo.font_size.y))),
+ }
+}
+
+func get_win_size(out syscall.Handle) coord {
+ err := get_console_screen_buffer_info(out, &tmp_info)
+ if err != nil {
+ panic(err)
+ }
+
+ min_size := get_win_min_size(out)
+
+ size := coord{
+ x: tmp_info.window.right - tmp_info.window.left + 1,
+ y: tmp_info.window.bottom - tmp_info.window.top + 1,
+ }
+
+ if size.x < min_size.x {
+ size.x = min_size.x
+ }
+
+ if size.y < min_size.y {
+ size.y = min_size.y
+ }
+
+ return size
+}
+
+func fix_win_size(out syscall.Handle, size coord) (err error) {
+ window := small_rect{}
+ window.top = 0
+ window.bottom = size.y - 1
+ window.left = 0
+ window.right = size.x - 1
+ return set_console_window_info(out, &window)
+}
+
+func update_size_maybe() {
+ size := get_win_size(out)
+ if size.x != term_size.x || size.y != term_size.y {
+ set_console_screen_buffer_size(out, size)
+ fix_win_size(out, size)
+ term_size = size
+ back_buffer.resize(int(size.x), int(size.y))
+ front_buffer.resize(int(size.x), int(size.y))
+ front_buffer.clear()
+ clear()
+
+ area := int(size.x) * int(size.y)
+ if cap(charbuf) < area {
+ charbuf = make([]char_info, 0, area)
+ }
+ }
+}
+
+var color_table_bg = []word{
+ 0, // default (black)
+ 0, // black
+ background_red,
+ background_green,
+ background_red | background_green, // yellow
+ background_blue,
+ background_red | background_blue, // magenta
+ background_green | background_blue, // cyan
+ background_red | background_blue | background_green, // white
+}
+
+var color_table_fg = []word{
+ foreground_red | foreground_blue | foreground_green, // default (white)
+ 0,
+ foreground_red,
+ foreground_green,
+ foreground_red | foreground_green, // yellow
+ foreground_blue,
+ foreground_red | foreground_blue, // magenta
+ foreground_green | foreground_blue, // cyan
+ foreground_red | foreground_blue | foreground_green, // white
+}
+
+const (
+ replacement_char = '\uFFFD'
+ max_rune = '\U0010FFFF'
+ surr1 = 0xd800
+ surr2 = 0xdc00
+ surr3 = 0xe000
+ surr_self = 0x10000
+)
+
+func append_diff_line(y int) int {
+ n := 0
+ for x := 0; x < front_buffer.width; {
+ cell_offset := y*front_buffer.width + x
+ back := &back_buffer.cells[cell_offset]
+ front := &front_buffer.cells[cell_offset]
+ attr, char := cell_to_char_info(*back)
+ charbuf = append(charbuf, char_info{attr: attr, char: char[0]})
+ *front = *back
+ n++
+ w := runewidth.RuneWidth(back.Ch)
+ if w == 0 || w == 2 && runewidth.IsAmbiguousWidth(back.Ch) {
+ w = 1
+ }
+ x += w
+ // If not CJK, fill trailing space with whitespace
+ if !is_cjk && w == 2 {
+ charbuf = append(charbuf, char_info{attr: attr, char: ' '})
+ }
+ }
+ return n
+}
+
+// compares 'back_buffer' with 'front_buffer' and prepares all changes in the form of
+// 'diff_msg's in the 'diff_buf'
+func prepare_diff_messages() {
+ // clear buffers
+ diffbuf = diffbuf[:0]
+ charbuf = charbuf[:0]
+
+ var diff diff_msg
+ gbeg := 0
+ for y := 0; y < front_buffer.height; y++ {
+ same := true
+ line_offset := y * front_buffer.width
+ for x := 0; x < front_buffer.width; x++ {
+ cell_offset := line_offset + x
+ back := &back_buffer.cells[cell_offset]
+ front := &front_buffer.cells[cell_offset]
+ if *back != *front {
+ same = false
+ break
+ }
+ }
+ if same && diff.lines > 0 {
+ diffbuf = append(diffbuf, diff)
+ diff = diff_msg{}
+ }
+ if !same {
+ beg := len(charbuf)
+ end := beg + append_diff_line(y)
+ if diff.lines == 0 {
+ diff.pos = short(y)
+ gbeg = beg
+ }
+ diff.lines++
+ diff.chars = charbuf[gbeg:end]
+ }
+ }
+ if diff.lines > 0 {
+ diffbuf = append(diffbuf, diff)
+ diff = diff_msg{}
+ }
+}
+
+func get_ct(table []word, idx int) word {
+ idx = idx & 0x0F
+ if idx >= len(table) {
+ idx = len(table) - 1
+ }
+ return table[idx]
+}
+
+func cell_to_char_info(c Cell) (attr word, wc [2]wchar) {
+ attr = get_ct(color_table_fg, int(c.Fg)) | get_ct(color_table_bg, int(c.Bg))
+ if c.Fg&AttrReverse|c.Bg&AttrReverse != 0 {
+ attr = (attr&0xF0)>>4 | (attr&0x0F)<<4
+ }
+ if c.Fg&AttrBold != 0 {
+ attr |= foreground_intensity
+ }
+ if c.Bg&AttrBold != 0 {
+ attr |= background_intensity
+ }
+
+ r0, r1 := utf16.EncodeRune(c.Ch)
+ if r0 == 0xFFFD {
+ wc[0] = wchar(c.Ch)
+ wc[1] = ' '
+ } else {
+ wc[0] = wchar(r0)
+ wc[1] = wchar(r1)
+ }
+ return
+}
+
+func move_cursor(x, y int) {
+ err := set_console_cursor_position(out, coord{short(x), short(y)})
+ if err != nil {
+ panic(err)
+ }
+}
+
+func show_cursor(visible bool) {
+ var v int32
+ if visible {
+ v = 1
+ }
+
+ var info console_cursor_info
+ info.size = 100
+ info.visible = v
+ err := set_console_cursor_info(out, &info)
+ if err != nil {
+ panic(err)
+ }
+}
+
+func clear() {
+ var err error
+ attr, char := cell_to_char_info(Cell{
+ ' ',
+ foreground,
+ background,
+ })
+
+ area := int(term_size.x) * int(term_size.y)
+ err = fill_console_output_attribute(out, attr, area)
+ if err != nil {
+ panic(err)
+ }
+ err = fill_console_output_character(out, char[0], area)
+ if err != nil {
+ panic(err)
+ }
+ if !is_cursor_hidden(cursor_x, cursor_y) {
+ move_cursor(cursor_x, cursor_y)
+ }
+}
+
+func key_event_record_to_event(r *key_event_record) (Event, bool) {
+ if r.key_down == 0 {
+ return Event{}, false
+ }
+
+ e := Event{Type: EventKey}
+ if input_mode&InputAlt != 0 {
+ if alt_mode_esc {
+ e.Mod = ModAlt
+ alt_mode_esc = false
+ }
+ if r.control_key_state&(left_alt_pressed|right_alt_pressed) != 0 {
+ e.Mod = ModAlt
+ }
+ }
+
+ ctrlpressed := r.control_key_state&(left_ctrl_pressed|right_ctrl_pressed) != 0
+
+ if r.virtual_key_code >= vk_f1 && r.virtual_key_code <= vk_f12 {
+ switch r.virtual_key_code {
+ case vk_f1:
+ e.Key = KeyF1
+ case vk_f2:
+ e.Key = KeyF2
+ case vk_f3:
+ e.Key = KeyF3
+ case vk_f4:
+ e.Key = KeyF4
+ case vk_f5:
+ e.Key = KeyF5
+ case vk_f6:
+ e.Key = KeyF6
+ case vk_f7:
+ e.Key = KeyF7
+ case vk_f8:
+ e.Key = KeyF8
+ case vk_f9:
+ e.Key = KeyF9
+ case vk_f10:
+ e.Key = KeyF10
+ case vk_f11:
+ e.Key = KeyF11
+ case vk_f12:
+ e.Key = KeyF12
+ default:
+ panic("unreachable")
+ }
+
+ return e, true
+ }
+
+ if r.virtual_key_code <= vk_delete {
+ switch r.virtual_key_code {
+ case vk_insert:
+ e.Key = KeyInsert
+ case vk_delete:
+ e.Key = KeyDelete
+ case vk_home:
+ e.Key = KeyHome
+ case vk_end:
+ e.Key = KeyEnd
+ case vk_pgup:
+ e.Key = KeyPgup
+ case vk_pgdn:
+ e.Key = KeyPgdn
+ case vk_arrow_up:
+ e.Key = KeyArrowUp
+ case vk_arrow_down:
+ e.Key = KeyArrowDown
+ case vk_arrow_left:
+ e.Key = KeyArrowLeft
+ case vk_arrow_right:
+ e.Key = KeyArrowRight
+ case vk_backspace:
+ if ctrlpressed {
+ e.Key = KeyBackspace2
+ } else {
+ e.Key = KeyBackspace
+ }
+ case vk_tab:
+ e.Key = KeyTab
+ case vk_enter:
+ if ctrlpressed {
+ e.Key = KeyCtrlJ
+ } else {
+ e.Key = KeyEnter
+ }
+ case vk_esc:
+ switch {
+ case input_mode&InputEsc != 0:
+ e.Key = KeyEsc
+ case input_mode&InputAlt != 0:
+ alt_mode_esc = true
+ return Event{}, false
+ }
+ case vk_space:
+ if ctrlpressed {
+ // manual return here, because KeyCtrlSpace is zero
+ e.Key = KeyCtrlSpace
+ return e, true
+ } else {
+ e.Key = KeySpace
+ }
+ }
+
+ if e.Key != 0 {
+ return e, true
+ }
+ }
+
+ if ctrlpressed {
+ if Key(r.unicode_char) >= KeyCtrlA && Key(r.unicode_char) <= KeyCtrlRsqBracket {
+ e.Key = Key(r.unicode_char)
+ if input_mode&InputAlt != 0 && e.Key == KeyEsc {
+ alt_mode_esc = true
+ return Event{}, false
+ }
+ return e, true
+ }
+ switch r.virtual_key_code {
+ case 192, 50:
+ // manual return here, because KeyCtrl2 is zero
+ e.Key = KeyCtrl2
+ return e, true
+ case 51:
+ if input_mode&InputAlt != 0 {
+ alt_mode_esc = true
+ return Event{}, false
+ }
+ e.Key = KeyCtrl3
+ case 52:
+ e.Key = KeyCtrl4
+ case 53:
+ e.Key = KeyCtrl5
+ case 54:
+ e.Key = KeyCtrl6
+ case 189, 191, 55:
+ e.Key = KeyCtrl7
+ case 8, 56:
+ e.Key = KeyCtrl8
+ }
+
+ if e.Key != 0 {
+ return e, true
+ }
+ }
+
+ if r.unicode_char != 0 {
+ e.Ch = rune(r.unicode_char)
+ return e, true
+ }
+
+ return Event{}, false
+}
+
+func input_event_producer() {
+ var r input_record
+ var err error
+ var last_button Key
+ var last_button_pressed Key
+ var last_state = dword(0)
+ var last_x, last_y = -1, -1
+ handles := []syscall.Handle{in, interrupt}
+ for {
+ err = wait_for_multiple_objects(handles)
+ if err != nil {
+ input_comm <- Event{Type: EventError, Err: err}
+ }
+
+ select {
+ case <-cancel_comm:
+ cancel_done_comm <- true
+ return
+ default:
+ }
+
+ err = read_console_input(in, &r)
+ if err != nil {
+ input_comm <- Event{Type: EventError, Err: err}
+ }
+
+ switch r.event_type {
+ case key_event:
+ kr := (*key_event_record)(unsafe.Pointer(&r.event))
+ ev, ok := key_event_record_to_event(kr)
+ if ok {
+ for i := 0; i < int(kr.repeat_count); i++ {
+ input_comm <- ev
+ }
+ }
+ case window_buffer_size_event:
+ sr := *(*window_buffer_size_record)(unsafe.Pointer(&r.event))
+ input_comm <- Event{
+ Type: EventResize,
+ Width: int(sr.size.x),
+ Height: int(sr.size.y),
+ }
+ case mouse_event:
+ mr := *(*mouse_event_record)(unsafe.Pointer(&r.event))
+ ev := Event{Type: EventMouse}
+ switch mr.event_flags {
+ case 0, 2:
+ // single or double click
+ cur_state := mr.button_state
+ switch {
+ case last_state&mouse_lmb == 0 && cur_state&mouse_lmb != 0:
+ last_button = MouseLeft
+ last_button_pressed = last_button
+ case last_state&mouse_rmb == 0 && cur_state&mouse_rmb != 0:
+ last_button = MouseRight
+ last_button_pressed = last_button
+ case last_state&mouse_mmb == 0 && cur_state&mouse_mmb != 0:
+ last_button = MouseMiddle
+ last_button_pressed = last_button
+ case last_state&mouse_lmb != 0 && cur_state&mouse_lmb == 0:
+ last_button = MouseRelease
+ case last_state&mouse_rmb != 0 && cur_state&mouse_rmb == 0:
+ last_button = MouseRelease
+ case last_state&mouse_mmb != 0 && cur_state&mouse_mmb == 0:
+ last_button = MouseRelease
+ default:
+ last_state = cur_state
+ continue
+ }
+ last_state = cur_state
+ ev.Key = last_button
+ last_x, last_y = int(mr.mouse_pos.x), int(mr.mouse_pos.y)
+ ev.MouseX = last_x
+ ev.MouseY = last_y
+ case 1:
+ // mouse motion
+ x, y := int(mr.mouse_pos.x), int(mr.mouse_pos.y)
+ if last_state != 0 && (last_x != x || last_y != y) {
+ ev.Key = last_button_pressed
+ ev.Mod = ModMotion
+ ev.MouseX = x
+ ev.MouseY = y
+ last_x, last_y = x, y
+ } else {
+ ev.Type = EventNone
+ }
+ case 4:
+ // mouse wheel
+ n := int16(mr.button_state >> 16)
+ if n > 0 {
+ ev.Key = MouseWheelUp
+ } else {
+ ev.Key = MouseWheelDown
+ }
+ last_x, last_y = int(mr.mouse_pos.x), int(mr.mouse_pos.y)
+ ev.MouseX = last_x
+ ev.MouseY = last_y
+ default:
+ ev.Type = EventNone
+ }
+ if ev.Type != EventNone {
+ input_comm <- ev
+ }
+ }
+ }
+}
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/terminfo.go b/examples/go-dashboard/src/github.com/nsf/termbox-go/terminfo.go
new file mode 100644
index 000000000..ab2e7a198
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/terminfo.go
@@ -0,0 +1,232 @@
+// +build !windows
+// This file contains a simple and incomplete implementation of the terminfo
+// database. Information was taken from the ncurses manpages term(5) and
+// terminfo(5). Currently, only the string capabilities for special keys and for
+// functions without parameters are actually used. Colors are still done with
+// ANSI escape sequences. Other special features that are not (yet?) supported
+// are reading from ~/.terminfo, the TERMINFO_DIRS variable, Berkeley database
+// format and extended capabilities.
+
+package termbox
+
+import (
+ "bytes"
+ "encoding/binary"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "strings"
+)
+
+const (
+ ti_magic = 0432
+ ti_header_length = 12
+ ti_mouse_enter = "\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h"
+ ti_mouse_leave = "\x1b[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l"
+)
+
+func load_terminfo() ([]byte, error) {
+ var data []byte
+ var err error
+
+ term := os.Getenv("TERM")
+ if term == "" {
+ return nil, fmt.Errorf("termbox: TERM not set")
+ }
+
+ // The following behaviour follows the one described in terminfo(5) as
+ // distributed by ncurses.
+
+ terminfo := os.Getenv("TERMINFO")
+ if terminfo != "" {
+ // if TERMINFO is set, no other directory should be searched
+ return ti_try_path(terminfo)
+ }
+
+ // next, consider ~/.terminfo
+ home := os.Getenv("HOME")
+ if home != "" {
+ data, err = ti_try_path(home + "/.terminfo")
+ if err == nil {
+ return data, nil
+ }
+ }
+
+ // next, TERMINFO_DIRS
+ dirs := os.Getenv("TERMINFO_DIRS")
+ if dirs != "" {
+ for _, dir := range strings.Split(dirs, ":") {
+ if dir == "" {
+ // "" -> "/usr/share/terminfo"
+ dir = "/usr/share/terminfo"
+ }
+ data, err = ti_try_path(dir)
+ if err == nil {
+ return data, nil
+ }
+ }
+ }
+
+ // next, /lib/terminfo
+ data, err = ti_try_path("/lib/terminfo")
+ if err == nil {
+ return data, nil
+ }
+
+ // fall back to /usr/share/terminfo
+ return ti_try_path("/usr/share/terminfo")
+}
+
+func ti_try_path(path string) (data []byte, err error) {
+ // load_terminfo already made sure it is set
+ term := os.Getenv("TERM")
+
+ // first try, the typical *nix path
+ terminfo := path + "/" + term[0:1] + "/" + term
+ data, err = ioutil.ReadFile(terminfo)
+ if err == nil {
+ return
+ }
+
+ // fallback to darwin specific dirs structure
+ terminfo = path + "/" + hex.EncodeToString([]byte(term[:1])) + "/" + term
+ data, err = ioutil.ReadFile(terminfo)
+ return
+}
+
+func setup_term_builtin() error {
+ name := os.Getenv("TERM")
+ if name == "" {
+ return errors.New("termbox: TERM environment variable not set")
+ }
+
+ for _, t := range terms {
+ if t.name == name {
+ keys = t.keys
+ funcs = t.funcs
+ return nil
+ }
+ }
+
+ compat_table := []struct {
+ partial string
+ keys []string
+ funcs []string
+ }{
+ {"xterm", xterm_keys, xterm_funcs},
+ {"rxvt", rxvt_unicode_keys, rxvt_unicode_funcs},
+ {"linux", linux_keys, linux_funcs},
+ {"Eterm", eterm_keys, eterm_funcs},
+ {"screen", screen_keys, screen_funcs},
+ // let's assume that 'cygwin' is xterm compatible
+ {"cygwin", xterm_keys, xterm_funcs},
+ {"st", xterm_keys, xterm_funcs},
+ }
+
+ // try compatibility variants
+ for _, it := range compat_table {
+ if strings.Contains(name, it.partial) {
+ keys = it.keys
+ funcs = it.funcs
+ return nil
+ }
+ }
+
+ return errors.New("termbox: unsupported terminal")
+}
+
+func setup_term() (err error) {
+ var data []byte
+ var header [6]int16
+ var str_offset, table_offset int16
+
+ data, err = load_terminfo()
+ if err != nil {
+ return setup_term_builtin()
+ }
+
+ rd := bytes.NewReader(data)
+ // 0: magic number, 1: size of names section, 2: size of boolean section, 3:
+ // size of numbers section (in integers), 4: size of the strings section (in
+ // integers), 5: size of the string table
+
+ err = binary.Read(rd, binary.LittleEndian, header[:])
+ if err != nil {
+ return
+ }
+
+ number_sec_len := int16(2)
+ if header[0] == 542 { // doc says it should be octal 0542, but what I see it terminfo files is 542, learn to program please... thank you..
+ number_sec_len = 4
+ }
+
+ if (header[1]+header[2])%2 != 0 {
+ // old quirk to align everything on word boundaries
+ header[2] += 1
+ }
+ str_offset = ti_header_length + header[1] + header[2] + number_sec_len*header[3]
+ table_offset = str_offset + 2*header[4]
+
+ keys = make([]string, 0xFFFF-key_min)
+ for i, _ := range keys {
+ keys[i], err = ti_read_string(rd, str_offset+2*ti_keys[i], table_offset)
+ if err != nil {
+ return
+ }
+ }
+ funcs = make([]string, t_max_funcs)
+ // the last two entries are reserved for mouse. because the table offset is
+ // not there, the two entries have to fill in manually
+ for i, _ := range funcs[:len(funcs)-2] {
+ funcs[i], err = ti_read_string(rd, str_offset+2*ti_funcs[i], table_offset)
+ if err != nil {
+ return
+ }
+ }
+ funcs[t_max_funcs-2] = ti_mouse_enter
+ funcs[t_max_funcs-1] = ti_mouse_leave
+ return nil
+}
+
+func ti_read_string(rd *bytes.Reader, str_off, table int16) (string, error) {
+ var off int16
+
+ _, err := rd.Seek(int64(str_off), 0)
+ if err != nil {
+ return "", err
+ }
+ err = binary.Read(rd, binary.LittleEndian, &off)
+ if err != nil {
+ return "", err
+ }
+ _, err = rd.Seek(int64(table+off), 0)
+ if err != nil {
+ return "", err
+ }
+ var bs []byte
+ for {
+ b, err := rd.ReadByte()
+ if err != nil {
+ return "", err
+ }
+ if b == byte(0x00) {
+ break
+ }
+ bs = append(bs, b)
+ }
+ return string(bs), nil
+}
+
+// "Maps" the function constants from termbox.go to the number of the respective
+// string capability in the terminfo file. Taken from (ncurses) term.h.
+var ti_funcs = []int16{
+ 28, 40, 16, 13, 5, 39, 36, 27, 26, 34, 89, 88,
+}
+
+// Same as above for the special keys.
+var ti_keys = []int16{
+ 66, 68 /* apparently not a typo; 67 is F10 for whatever reason */, 69, 70,
+ 71, 72, 73, 74, 75, 67, 216, 217, 77, 59, 76, 164, 82, 81, 87, 61, 79, 83,
+}
diff --git a/examples/go-dashboard/src/github.com/nsf/termbox-go/terminfo_builtin.go b/examples/go-dashboard/src/github.com/nsf/termbox-go/terminfo_builtin.go
new file mode 100644
index 000000000..a94866067
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/nsf/termbox-go/terminfo_builtin.go
@@ -0,0 +1,64 @@
+// +build !windows
+
+package termbox
+
+// Eterm
+var eterm_keys = []string{
+ "\x1b[11~", "\x1b[12~", "\x1b[13~", "\x1b[14~", "\x1b[15~", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1b[7~", "\x1b[8~", "\x1b[5~", "\x1b[6~", "\x1b[A", "\x1b[B", "\x1b[D", "\x1b[C",
+}
+var eterm_funcs = []string{
+ "\x1b7\x1b[?47h", "\x1b[2J\x1b[?47l\x1b8", "\x1b[?25h", "\x1b[?25l", "\x1b[H\x1b[2J", "\x1b[m\x0f", "\x1b[4m", "\x1b[1m", "\x1b[5m", "\x1b[7m", "", "", "", "",
+}
+
+// screen
+var screen_keys = []string{
+ "\x1bOP", "\x1bOQ", "\x1bOR", "\x1bOS", "\x1b[15~", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1b[1~", "\x1b[4~", "\x1b[5~", "\x1b[6~", "\x1bOA", "\x1bOB", "\x1bOD", "\x1bOC",
+}
+var screen_funcs = []string{
+ "\x1b[?1049h", "\x1b[?1049l", "\x1b[34h\x1b[?25h", "\x1b[?25l", "\x1b[H\x1b[J", "\x1b[m\x0f", "\x1b[4m", "\x1b[1m", "\x1b[5m", "\x1b[7m", "\x1b[?1h\x1b=", "\x1b[?1l\x1b>", ti_mouse_enter, ti_mouse_leave,
+}
+
+// xterm
+var xterm_keys = []string{
+ "\x1bOP", "\x1bOQ", "\x1bOR", "\x1bOS", "\x1b[15~", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1bOH", "\x1bOF", "\x1b[5~", "\x1b[6~", "\x1bOA", "\x1bOB", "\x1bOD", "\x1bOC",
+}
+var xterm_funcs = []string{
+ "\x1b[?1049h", "\x1b[?1049l", "\x1b[?12l\x1b[?25h", "\x1b[?25l", "\x1b[H\x1b[2J", "\x1b(B\x1b[m", "\x1b[4m", "\x1b[1m", "\x1b[5m", "\x1b[7m", "\x1b[?1h\x1b=", "\x1b[?1l\x1b>", ti_mouse_enter, ti_mouse_leave,
+}
+
+// rxvt-unicode
+var rxvt_unicode_keys = []string{
+ "\x1b[11~", "\x1b[12~", "\x1b[13~", "\x1b[14~", "\x1b[15~", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1b[7~", "\x1b[8~", "\x1b[5~", "\x1b[6~", "\x1b[A", "\x1b[B", "\x1b[D", "\x1b[C",
+}
+var rxvt_unicode_funcs = []string{
+ "\x1b[?1049h", "\x1b[r\x1b[?1049l", "\x1b[?25h", "\x1b[?25l", "\x1b[H\x1b[2J", "\x1b[m\x1b(B", "\x1b[4m", "\x1b[1m", "\x1b[5m", "\x1b[7m", "\x1b=", "\x1b>", ti_mouse_enter, ti_mouse_leave,
+}
+
+// linux
+var linux_keys = []string{
+ "\x1b[[A", "\x1b[[B", "\x1b[[C", "\x1b[[D", "\x1b[[E", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1b[1~", "\x1b[4~", "\x1b[5~", "\x1b[6~", "\x1b[A", "\x1b[B", "\x1b[D", "\x1b[C",
+}
+var linux_funcs = []string{
+ "", "", "\x1b[?25h\x1b[?0c", "\x1b[?25l\x1b[?1c", "\x1b[H\x1b[J", "\x1b[0;10m", "\x1b[4m", "\x1b[1m", "\x1b[5m", "\x1b[7m", "", "", "", "",
+}
+
+// rxvt-256color
+var rxvt_256color_keys = []string{
+ "\x1b[11~", "\x1b[12~", "\x1b[13~", "\x1b[14~", "\x1b[15~", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1b[7~", "\x1b[8~", "\x1b[5~", "\x1b[6~", "\x1b[A", "\x1b[B", "\x1b[D", "\x1b[C",
+}
+var rxvt_256color_funcs = []string{
+ "\x1b7\x1b[?47h", "\x1b[2J\x1b[?47l\x1b8", "\x1b[?25h", "\x1b[?25l", "\x1b[H\x1b[2J", "\x1b[m\x0f", "\x1b[4m", "\x1b[1m", "\x1b[5m", "\x1b[7m", "\x1b=", "\x1b>", ti_mouse_enter, ti_mouse_leave,
+}
+
+var terms = []struct {
+ name string
+ keys []string
+ funcs []string
+}{
+ {"Eterm", eterm_keys, eterm_funcs},
+ {"screen", screen_keys, screen_funcs},
+ {"xterm", xterm_keys, xterm_funcs},
+ {"rxvt-unicode", rxvt_unicode_keys, rxvt_unicode_funcs},
+ {"linux", linux_keys, linux_funcs},
+ {"rxvt-256color", rxvt_256color_keys, rxvt_256color_funcs},
+}