diff options
Diffstat (limited to 'examples/go-dashboard/src/github.com')
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 +============ + +[](https://travis-ci.org/mattn/go-runewidth) +[](https://codecov.io/gh/mattn/go-runewidth) +[](http://godoc.org/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 @@ +[](https://godoc.org/github.com/mum4k/termdash) +[](https://travis-ci.com/mum4k/termdash) +[](https://sourcegraph.com/github.com/mum4k/termdash?badge) +[](https://coveralls.io/github/mum4k/termdash?branch=master) +[](https://goreportcard.com/report/github.com/mum4k/termdash) +[](https://github.com/mum4k/termdash/blob/master/LICENSE) +[](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 @@ +[](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}, +} |