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