mirror of https://github.com/miguelmota/cointop
rm panicparse dep
Former-commit-id: 03f60c917e961b9a602201f44db08d96f9633063 [formerly 03f60c917e961b9a602201f44db08d96f9633063 [formerly ebc717d2c1907fc7a7840d674e260a9a033b4554 [formerly 664ed54783c9fdfd300eb39fd4b4a648a410659f]]] Former-commit-id: aac9a01875dc0f2bb9cafdf6a70743e0268c0475 Former-commit-id: 587aa697ce2b26cd6a11406742349b60abad2c11 [formerly 61857f48e24c87948a072aa86c331f0a644bc26e] Former-commit-id: 0690a6a5fce4865803e25015fb141e316279bf8fpull/15/head
parent
fe5c9ec9d8
commit
01b248ee82
@ -0,0 +1,229 @@
|
|||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
|
||||||
|
// Hline is a horizontal line.
|
||||||
|
type Hline struct {
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
Len int
|
||||||
|
Fg Attribute
|
||||||
|
Bg Attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vline is a vertical line.
|
||||||
|
type Vline struct {
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
Len int
|
||||||
|
Fg Attribute
|
||||||
|
Bg Attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer draws a horizontal line.
|
||||||
|
func (l Hline) Buffer() Buffer {
|
||||||
|
if l.Len <= 0 {
|
||||||
|
return NewBuffer()
|
||||||
|
}
|
||||||
|
return NewFilledBuffer(l.X, l.Y, l.X+l.Len, l.Y+1, HORIZONTAL_LINE, l.Fg, l.Bg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer draws a vertical line.
|
||||||
|
func (l Vline) Buffer() Buffer {
|
||||||
|
if l.Len <= 0 {
|
||||||
|
return NewBuffer()
|
||||||
|
}
|
||||||
|
return NewFilledBuffer(l.X, l.Y, l.X+1, l.Y+l.Len, VERTICAL_LINE, l.Fg, l.Bg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer draws a box border.
|
||||||
|
func (b Block) drawBorder(buf Buffer) {
|
||||||
|
if !b.Border {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
min := b.area.Min
|
||||||
|
max := b.area.Max
|
||||||
|
|
||||||
|
x0 := min.X
|
||||||
|
y0 := min.Y
|
||||||
|
x1 := max.X - 1
|
||||||
|
y1 := max.Y - 1
|
||||||
|
|
||||||
|
// draw lines
|
||||||
|
if b.BorderTop {
|
||||||
|
buf.Merge(Hline{x0, y0, x1 - x0, b.BorderFg, b.BorderBg}.Buffer())
|
||||||
|
}
|
||||||
|
if b.BorderBottom {
|
||||||
|
buf.Merge(Hline{x0, y1, x1 - x0, b.BorderFg, b.BorderBg}.Buffer())
|
||||||
|
}
|
||||||
|
if b.BorderLeft {
|
||||||
|
buf.Merge(Vline{x0, y0, y1 - y0, b.BorderFg, b.BorderBg}.Buffer())
|
||||||
|
}
|
||||||
|
if b.BorderRight {
|
||||||
|
buf.Merge(Vline{x1, y0, y1 - y0, b.BorderFg, b.BorderBg}.Buffer())
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw corners
|
||||||
|
if b.BorderTop && b.BorderLeft && b.area.Dx() > 0 && b.area.Dy() > 0 {
|
||||||
|
buf.Set(x0, y0, Cell{TOP_LEFT, b.BorderFg, b.BorderBg})
|
||||||
|
}
|
||||||
|
if b.BorderTop && b.BorderRight && b.area.Dx() > 1 && b.area.Dy() > 0 {
|
||||||
|
buf.Set(x1, y0, Cell{TOP_RIGHT, b.BorderFg, b.BorderBg})
|
||||||
|
}
|
||||||
|
if b.BorderBottom && b.BorderLeft && b.area.Dx() > 0 && b.area.Dy() > 1 {
|
||||||
|
buf.Set(x0, y1, Cell{BOTTOM_LEFT, b.BorderFg, b.BorderBg})
|
||||||
|
}
|
||||||
|
if b.BorderBottom && b.BorderRight && b.area.Dx() > 1 && b.area.Dy() > 1 {
|
||||||
|
buf.Set(x1, y1, Cell{BOTTOM_RIGHT, b.BorderFg, b.BorderBg})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block is a base struct for all other upper level widgets,
|
||||||
|
// consider it as css: display:block.
|
||||||
|
// Normally you do not need to create it manually.
|
||||||
|
type Block struct {
|
||||||
|
area image.Rectangle
|
||||||
|
innerArea image.Rectangle
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
Border bool
|
||||||
|
BorderFg Attribute
|
||||||
|
BorderBg Attribute
|
||||||
|
BorderLeft bool
|
||||||
|
BorderRight bool
|
||||||
|
BorderTop bool
|
||||||
|
BorderBottom bool
|
||||||
|
BorderLabel string
|
||||||
|
BorderLabelFg Attribute
|
||||||
|
BorderLabelBg Attribute
|
||||||
|
Display bool
|
||||||
|
Bg Attribute
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
PaddingTop int
|
||||||
|
PaddingBottom int
|
||||||
|
PaddingLeft int
|
||||||
|
PaddingRight int
|
||||||
|
id string
|
||||||
|
Float Align
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBlock returns a *Block which inherits styles from current theme.
|
||||||
|
func NewBlock() *Block {
|
||||||
|
b := Block{}
|
||||||
|
b.Display = true
|
||||||
|
b.Border = true
|
||||||
|
b.BorderLeft = true
|
||||||
|
b.BorderRight = true
|
||||||
|
b.BorderTop = true
|
||||||
|
b.BorderBottom = true
|
||||||
|
b.BorderBg = ThemeAttr("border.bg")
|
||||||
|
b.BorderFg = ThemeAttr("border.fg")
|
||||||
|
b.BorderLabelBg = ThemeAttr("label.bg")
|
||||||
|
b.BorderLabelFg = ThemeAttr("label.fg")
|
||||||
|
b.Bg = ThemeAttr("block.bg")
|
||||||
|
b.Width = 2
|
||||||
|
b.Height = 2
|
||||||
|
b.id = GenId()
|
||||||
|
b.Float = AlignNone
|
||||||
|
return &b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Block) Id() string {
|
||||||
|
return b.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Align computes box model
|
||||||
|
func (b *Block) Align() {
|
||||||
|
// outer
|
||||||
|
b.area.Min.X = 0
|
||||||
|
b.area.Min.Y = 0
|
||||||
|
b.area.Max.X = b.Width
|
||||||
|
b.area.Max.Y = b.Height
|
||||||
|
|
||||||
|
// float
|
||||||
|
b.area = AlignArea(TermRect(), b.area, b.Float)
|
||||||
|
b.area = MoveArea(b.area, b.X, b.Y)
|
||||||
|
|
||||||
|
// inner
|
||||||
|
b.innerArea.Min.X = b.area.Min.X + b.PaddingLeft
|
||||||
|
b.innerArea.Min.Y = b.area.Min.Y + b.PaddingTop
|
||||||
|
b.innerArea.Max.X = b.area.Max.X - b.PaddingRight
|
||||||
|
b.innerArea.Max.Y = b.area.Max.Y - b.PaddingBottom
|
||||||
|
|
||||||
|
if b.Border {
|
||||||
|
if b.BorderLeft {
|
||||||
|
b.innerArea.Min.X++
|
||||||
|
}
|
||||||
|
if b.BorderRight {
|
||||||
|
b.innerArea.Max.X--
|
||||||
|
}
|
||||||
|
if b.BorderTop {
|
||||||
|
b.innerArea.Min.Y++
|
||||||
|
}
|
||||||
|
if b.BorderBottom {
|
||||||
|
b.innerArea.Max.Y--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InnerBounds returns the internal bounds of the block after aligning and
|
||||||
|
// calculating the padding and border, if any.
|
||||||
|
func (b *Block) InnerBounds() image.Rectangle {
|
||||||
|
b.Align()
|
||||||
|
return b.innerArea
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer implements Bufferer interface.
|
||||||
|
// Draw background and border (if any).
|
||||||
|
func (b *Block) Buffer() Buffer {
|
||||||
|
b.Align()
|
||||||
|
|
||||||
|
buf := NewBuffer()
|
||||||
|
buf.SetArea(b.area)
|
||||||
|
buf.Fill(' ', ColorDefault, b.Bg)
|
||||||
|
|
||||||
|
b.drawBorder(buf)
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeight implements GridBufferer.
|
||||||
|
// It returns current height of the block.
|
||||||
|
func (b Block) GetHeight() int {
|
||||||
|
return b.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetX implements GridBufferer interface, which sets block's x position.
|
||||||
|
func (b *Block) SetX(x int) {
|
||||||
|
b.X = x
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetY implements GridBufferer interface, it sets y position for block.
|
||||||
|
func (b *Block) SetY(y int) {
|
||||||
|
b.Y = y
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWidth implements GridBuffer interface, it sets block's width.
|
||||||
|
func (b *Block) SetWidth(w int) {
|
||||||
|
b.Width = w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Block) InnerWidth() int {
|
||||||
|
return b.innerArea.Dx()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Block) InnerHeight() int {
|
||||||
|
return b.innerArea.Dy()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Block) InnerX() int {
|
||||||
|
return b.innerArea.Min.X
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Block) InnerY() int { return b.innerArea.Min.Y }
|
@ -0,0 +1,20 @@
|
|||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
const TOP_RIGHT = '┐'
|
||||||
|
const VERTICAL_LINE = '│'
|
||||||
|
const HORIZONTAL_LINE = '─'
|
||||||
|
const TOP_LEFT = '┌'
|
||||||
|
const BOTTOM_RIGHT = '┘'
|
||||||
|
const BOTTOM_LEFT = '└'
|
||||||
|
const VERTICAL_LEFT = '┤'
|
||||||
|
const VERTICAL_RIGHT = '├'
|
||||||
|
const HORIZONTAL_DOWN = '┬'
|
||||||
|
const HORIZONTAL_UP = '┴'
|
||||||
|
const QUOTA_LEFT = '«'
|
||||||
|
const QUOTA_RIGHT = '»'
|
@ -0,0 +1,14 @@
|
|||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
const TOP_RIGHT = '+'
|
||||||
|
const VERTICAL_LINE = '|'
|
||||||
|
const HORIZONTAL_LINE = '-'
|
||||||
|
const TOP_LEFT = '+'
|
||||||
|
const BOTTOM_RIGHT = '+'
|
||||||
|
const BOTTOM_LEFT = '+'
|
@ -0,0 +1,106 @@
|
|||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
|
||||||
|
// Cell is a rune with assigned Fg and Bg
|
||||||
|
type Cell struct {
|
||||||
|
Ch rune
|
||||||
|
Fg Attribute
|
||||||
|
Bg Attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer is a renderable rectangle cell data container.
|
||||||
|
type Buffer struct {
|
||||||
|
Area image.Rectangle // selected drawing area
|
||||||
|
CellMap map[image.Point]Cell
|
||||||
|
}
|
||||||
|
|
||||||
|
// At returns the cell at (x,y).
|
||||||
|
func (b Buffer) At(x, y int) Cell {
|
||||||
|
return b.CellMap[image.Pt(x, y)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set assigns a char to (x,y)
|
||||||
|
func (b Buffer) Set(x, y int, c Cell) {
|
||||||
|
b.CellMap[image.Pt(x, y)] = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounds returns the domain for which At can return non-zero color.
|
||||||
|
func (b Buffer) Bounds() image.Rectangle {
|
||||||
|
x0, y0, x1, y1 := 0, 0, 0, 0
|
||||||
|
for p := range b.CellMap {
|
||||||
|
if p.X > x1 {
|
||||||
|
x1 = p.X
|
||||||
|
}
|
||||||
|
if p.X < x0 {
|
||||||
|
x0 = p.X
|
||||||
|
}
|
||||||
|
if p.Y > y1 {
|
||||||
|
y1 = p.Y
|
||||||
|
}
|
||||||
|
if p.Y < y0 {
|
||||||
|
y0 = p.Y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return image.Rect(x0, y0, x1+1, y1+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetArea assigns a new rect area to Buffer b.
|
||||||
|
func (b *Buffer) SetArea(r image.Rectangle) {
|
||||||
|
b.Area.Max = r.Max
|
||||||
|
b.Area.Min = r.Min
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync sets drawing area to the buffer's bound
|
||||||
|
func (b *Buffer) Sync() {
|
||||||
|
b.SetArea(b.Bounds())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCell returns a new cell
|
||||||
|
func NewCell(ch rune, fg, bg Attribute) Cell {
|
||||||
|
return Cell{ch, fg, bg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge merges bs Buffers onto b
|
||||||
|
func (b *Buffer) Merge(bs ...Buffer) {
|
||||||
|
for _, buf := range bs {
|
||||||
|
for p, v := range buf.CellMap {
|
||||||
|
b.Set(p.X, p.Y, v)
|
||||||
|
}
|
||||||
|
b.SetArea(b.Area.Union(buf.Area))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBuffer returns a new Buffer
|
||||||
|
func NewBuffer() Buffer {
|
||||||
|
return Buffer{
|
||||||
|
CellMap: make(map[image.Point]Cell),
|
||||||
|
Area: image.Rectangle{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill fills the Buffer b with ch,fg and bg.
|
||||||
|
func (b Buffer) Fill(ch rune, fg, bg Attribute) {
|
||||||
|
for x := b.Area.Min.X; x < b.Area.Max.X; x++ {
|
||||||
|
for y := b.Area.Min.Y; y < b.Area.Max.Y; y++ {
|
||||||
|
b.Set(x, y, Cell{ch, fg, bg})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFilledBuffer returns a new Buffer filled with ch, fb and bg.
|
||||||
|
func NewFilledBuffer(x0, y0, x1, y1 int, ch rune, fg, bg Attribute) Buffer {
|
||||||
|
buf := NewBuffer()
|
||||||
|
buf.Area.Min = image.Pt(x0, y0)
|
||||||
|
buf.Area.Max = image.Pt(x1, y1)
|
||||||
|
|
||||||
|
for x := buf.Area.Min.X; x < buf.Area.Max.X; x++ {
|
||||||
|
for y := buf.Area.Min.Y; y < buf.Area.Max.Y; y++ {
|
||||||
|
buf.Set(x, y, Cell{ch, fg, bg})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
/*
|
||||||
|
dots:
|
||||||
|
,___,
|
||||||
|
|1 4|
|
||||||
|
|2 5|
|
||||||
|
|3 6|
|
||||||
|
|7 8|
|
||||||
|
`````
|
||||||
|
*/
|
||||||
|
|
||||||
|
var brailleBase = '\u2800'
|
||||||
|
|
||||||
|
var brailleOftMap = [4][2]rune{
|
||||||
|
{'\u0001', '\u0008'},
|
||||||
|
{'\u0002', '\u0010'},
|
||||||
|
{'\u0004', '\u0020'},
|
||||||
|
{'\u0040', '\u0080'}}
|
||||||
|
|
||||||
|
// Canvas contains drawing map: i,j -> rune
|
||||||
|
type Canvas map[[2]int]rune
|
||||||
|
|
||||||
|
// NewCanvas returns an empty Canvas
|
||||||
|
func NewCanvas() Canvas {
|
||||||
|
return make(map[[2]int]rune)
|
||||||
|
}
|
||||||
|
|
||||||
|
func chOft(x, y int) rune {
|
||||||
|
return brailleOftMap[y%4][x%2]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Canvas) rawCh(x, y int) rune {
|
||||||
|
if ch, ok := c[[2]int{x, y}]; ok {
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
return '\u0000' //brailleOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
// return coordinate in terminal
|
||||||
|
func chPos(x, y int) (int, int) {
|
||||||
|
return y / 4, x / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets a point (x,y) in the virtual coordinate
|
||||||
|
func (c Canvas) Set(x, y int) {
|
||||||
|
i, j := chPos(x, y)
|
||||||
|
ch := c.rawCh(i, j)
|
||||||
|
ch |= chOft(x, y)
|
||||||
|
c[[2]int{i, j}] = ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unset removes point (x,y)
|
||||||
|
func (c Canvas) Unset(x, y int) {
|
||||||
|
i, j := chPos(x, y)
|
||||||
|
ch := c.rawCh(i, j)
|
||||||
|
ch &= ^chOft(x, y)
|
||||||
|
c[[2]int{i, j}] = ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer returns un-styled points
|
||||||
|
func (c Canvas) Buffer() Buffer {
|
||||||
|
buf := NewBuffer()
|
||||||
|
for k, v := range c {
|
||||||
|
buf.Set(k[0], k[1], Cell{Ch: v + brailleBase})
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import io
|
||||||
|
|
||||||
|
copyright = """// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
exclude_dirs = [".git", "_docs"]
|
||||||
|
exclude_files = []
|
||||||
|
include_dirs = [".", "debug", "extra", "test", "_example"]
|
||||||
|
|
||||||
|
|
||||||
|
def is_target(fpath):
|
||||||
|
if os.path.splitext(fpath)[-1] == ".go":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def update_copyright(fpath):
|
||||||
|
print("processing " + fpath)
|
||||||
|
f = io.open(fpath, 'r', encoding='utf-8')
|
||||||
|
fstr = f.read()
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
# remove old
|
||||||
|
m = re.search('^// Copyright .+?\r?\n\r?\n', fstr, re.MULTILINE|re.DOTALL)
|
||||||
|
if m:
|
||||||
|
fstr = fstr[m.end():]
|
||||||
|
|
||||||
|
# add new
|
||||||
|
fstr = copyright + fstr
|
||||||
|
f = io.open(fpath, 'w',encoding='utf-8')
|
||||||
|
f.write(fstr)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
for d in include_dirs:
|
||||||
|
files = [
|
||||||
|
os.path.join(d, f) for f in os.listdir(d)
|
||||||
|
if os.path.isfile(os.path.join(d, f))
|
||||||
|
]
|
||||||
|
for f in files:
|
||||||
|
if is_target(f):
|
||||||
|
update_copyright(f)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -0,0 +1,29 @@
|
|||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package termui is a library designed for creating command line UI. For more info, goto http://github.com/gizak/termui
|
||||||
|
|
||||||
|
A simplest example:
|
||||||
|
package main
|
||||||
|
|
||||||
|
import ui "github.com/gizak/termui"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err:=ui.Init(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer ui.Close()
|
||||||
|
|
||||||
|
g := ui.NewGauge()
|
||||||
|
g.Percent = 50
|
||||||
|
g.Width = 50
|
||||||
|
g.BorderLabel = "Gauge"
|
||||||
|
|
||||||
|
ui.Render(g)
|
||||||
|
|
||||||
|
ui.Loop()
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
package termui
|
@ -0,0 +1,323 @@
|
|||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nsf/termbox-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
Type string
|
||||||
|
Path string
|
||||||
|
From string
|
||||||
|
To string
|
||||||
|
Data interface{}
|
||||||
|
Time int64
|
||||||
|
}
|
||||||
|
|
||||||
|
var sysEvtChs []chan Event
|
||||||
|
|
||||||
|
type EvtKbd struct {
|
||||||
|
KeyStr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func evtKbd(e termbox.Event) EvtKbd {
|
||||||
|
ek := EvtKbd{}
|
||||||
|
|
||||||
|
k := string(e.Ch)
|
||||||
|
pre := ""
|
||||||
|
mod := ""
|
||||||
|
|
||||||
|
if e.Mod == termbox.ModAlt {
|
||||||
|
mod = "M-"
|
||||||
|
}
|
||||||
|
if e.Ch == 0 {
|
||||||
|
if e.Key > 0xFFFF-12 {
|
||||||
|
k = "<f" + strconv.Itoa(0xFFFF-int(e.Key)+1) + ">"
|
||||||
|
} else if e.Key > 0xFFFF-25 {
|
||||||
|
ks := []string{"<insert>", "<delete>", "<home>", "<end>", "<previous>", "<next>", "<up>", "<down>", "<left>", "<right>"}
|
||||||
|
k = ks[0xFFFF-int(e.Key)-12]
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Key <= 0x7F {
|
||||||
|
pre = "C-"
|
||||||
|
k = string('a' - 1 + int(e.Key))
|
||||||
|
kmap := map[termbox.Key][2]string{
|
||||||
|
termbox.KeyCtrlSpace: {"C-", "<space>"},
|
||||||
|
termbox.KeyBackspace: {"", "<backspace>"},
|
||||||
|
termbox.KeyTab: {"", "<tab>"},
|
||||||
|
termbox.KeyEnter: {"", "<enter>"},
|
||||||
|
termbox.KeyEsc: {"", "<escape>"},
|
||||||
|
termbox.KeyCtrlBackslash: {"C-", "\\"},
|
||||||
|
termbox.KeyCtrlSlash: {"C-", "/"},
|
||||||
|
termbox.KeySpace: {"", "<space>"},
|
||||||
|
termbox.KeyCtrl8: {"C-", "8"},
|
||||||
|
}
|
||||||
|
if sk, ok := kmap[e.Key]; ok {
|
||||||
|
pre = sk[0]
|
||||||
|
k = sk[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ek.KeyStr = pre + mod + k
|
||||||
|
return ek
|
||||||
|
}
|
||||||
|
|
||||||
|
func crtTermboxEvt(e termbox.Event) Event {
|
||||||
|
systypemap := map[termbox.EventType]string{
|
||||||
|
termbox.EventKey: "keyboard",
|
||||||
|
termbox.EventResize: "window",
|
||||||
|
termbox.EventMouse: "mouse",
|
||||||
|
termbox.EventError: "error",
|
||||||
|
termbox.EventInterrupt: "interrupt",
|
||||||
|
}
|
||||||
|
ne := Event{From: "/sys", Time: time.Now().Unix()}
|
||||||
|
typ := e.Type
|
||||||
|
ne.Type = systypemap[typ]
|
||||||
|
|
||||||
|
switch typ {
|
||||||
|
case termbox.EventKey:
|
||||||
|
kbd := evtKbd(e)
|
||||||
|
ne.Path = "/sys/kbd/" + kbd.KeyStr
|
||||||
|
ne.Data = kbd
|
||||||
|
case termbox.EventResize:
|
||||||
|
wnd := EvtWnd{}
|
||||||
|
wnd.Width = e.Width
|
||||||
|
wnd.Height = e.Height
|
||||||
|
ne.Path = "/sys/wnd/resize"
|
||||||
|
ne.Data = wnd
|
||||||
|
case termbox.EventError:
|
||||||
|
err := EvtErr(e.Err)
|
||||||
|
ne.Path = "/sys/err"
|
||||||
|
ne.Data = err
|
||||||
|
case termbox.EventMouse:
|
||||||
|
m := EvtMouse{}
|
||||||
|
m.X = e.MouseX
|
||||||
|
m.Y = e.MouseY
|
||||||
|
ne.Path = "/sys/mouse"
|
||||||
|
ne.Data = m
|
||||||
|
}
|
||||||
|
return ne
|
||||||
|
}
|
||||||
|
|
||||||
|
type EvtWnd struct {
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
}
|
||||||
|
|
||||||
|
type EvtMouse struct {
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
Press string
|
||||||
|
}
|
||||||
|
|
||||||
|
type EvtErr error
|
||||||
|
|
||||||
|
func hookTermboxEvt() {
|
||||||
|
for {
|
||||||
|
e := termbox.PollEvent()
|
||||||
|
|
||||||
|
for _, c := range sysEvtChs {
|
||||||
|
go func(ch chan Event) {
|
||||||
|
ch <- crtTermboxEvt(e)
|
||||||
|
}(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSysEvtCh() chan Event {
|
||||||
|
ec := make(chan Event)
|
||||||
|
sysEvtChs = append(sysEvtChs, ec)
|
||||||
|
return ec
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultEvtStream = NewEvtStream()
|
||||||
|
|
||||||
|
type EvtStream struct {
|
||||||
|
sync.RWMutex
|
||||||
|
srcMap map[string]chan Event
|
||||||
|
stream chan Event
|
||||||
|
wg sync.WaitGroup
|
||||||
|
sigStopLoop chan Event
|
||||||
|
Handlers map[string]func(Event)
|
||||||
|
hook func(Event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEvtStream() *EvtStream {
|
||||||
|
return &EvtStream{
|
||||||
|
srcMap: make(map[string]chan Event),
|
||||||
|
stream: make(chan Event),
|
||||||
|
Handlers: make(map[string]func(Event)),
|
||||||
|
sigStopLoop: make(chan Event),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EvtStream) Init() {
|
||||||
|
es.Merge("internal", es.sigStopLoop)
|
||||||
|
go func() {
|
||||||
|
es.wg.Wait()
|
||||||
|
close(es.stream)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanPath(p string) string {
|
||||||
|
if p == "" {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
if p[0] != '/' {
|
||||||
|
p = "/" + p
|
||||||
|
}
|
||||||
|
return path.Clean(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPathMatch(pattern, path string) bool {
|
||||||
|
if len(pattern) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
n := len(pattern)
|
||||||
|
return len(path) >= n && path[0:n] == pattern
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EvtStream) Merge(name string, ec chan Event) {
|
||||||
|
es.Lock()
|
||||||
|
defer es.Unlock()
|
||||||
|
|
||||||
|
es.wg.Add(1)
|
||||||
|
es.srcMap[name] = ec
|
||||||
|
|
||||||
|
go func(a chan Event) {
|
||||||
|
for n := range a {
|
||||||
|
n.From = name
|
||||||
|
es.stream <- n
|
||||||
|
}
|
||||||
|
es.wg.Done()
|
||||||
|
}(ec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EvtStream) Handle(path string, handler func(Event)) {
|
||||||
|
es.Handlers[cleanPath(path)] = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func findMatch(mux map[string]func(Event), path string) string {
|
||||||
|
n := -1
|
||||||
|
pattern := ""
|
||||||
|
for m := range mux {
|
||||||
|
if !isPathMatch(m, path) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(m) > n {
|
||||||
|
pattern = m
|
||||||
|
n = len(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pattern
|
||||||
|
|
||||||
|
}
|
||||||
|
// Remove all existing defined Handlers from the map
|
||||||
|
func (es *EvtStream) ResetHandlers() {
|
||||||
|
for Path, _ := range es.Handlers {
|
||||||
|
delete(es.Handlers, Path)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EvtStream) match(path string) string {
|
||||||
|
return findMatch(es.Handlers, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EvtStream) Hook(f func(Event)) {
|
||||||
|
es.hook = f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EvtStream) Loop() {
|
||||||
|
for e := range es.stream {
|
||||||
|
switch e.Path {
|
||||||
|
case "/sig/stoploop":
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go func(a Event) {
|
||||||
|
es.RLock()
|
||||||
|
defer es.RUnlock()
|
||||||
|
if pattern := es.match(a.Path); pattern != "" {
|
||||||
|
es.Handlers[pattern](a)
|
||||||
|
}
|
||||||
|
}(e)
|
||||||
|
if es.hook != nil {
|
||||||
|
es.hook(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EvtStream) StopLoop() {
|
||||||
|
go func() {
|
||||||
|
e := Event{
|
||||||
|
Path: "/sig/stoploop",
|
||||||
|
}
|
||||||
|
es.sigStopLoop <- e
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Merge(name string, ec chan Event) {
|
||||||
|
DefaultEvtStream.Merge(name, ec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Handle(path string, handler func(Event)) {
|
||||||
|
DefaultEvtStream.Handle(path, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Loop() {
|
||||||
|
DefaultEvtStream.Loop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func StopLoop() {
|
||||||
|
DefaultEvtStream.StopLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
type EvtTimer struct {
|
||||||
|
Duration time.Duration
|
||||||
|
Count uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTimerCh(du time.Duration) chan Event {
|
||||||
|
t := make(chan Event)
|
||||||
|
|
||||||
|
go func(a chan Event) {
|
||||||
|
n := uint64(0)
|
||||||
|
for {
|
||||||
|
n++
|
||||||
|
time.Sleep(du)
|
||||||
|
e := Event{}
|
||||||
|
e.Type = "timer"
|
||||||
|
e.Path = "/timer/" + du.String()
|
||||||
|
e.Time = time.Now().Unix()
|
||||||
|
e.Data = EvtTimer{
|
||||||
|
Duration: du,
|
||||||
|
Count: n,
|
||||||
|
}
|
||||||
|
t <- e
|
||||||
|
|
||||||
|
}
|
||||||
|
}(t)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefualtHandler = func(e Event) {
|
||||||
|
}
|
||||||
|
|
||||||
|
var usrEvtCh = make(chan Event)
|
||||||
|
|
||||||
|
func SendCustomEvt(path string, data interface{}) {
|
||||||
|
e := Event{}
|
||||||
|
e.Path = path
|
||||||
|
e.Data = data
|
||||||
|
e.Time = time.Now().Unix()
|
||||||
|
usrEvtCh <- e
|
||||||
|
}
|
@ -0,0 +1,279 @@
|
|||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
// GridBufferer introduces a Bufferer that can be manipulated by Grid.
|
||||||
|
type GridBufferer interface {
|
||||||
|
Bufferer
|
||||||
|
GetHeight() int
|
||||||
|
SetWidth(int)
|
||||||
|
SetX(int)
|
||||||
|
SetY(int)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row builds a layout tree
|
||||||
|
type Row struct {
|
||||||
|
Cols []*Row //children
|
||||||
|
Widget GridBufferer // root
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
Span int
|
||||||
|
Offset int
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate and set the underlying layout tree's x, y, height and width.
|
||||||
|
func (r *Row) calcLayout() {
|
||||||
|
r.assignWidth(r.Width)
|
||||||
|
r.Height = r.solveHeight()
|
||||||
|
r.assignX(r.X)
|
||||||
|
r.assignY(r.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tell if the node is leaf in the tree.
|
||||||
|
func (r *Row) isLeaf() bool {
|
||||||
|
return r.Cols == nil || len(r.Cols) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Row) isRenderableLeaf() bool {
|
||||||
|
return r.isLeaf() && r.Widget != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// assign widgets' (and their parent rows') width recursively.
|
||||||
|
func (r *Row) assignWidth(w int) {
|
||||||
|
r.SetWidth(w)
|
||||||
|
|
||||||
|
accW := 0 // acc span and offset
|
||||||
|
calcW := make([]int, len(r.Cols)) // calculated width
|
||||||
|
calcOftX := make([]int, len(r.Cols)) // computated start position of x
|
||||||
|
|
||||||
|
for i, c := range r.Cols {
|
||||||
|
accW += c.Span + c.Offset
|
||||||
|
cw := int(float64(c.Span*r.Width) / 12.0)
|
||||||
|
|
||||||
|
if i >= 1 {
|
||||||
|
calcOftX[i] = calcOftX[i-1] +
|
||||||
|
calcW[i-1] +
|
||||||
|
int(float64(r.Cols[i-1].Offset*r.Width)/12.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// use up the space if it is the last col
|
||||||
|
if i == len(r.Cols)-1 && accW == 12 {
|
||||||
|
cw = r.Width - calcOftX[i]
|
||||||
|
}
|
||||||
|
calcW[i] = cw
|
||||||
|
r.Cols[i].assignWidth(cw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bottom up calc and set rows' (and their widgets') height,
|
||||||
|
// return r's total height.
|
||||||
|
func (r *Row) solveHeight() int {
|
||||||
|
if r.isRenderableLeaf() {
|
||||||
|
r.Height = r.Widget.GetHeight()
|
||||||
|
return r.Widget.GetHeight()
|
||||||
|
}
|
||||||
|
|
||||||
|
maxh := 0
|
||||||
|
if !r.isLeaf() {
|
||||||
|
for _, c := range r.Cols {
|
||||||
|
nh := c.solveHeight()
|
||||||
|
// when embed rows in Cols, row widgets stack up
|
||||||
|
if r.Widget != nil {
|
||||||
|
nh += r.Widget.GetHeight()
|
||||||
|
}
|
||||||
|
if nh > maxh {
|
||||||
|
maxh = nh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Height = maxh
|
||||||
|
return maxh
|
||||||
|
}
|
||||||
|
|
||||||
|
// recursively assign x position for r tree.
|
||||||
|
func (r *Row) assignX(x int) {
|
||||||
|
r.SetX(x)
|
||||||
|
|
||||||
|
if !r.isLeaf() {
|
||||||
|
acc := 0
|
||||||
|
for i, c := range r.Cols {
|
||||||
|
if c.Offset != 0 {
|
||||||
|
acc += int(float64(c.Offset*r.Width) / 12.0)
|
||||||
|
}
|
||||||
|
r.Cols[i].assignX(x + acc)
|
||||||
|
acc += c.Width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// recursively assign y position to r.
|
||||||
|
func (r *Row) assignY(y int) {
|
||||||
|
r.SetY(y)
|
||||||
|
|
||||||
|
if r.isLeaf() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range r.Cols {
|
||||||
|
acc := 0
|
||||||
|
if r.Widget != nil {
|
||||||
|
acc = r.Widget.GetHeight()
|
||||||
|
}
|
||||||
|
r.Cols[i].assignY(y + acc)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeight implements GridBufferer interface.
|
||||||
|
func (r Row) GetHeight() int {
|
||||||
|
return r.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetX implements GridBufferer interface.
|
||||||
|
func (r *Row) SetX(x int) {
|
||||||
|
r.X = x
|
||||||
|
if r.Widget != nil {
|
||||||
|
r.Widget.SetX(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetY implements GridBufferer interface.
|
||||||
|
func (r *Row) SetY(y int) {
|
||||||
|
r.Y = y
|
||||||
|
if r.Widget != nil {
|
||||||
|
r.Widget.SetY(y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWidth implements GridBufferer interface.
|
||||||
|
func (r *Row) SetWidth(w int) {
|
||||||
|
r.Width = w
|
||||||
|
if r.Widget != nil {
|
||||||
|
r.Widget.SetWidth(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer implements Bufferer interface,
|
||||||
|
// recursively merge all widgets buffer
|
||||||
|
func (r *Row) Buffer() Buffer {
|
||||||
|
merged := NewBuffer()
|
||||||
|
|
||||||
|
if r.isRenderableLeaf() {
|
||||||
|
return r.Widget.Buffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// for those are not leaves but have a renderable widget
|
||||||
|
if r.Widget != nil {
|
||||||
|
merged.Merge(r.Widget.Buffer())
|
||||||
|
}
|
||||||
|
|
||||||
|
// collect buffer from children
|
||||||
|
if !r.isLeaf() {
|
||||||
|
for _, c := range r.Cols {
|
||||||
|
merged.Merge(c.Buffer())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grid implements 12 columns system.
|
||||||
|
// A simple example:
|
||||||
|
/*
|
||||||
|
import ui "github.com/gizak/termui"
|
||||||
|
// init and create widgets...
|
||||||
|
|
||||||
|
// build
|
||||||
|
ui.Body.AddRows(
|
||||||
|
ui.NewRow(
|
||||||
|
ui.NewCol(6, 0, widget0),
|
||||||
|
ui.NewCol(6, 0, widget1)),
|
||||||
|
ui.NewRow(
|
||||||
|
ui.NewCol(3, 0, widget2),
|
||||||
|
ui.NewCol(3, 0, widget30, widget31, widget32),
|
||||||
|
ui.NewCol(6, 0, widget4)))
|
||||||
|
|
||||||
|
// calculate layout
|
||||||
|
ui.Body.Align()
|
||||||
|
|
||||||
|
ui.Render(ui.Body)
|
||||||
|
*/
|
||||||
|
type Grid struct {
|
||||||
|
Rows []*Row
|
||||||
|
Width int
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
BgColor Attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGrid returns *Grid with given rows.
|
||||||
|
func NewGrid(rows ...*Row) *Grid {
|
||||||
|
return &Grid{Rows: rows}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRows appends given rows to Grid.
|
||||||
|
func (g *Grid) AddRows(rs ...*Row) {
|
||||||
|
g.Rows = append(g.Rows, rs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRow creates a new row out of given columns.
|
||||||
|
func NewRow(cols ...*Row) *Row {
|
||||||
|
rs := &Row{Span: 12, Cols: cols}
|
||||||
|
return rs
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCol accepts: widgets are LayoutBufferer or widgets is A NewRow.
|
||||||
|
// Note that if multiple widgets are provided, they will stack up in the col.
|
||||||
|
func NewCol(span, offset int, widgets ...GridBufferer) *Row {
|
||||||
|
r := &Row{Span: span, Offset: offset}
|
||||||
|
|
||||||
|
if widgets != nil && len(widgets) == 1 {
|
||||||
|
wgt := widgets[0]
|
||||||
|
nw, isRow := wgt.(*Row)
|
||||||
|
if isRow {
|
||||||
|
r.Cols = nw.Cols
|
||||||
|
} else {
|
||||||
|
r.Widget = wgt
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Cols = []*Row{}
|
||||||
|
ir := r
|
||||||
|
for _, w := range widgets {
|
||||||
|
nr := &Row{Span: 12, Widget: w}
|
||||||
|
ir.Cols = []*Row{nr}
|
||||||
|
ir = nr
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Align calculate each rows' layout.
|
||||||
|
func (g *Grid) Align() {
|
||||||
|
h := 0
|
||||||
|
for _, r := range g.Rows {
|
||||||
|
r.SetWidth(g.Width)
|
||||||
|
r.SetX(g.X)
|
||||||
|
r.SetY(g.Y + h)
|
||||||
|
r.calcLayout()
|
||||||
|
h += r.GetHeight()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer implments Bufferer interface.
|
||||||
|
func (g Grid) Buffer() Buffer {
|
||||||
|
buf := NewBuffer()
|
||||||
|
|
||||||
|
for _, r := range g.Rows {
|
||||||
|
buf.Merge(r.Buffer())
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
var Body *Grid
|
@ -0,0 +1,222 @@
|
|||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
tm "github.com/nsf/termbox-go"
|
||||||
|
)
|
||||||
|
import rw "github.com/mattn/go-runewidth"
|
||||||
|
|
||||||
|
/* ---------------Port from termbox-go --------------------- */
|
||||||
|
|
||||||
|
// Attribute is printable cell's color and style.
|
||||||
|
type Attribute uint16
|
||||||
|
|
||||||
|
// 8 basic clolrs
|
||||||
|
const (
|
||||||
|
ColorDefault Attribute = iota
|
||||||
|
ColorBlack
|
||||||
|
ColorRed
|
||||||
|
ColorGreen
|
||||||
|
ColorYellow
|
||||||
|
ColorBlue
|
||||||
|
ColorMagenta
|
||||||
|
ColorCyan
|
||||||
|
ColorWhite
|
||||||
|
)
|
||||||
|
|
||||||
|
//Have a constant that defines number of colors
|
||||||
|
const NumberofColors = 8
|
||||||
|
|
||||||
|
// Text style
|
||||||
|
const (
|
||||||
|
AttrBold Attribute = 1 << (iota + 9)
|
||||||
|
AttrUnderline
|
||||||
|
AttrReverse
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dot = "…"
|
||||||
|
dotw = rw.StringWidth(dot)
|
||||||
|
)
|
||||||
|
|
||||||
|
/* ----------------------- End ----------------------------- */
|
||||||
|
|
||||||
|
func toTmAttr(x Attribute) tm.Attribute {
|
||||||
|
return tm.Attribute(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func str2runes(s string) []rune {
|
||||||
|
return []rune(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here for backwards-compatibility.
|
||||||
|
func trimStr2Runes(s string, w int) []rune {
|
||||||
|
return TrimStr2Runes(s, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimStr2Runes trims string to w[-1 rune], appends …, and returns the runes
|
||||||
|
// of that string if string is grather then n. If string is small then w,
|
||||||
|
// return the runes.
|
||||||
|
func TrimStr2Runes(s string, w int) []rune {
|
||||||
|
if w <= 0 {
|
||||||
|
return []rune{}
|
||||||
|
}
|
||||||
|
|
||||||
|
sw := rw.StringWidth(s)
|
||||||
|
if sw > w {
|
||||||
|
return []rune(rw.Truncate(s, w, dot))
|
||||||
|
}
|
||||||
|
return str2runes(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimStrIfAppropriate trim string to "s[:-1] + …"
|
||||||
|
// if string > width otherwise return string
|
||||||
|
func TrimStrIfAppropriate(s string, w int) string {
|
||||||
|
if w <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
sw := rw.StringWidth(s)
|
||||||
|
if sw > w {
|
||||||
|
return rw.Truncate(s, w, dot)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func strWidth(s string) int {
|
||||||
|
return rw.StringWidth(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func charWidth(ch rune) int {
|
||||||
|
return rw.RuneWidth(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
var whiteSpaceRegex = regexp.MustCompile(`\s`)
|
||||||
|
|
||||||
|
// StringToAttribute converts text to a termui attribute. You may specifiy more
|
||||||
|
// then one attribute like that: "BLACK, BOLD, ...". All whitespaces
|
||||||
|
// are ignored.
|
||||||
|
func StringToAttribute(text string) Attribute {
|
||||||
|
text = whiteSpaceRegex.ReplaceAllString(strings.ToLower(text), "")
|
||||||
|
attributes := strings.Split(text, ",")
|
||||||
|
result := Attribute(0)
|
||||||
|
|
||||||
|
for _, theAttribute := range attributes {
|
||||||
|
var match Attribute
|
||||||
|
switch theAttribute {
|
||||||
|
case "reset", "default":
|
||||||
|
match = ColorDefault
|
||||||
|
|
||||||
|
case "black":
|
||||||
|
match = ColorBlack
|
||||||
|
|
||||||
|
case "red":
|
||||||
|
match = ColorRed
|
||||||
|
|
||||||
|
case "green":
|
||||||
|
match = ColorGreen
|
||||||
|
|
||||||
|
case "yellow":
|
||||||
|
match = ColorYellow
|
||||||
|
|
||||||
|
case "blue":
|
||||||
|
match = ColorBlue
|
||||||
|
|
||||||
|
case "magenta":
|
||||||
|
match = ColorMagenta
|
||||||
|
|
||||||
|
case "cyan":
|
||||||
|
match = ColorCyan
|
||||||
|
|
||||||
|
case "white":
|
||||||
|
match = ColorWhite
|
||||||
|
|
||||||
|
case "bold":
|
||||||
|
match = AttrBold
|
||||||
|
|
||||||
|
case "underline":
|
||||||
|
match = AttrUnderline
|
||||||
|
|
||||||
|
case "reverse":
|
||||||
|
match = AttrReverse
|
||||||
|
}
|
||||||
|
|
||||||
|
result |= match
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextCells returns a coloured text cells []Cell
|
||||||
|
func TextCells(s string, fg, bg Attribute) []Cell {
|
||||||
|
cs := make([]Cell, 0, len(s))
|
||||||
|
|
||||||
|
// sequence := MarkdownTextRendererFactory{}.TextRenderer(s).Render(fg, bg)
|
||||||
|
// runes := []rune(sequence.NormalizedText)
|
||||||
|
runes := str2runes(s)
|
||||||
|
|
||||||
|
for n := range runes {
|
||||||
|
// point, _ := sequence.PointAt(n, 0, 0)
|
||||||
|
// cs = append(cs, Cell{point.Ch, point.Fg, point.Bg})
|
||||||
|
cs = append(cs, Cell{runes[n], fg, bg})
|
||||||
|
}
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Width returns the actual screen space the cell takes (usually 1 or 2).
|
||||||
|
func (c Cell) Width() int {
|
||||||
|
return charWidth(c.Ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy return a copy of c
|
||||||
|
func (c Cell) Copy() Cell {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimTxCells trims the overflowed text cells sequence.
|
||||||
|
func TrimTxCells(cs []Cell, w int) []Cell {
|
||||||
|
if len(cs) <= w {
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
return cs[:w]
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTrimTxCls trims the overflowed text cells sequence and append dots at the end.
|
||||||
|
func DTrimTxCls(cs []Cell, w int) []Cell {
|
||||||
|
l := len(cs)
|
||||||
|
if l <= 0 {
|
||||||
|
return []Cell{}
|
||||||
|
}
|
||||||
|
|
||||||
|
rt := make([]Cell, 0, w)
|
||||||
|
csw := 0
|
||||||
|
for i := 0; i < l && csw <= w; i++ {
|
||||||
|
c := cs[i]
|
||||||
|
cw := c.Width()
|
||||||
|
|
||||||
|
if cw+csw < w {
|
||||||
|
rt = append(rt, c)
|
||||||
|
csw += cw
|
||||||
|
} else {
|
||||||
|
rt = append(rt, Cell{'…', c.Fg, c.Bg})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rt
|
||||||
|
}
|
||||||
|
|
||||||
|
func CellsToStr(cs []Cell) string {
|
||||||
|
str := ""
|
||||||
|
for _, c := range cs {
|
||||||
|
str += string(c.Ch)
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
@ -0,0 +1,331 @@
|
|||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// only 16 possible combinations, why bother
|
||||||
|
var braillePatterns = map[[2]int]rune{
|
||||||
|
[2]int{0, 0}: '⣀',
|
||||||
|
[2]int{0, 1}: '⡠',
|
||||||
|
[2]int{0, 2}: '⡐',
|
||||||
|
[2]int{0, 3}: '⡈',
|
||||||
|
|
||||||
|
[2]int{1, 0}: '⢄',
|
||||||
|
[2]int{1, 1}: '⠤',
|
||||||
|
[2]int{1, 2}: '⠔',
|
||||||
|
[2]int{1, 3}: '⠌',
|
||||||
|
|
||||||
|
[2]int{2, 0}: '⢂',
|
||||||
|
[2]int{2, 1}: '⠢',
|
||||||
|
[2]int{2, 2}: '⠒',
|
||||||
|
[2]int{2, 3}: '⠊',
|
||||||
|
|
||||||
|
[2]int{3, 0}: '⢁',
|
||||||
|
[2]int{3, 1}: '⠡',
|
||||||
|
[2]int{3, 2}: '⠑',
|
||||||
|
[2]int{3, 3}: '⠉',
|
||||||
|
}
|
||||||
|
|
||||||
|
var lSingleBraille = [4]rune{'\u2840', '⠄', '⠂', '⠁'}
|
||||||
|
var rSingleBraille = [4]rune{'\u2880', '⠠', '⠐', '⠈'}
|
||||||
|
|
||||||
|
// LineChart has two modes: braille(default) and dot. Using braille gives 2x capicity as dot mode,
|
||||||
|
// because one braille char can represent two data points.
|
||||||
|
/*
|
||||||
|
lc := termui.NewLineChart()
|
||||||
|
lc.BorderLabel = "braille-mode Line Chart"
|
||||||
|
lc.Data = [1.2, 1.3, 1.5, 1.7, 1.5, 1.6, 1.8, 2.0]
|
||||||
|
lc.Width = 50
|
||||||
|
lc.Height = 12
|
||||||
|
lc.AxesColor = termui.ColorWhite
|
||||||
|
lc.LineColor = termui.ColorGreen | termui.AttrBold
|
||||||
|
// termui.Render(lc)...
|
||||||
|
*/
|
||||||
|
type LineChart struct {
|
||||||
|
Block
|
||||||
|
Data []float64
|
||||||
|
DataLabels []string // if unset, the data indices will be used
|
||||||
|
Mode string // braille | dot
|
||||||
|
DotStyle rune
|
||||||
|
LineColor Attribute
|
||||||
|
scale float64 // data span per cell on y-axis
|
||||||
|
AxesColor Attribute
|
||||||
|
drawingX int
|
||||||
|
drawingY int
|
||||||
|
axisYHeight int
|
||||||
|
axisXWidth int
|
||||||
|
axisYLabelGap int
|
||||||
|
axisXLabelGap int
|
||||||
|
topValue float64
|
||||||
|
bottomValue float64
|
||||||
|
labelX [][]rune
|
||||||
|
labelY [][]rune
|
||||||
|
labelYSpace int
|
||||||
|
maxY float64
|
||||||
|
minY float64
|
||||||
|
autoLabels bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLineChart returns a new LineChart with current theme.
|
||||||
|
func NewLineChart() *LineChart {
|
||||||
|
lc := &LineChart{Block: *NewBlock()}
|
||||||
|
lc.AxesColor = ThemeAttr("linechart.axes.fg")
|
||||||
|
lc.LineColor = ThemeAttr("linechart.line.fg")
|
||||||
|
lc.Mode = "braille"
|
||||||
|
lc.DotStyle = '•'
|
||||||
|
lc.axisXLabelGap = 2
|
||||||
|
lc.axisYLabelGap = 1
|
||||||
|
lc.bottomValue = math.Inf(1)
|
||||||
|
lc.topValue = math.Inf(-1)
|
||||||
|
return lc
|
||||||
|
}
|
||||||
|
|
||||||
|
// one cell contains two data points
|
||||||
|
// so the capicity is 2x as dot-mode
|
||||||
|
func (lc *LineChart) renderBraille() Buffer {
|
||||||
|
buf := NewBuffer()
|
||||||
|
|
||||||
|
// return: b -> which cell should the point be in
|
||||||
|
// m -> in the cell, divided into 4 equal height levels, which subcell?
|
||||||
|
getPos := func(d float64) (b, m int) {
|
||||||
|
cnt4 := int((d-lc.bottomValue)/(lc.scale/4) + 0.5)
|
||||||
|
b = cnt4 / 4
|
||||||
|
m = cnt4 % 4
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// plot points
|
||||||
|
for i := 0; 2*i+1 < len(lc.Data) && i < lc.axisXWidth; i++ {
|
||||||
|
b0, m0 := getPos(lc.Data[2*i])
|
||||||
|
b1, m1 := getPos(lc.Data[2*i+1])
|
||||||
|
|
||||||
|
if b0 == b1 {
|
||||||
|
c := Cell{
|
||||||
|
Ch: braillePatterns[[2]int{m0, m1}],
|
||||||
|
Bg: lc.Bg,
|
||||||
|
Fg: lc.LineColor,
|
||||||
|
}
|
||||||
|
y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
|
||||||
|
x := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
} else {
|
||||||
|
c0 := Cell{Ch: lSingleBraille[m0],
|
||||||
|
Fg: lc.LineColor,
|
||||||
|
Bg: lc.Bg}
|
||||||
|
x0 := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
|
||||||
|
y0 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
|
||||||
|
buf.Set(x0, y0, c0)
|
||||||
|
|
||||||
|
c1 := Cell{Ch: rSingleBraille[m1],
|
||||||
|
Fg: lc.LineColor,
|
||||||
|
Bg: lc.Bg}
|
||||||
|
x1 := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
|
||||||
|
y1 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b1
|
||||||
|
buf.Set(x1, y1, c1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *LineChart) renderDot() Buffer {
|
||||||
|
buf := NewBuffer()
|
||||||
|
for i := 0; i < len(lc.Data) && i < lc.axisXWidth; i++ {
|
||||||
|
c := Cell{
|
||||||
|
Ch: lc.DotStyle,
|
||||||
|
Fg: lc.LineColor,
|
||||||
|
Bg: lc.Bg,
|
||||||
|
}
|
||||||
|
x := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
|
||||||
|
y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - int((lc.Data[i]-lc.bottomValue)/lc.scale+0.5)
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *LineChart) calcLabelX() {
|
||||||
|
lc.labelX = [][]rune{}
|
||||||
|
|
||||||
|
for i, l := 0, 0; i < len(lc.DataLabels) && l < lc.axisXWidth; i++ {
|
||||||
|
if lc.Mode == "dot" {
|
||||||
|
if l >= len(lc.DataLabels) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
s := str2runes(lc.DataLabels[l])
|
||||||
|
w := strWidth(lc.DataLabels[l])
|
||||||
|
if l+w <= lc.axisXWidth {
|
||||||
|
lc.labelX = append(lc.labelX, s)
|
||||||
|
}
|
||||||
|
l += w + lc.axisXLabelGap
|
||||||
|
} else { // braille
|
||||||
|
if 2*l >= len(lc.DataLabels) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
s := str2runes(lc.DataLabels[2*l])
|
||||||
|
w := strWidth(lc.DataLabels[2*l])
|
||||||
|
if l+w <= lc.axisXWidth {
|
||||||
|
lc.labelX = append(lc.labelX, s)
|
||||||
|
}
|
||||||
|
l += w + lc.axisXLabelGap
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shortenFloatVal(x float64) string {
|
||||||
|
s := fmt.Sprintf("%.2f", x)
|
||||||
|
if len(s)-3 > 3 {
|
||||||
|
s = fmt.Sprintf("%.2e", x)
|
||||||
|
}
|
||||||
|
|
||||||
|
if x < 0 {
|
||||||
|
s = fmt.Sprintf("%.2f", x)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *LineChart) calcLabelY() {
|
||||||
|
span := lc.topValue - lc.bottomValue
|
||||||
|
lc.scale = span / float64(lc.axisYHeight)
|
||||||
|
|
||||||
|
n := (1 + lc.axisYHeight) / (lc.axisYLabelGap + 1)
|
||||||
|
lc.labelY = make([][]rune, n)
|
||||||
|
maxLen := 0
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
s := str2runes(shortenFloatVal(lc.bottomValue + float64(i)*span/float64(n)))
|
||||||
|
if len(s) > maxLen {
|
||||||
|
maxLen = len(s)
|
||||||
|
}
|
||||||
|
lc.labelY[i] = s
|
||||||
|
}
|
||||||
|
|
||||||
|
lc.labelYSpace = maxLen
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *LineChart) calcLayout() {
|
||||||
|
// set datalabels if it is not provided
|
||||||
|
if (lc.DataLabels == nil || len(lc.DataLabels) == 0) || lc.autoLabels {
|
||||||
|
lc.autoLabels = true
|
||||||
|
lc.DataLabels = make([]string, len(lc.Data))
|
||||||
|
for i := range lc.Data {
|
||||||
|
lc.DataLabels[i] = fmt.Sprint(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// lazy increase, to avoid y shaking frequently
|
||||||
|
// update bound Y when drawing is gonna overflow
|
||||||
|
lc.minY = lc.Data[0]
|
||||||
|
lc.maxY = lc.Data[0]
|
||||||
|
|
||||||
|
// valid visible range
|
||||||
|
vrange := lc.innerArea.Dx()
|
||||||
|
if lc.Mode == "braille" {
|
||||||
|
vrange = 2 * lc.innerArea.Dx()
|
||||||
|
}
|
||||||
|
if vrange > len(lc.Data) {
|
||||||
|
vrange = len(lc.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range lc.Data[:vrange] {
|
||||||
|
if v > lc.maxY {
|
||||||
|
lc.maxY = v
|
||||||
|
}
|
||||||
|
if v < lc.minY {
|
||||||
|
lc.minY = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span := lc.maxY - lc.minY
|
||||||
|
|
||||||
|
if lc.minY < lc.bottomValue {
|
||||||
|
lc.bottomValue = lc.minY - 0.2*span
|
||||||
|
}
|
||||||
|
|
||||||
|
if lc.maxY > lc.topValue {
|
||||||
|
lc.topValue = lc.maxY + 0.2*span
|
||||||
|
}
|
||||||
|
|
||||||
|
lc.axisYHeight = lc.innerArea.Dy() - 2
|
||||||
|
lc.calcLabelY()
|
||||||
|
|
||||||
|
lc.axisXWidth = lc.innerArea.Dx() - 1 - lc.labelYSpace
|
||||||
|
lc.calcLabelX()
|
||||||
|
|
||||||
|
lc.drawingX = lc.innerArea.Min.X + 1 + lc.labelYSpace
|
||||||
|
lc.drawingY = lc.innerArea.Min.Y
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *LineChart) plotAxes() Buffer {
|
||||||
|
buf := NewBuffer()
|
||||||
|
|
||||||
|
origY := lc.innerArea.Min.Y + lc.innerArea.Dy() - 2
|
||||||
|
origX := lc.innerArea.Min.X + lc.labelYSpace
|
||||||
|
|
||||||
|
buf.Set(origX, origY, Cell{Ch: ORIGIN, Fg: lc.AxesColor, Bg: lc.Bg})
|
||||||
|
|
||||||
|
for x := origX + 1; x < origX+lc.axisXWidth; x++ {
|
||||||
|
buf.Set(x, origY, Cell{Ch: HDASH, Fg: lc.AxesColor, Bg: lc.Bg})
|
||||||
|
}
|
||||||
|
|
||||||
|
for dy := 1; dy <= lc.axisYHeight; dy++ {
|
||||||
|
buf.Set(origX, origY-dy, Cell{Ch: VDASH, Fg: lc.AxesColor, Bg: lc.Bg})
|
||||||
|
}
|
||||||
|
|
||||||
|
// x label
|
||||||
|
oft := 0
|
||||||
|
for _, rs := range lc.labelX {
|
||||||
|
if oft+len(rs) > lc.axisXWidth {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for j, r := range rs {
|
||||||
|
c := Cell{
|
||||||
|
Ch: r,
|
||||||
|
Fg: lc.AxesColor,
|
||||||
|
Bg: lc.Bg,
|
||||||
|
}
|
||||||
|
x := origX + oft + j
|
||||||
|
y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 1
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
}
|
||||||
|
oft += len(rs) + lc.axisXLabelGap
|
||||||
|
}
|
||||||
|
|
||||||
|
// y labels
|
||||||
|
for i, rs := range lc.labelY {
|
||||||
|
for j, r := range rs {
|
||||||
|
buf.Set(
|
||||||
|
lc.innerArea.Min.X+j,
|
||||||
|
origY-i*(lc.axisYLabelGap+1),
|
||||||
|
Cell{Ch: r, Fg: lc.AxesColor, Bg: lc.Bg})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer implements Bufferer interface.
|
||||||
|
func (lc *LineChart) Buffer() Buffer {
|
||||||
|
buf := lc.Block.Buffer()
|
||||||
|
|
||||||
|
if lc.Data == nil || len(lc.Data) == 0 {
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
lc.calcLayout()
|
||||||
|
buf.Merge(lc.plotAxes())
|
||||||
|
|
||||||
|
if lc.Mode == "dot" {
|
||||||
|
buf.Merge(lc.renderDot())
|
||||||
|
} else {
|
||||||
|
buf.Merge(lc.renderBraille())
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
const VDASH = '┊'
|
||||||
|
const HDASH = '┈'
|
||||||
|
const ORIGIN = '└'
|
@ -0,0 +1,11 @@
|
|||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
const VDASH = '|'
|
||||||
|
const HDASH = '-'
|
||||||
|
const ORIGIN = '+'
|
@ -0,0 +1,78 @@
|
|||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
|
||||||
|
// Align is the position of the gauge's label.
|
||||||
|
type Align uint
|
||||||
|
|
||||||
|
// All supported positions.
|
||||||
|
const (
|
||||||
|
AlignNone Align = 0
|
||||||
|
AlignLeft Align = 1 << iota
|
||||||
|
AlignRight
|
||||||
|
AlignBottom
|
||||||
|
AlignTop
|
||||||
|
AlignCenterVertical
|
||||||
|
AlignCenterHorizontal
|
||||||
|
AlignCenter = AlignCenterVertical | AlignCenterHorizontal
|
||||||
|
)
|
||||||
|
|
||||||
|
func AlignArea(parent, child image.Rectangle, a Align) image.Rectangle {
|
||||||
|
w, h := child.Dx(), child.Dy()
|
||||||
|
|
||||||
|
// parent center
|
||||||
|
pcx, pcy := parent.Min.X+parent.Dx()/2, parent.Min.Y+parent.Dy()/2
|
||||||
|
// child center
|
||||||
|
ccx, ccy := child.Min.X+child.Dx()/2, child.Min.Y+child.Dy()/2
|
||||||
|
|
||||||
|
if a&AlignLeft == AlignLeft {
|
||||||
|
child.Min.X = parent.Min.X
|
||||||
|
child.Max.X = child.Min.X + w
|
||||||
|
}
|
||||||
|
|
||||||
|
if a&AlignRight == AlignRight {
|
||||||
|
child.Max.X = parent.Max.X
|
||||||
|
child.Min.X = child.Max.X - w
|
||||||
|
}
|
||||||
|
|
||||||
|
if a&AlignBottom == AlignBottom {
|
||||||
|
child.Max.Y = parent.Max.Y
|
||||||
|
child.Min.Y = child.Max.Y - h
|
||||||
|
}
|
||||||
|
|
||||||
|
if a&AlignTop == AlignRight {
|
||||||
|
child.Min.Y = parent.Min.Y
|
||||||
|
child.Max.Y = child.Min.Y + h
|
||||||
|
}
|
||||||
|
|
||||||
|
if a&AlignCenterHorizontal == AlignCenterHorizontal {
|
||||||
|
child.Min.X += pcx - ccx
|
||||||
|
child.Max.X = child.Min.X + w
|
||||||
|
}
|
||||||
|
|
||||||
|
if a&AlignCenterVertical == AlignCenterVertical {
|
||||||
|
child.Min.Y += pcy - ccy
|
||||||
|
child.Max.Y = child.Min.Y + h
|
||||||
|
}
|
||||||
|
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
|
||||||
|
func MoveArea(a image.Rectangle, dx, dy int) image.Rectangle {
|
||||||
|
a.Min.X += dx
|
||||||
|
a.Max.X += dx
|
||||||
|
a.Min.Y += dy
|
||||||
|
a.Max.Y += dy
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
var termWidth int
|
||||||
|
var termHeight int
|
||||||
|
|
||||||
|
func TermRect() image.Rectangle {
|
||||||
|
return image.Rect(0, 0, termWidth, termHeight)
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
tm "github.com/nsf/termbox-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bufferer should be implemented by all renderable components.
|
||||||
|
type Bufferer interface {
|
||||||
|
Buffer() Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close finalizes termui library,
|
||||||
|
// should be called after successful initialization when termui's functionality isn't required anymore.
|
||||||
|
func Close() {
|
||||||
|
tm.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
var renderLock sync.Mutex
|
||||||
|
|
||||||
|
func termSync() {
|
||||||
|
renderLock.Lock()
|
||||||
|
tm.Sync()
|
||||||
|
termWidth, termHeight = tm.Size()
|
||||||
|
renderLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TermWidth returns the current terminal's width.
|
||||||
|
func TermWidth() int {
|
||||||
|
termSync()
|
||||||
|
return termWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// TermHeight returns the current terminal's height.
|
||||||
|
func TermHeight() int {
|
||||||
|
termSync()
|
||||||
|
return termHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
func Clear() {
|
||||||
|
tm.Clear(tm.ColorDefault, toTmAttr(ThemeAttr("bg")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearArea(r image.Rectangle, bg Attribute) {
|
||||||
|
for i := r.Min.X; i < r.Max.X; i++ {
|
||||||
|
for j := r.Min.Y; j < r.Max.Y; j++ {
|
||||||
|
tm.SetCell(i, j, ' ', tm.ColorDefault, toTmAttr(bg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClearArea(r image.Rectangle, bg Attribute) {
|
||||||
|
clearArea(r, bg)
|
||||||
|
tm.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
var renderJobs chan []Bufferer
|
||||||
|
|
||||||
|
func Render(bs ...Bufferer) {
|
||||||
|
//go func() { renderJobs <- bs }()
|
||||||
|
renderJobs <- bs
|
||||||
|
}
|
@ -0,0 +1,167 @@
|
|||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
// Sparkline is like: ▅▆▂▂▅▇▂▂▃▆▆▆▅▃. The data points should be non-negative integers.
|
||||||
|
/*
|
||||||
|
data := []int{4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1}
|
||||||
|
spl := termui.NewSparkline()
|
||||||
|
spl.Data = data
|
||||||
|
spl.Title = "Sparkline 0"
|
||||||
|
spl.LineColor = termui.ColorGreen
|
||||||
|
*/
|
||||||
|
type Sparkline struct {
|
||||||
|
Data []int
|
||||||
|
Height int
|
||||||
|
Title string
|
||||||
|
TitleColor Attribute
|
||||||
|
LineColor Attribute
|
||||||
|
displayHeight int
|
||||||
|
scale float32
|
||||||
|
max int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sparklines is a renderable widget which groups together the given sparklines.
|
||||||
|
/*
|
||||||
|
spls := termui.NewSparklines(spl0,spl1,spl2) //...
|
||||||
|
spls.Height = 2
|
||||||
|
spls.Width = 20
|
||||||
|
*/
|
||||||
|
type Sparklines struct {
|
||||||
|
Block
|
||||||
|
Lines []Sparkline
|
||||||
|
displayLines int
|
||||||
|
displayWidth int
|
||||||
|
}
|
||||||
|
|
||||||
|
var sparks = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
||||||
|
|
||||||
|
// Add appends a given Sparkline to s *Sparklines.
|
||||||
|
func (s *Sparklines) Add(sl Sparkline) {
|
||||||
|
s.Lines = append(s.Lines, sl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSparkline returns a unrenderable single sparkline that intended to be added into Sparklines.
|
||||||
|
func NewSparkline() Sparkline {
|
||||||
|
return Sparkline{
|
||||||
|
Height: 1,
|
||||||
|
TitleColor: ThemeAttr("sparkline.title.fg"),
|
||||||
|
LineColor: ThemeAttr("sparkline.line.fg")}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSparklines return a new *Spaklines with given Sparkline(s), you can always add a new Sparkline later.
|
||||||
|
func NewSparklines(ss ...Sparkline) *Sparklines {
|
||||||
|
s := &Sparklines{Block: *NewBlock(), Lines: ss}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *Sparklines) update() {
|
||||||
|
for i, v := range sl.Lines {
|
||||||
|
if v.Title == "" {
|
||||||
|
sl.Lines[i].displayHeight = v.Height
|
||||||
|
} else {
|
||||||
|
sl.Lines[i].displayHeight = v.Height + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sl.displayWidth = sl.innerArea.Dx()
|
||||||
|
|
||||||
|
// get how many lines gotta display
|
||||||
|
h := 0
|
||||||
|
sl.displayLines = 0
|
||||||
|
for _, v := range sl.Lines {
|
||||||
|
if h+v.displayHeight <= sl.innerArea.Dy() {
|
||||||
|
sl.displayLines++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
h += v.displayHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < sl.displayLines; i++ {
|
||||||
|
data := sl.Lines[i].Data
|
||||||
|
|
||||||
|
max := 0
|
||||||
|
for _, v := range data {
|
||||||
|
if max < v {
|
||||||
|
max = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sl.Lines[i].max = max
|
||||||
|
if max != 0 {
|
||||||
|
sl.Lines[i].scale = float32(8*sl.Lines[i].Height) / float32(max)
|
||||||
|
} else { // when all negative
|
||||||
|
sl.Lines[i].scale = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer implements Bufferer interface.
|
||||||
|
func (sl *Sparklines) Buffer() Buffer {
|
||||||
|
buf := sl.Block.Buffer()
|
||||||
|
sl.update()
|
||||||
|
|
||||||
|
oftY := 0
|
||||||
|
for i := 0; i < sl.displayLines; i++ {
|
||||||
|
l := sl.Lines[i]
|
||||||
|
data := l.Data
|
||||||
|
|
||||||
|
if len(data) > sl.innerArea.Dx() {
|
||||||
|
data = data[len(data)-sl.innerArea.Dx():]
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.Title != "" {
|
||||||
|
rs := trimStr2Runes(l.Title, sl.innerArea.Dx())
|
||||||
|
oftX := 0
|
||||||
|
for _, v := range rs {
|
||||||
|
w := charWidth(v)
|
||||||
|
c := Cell{
|
||||||
|
Ch: v,
|
||||||
|
Fg: l.TitleColor,
|
||||||
|
Bg: sl.Bg,
|
||||||
|
}
|
||||||
|
x := sl.innerArea.Min.X + oftX
|
||||||
|
y := sl.innerArea.Min.Y + oftY
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
oftX += w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for j, v := range data {
|
||||||
|
// display height of the data point, zero when data is negative
|
||||||
|
h := int(float32(v)*l.scale + 0.5)
|
||||||
|
if v < 0 {
|
||||||
|
h = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
barCnt := h / 8
|
||||||
|
barMod := h % 8
|
||||||
|
for jj := 0; jj < barCnt; jj++ {
|
||||||
|
c := Cell{
|
||||||
|
Ch: ' ', // => sparks[7]
|
||||||
|
Bg: l.LineColor,
|
||||||
|
}
|
||||||
|
x := sl.innerArea.Min.X + j
|
||||||
|
y := sl.innerArea.Min.Y + oftY + l.Height - jj
|
||||||
|
|
||||||
|
//p.Bg = sl.BgColor
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
}
|
||||||
|
if barMod != 0 {
|
||||||
|
c := Cell{
|
||||||
|
Ch: sparks[barMod-1],
|
||||||
|
Fg: l.LineColor,
|
||||||
|
Bg: sl.Bg,
|
||||||
|
}
|
||||||
|
x := sl.innerArea.Min.X + j
|
||||||
|
y := sl.innerArea.Min.Y + oftY + l.Height - barCnt
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oftY += l.displayHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
@ -0,0 +1,140 @@
|
|||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
/*
|
||||||
|
// A ColorScheme represents the current look-and-feel of the dashboard.
|
||||||
|
type ColorScheme struct {
|
||||||
|
BodyBg Attribute
|
||||||
|
BlockBg Attribute
|
||||||
|
HasBorder bool
|
||||||
|
BorderFg Attribute
|
||||||
|
BorderBg Attribute
|
||||||
|
BorderLabelTextFg Attribute
|
||||||
|
BorderLabelTextBg Attribute
|
||||||
|
ParTextFg Attribute
|
||||||
|
ParTextBg Attribute
|
||||||
|
SparklineLine Attribute
|
||||||
|
SparklineTitle Attribute
|
||||||
|
GaugeBar Attribute
|
||||||
|
GaugePercent Attribute
|
||||||
|
LineChartLine Attribute
|
||||||
|
LineChartAxes Attribute
|
||||||
|
ListItemFg Attribute
|
||||||
|
ListItemBg Attribute
|
||||||
|
BarChartBar Attribute
|
||||||
|
BarChartText Attribute
|
||||||
|
BarChartNum Attribute
|
||||||
|
MBarChartBar Attribute
|
||||||
|
MBarChartText Attribute
|
||||||
|
MBarChartNum Attribute
|
||||||
|
TabActiveBg Attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
// default color scheme depends on the user's terminal setting.
|
||||||
|
var themeDefault = ColorScheme{HasBorder: true}
|
||||||
|
|
||||||
|
var themeHelloWorld = ColorScheme{
|
||||||
|
BodyBg: ColorBlack,
|
||||||
|
BlockBg: ColorBlack,
|
||||||
|
HasBorder: true,
|
||||||
|
BorderFg: ColorWhite,
|
||||||
|
BorderBg: ColorBlack,
|
||||||
|
BorderLabelTextBg: ColorBlack,
|
||||||
|
BorderLabelTextFg: ColorGreen,
|
||||||
|
ParTextBg: ColorBlack,
|
||||||
|
ParTextFg: ColorWhite,
|
||||||
|
SparklineLine: ColorMagenta,
|
||||||
|
SparklineTitle: ColorWhite,
|
||||||
|
GaugeBar: ColorRed,
|
||||||
|
GaugePercent: ColorWhite,
|
||||||
|
LineChartLine: ColorYellow | AttrBold,
|
||||||
|
LineChartAxes: ColorWhite,
|
||||||
|
ListItemBg: ColorBlack,
|
||||||
|
ListItemFg: ColorYellow,
|
||||||
|
BarChartBar: ColorRed,
|
||||||
|
BarChartNum: ColorWhite,
|
||||||
|
BarChartText: ColorCyan,
|
||||||
|
MBarChartBar: ColorRed,
|
||||||
|
MBarChartNum: ColorWhite,
|
||||||
|
MBarChartText: ColorCyan,
|
||||||
|
TabActiveBg: ColorMagenta,
|
||||||
|
}
|
||||||
|
|
||||||
|
var theme = themeDefault // global dep
|
||||||
|
|
||||||
|
// Theme returns the currently used theme.
|
||||||
|
func Theme() ColorScheme {
|
||||||
|
return theme
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTheme sets a new, custom theme.
|
||||||
|
func SetTheme(newTheme ColorScheme) {
|
||||||
|
theme = newTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseTheme sets a predefined scheme. Currently available: "hello-world" and
|
||||||
|
// "black-and-white".
|
||||||
|
func UseTheme(th string) {
|
||||||
|
switch th {
|
||||||
|
case "helloworld":
|
||||||
|
theme = themeHelloWorld
|
||||||
|
default:
|
||||||
|
theme = themeDefault
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
var ColorMap = map[string]Attribute{
|
||||||
|
"fg": ColorWhite,
|
||||||
|
"bg": ColorDefault,
|
||||||
|
"border.fg": ColorWhite,
|
||||||
|
"label.fg": ColorGreen,
|
||||||
|
"par.fg": ColorYellow,
|
||||||
|
"par.label.bg": ColorWhite,
|
||||||
|
}
|
||||||
|
|
||||||
|
func ThemeAttr(name string) Attribute {
|
||||||
|
return lookUpAttr(ColorMap, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookUpAttr(clrmap map[string]Attribute, name string) Attribute {
|
||||||
|
|
||||||
|
a, ok := clrmap[name]
|
||||||
|
if ok {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
ns := strings.Split(name, ".")
|
||||||
|
for i := range ns {
|
||||||
|
nn := strings.Join(ns[i:len(ns)], ".")
|
||||||
|
a, ok = ColorMap[nn]
|
||||||
|
if ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0<=r,g,b <= 5
|
||||||
|
func ColorRGB(r, g, b int) Attribute {
|
||||||
|
within := func(n int) int {
|
||||||
|
if n < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if n > 5 {
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
r, b, g = within(r), within(b), within(g)
|
||||||
|
return Attribute(0x0f + 36*r + 6*g + b)
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// event mixins
|
||||||
|
type WgtMgr map[string]WgtInfo
|
||||||
|
|
||||||
|
type WgtInfo struct {
|
||||||
|
Handlers map[string]func(Event)
|
||||||
|
WgtRef Widget
|
||||||
|
Id string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Widget interface {
|
||||||
|
Id() string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWgtInfo(wgt Widget) WgtInfo {
|
||||||
|
return WgtInfo{
|
||||||
|
Handlers: make(map[string]func(Event)),
|
||||||
|
WgtRef: wgt,
|
||||||
|
Id: wgt.Id(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWgtMgr() WgtMgr {
|
||||||
|
wm := WgtMgr(make(map[string]WgtInfo))
|
||||||
|
return wm
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm WgtMgr) AddWgt(wgt Widget) {
|
||||||
|
wm[wgt.Id()] = NewWgtInfo(wgt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm WgtMgr) RmWgt(wgt Widget) {
|
||||||
|
wm.RmWgtById(wgt.Id())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm WgtMgr) RmWgtById(id string) {
|
||||||
|
delete(wm, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm WgtMgr) AddWgtHandler(id, path string, h func(Event)) {
|
||||||
|
if w, ok := wm[id]; ok {
|
||||||
|
w.Handlers[path] = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm WgtMgr) RmWgtHandler(id, path string) {
|
||||||
|
if w, ok := wm[id]; ok {
|
||||||
|
delete(w.Handlers, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var counter struct {
|
||||||
|
sync.RWMutex
|
||||||
|
count int
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenId() string {
|
||||||
|
counter.Lock()
|
||||||
|
defer counter.Unlock()
|
||||||
|
|
||||||
|
counter.count += 1
|
||||||
|
return fmt.Sprintf("%d", counter.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm WgtMgr) WgtHandlersHook() func(Event) {
|
||||||
|
return func(e Event) {
|
||||||
|
for _, v := range wm {
|
||||||
|
if k := findMatch(v.Handlers, e.Path); k != "" {
|
||||||
|
v.Handlers[k](e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultWgtMgr WgtMgr
|
||||||
|
|
||||||
|
func (b *Block) Handle(path string, handler func(Event)) {
|
||||||
|
if _, ok := DefaultWgtMgr[b.Id()]; !ok {
|
||||||
|
DefaultWgtMgr.AddWgt(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultWgtMgr.AddWgtHandler(b.Id(), path, handler)
|
||||||
|
}
|
Loading…
Reference in New Issue