Removed go-dashboard example.

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
This commit is contained in:
Toni Uhlig
2022-03-06 17:40:35 +01:00
parent 46f68501d5
commit 29c72fb30b
93 changed files with 0 additions and 14115 deletions

View File

@@ -1,218 +0,0 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"io"
"log"
"net"
"os"
"strconv"
"strings"
"ui"
)
var (
WarningLogger *log.Logger
InfoLogger *log.Logger
ErrorLogger *log.Logger
NETWORK_BUFFER_MAX_SIZE uint16 = 13312
NETWORK_BUFFER_LENGTH_DIGITS uint16 = 5
)
type packet_event struct {
ThreadID uint8 `json:"thread_id"`
PacketID uint64 `json:"packet_id"`
FlowID uint32 `json:"flow_id"`
FlowPacketID uint64 `json:"flow_packet_id"`
Timestamp uint64 `json:"ts_msec"`
PacketEventID uint8 `json:"packet_event_id"`
PacketEventName string `json:"packet_event_name"`
PacketOversize bool `json:"pkt_oversize"`
PacketLength uint32 `json:"pkt_len"`
PacketL4Length uint32 `json:"pkt_l4_len"`
Packet string `json:"pkt"`
PacketCaptureLength uint32 `json:"pkt_caplen"`
PacketType uint32 `json:"pkt_type"`
PacketL3Offset uint32 `json:"pkt_l3_offset"`
PacketL4Offset uint32 `json:"pkt_l4_offset"`
}
type flow_event struct {
ThreadID uint8 `json:"thread_id"`
PacketID uint64 `json:"packet_id"`
FlowID uint32 `json:"flow_id"`
FlowPacketID uint64 `json:"flow_packets_processed"`
FlowFirstSeen uint64 `json:"flow_first_seen"`
FlowLastSeen uint64 `json:"flow_last_seen"`
FlowTotalLayer4DataLength uint64 `json:"flow_tot_l4_data_len"`
FlowMinLayer4DataLength uint64 `json:"flow_min_l4_data_len"`
FlowMaxLayer4DataLength uint64 `json:"flow_max_l4_data_len"`
FlowAvgLayer4DataLength uint64 `json:"flow_avg_l4_data_len"`
FlowDatalinkLayer uint8 `json:"flow_datalink"`
MaxPackets uint8 `json:"flow_max_packets"`
IsMidstreamFlow uint32 `json:"midstream"`
}
type basic_event struct {
ThreadID uint8 `json:"thread_id"`
PacketID uint64 `json:"packet_id"`
BasicEventID uint8 `json:"basic_event_id"`
BasicEventName string `json:"basic_event_name"`
}
func processJson(jsonStr string) {
jsonMap := make(map[string]interface{})
err := json.Unmarshal([]byte(jsonStr), &jsonMap)
if err != nil {
ErrorLogger.Printf("BUG: JSON error: %v\n", err)
os.Exit(1)
}
if jsonMap["packet_event_id"] != nil {
pe := packet_event{}
if err := json.Unmarshal([]byte(jsonStr), &pe); err != nil {
ErrorLogger.Printf("BUG: JSON Unmarshal error: %v\n", err)
os.Exit(1)
}
InfoLogger.Printf("PACKET EVENT %v\n", pe)
} else if jsonMap["flow_event_id"] != nil {
fe := flow_event{}
if err := json.Unmarshal([]byte(jsonStr), &fe); err != nil {
ErrorLogger.Printf("BUG: JSON Unmarshal error: %v\n", err)
os.Exit(1)
}
InfoLogger.Printf("FLOW EVENT %v\n", fe)
} else if jsonMap["basic_event_id"] != nil {
be := basic_event{}
if err := json.Unmarshal([]byte(jsonStr), &be); err != nil {
ErrorLogger.Printf("BUG: JSON Unmarshal error: %v\n", err)
os.Exit(1)
}
InfoLogger.Printf("BASIC EVENT %v\n", be)
} else {
ErrorLogger.Printf("BUG: Unknown JSON: %v\n", jsonStr)
os.Exit(1)
}
//InfoLogger.Printf("JSON map: %v\n-------------------------------------------------------\n", jsonMap)
}
func eventHandler(ui *ui.Tui, wdgts *ui.Widgets, reader chan string) {
for {
select {
case <-ui.MainTicker.C:
if err := wdgts.RawJson.Write(fmt.Sprintf("%s\n", "--- HEARTBEAT ---")); err != nil {
panic(err)
}
case <-ui.Context.Done():
return
case jsonStr := <-reader:
if err := wdgts.RawJson.Write(fmt.Sprintf("%s\n", jsonStr)); err != nil {
panic(err)
}
}
}
}
func main() {
InfoLogger = log.New(os.Stderr, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
WarningLogger = log.New(os.Stderr, "WARNING: ", log.Ldate|log.Ltime|log.Lshortfile)
ErrorLogger = log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
writer := make(chan string, 256)
go func(writer chan string) {
con, err := net.Dial("tcp", "127.0.0.1:7000")
if err != nil {
ErrorLogger.Printf("Connection failed: %v\n", err)
os.Exit(1)
}
buf := make([]byte, NETWORK_BUFFER_MAX_SIZE)
jsonStr := string("")
jsonStrLen := uint16(0)
jsonLen := uint16(0)
brd := bufio.NewReaderSize(con, int(NETWORK_BUFFER_MAX_SIZE))
for {
nread, err := brd.Read(buf)
if err != nil {
if err != io.EOF {
ErrorLogger.Printf("Read Error: %v\n", err)
break
}
}
if nread == 0 || err == io.EOF {
WarningLogger.Printf("Disconnect from Server\n")
break
}
jsonStr += string(buf[:nread])
jsonStrLen += uint16(nread)
for {
if jsonStrLen < NETWORK_BUFFER_LENGTH_DIGITS+1 {
break
}
if jsonStr[NETWORK_BUFFER_LENGTH_DIGITS] != '{' {
ErrorLogger.Printf("BUG: JSON invalid opening character at position %d: '%s' (%x)\n",
NETWORK_BUFFER_LENGTH_DIGITS,
string(jsonStr[:NETWORK_BUFFER_LENGTH_DIGITS]), jsonStr[NETWORK_BUFFER_LENGTH_DIGITS])
os.Exit(1)
}
if jsonLen == 0 {
var tmp uint64
if tmp, err = strconv.ParseUint(strings.TrimLeft(jsonStr[:NETWORK_BUFFER_LENGTH_DIGITS], "0"), 10, 16); err != nil {
ErrorLogger.Printf("BUG: Could not parse length of a JSON string: %v\n", err)
os.Exit(1)
} else {
jsonLen = uint16(tmp)
}
}
if jsonStrLen < jsonLen+NETWORK_BUFFER_LENGTH_DIGITS {
break
}
if jsonStr[jsonLen+NETWORK_BUFFER_LENGTH_DIGITS-2] != '}' || jsonStr[jsonLen+NETWORK_BUFFER_LENGTH_DIGITS-1] != '\n' {
ErrorLogger.Printf("BUG: JSON invalid closing character at position %d: '%s'\n",
jsonLen+NETWORK_BUFFER_LENGTH_DIGITS,
string(jsonStr[jsonLen+NETWORK_BUFFER_LENGTH_DIGITS-1]))
os.Exit(1)
}
writer <- jsonStr[NETWORK_BUFFER_LENGTH_DIGITS : NETWORK_BUFFER_LENGTH_DIGITS+jsonLen]
jsonStr = jsonStr[jsonLen+NETWORK_BUFFER_LENGTH_DIGITS:]
jsonStrLen -= (jsonLen + NETWORK_BUFFER_LENGTH_DIGITS)
jsonLen = 0
}
}
}(writer)
tui, wdgts := ui.Init()
go eventHandler(tui, wdgts, writer)
ui.Run(tui)
/*
for {
select {
case _ = <-writer:
break
}
}
*/
}

View File

@@ -1,16 +0,0 @@
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)

View File

@@ -1,21 +0,0 @@
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.

View File

@@ -1,27 +0,0 @@
go-runewidth
============
[![Build Status](https://travis-ci.org/mattn/go-runewidth.png?branch=master)](https://travis-ci.org/mattn/go-runewidth)
[![Codecov](https://codecov.io/gh/mattn/go-runewidth/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-runewidth)
[![GoDoc](https://godoc.org/github.com/mattn/go-runewidth?status.svg)](http://godoc.org/github.com/mattn/go-runewidth)
[![Go Report Card](https://goreportcard.com/badge/github.com/mattn/go-runewidth)](https://goreportcard.com/report/github.com/mattn/go-runewidth)
Provides functions to get fixed width of the character or string.
Usage
-----
```go
runewidth.StringWidth("つのだ☆HIRO") == 12
```
Author
------
Yasuhiro Matsumoto
License
-------
under the MIT License: http://mattn.mit-license.org/2013

View File

@@ -1,3 +0,0 @@
module github.com/mattn/go-runewidth
go 1.9

View File

@@ -1,12 +0,0 @@
#!/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

View File

@@ -1,257 +0,0 @@
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)
}

View File

@@ -1,8 +0,0 @@
// +build appengine
package runewidth
// IsEastAsian return true if the current locale is CJK
func IsEastAsian() bool {
return false
}

View File

@@ -1,9 +0,0 @@
// +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
}

View File

@@ -1,82 +0,0 @@
// +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)
}

View File

@@ -1,437 +0,0 @@
// 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},
}

View File

@@ -1,28 +0,0 @@
// +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
}

View File

@@ -1,2 +0,0 @@
# Exclude MacOS attribute files.
.DS_Store

View File

@@ -1,17 +0,0 @@
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

View File

@@ -1,361 +0,0 @@
# 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

View File

@@ -1,38 +0,0 @@
# 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/).

View File

@@ -1,201 +0,0 @@
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.

View File

@@ -1,215 +0,0 @@
[![Doc Status](https://godoc.org/github.com/mum4k/termdash?status.png)](https://godoc.org/github.com/mum4k/termdash)
[![Build Status](https://travis-ci.com/mum4k/termdash.svg?branch=master)](https://travis-ci.com/mum4k/termdash)
[![Sourcegraph](https://sourcegraph.com/github.com/mum4k/termdash/-/badge.svg)](https://sourcegraph.com/github.com/mum4k/termdash?badge)
[![Coverage Status](https://coveralls.io/repos/github/mum4k/termdash/badge.svg?branch=master)](https://coveralls.io/github/mum4k/termdash?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/mum4k/termdash)](https://goreportcard.com/report/github.com/mum4k/termdash)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/mum4k/termdash/blob/master/LICENSE)
[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)
# [<img src="./doc/images/termdash.png" alt="termdashlogo" type="image/png" width="30%">](http://github.com/mum4k/termdash/wiki)
Termdash is a cross-platform customizable terminal based dashboard.
[<img src="./doc/images/termdashdemo_0_9_0.gif" alt="termdashdemo" type="image/gif">](termdashdemo/termdashdemo.go)
The feature set is inspired by the
[gizak/termui](http://github.com/gizak/termui) project, which in turn was
inspired by
[yaronn/blessed-contrib](http://github.com/yaronn/blessed-contrib).
This rewrite focuses on code readability, maintainability and testability, see
the [design goals](doc/design_goals.md). It aims to achieve the following
[requirements](doc/requirements.md). See the [high-level design](doc/hld.md)
for more details.
# Public API and status
The public API surface is documented in the
[wiki](http://github.com/mum4k/termdash/wiki).
Private packages can be identified by the presence of the **/private/**
directory in their import path. Stability of the private packages isn't
guaranteed and changes won't be backward compatible.
There might still be breaking changes to the public API, at least until the
project reaches version 1.0.0. Any breaking changes will be published in the
[changelog](CHANGELOG.md).
# Current feature set
- Full support for terminal window resizing throughout the infrastructure.
- Customizable layout, widget placement, borders, margins, padding, colors, etc.
- Dynamic layout changes at runtime.
- Binary tree and Grid forms of setting up the layout.
- Focusable containers and widgets.
- Processing of keyboard and mouse events.
- Periodic and event driven screen redraw.
- A library of widgets, see below.
- UTF-8 for all text elements.
- Drawing primitives (Go functions) for widget development with character and
sub-character resolution.
# Installation
To install this library, run the following:
```go
go get -u github.com/mum4k/termdash
```
# Usage
The usage of most of these elements is demonstrated in
[termdashdemo.go](termdashdemo/termdashdemo.go). To execute the demo:
```go
go run github.com/mum4k/termdash/termdashdemo/termdashdemo.go
```
# Documentation
Please refer to the [Termdash wiki](http://github.com/mum4k/termdash/wiki) for
all documentation and resources.
# Implemented Widgets
## The Button
Allows users to interact with the application, each button press runs a callback function.
Run the
[buttondemo](widgets/button/buttondemo/buttondemo.go).
```go
go run github.com/mum4k/termdash/widgets/button/buttondemo/buttondemo.go
```
[<img src="./doc/images/buttondemo.gif" alt="buttondemo" type="image/gif" width="50%">](widgets/button/buttondemo/buttondemo.go)
## The TextInput
Allows users to interact with the application by entering, editing and
submitting text data. Run the
[textinputdemo](widgets/textinput/textinputdemo/textinputdemo.go).
```go
go run github.com/mum4k/termdash/widgets/textinput/textinputdemo/textinputdemo.go
```
[<img src="./doc/images/textinputdemo.gif" alt="textinputdemo" type="image/gif" width="80%">](widgets/textinput/textinputdemo/textinputdemo.go)
## The Gauge
Displays the progress of an operation. Run the
[gaugedemo](widgets/gauge/gaugedemo/gaugedemo.go).
```go
go run github.com/mum4k/termdash/widgets/gauge/gaugedemo/gaugedemo.go
```
[<img src="./doc/images/gaugedemo.gif" alt="gaugedemo" type="image/gif">](widgets/gauge/gaugedemo/gaugedemo.go)
## The Donut
Visualizes progress of an operation as a partial or a complete donut. Run the
[donutdemo](widgets/donut/donutdemo/donutdemo.go).
```go
go run github.com/mum4k/termdash/widgets/donut/donutdemo/donutdemo.go
```
[<img src="./doc/images/donutdemo.gif" alt="donutdemo" type="image/gif">](widgets/donut/donutdemo/donutdemo.go)
## The Text
Displays text content, supports trimming and scrolling of content. Run the
[textdemo](widgets/text/textdemo/textdemo.go).
```go
go run github.com/mum4k/termdash/widgets/text/textdemo/textdemo.go
```
[<img src="./doc/images/textdemo.gif" alt="textdemo" type="image/gif">](widgets/text/textdemo/textdemo.go)
## The SparkLine
Draws a graph showing a series of values as vertical bars. The bars can have
sub-cell height. Run the
[sparklinedemo](widgets/sparkline/sparklinedemo/sparklinedemo.go).
```go
go run github.com/mum4k/termdash/widgets/sparkline/sparklinedemo/sparklinedemo.go
```
[<img src="./doc/images/sparklinedemo.gif" alt="sparklinedemo" type="image/gif" width="50%">](widgets/sparkline/sparklinedemo/sparklinedemo.go)
## The BarChart
Displays multiple bars showing relative ratios of values. Run the
[barchartdemo](widgets/barchart/barchartdemo/barchartdemo.go).
```go
go run github.com/mum4k/termdash/widgets/barchart/barchartdemo/barchartdemo.go
```
[<img src="./doc/images/barchartdemo.gif" alt="barchartdemo" type="image/gif" width="50%">](widgets/barchart/barchartdemo/barchartdemo.go)
## The LineChart
Displays series of values on a line chart, supports zoom triggered by mouse
events. Run the
[linechartdemo](widgets/linechart/linechartdemo/linechartdemo.go).
```go
go run github.com/mum4k/termdash/widgets/linechart/linechartdemo/linechartdemo.go
```
[<img src="./doc/images/linechartdemo.gif" alt="linechartdemo" type="image/gif" width="70%">](widgets/linechart/linechartdemo/linechartdemo.go)
## The SegmentDisplay
Displays text by simulating a 16-segment display. Run the
[segmentdisplaydemo](widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go).
```go
go run github.com/mum4k/termdash/widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go
```
[<img src="./doc/images/segmentdisplaydemo.gif" alt="segmentdisplaydemo" type="image/gif">](widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go)
# Contributing
If you are willing to contribute, improve the infrastructure or develop a
widget, first of all Thank You! Your help is appreciated.
Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines related
to the Google's CLA, and code review requirements.
As stated above the primary goal of this project is to develop readable, well
designed code, the functionality and efficiency come second. This is achieved
through detailed code reviews, design discussions and following of the [design
guidelines](doc/design_guidelines.md). Please familiarize yourself with these
before contributing.
If you're developing a new widget, please see the [widget
development](doc/widget_development.md) section.
Termdash uses [this branching model](https://nvie.com/posts/a-successful-git-branching-model/). When you fork the repository, base your changes off the [devel](https://github.com/mum4k/termdash/tree/devel) branch and the pull request should merge it back to the devel branch. Commits to the master branch are limited to releases, major bug fixes and documentation updates.
# Similar projects in Go
- [clui](https://github.com/VladimirMarkelov/clui)
- [gocui](https://github.com/jroimartin/gocui)
- [gowid](https://github.com/gcla/gowid)
- [termui](https://github.com/gizak/termui)
- [tui-go](https://github.com/marcusolsson/tui-go)
- [tview](https://github.com/rivo/tview)
# Projects using Termdash
- [datadash](https://github.com/keithknott26/datadash): Visualize streaming or tabular data inside the terminal.
- [grafterm](https://github.com/slok/grafterm): Metrics dashboards visualization on the terminal.
- [perfstat](https://github.com/flaviostutz/perfstat): Analyze and show tips about possible bottlenecks in Linux systems.
# Disclaimer
This is not an official Google product.

View File

@@ -1,70 +0,0 @@
// 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
)

View File

@@ -1,64 +0,0 @@
// 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
})
}

View File

@@ -1,106 +0,0 @@
// 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)
}

View File

@@ -1,471 +0,0 @@
// 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,
}
}

View File

@@ -1,175 +0,0 @@
// 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
}

View File

@@ -1,116 +0,0 @@
// 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
}

View File

@@ -1,817 +0,0 @@
// 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
})
}

View File

@@ -1,86 +0,0 @@
// 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
}

View File

@@ -1,10 +0,0 @@
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
)

View File

@@ -1,172 +0,0 @@
// 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
)

View File

@@ -1,51 +0,0 @@
// 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
)

View File

@@ -1,48 +0,0 @@
// 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
)

View File

@@ -1,128 +0,0 @@
// 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
}

View File

@@ -1,258 +0,0 @@
// 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
}

View File

@@ -1,135 +0,0 @@
// 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
}
}

View File

@@ -1,284 +0,0 @@
// 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}
}

View File

@@ -1,188 +0,0 @@
// 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]),
}
}

View File

@@ -1,247 +0,0 @@
// 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)
}

View File

@@ -1,182 +0,0 @@
// 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
}

View File

@@ -1,263 +0,0 @@
// 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
}

View File

@@ -1,160 +0,0 @@
// 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
}

View File

@@ -1,204 +0,0 @@
// 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
}

View File

@@ -1,17 +0,0 @@
// 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

View File

@@ -1,207 +0,0 @@
// 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]
}

View File

@@ -1,206 +0,0 @@
// 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
}

View File

@@ -1,129 +0,0 @@
// 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
)

View File

@@ -1,93 +0,0 @@
// 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
}

View File

@@ -1,195 +0,0 @@
// 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})
}

View File

@@ -1,120 +0,0 @@
// 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
}

View File

@@ -1,260 +0,0 @@
// 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
}

View File

@@ -1,231 +0,0 @@
// 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)
}

View File

@@ -1,222 +0,0 @@
// 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))),
}
}

View File

@@ -1,224 +0,0 @@
// 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
}

View File

@@ -1,98 +0,0 @@
// 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},
}

View File

@@ -1,409 +0,0 @@
// 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
}

View File

@@ -1,362 +0,0 @@
// 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
}

View File

@@ -1,37 +0,0 @@
// 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)
}

View File

@@ -1,38 +0,0 @@
// 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)
}
}

View File

@@ -1,179 +0,0 @@
// 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),
}
}
}

View File

@@ -1,164 +0,0 @@
// 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()
}

View File

@@ -1,60 +0,0 @@
// 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
)

View File

@@ -1,106 +0,0 @@
// 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)
}

View File

@@ -1,56 +0,0 @@
// 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()
}

View File

@@ -1,185 +0,0 @@
// 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
}

View File

@@ -1,117 +0,0 @@
// 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
}

View File

@@ -1,156 +0,0 @@
// 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
})
}

View File

@@ -1,165 +0,0 @@
// 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
}

View File

@@ -1,286 +0,0 @@
// 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,
}
}

View File

@@ -1,67 +0,0 @@
// 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
})
}

View File

@@ -1,4 +0,0 @@
# Please keep this file sorted.
Georg Reinke <guelfey@googlemail.com>
nsf <no.smile.face@gmail.com>

View File

@@ -1,19 +0,0 @@
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.

View File

@@ -1,51 +0,0 @@
[![GoDoc](https://godoc.org/github.com/nsf/termbox-go?status.svg)](http://godoc.org/github.com/nsf/termbox-go)
## IMPORTANT
This library is somewhat not maintained anymore. But I'm glad that it did what I wanted the most. It moved people away from "ncurses" mindset and these days we see both re-implementations of termbox API in various languages and even possibly better libs with similar API design. If you're looking for a Go lib that provides terminal-based user interface facilities, I've heard that https://github.com/gdamore/tcell is good (never used it myself). Also for more complicated interfaces and/or computer games I recommend you to consider using HTML-based UI. Having said that, termbox still somewhat works. In fact I'm writing this line of text right now in godit (which is a text editor written using termbox-go). So, be aware. Good luck and have a nice day.
## Termbox
Termbox is a library that provides a minimalistic API which allows the programmer to write text-based user interfaces. The library is crossplatform and has both terminal-based implementations on *nix operating systems and a winapi console based implementation for windows operating systems. The basic idea is an abstraction of the greatest common subset of features available on all major terminals and other terminal-like APIs in a minimalistic fashion. Small API means it is easy to implement, test, maintain and learn it, that's what makes the termbox a distinct library in its area.
### Installation
Install and update this go package with `go get -u github.com/nsf/termbox-go`
### Examples
For examples of what can be done take a look at demos in the _demos directory. You can try them with go run: `go run _demos/keyboard.go`
There are also some interesting projects using termbox-go:
- [godit](https://github.com/nsf/godit) is an emacsish lightweight text editor written using termbox.
- [gotetris](https://github.com/jjinux/gotetris) is an implementation of Tetris.
- [sokoban-go](https://github.com/rn2dy/sokoban-go) is an implementation of sokoban game.
- [hecate](https://github.com/evanmiller/hecate) is a hex editor designed by Satan.
- [httopd](https://github.com/verdverm/httopd) is top for httpd logs.
- [mop](https://github.com/mop-tracker/mop) is stock market tracker for hackers.
- [termui](https://github.com/gizak/termui) is a terminal dashboard.
- [termdash](https://github.com/mum4k/termdash) is a terminal dashboard.
- [termloop](https://github.com/JoelOtter/termloop) is a terminal game engine.
- [xterm-color-chart](https://github.com/kutuluk/xterm-color-chart) is a XTerm 256 color chart.
- [gocui](https://github.com/jroimartin/gocui) is a minimalist Go library aimed at creating console user interfaces.
- [dry](https://github.com/moncho/dry) is an interactive cli to manage Docker containers.
- [pxl](https://github.com/ichinaski/pxl) displays images in the terminal.
- [snake-game](https://github.com/DyegoCosta/snake-game) is an implementation of the Snake game.
- [gone](https://github.com/guillaumebreton/gone) is a CLI pomodoro® timer.
- [Spoof.go](https://github.com/sabey/spoofgo) controllable movement spoofing from the cli
- [lf](https://github.com/gokcehan/lf) is a terminal file manager
- [rat](https://github.com/ericfreese/rat) lets you compose shell commands to build terminal applications.
- [httplab](https://github.com/gchaincl/httplab) An interactive web server.
- [tetris](https://github.com/MichaelS11/tetris) Go Tetris with AI option
- [wot](https://github.com/kyu-suke/wot) Wait time during command is completed.
- [2048-go](https://github.com/1984weed/2048-go) is 2048 in Go
- [jv](https://github.com/maxzender/jv) helps you view JSON on the command-line.
- [pinger](https://github.com/hirose31/pinger) helps you to monitor numerous hosts using ICMP ECHO_REQUEST.
- [vixl44](https://github.com/sebashwa/vixl44) lets you create pixel art inside your terminal using vim movements
- [zterm](https://github.com/varunrau/zterm) is a typing game inspired by http://zty.pe/
- [gotypist](https://github.com/pb-/gotypist) is a fun touch-typing tutor following Steve Yegge's method.
- [cointop](https://github.com/miguelmota/cointop) is an interactive terminal based UI application for tracking cryptocurrencies.
- [pexpo](https://github.com/nnao45/pexpo) is a terminal sending ping tool written in Go.
- [jid](https://github.com/simeji/jid) is an interactive JSON drill down tool using filtering queries like jq.
- [nonograminGo](https://github.com/N0RM4L15T/nonograminGo) is a nonogram(aka. picross) in Go
- [tower-of-go](https://github.com/kjirou/tower-of-go) is a tiny maze game that runs on the terminal.
### API reference
[godoc.org/github.com/nsf/termbox-go](http://godoc.org/github.com/nsf/termbox-go)

View File

@@ -1,500 +0,0 @@
// +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()
}

View File

@@ -1,187 +0,0 @@
// 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
)

View File

@@ -1,257 +0,0 @@
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
}

View File

@@ -1,110 +0,0 @@
#!/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)

View File

@@ -1,11 +0,0 @@
// +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
}

View File

@@ -1,9 +0,0 @@
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
}

View File

@@ -1,41 +0,0 @@
// 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
)

View File

@@ -1,40 +0,0 @@
// 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
)

View File

@@ -1,39 +0,0 @@
// 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
)

View File

@@ -1,39 +0,0 @@
// 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
)

View File

@@ -1,33 +0,0 @@
// 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
)

View File

@@ -1,39 +0,0 @@
// 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
)

View File

@@ -1,39 +0,0 @@
// 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
)

View File

@@ -1,61 +0,0 @@
// 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
)

View File

@@ -1,529 +0,0 @@
// +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
}

View File

@@ -1,59 +0,0 @@
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
}

View File

@@ -1,952 +0,0 @@
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
}
}
}
}

View File

@@ -1,232 +0,0 @@
// +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,
}

View File

@@ -1,64 +0,0 @@
// +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},
}

View File

@@ -1,8 +0,0 @@
module github.com/lnslbrty/nDPId/examples/go-dashboard/ui
go 1.14
require (
github.com/mum4k/termdash v0.12.3-0.20200901030524-fe3e97353191
github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1 // indirect
)

View File

@@ -1,104 +0,0 @@
package ui
import (
"context"
"time"
"github.com/mum4k/termdash"
"github.com/mum4k/termdash/container"
"github.com/mum4k/termdash/keyboard"
"github.com/mum4k/termdash/linestyle"
"github.com/mum4k/termdash/terminal/termbox"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgets/text"
)
const rootID = "root"
const redrawInterval = 250 * time.Millisecond
type Tui struct {
Term terminalapi.Terminal
Context context.Context
Cancel context.CancelFunc
Container *container.Container
MainTicker *time.Ticker
}
type Widgets struct {
Menu *text.Text
RawJson *text.Text
}
func newWidgets(ctx context.Context) (*Widgets, error) {
menu, err := text.New()
if err != nil {
panic(err)
}
rawJson, err := text.New(text.RollContent(), text.WrapAtWords())
if err != nil {
panic(err)
}
return &Widgets{
Menu: menu,
RawJson: rawJson,
}, nil
}
func Init() (*Tui, *Widgets) {
var err error
ui := Tui{}
ui.Term, err = termbox.New(termbox.ColorMode(terminalapi.ColorMode256))
if err != nil {
panic(err)
}
ui.Context, ui.Cancel = context.WithCancel(context.Background())
wdgts, err := newWidgets(ui.Context)
if err != nil {
panic(err)
}
ui.Container, err = container.New(ui.Term,
container.Border(linestyle.None),
container.BorderTitle("[ESC to Quit]"),
container.SplitHorizontal(
container.Top(
container.Border(linestyle.Light),
container.BorderTitle("Go nDPId Dashboard"),
container.PlaceWidget(wdgts.Menu),
),
container.Bottom(
container.Border(linestyle.Light),
container.BorderTitle("Raw JSON"),
container.PlaceWidget(wdgts.RawJson),
),
container.SplitFixed(3),
),
)
if err != nil {
panic(err)
}
ui.MainTicker = time.NewTicker(1 * time.Second)
return &ui, wdgts
}
func Run(ui *Tui) {
defer ui.Term.Close()
quitter := func(k *terminalapi.Keyboard) {
if k.Key == keyboard.KeyEsc || k.Key == keyboard.KeyCtrlC {
ui.Cancel()
}
}
if err := termdash.Run(ui.Context, ui.Term, ui.Container, termdash.KeyboardSubscriber(quitter), termdash.RedrawInterval(redrawInterval)); err != nil {
panic(err)
}
}

View File

@@ -1,8 +0,0 @@
module github.com/lnslbrty/nDPId/examples/go-dashboard/ui
go 1.14
require (
github.com/mum4k/termdash v0.12.3-0.20200901030524-fe3e97353191
github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1 // indirect
)

View File

@@ -1,104 +0,0 @@
package ui
import (
"context"
"time"
"github.com/mum4k/termdash"
"github.com/mum4k/termdash/container"
"github.com/mum4k/termdash/keyboard"
"github.com/mum4k/termdash/linestyle"
"github.com/mum4k/termdash/terminal/termbox"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgets/text"
)
const rootID = "root"
const redrawInterval = 250 * time.Millisecond
type Tui struct {
Term terminalapi.Terminal
Context context.Context
Cancel context.CancelFunc
Container *container.Container
MainTicker *time.Ticker
}
type Widgets struct {
Menu *text.Text
RawJson *text.Text
}
func newWidgets(ctx context.Context) (*Widgets, error) {
menu, err := text.New()
if err != nil {
panic(err)
}
rawJson, err := text.New(text.RollContent(), text.WrapAtWords())
if err != nil {
panic(err)
}
return &Widgets{
Menu: menu,
RawJson: rawJson,
}, nil
}
func Init() (*Tui, *Widgets) {
var err error
ui := Tui{}
ui.Term, err = termbox.New(termbox.ColorMode(terminalapi.ColorMode256))
if err != nil {
panic(err)
}
ui.Context, ui.Cancel = context.WithCancel(context.Background())
wdgts, err := newWidgets(ui.Context)
if err != nil {
panic(err)
}
ui.Container, err = container.New(ui.Term,
container.Border(linestyle.None),
container.BorderTitle("[ESC to Quit]"),
container.SplitHorizontal(
container.Top(
container.Border(linestyle.Light),
container.BorderTitle("Go nDPId Dashboard"),
container.PlaceWidget(wdgts.Menu),
),
container.Bottom(
container.Border(linestyle.Light),
container.BorderTitle("Raw JSON"),
container.PlaceWidget(wdgts.RawJson),
),
container.SplitFixed(3),
),
)
if err != nil {
panic(err)
}
ui.MainTicker = time.NewTicker(1 * time.Second)
return &ui, wdgts
}
func Run(ui *Tui) {
defer ui.Term.Close()
quitter := func(k *terminalapi.Keyboard) {
if k.Key == keyboard.KeyEsc || k.Key == keyboard.KeyCtrlC {
ui.Cancel()
}
}
if err := termdash.Run(ui.Context, ui.Term, ui.Container, termdash.KeyboardSubscriber(quitter), termdash.RedrawInterval(redrawInterval)); err != nil {
panic(err)
}
}