Move source files into v3 directory
This commit is contained in:
9
v3/alignment.go
Normal file
9
v3/alignment.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package termui
|
||||
|
||||
type Alignment uint
|
||||
|
||||
const (
|
||||
AlignLeft Alignment = iota
|
||||
AlignCenter
|
||||
AlignRight
|
||||
)
|
||||
35
v3/backend.go
Normal file
35
v3/backend.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// 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 (
|
||||
tb "github.com/nsf/termbox-go"
|
||||
)
|
||||
|
||||
// Init initializes termbox-go and is required to render anything.
|
||||
// After initialization, the library must be finalized with `Close`.
|
||||
func Init() error {
|
||||
if err := tb.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
tb.SetInputMode(tb.InputEsc | tb.InputMouse)
|
||||
tb.SetOutputMode(tb.Output256)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes termbox-go.
|
||||
func Close() {
|
||||
tb.Close()
|
||||
}
|
||||
|
||||
func TerminalDimensions() (int, int) {
|
||||
tb.Sync()
|
||||
width, height := tb.Size()
|
||||
return width, height
|
||||
}
|
||||
|
||||
func Clear() {
|
||||
tb.Clear(tb.ColorDefault, tb.Attribute(Theme.Default.Bg+1))
|
||||
}
|
||||
105
v3/block.go
Normal file
105
v3/block.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// Block is the base struct inherited by most widgets.
|
||||
// Block manages size, position, border, and title.
|
||||
// It implements all 3 of the methods needed for the `Drawable` interface.
|
||||
// Custom widgets will override the Draw method.
|
||||
type Block struct {
|
||||
Border bool
|
||||
BorderStyle Style
|
||||
|
||||
BorderLeft, BorderRight, BorderTop, BorderBottom bool
|
||||
|
||||
PaddingLeft, PaddingRight, PaddingTop, PaddingBottom int
|
||||
|
||||
image.Rectangle
|
||||
Inner image.Rectangle
|
||||
|
||||
Title string
|
||||
TitleStyle Style
|
||||
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func NewBlock() *Block {
|
||||
return &Block{
|
||||
Border: true,
|
||||
BorderStyle: Theme.Block.Border,
|
||||
BorderLeft: true,
|
||||
BorderRight: true,
|
||||
BorderTop: true,
|
||||
BorderBottom: true,
|
||||
|
||||
TitleStyle: Theme.Block.Title,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Block) drawBorder(buf *Buffer) {
|
||||
verticalCell := Cell{VERTICAL_LINE, self.BorderStyle}
|
||||
horizontalCell := Cell{HORIZONTAL_LINE, self.BorderStyle}
|
||||
|
||||
// draw lines
|
||||
if self.BorderTop {
|
||||
buf.Fill(horizontalCell, image.Rect(self.Min.X, self.Min.Y, self.Max.X, self.Min.Y+1))
|
||||
}
|
||||
if self.BorderBottom {
|
||||
buf.Fill(horizontalCell, image.Rect(self.Min.X, self.Max.Y-1, self.Max.X, self.Max.Y))
|
||||
}
|
||||
if self.BorderLeft {
|
||||
buf.Fill(verticalCell, image.Rect(self.Min.X, self.Min.Y, self.Min.X+1, self.Max.Y))
|
||||
}
|
||||
if self.BorderRight {
|
||||
buf.Fill(verticalCell, image.Rect(self.Max.X-1, self.Min.Y, self.Max.X, self.Max.Y))
|
||||
}
|
||||
|
||||
// draw corners
|
||||
if self.BorderTop && self.BorderLeft {
|
||||
buf.SetCell(Cell{TOP_LEFT, self.BorderStyle}, self.Min)
|
||||
}
|
||||
if self.BorderTop && self.BorderRight {
|
||||
buf.SetCell(Cell{TOP_RIGHT, self.BorderStyle}, image.Pt(self.Max.X-1, self.Min.Y))
|
||||
}
|
||||
if self.BorderBottom && self.BorderLeft {
|
||||
buf.SetCell(Cell{BOTTOM_LEFT, self.BorderStyle}, image.Pt(self.Min.X, self.Max.Y-1))
|
||||
}
|
||||
if self.BorderBottom && self.BorderRight {
|
||||
buf.SetCell(Cell{BOTTOM_RIGHT, self.BorderStyle}, self.Max.Sub(image.Pt(1, 1)))
|
||||
}
|
||||
}
|
||||
|
||||
// Draw implements the Drawable interface.
|
||||
func (self *Block) Draw(buf *Buffer) {
|
||||
if self.Border {
|
||||
self.drawBorder(buf)
|
||||
}
|
||||
buf.SetString(
|
||||
self.Title,
|
||||
self.TitleStyle,
|
||||
image.Pt(self.Min.X+2, self.Min.Y),
|
||||
)
|
||||
}
|
||||
|
||||
// SetRect implements the Drawable interface.
|
||||
func (self *Block) SetRect(x1, y1, x2, y2 int) {
|
||||
self.Rectangle = image.Rect(x1, y1, x2, y2)
|
||||
self.Inner = image.Rect(
|
||||
self.Min.X+1+self.PaddingLeft,
|
||||
self.Min.Y+1+self.PaddingTop,
|
||||
self.Max.X-1-self.PaddingRight,
|
||||
self.Max.Y-1-self.PaddingBottom,
|
||||
)
|
||||
}
|
||||
|
||||
// GetRect implements the Drawable interface.
|
||||
func (self *Block) GetRect() image.Rectangle {
|
||||
return self.Rectangle
|
||||
}
|
||||
76
v3/buffer.go
Normal file
76
v3/buffer.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// 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"
|
||||
|
||||
rw "github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
// Cell represents a viewable terminal cell
|
||||
type Cell struct {
|
||||
Rune rune
|
||||
Style Style
|
||||
}
|
||||
|
||||
var CellClear = Cell{
|
||||
Rune: ' ',
|
||||
Style: StyleClear,
|
||||
}
|
||||
|
||||
// NewCell takes 1 to 2 arguments
|
||||
// 1st argument = rune
|
||||
// 2nd argument = optional style
|
||||
func NewCell(rune rune, args ...interface{}) Cell {
|
||||
style := StyleClear
|
||||
if len(args) == 1 {
|
||||
style = args[0].(Style)
|
||||
}
|
||||
return Cell{
|
||||
Rune: rune,
|
||||
Style: style,
|
||||
}
|
||||
}
|
||||
|
||||
// Buffer represents a section of a terminal and is a renderable rectangle of cells.
|
||||
type Buffer struct {
|
||||
image.Rectangle
|
||||
CellMap map[image.Point]Cell
|
||||
}
|
||||
|
||||
func NewBuffer(r image.Rectangle) *Buffer {
|
||||
buf := &Buffer{
|
||||
Rectangle: r,
|
||||
CellMap: make(map[image.Point]Cell),
|
||||
}
|
||||
buf.Fill(CellClear, r) // clears out area
|
||||
return buf
|
||||
}
|
||||
|
||||
func (self *Buffer) GetCell(p image.Point) Cell {
|
||||
return self.CellMap[p]
|
||||
}
|
||||
|
||||
func (self *Buffer) SetCell(c Cell, p image.Point) {
|
||||
self.CellMap[p] = c
|
||||
}
|
||||
|
||||
func (self *Buffer) Fill(c Cell, rect image.Rectangle) {
|
||||
for x := rect.Min.X; x < rect.Max.X; x++ {
|
||||
for y := rect.Min.Y; y < rect.Max.Y; y++ {
|
||||
self.SetCell(c, image.Pt(x, y))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Buffer) SetString(s string, style Style, p image.Point) {
|
||||
runes := []rune(s)
|
||||
x := 0
|
||||
for _, char := range runes {
|
||||
self.SetCell(Cell{char, style}, image.Pt(p.X+x, p.Y))
|
||||
x += rw.RuneWidth(char)
|
||||
}
|
||||
}
|
||||
43
v3/canvas.go
Normal file
43
v3/canvas.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package termui
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"github.com/gizak/termui/v3/drawille"
|
||||
)
|
||||
|
||||
type Canvas struct {
|
||||
Block
|
||||
drawille.Canvas
|
||||
}
|
||||
|
||||
func NewCanvas() *Canvas {
|
||||
return &Canvas{
|
||||
Block: *NewBlock(),
|
||||
Canvas: *drawille.NewCanvas(),
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Canvas) SetPoint(p image.Point, color Color) {
|
||||
self.Canvas.SetPoint(p, drawille.Color(color))
|
||||
}
|
||||
|
||||
func (self *Canvas) SetLine(p0, p1 image.Point, color Color) {
|
||||
self.Canvas.SetLine(p0, p1, drawille.Color(color))
|
||||
}
|
||||
|
||||
func (self *Canvas) Draw(buf *Buffer) {
|
||||
for point, cell := range self.Canvas.GetCells() {
|
||||
if point.In(self.Rectangle) {
|
||||
convertedCell := Cell{
|
||||
cell.Rune,
|
||||
Style{
|
||||
Color(cell.Color),
|
||||
ColorClear,
|
||||
ModifierClear,
|
||||
},
|
||||
}
|
||||
buf.SetCell(convertedCell, point)
|
||||
}
|
||||
}
|
||||
}
|
||||
8
v3/doc.go
Normal file
8
v3/doc.go
Normal file
@@ -0,0 +1,8 @@
|
||||
// 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 for creating terminal user interfaces (TUIs) using widgets.
|
||||
*/
|
||||
package termui
|
||||
90
v3/drawille/drawille.go
Normal file
90
v3/drawille/drawille.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package drawille
|
||||
|
||||
import (
|
||||
"image"
|
||||
)
|
||||
|
||||
const BRAILLE_OFFSET = '\u2800'
|
||||
|
||||
var BRAILLE = [4][2]rune{
|
||||
{'\u0001', '\u0008'},
|
||||
{'\u0002', '\u0010'},
|
||||
{'\u0004', '\u0020'},
|
||||
{'\u0040', '\u0080'},
|
||||
}
|
||||
|
||||
type Color int
|
||||
|
||||
type Cell struct {
|
||||
Rune rune
|
||||
Color Color
|
||||
}
|
||||
|
||||
type Canvas struct {
|
||||
CellMap map[image.Point]Cell
|
||||
}
|
||||
|
||||
func NewCanvas() *Canvas {
|
||||
return &Canvas{
|
||||
CellMap: make(map[image.Point]Cell),
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Canvas) SetPoint(p image.Point, color Color) {
|
||||
point := image.Pt(p.X/2, p.Y/4)
|
||||
self.CellMap[point] = Cell{
|
||||
self.CellMap[point].Rune | BRAILLE[p.Y%4][p.X%2],
|
||||
color,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Canvas) SetLine(p0, p1 image.Point, color Color) {
|
||||
for _, p := range line(p0, p1) {
|
||||
self.SetPoint(p, color)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Canvas) GetCells() map[image.Point]Cell {
|
||||
cellMap := make(map[image.Point]Cell)
|
||||
for point, cell := range self.CellMap {
|
||||
cellMap[point] = Cell{cell.Rune + BRAILLE_OFFSET, cell.Color}
|
||||
}
|
||||
return cellMap
|
||||
}
|
||||
|
||||
func line(p0, p1 image.Point) []image.Point {
|
||||
points := []image.Point{}
|
||||
|
||||
leftPoint, rightPoint := p0, p1
|
||||
if leftPoint.X > rightPoint.X {
|
||||
leftPoint, rightPoint = rightPoint, leftPoint
|
||||
}
|
||||
|
||||
xDistance := absInt(leftPoint.X - rightPoint.X)
|
||||
yDistance := absInt(leftPoint.Y - rightPoint.Y)
|
||||
slope := float64(yDistance) / float64(xDistance)
|
||||
slopeSign := 1
|
||||
if rightPoint.Y < leftPoint.Y {
|
||||
slopeSign = -1
|
||||
}
|
||||
|
||||
targetYCoordinate := float64(leftPoint.Y)
|
||||
currentYCoordinate := leftPoint.Y
|
||||
for i := leftPoint.X; i < rightPoint.X; i++ {
|
||||
points = append(points, image.Pt(i, currentYCoordinate))
|
||||
targetYCoordinate += (slope * float64(slopeSign))
|
||||
for currentYCoordinate != int(targetYCoordinate) {
|
||||
points = append(points, image.Pt(i, currentYCoordinate))
|
||||
currentYCoordinate += slopeSign
|
||||
}
|
||||
}
|
||||
|
||||
return points
|
||||
}
|
||||
|
||||
func absInt(x int) int {
|
||||
if x >= 0 {
|
||||
return x
|
||||
}
|
||||
return -x
|
||||
}
|
||||
211
v3/events.go
Normal file
211
v3/events.go
Normal file
@@ -0,0 +1,211 @@
|
||||
// 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"
|
||||
|
||||
tb "github.com/nsf/termbox-go"
|
||||
)
|
||||
|
||||
/*
|
||||
List of events:
|
||||
mouse events:
|
||||
<MouseLeft> <MouseRight> <MouseMiddle>
|
||||
<MouseWheelUp> <MouseWheelDown>
|
||||
keyboard events:
|
||||
any uppercase or lowercase letter like j or J
|
||||
<C-d> etc
|
||||
<M-d> etc
|
||||
<Up> <Down> <Left> <Right>
|
||||
<Insert> <Delete> <Home> <End> <Previous> <Next>
|
||||
<Backspace> <Tab> <Enter> <Escape> <Space>
|
||||
<C-<Space>> etc
|
||||
terminal events:
|
||||
<Resize>
|
||||
|
||||
keyboard events that do not work:
|
||||
<C-->
|
||||
<C-2> <C-~>
|
||||
<C-h>
|
||||
<C-i>
|
||||
<C-m>
|
||||
<C-[> <C-3>
|
||||
<C-\\>
|
||||
<C-]>
|
||||
<C-/> <C-_>
|
||||
<C-8>
|
||||
*/
|
||||
|
||||
type EventType uint
|
||||
|
||||
const (
|
||||
KeyboardEvent EventType = iota
|
||||
MouseEvent
|
||||
ResizeEvent
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Type EventType
|
||||
ID string
|
||||
Payload interface{}
|
||||
}
|
||||
|
||||
// Mouse payload.
|
||||
type Mouse struct {
|
||||
Drag bool
|
||||
X int
|
||||
Y int
|
||||
}
|
||||
|
||||
// Resize payload.
|
||||
type Resize struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
// PollEvents gets events from termbox, converts them, then sends them to each of its channels.
|
||||
func PollEvents() <-chan Event {
|
||||
ch := make(chan Event)
|
||||
go func() {
|
||||
for {
|
||||
ch <- convertTermboxEvent(tb.PollEvent())
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
var keyboardMap = map[tb.Key]string{
|
||||
tb.KeyF1: "<F1>",
|
||||
tb.KeyF2: "<F2>",
|
||||
tb.KeyF3: "<F3>",
|
||||
tb.KeyF4: "<F4>",
|
||||
tb.KeyF5: "<F5>",
|
||||
tb.KeyF6: "<F6>",
|
||||
tb.KeyF7: "<F7>",
|
||||
tb.KeyF8: "<F8>",
|
||||
tb.KeyF9: "<F9>",
|
||||
tb.KeyF10: "<F10>",
|
||||
tb.KeyF11: "<F11>",
|
||||
tb.KeyF12: "<F12>",
|
||||
tb.KeyInsert: "<Insert>",
|
||||
tb.KeyDelete: "<Delete>",
|
||||
tb.KeyHome: "<Home>",
|
||||
tb.KeyEnd: "<End>",
|
||||
tb.KeyPgup: "<PageUp>",
|
||||
tb.KeyPgdn: "<PageDown>",
|
||||
tb.KeyArrowUp: "<Up>",
|
||||
tb.KeyArrowDown: "<Down>",
|
||||
tb.KeyArrowLeft: "<Left>",
|
||||
tb.KeyArrowRight: "<Right>",
|
||||
|
||||
tb.KeyCtrlSpace: "<C-<Space>>", // tb.KeyCtrl2 tb.KeyCtrlTilde
|
||||
tb.KeyCtrlA: "<C-a>",
|
||||
tb.KeyCtrlB: "<C-b>",
|
||||
tb.KeyCtrlC: "<C-c>",
|
||||
tb.KeyCtrlD: "<C-d>",
|
||||
tb.KeyCtrlE: "<C-e>",
|
||||
tb.KeyCtrlF: "<C-f>",
|
||||
tb.KeyCtrlG: "<C-g>",
|
||||
tb.KeyBackspace: "<C-<Backspace>>", // tb.KeyCtrlH
|
||||
tb.KeyTab: "<Tab>", // tb.KeyCtrlI
|
||||
tb.KeyCtrlJ: "<C-j>",
|
||||
tb.KeyCtrlK: "<C-k>",
|
||||
tb.KeyCtrlL: "<C-l>",
|
||||
tb.KeyEnter: "<Enter>", // tb.KeyCtrlM
|
||||
tb.KeyCtrlN: "<C-n>",
|
||||
tb.KeyCtrlO: "<C-o>",
|
||||
tb.KeyCtrlP: "<C-p>",
|
||||
tb.KeyCtrlQ: "<C-q>",
|
||||
tb.KeyCtrlR: "<C-r>",
|
||||
tb.KeyCtrlS: "<C-s>",
|
||||
tb.KeyCtrlT: "<C-t>",
|
||||
tb.KeyCtrlU: "<C-u>",
|
||||
tb.KeyCtrlV: "<C-v>",
|
||||
tb.KeyCtrlW: "<C-w>",
|
||||
tb.KeyCtrlX: "<C-x>",
|
||||
tb.KeyCtrlY: "<C-y>",
|
||||
tb.KeyCtrlZ: "<C-z>",
|
||||
tb.KeyEsc: "<Escape>", // tb.KeyCtrlLsqBracket tb.KeyCtrl3
|
||||
tb.KeyCtrl4: "<C-4>", // tb.KeyCtrlBackslash
|
||||
tb.KeyCtrl5: "<C-5>", // tb.KeyCtrlRsqBracket
|
||||
tb.KeyCtrl6: "<C-6>",
|
||||
tb.KeyCtrl7: "<C-7>", // tb.KeyCtrlSlash tb.KeyCtrlUnderscore
|
||||
tb.KeySpace: "<Space>",
|
||||
tb.KeyBackspace2: "<Backspace>", // tb.KeyCtrl8:
|
||||
}
|
||||
|
||||
// convertTermboxKeyboardEvent converts a termbox keyboard event to a more friendly string format.
|
||||
// Combines modifiers into the string instead of having them as additional fields in an event.
|
||||
func convertTermboxKeyboardEvent(e tb.Event) Event {
|
||||
ID := "%s"
|
||||
if e.Mod == tb.ModAlt {
|
||||
ID = "<M-%s>"
|
||||
}
|
||||
|
||||
if e.Ch != 0 {
|
||||
ID = fmt.Sprintf(ID, string(e.Ch))
|
||||
} else {
|
||||
converted, ok := keyboardMap[e.Key]
|
||||
if !ok {
|
||||
converted = ""
|
||||
}
|
||||
ID = fmt.Sprintf(ID, converted)
|
||||
}
|
||||
|
||||
return Event{
|
||||
Type: KeyboardEvent,
|
||||
ID: ID,
|
||||
}
|
||||
}
|
||||
|
||||
var mouseButtonMap = map[tb.Key]string{
|
||||
tb.MouseLeft: "<MouseLeft>",
|
||||
tb.MouseMiddle: "<MouseMiddle>",
|
||||
tb.MouseRight: "<MouseRight>",
|
||||
tb.MouseRelease: "<MouseRelease>",
|
||||
tb.MouseWheelUp: "<MouseWheelUp>",
|
||||
tb.MouseWheelDown: "<MouseWheelDown>",
|
||||
}
|
||||
|
||||
func convertTermboxMouseEvent(e tb.Event) Event {
|
||||
converted, ok := mouseButtonMap[e.Key]
|
||||
if !ok {
|
||||
converted = "Unknown_Mouse_Button"
|
||||
}
|
||||
Drag := e.Mod == tb.ModMotion
|
||||
return Event{
|
||||
Type: MouseEvent,
|
||||
ID: converted,
|
||||
Payload: Mouse{
|
||||
X: e.MouseX,
|
||||
Y: e.MouseY,
|
||||
Drag: Drag,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// convertTermboxEvent turns a termbox event into a termui event.
|
||||
func convertTermboxEvent(e tb.Event) Event {
|
||||
if e.Type == tb.EventError {
|
||||
panic(e.Err)
|
||||
}
|
||||
switch e.Type {
|
||||
case tb.EventKey:
|
||||
return convertTermboxKeyboardEvent(e)
|
||||
case tb.EventMouse:
|
||||
return convertTermboxMouseEvent(e)
|
||||
case tb.EventResize:
|
||||
return Event{
|
||||
Type: ResizeEvent,
|
||||
ID: "<Resize>",
|
||||
Payload: Resize{
|
||||
Width: e.Width,
|
||||
Height: e.Height,
|
||||
},
|
||||
}
|
||||
}
|
||||
return Event{}
|
||||
}
|
||||
7
v3/go.mod
Normal file
7
v3/go.mod
Normal file
@@ -0,0 +1,7 @@
|
||||
module github.com/gizak/termui/v3
|
||||
|
||||
require (
|
||||
github.com/mattn/go-runewidth v0.0.2
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7
|
||||
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d
|
||||
)
|
||||
6
v3/go.sum
Normal file
6
v3/go.sum
Normal file
@@ -0,0 +1,6 @@
|
||||
github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
|
||||
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
|
||||
160
v3/grid.go
Normal file
160
v3/grid.go
Normal file
@@ -0,0 +1,160 @@
|
||||
// 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
|
||||
|
||||
type gridItemType uint
|
||||
|
||||
const (
|
||||
col gridItemType = 0
|
||||
row gridItemType = 1
|
||||
)
|
||||
|
||||
type Grid struct {
|
||||
Block
|
||||
Items []*GridItem
|
||||
}
|
||||
|
||||
// GridItem represents either a Row or Column in a grid.
|
||||
// Holds sizing information and either an []GridItems or a widget.
|
||||
type GridItem struct {
|
||||
Type gridItemType
|
||||
XRatio float64
|
||||
YRatio float64
|
||||
WidthRatio float64
|
||||
HeightRatio float64
|
||||
Entry interface{} // Entry.type == GridBufferer if IsLeaf else []GridItem
|
||||
IsLeaf bool
|
||||
ratio float64
|
||||
}
|
||||
|
||||
func NewGrid() *Grid {
|
||||
g := &Grid{
|
||||
Block: *NewBlock(),
|
||||
}
|
||||
g.Border = false
|
||||
return g
|
||||
}
|
||||
|
||||
// NewCol takes a height percentage and either a widget or a Row or Column
|
||||
func NewCol(ratio float64, i ...interface{}) GridItem {
|
||||
_, ok := i[0].(Drawable)
|
||||
entry := i[0]
|
||||
if !ok {
|
||||
entry = i
|
||||
}
|
||||
return GridItem{
|
||||
Type: col,
|
||||
Entry: entry,
|
||||
IsLeaf: ok,
|
||||
ratio: ratio,
|
||||
}
|
||||
}
|
||||
|
||||
// NewRow takes a width percentage and either a widget or a Row or Column
|
||||
func NewRow(ratio float64, i ...interface{}) GridItem {
|
||||
_, ok := i[0].(Drawable)
|
||||
entry := i[0]
|
||||
if !ok {
|
||||
entry = i
|
||||
}
|
||||
return GridItem{
|
||||
Type: row,
|
||||
Entry: entry,
|
||||
IsLeaf: ok,
|
||||
ratio: ratio,
|
||||
}
|
||||
}
|
||||
|
||||
// Set is used to add Columns and Rows to the grid.
|
||||
// It recursively searches the GridItems, adding leaves to the grid and calculating the dimensions of the leaves.
|
||||
func (self *Grid) Set(entries ...interface{}) {
|
||||
entry := GridItem{
|
||||
Type: row,
|
||||
Entry: entries,
|
||||
IsLeaf: false,
|
||||
ratio: 1.0,
|
||||
}
|
||||
self.setHelper(entry, 1.0, 1.0)
|
||||
}
|
||||
|
||||
func (self *Grid) setHelper(item GridItem, parentWidthRatio, parentHeightRatio float64) {
|
||||
var HeightRatio float64
|
||||
var WidthRatio float64
|
||||
switch item.Type {
|
||||
case col:
|
||||
HeightRatio = 1.0
|
||||
WidthRatio = item.ratio
|
||||
case row:
|
||||
HeightRatio = item.ratio
|
||||
WidthRatio = 1.0
|
||||
}
|
||||
item.WidthRatio = parentWidthRatio * WidthRatio
|
||||
item.HeightRatio = parentHeightRatio * HeightRatio
|
||||
|
||||
if item.IsLeaf {
|
||||
self.Items = append(self.Items, &item)
|
||||
} else {
|
||||
XRatio := 0.0
|
||||
YRatio := 0.0
|
||||
cols := false
|
||||
rows := false
|
||||
|
||||
children := InterfaceSlice(item.Entry)
|
||||
|
||||
for i := 0; i < len(children); i++ {
|
||||
if children[i] == nil {
|
||||
continue
|
||||
}
|
||||
child, _ := children[i].(GridItem)
|
||||
|
||||
child.XRatio = item.XRatio + (item.WidthRatio * XRatio)
|
||||
child.YRatio = item.YRatio + (item.HeightRatio * YRatio)
|
||||
|
||||
switch child.Type {
|
||||
case col:
|
||||
cols = true
|
||||
XRatio += child.ratio
|
||||
if rows {
|
||||
item.HeightRatio /= 2
|
||||
}
|
||||
case row:
|
||||
rows = true
|
||||
YRatio += child.ratio
|
||||
if cols {
|
||||
item.WidthRatio /= 2
|
||||
}
|
||||
}
|
||||
|
||||
self.setHelper(child, item.WidthRatio, item.HeightRatio)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Grid) Draw(buf *Buffer) {
|
||||
width := float64(self.Dx()) + 1
|
||||
height := float64(self.Dy()) + 1
|
||||
|
||||
for _, item := range self.Items {
|
||||
entry, _ := item.Entry.(Drawable)
|
||||
|
||||
x := int(width*item.XRatio) + self.Min.X
|
||||
y := int(height*item.YRatio) + self.Min.Y
|
||||
w := int(width * item.WidthRatio)
|
||||
h := int(height * item.HeightRatio)
|
||||
|
||||
if x+w > self.Dx() {
|
||||
w--
|
||||
}
|
||||
if y+h > self.Dy() {
|
||||
h--
|
||||
}
|
||||
|
||||
entry.SetRect(x, y, x+w, y+h)
|
||||
|
||||
entry.Lock()
|
||||
entry.Draw(buf)
|
||||
entry.Unlock()
|
||||
}
|
||||
}
|
||||
38
v3/render.go
Normal file
38
v3/render.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// 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"
|
||||
|
||||
tb "github.com/nsf/termbox-go"
|
||||
)
|
||||
|
||||
type Drawable interface {
|
||||
GetRect() image.Rectangle
|
||||
SetRect(int, int, int, int)
|
||||
Draw(*Buffer)
|
||||
sync.Locker
|
||||
}
|
||||
|
||||
func Render(items ...Drawable) {
|
||||
for _, item := range items {
|
||||
buf := NewBuffer(item.GetRect())
|
||||
item.Lock()
|
||||
item.Draw(buf)
|
||||
item.Unlock()
|
||||
for point, cell := range buf.CellMap {
|
||||
if point.In(buf.Rectangle) {
|
||||
tb.SetCell(
|
||||
point.X, point.Y,
|
||||
cell.Rune,
|
||||
tb.Attribute(cell.Style.Fg+1)|tb.Attribute(cell.Style.Modifier), tb.Attribute(cell.Style.Bg+1),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
tb.Flush()
|
||||
}
|
||||
65
v3/style.go
Normal file
65
v3/style.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package termui
|
||||
|
||||
// Color is an integer from -1 to 255
|
||||
// -1 = ColorClear
|
||||
// 0-255 = Xterm colors
|
||||
type Color int
|
||||
|
||||
// ColorClear clears the Fg or Bg color of a Style
|
||||
const ColorClear Color = -1
|
||||
|
||||
// Basic terminal colors
|
||||
const (
|
||||
ColorBlack Color = 0
|
||||
ColorRed Color = 1
|
||||
ColorGreen Color = 2
|
||||
ColorYellow Color = 3
|
||||
ColorBlue Color = 4
|
||||
ColorMagenta Color = 5
|
||||
ColorCyan Color = 6
|
||||
ColorWhite Color = 7
|
||||
)
|
||||
|
||||
type Modifier uint
|
||||
|
||||
const (
|
||||
// ModifierClear clears any modifiers
|
||||
ModifierClear Modifier = 0
|
||||
ModifierBold Modifier = 1 << 9
|
||||
ModifierUnderline Modifier = 1 << 10
|
||||
ModifierReverse Modifier = 1 << 11
|
||||
)
|
||||
|
||||
// Style represents the style of one terminal cell
|
||||
type Style struct {
|
||||
Fg Color
|
||||
Bg Color
|
||||
Modifier Modifier
|
||||
}
|
||||
|
||||
// StyleClear represents a default Style, with no colors or modifiers
|
||||
var StyleClear = Style{
|
||||
Fg: ColorClear,
|
||||
Bg: ColorClear,
|
||||
Modifier: ModifierClear,
|
||||
}
|
||||
|
||||
// NewStyle takes 1 to 3 arguments
|
||||
// 1st argument = Fg
|
||||
// 2nd argument = optional Bg
|
||||
// 3rd argument = optional Modifier
|
||||
func NewStyle(fg Color, args ...interface{}) Style {
|
||||
bg := ColorClear
|
||||
modifier := ModifierClear
|
||||
if len(args) >= 1 {
|
||||
bg = args[0].(Color)
|
||||
}
|
||||
if len(args) == 2 {
|
||||
modifier = args[1].(Modifier)
|
||||
}
|
||||
return Style{
|
||||
fg,
|
||||
bg,
|
||||
modifier,
|
||||
}
|
||||
}
|
||||
156
v3/style_parser.go
Normal file
156
v3/style_parser.go
Normal file
@@ -0,0 +1,156 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
const (
|
||||
tokenFg = "fg"
|
||||
tokenBg = "bg"
|
||||
tokenModifier = "mod"
|
||||
|
||||
tokenItemSeparator = ","
|
||||
tokenValueSeparator = ":"
|
||||
|
||||
tokenBeginStyledText = '['
|
||||
tokenEndStyledText = ']'
|
||||
|
||||
tokenBeginStyle = '('
|
||||
tokenEndStyle = ')'
|
||||
)
|
||||
|
||||
type parserState uint
|
||||
|
||||
const (
|
||||
parserStateDefault parserState = iota
|
||||
parserStateStyleItems
|
||||
parserStateStyledText
|
||||
)
|
||||
|
||||
// StyleParserColorMap can be modified to add custom color parsing to text
|
||||
var StyleParserColorMap = map[string]Color{
|
||||
"red": ColorRed,
|
||||
"blue": ColorBlue,
|
||||
"black": ColorBlack,
|
||||
"cyan": ColorCyan,
|
||||
"yellow": ColorYellow,
|
||||
"white": ColorWhite,
|
||||
"clear": ColorClear,
|
||||
"green": ColorGreen,
|
||||
"magenta": ColorMagenta,
|
||||
}
|
||||
|
||||
var modifierMap = map[string]Modifier{
|
||||
"bold": ModifierBold,
|
||||
"underline": ModifierUnderline,
|
||||
"reverse": ModifierReverse,
|
||||
}
|
||||
|
||||
// readStyle translates an []rune like `fg:red,mod:bold,bg:white` to a style
|
||||
func readStyle(runes []rune, defaultStyle Style) Style {
|
||||
style := defaultStyle
|
||||
split := strings.Split(string(runes), tokenItemSeparator)
|
||||
for _, item := range split {
|
||||
pair := strings.Split(item, tokenValueSeparator)
|
||||
if len(pair) == 2 {
|
||||
switch pair[0] {
|
||||
case tokenFg:
|
||||
style.Fg = StyleParserColorMap[pair[1]]
|
||||
case tokenBg:
|
||||
style.Bg = StyleParserColorMap[pair[1]]
|
||||
case tokenModifier:
|
||||
style.Modifier = modifierMap[pair[1]]
|
||||
}
|
||||
}
|
||||
}
|
||||
return style
|
||||
}
|
||||
|
||||
// ParseStyles parses a string for embedded Styles and returns []Cell with the correct styling.
|
||||
// Uses defaultStyle for any text without an embedded style.
|
||||
// Syntax is of the form [text](fg:<color>,mod:<attribute>,bg:<color>).
|
||||
// Ordering does not matter. All fields are optional.
|
||||
func ParseStyles(s string, defaultStyle Style) []Cell {
|
||||
cells := []Cell{}
|
||||
runes := []rune(s)
|
||||
state := parserStateDefault
|
||||
styledText := []rune{}
|
||||
styleItems := []rune{}
|
||||
squareCount := 0
|
||||
|
||||
reset := func() {
|
||||
styledText = []rune{}
|
||||
styleItems = []rune{}
|
||||
state = parserStateDefault
|
||||
squareCount = 0
|
||||
}
|
||||
|
||||
rollback := func() {
|
||||
cells = append(cells, RunesToStyledCells(styledText, defaultStyle)...)
|
||||
cells = append(cells, RunesToStyledCells(styleItems, defaultStyle)...)
|
||||
reset()
|
||||
}
|
||||
|
||||
// chop first and last runes
|
||||
chop := func(s []rune) []rune {
|
||||
return s[1 : len(s)-1]
|
||||
}
|
||||
|
||||
for i, _rune := range runes {
|
||||
switch state {
|
||||
case parserStateDefault:
|
||||
if _rune == tokenBeginStyledText {
|
||||
state = parserStateStyledText
|
||||
squareCount = 1
|
||||
styledText = append(styledText, _rune)
|
||||
} else {
|
||||
cells = append(cells, Cell{_rune, defaultStyle})
|
||||
}
|
||||
case parserStateStyledText:
|
||||
switch {
|
||||
case squareCount == 0:
|
||||
switch _rune {
|
||||
case tokenBeginStyle:
|
||||
state = parserStateStyleItems
|
||||
styleItems = append(styleItems, _rune)
|
||||
default:
|
||||
rollback()
|
||||
switch _rune {
|
||||
case tokenBeginStyledText:
|
||||
state = parserStateStyledText
|
||||
squareCount = 1
|
||||
styleItems = append(styleItems, _rune)
|
||||
default:
|
||||
cells = append(cells, Cell{_rune, defaultStyle})
|
||||
}
|
||||
}
|
||||
case len(runes) == i+1:
|
||||
rollback()
|
||||
styledText = append(styledText, _rune)
|
||||
case _rune == tokenBeginStyledText:
|
||||
squareCount++
|
||||
styledText = append(styledText, _rune)
|
||||
case _rune == tokenEndStyledText:
|
||||
squareCount--
|
||||
styledText = append(styledText, _rune)
|
||||
default:
|
||||
styledText = append(styledText, _rune)
|
||||
}
|
||||
case parserStateStyleItems:
|
||||
styleItems = append(styleItems, _rune)
|
||||
if _rune == tokenEndStyle {
|
||||
style := readStyle(chop(styleItems), defaultStyle)
|
||||
cells = append(cells, RunesToStyledCells(chop(styledText), style)...)
|
||||
reset()
|
||||
} else if len(runes) == i+1 {
|
||||
rollback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cells
|
||||
}
|
||||
53
v3/symbols.go
Normal file
53
v3/symbols.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package termui
|
||||
|
||||
const (
|
||||
DOT = '•'
|
||||
ELLIPSES = '…'
|
||||
|
||||
UP_ARROW = '▲'
|
||||
DOWN_ARROW = '▼'
|
||||
)
|
||||
|
||||
var (
|
||||
BARS = [...]rune{' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
||||
|
||||
SHADED_BLOCKS = [...]rune{' ', '░', '▒', '▓', '█'}
|
||||
|
||||
IRREGULAR_BLOCKS = [...]rune{
|
||||
' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛',
|
||||
'▗', '▚', '▐', '▜', '▄', '▙', '▟', '█',
|
||||
}
|
||||
|
||||
BRAILLE_OFFSET = '\u2800'
|
||||
BRAILLE = [4][2]rune{
|
||||
{'\u0001', '\u0008'},
|
||||
{'\u0002', '\u0010'},
|
||||
{'\u0004', '\u0020'},
|
||||
{'\u0040', '\u0080'},
|
||||
}
|
||||
|
||||
DOUBLE_BRAILLE = 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}: '⠉',
|
||||
}
|
||||
|
||||
SINGLE_BRAILLE_LEFT = [4]rune{'\u2840', '⠄', '⠂', '⠁'}
|
||||
SINGLE_BRAILLE_RIGHT = [4]rune{'\u2880', '⠠', '⠐', '⠈'}
|
||||
)
|
||||
28
v3/symbols_other.go
Normal file
28
v3/symbols_other.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// 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_LEFT = '┌'
|
||||
TOP_RIGHT = '┐'
|
||||
BOTTOM_LEFT = '└'
|
||||
BOTTOM_RIGHT = '┘'
|
||||
|
||||
VERTICAL_LINE = '│'
|
||||
HORIZONTAL_LINE = '─'
|
||||
|
||||
VERTICAL_LEFT = '┤'
|
||||
VERTICAL_RIGHT = '├'
|
||||
HORIZONTAL_UP = '┴'
|
||||
HORIZONTAL_DOWN = '┬'
|
||||
|
||||
QUOTA_LEFT = '«'
|
||||
QUOTA_RIGHT = '»'
|
||||
|
||||
VERTICAL_DASH = '┊'
|
||||
HORIZONTAL_DASH = '┈'
|
||||
)
|
||||
28
v3/symbols_windows.go
Normal file
28
v3/symbols_windows.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// 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_LEFT = '+'
|
||||
TOP_RIGHT = '+'
|
||||
BOTTOM_LEFT = '+'
|
||||
BOTTOM_RIGHT = '+'
|
||||
|
||||
VERTICAL_LINE = '|'
|
||||
HORIZONTAL_LINE = '-'
|
||||
|
||||
VERTICAL_LEFT = '+'
|
||||
VERTICAL_RIGHT = '+'
|
||||
HORIZONTAL_UP = '+'
|
||||
HORIZONTAL_DOWN = '+'
|
||||
|
||||
QUOTA_LEFT = '<'
|
||||
QUOTA_RIGHT = '>'
|
||||
|
||||
VERTICAL_DASH = '|'
|
||||
HORIZONTAL_DASH = '-'
|
||||
)
|
||||
154
v3/theme.go
Normal file
154
v3/theme.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// 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
|
||||
|
||||
var StandardColors = []Color{
|
||||
ColorRed,
|
||||
ColorGreen,
|
||||
ColorYellow,
|
||||
ColorBlue,
|
||||
ColorMagenta,
|
||||
ColorCyan,
|
||||
ColorWhite,
|
||||
}
|
||||
|
||||
var StandardStyles = []Style{
|
||||
NewStyle(ColorRed),
|
||||
NewStyle(ColorGreen),
|
||||
NewStyle(ColorYellow),
|
||||
NewStyle(ColorBlue),
|
||||
NewStyle(ColorMagenta),
|
||||
NewStyle(ColorCyan),
|
||||
NewStyle(ColorWhite),
|
||||
}
|
||||
|
||||
type RootTheme struct {
|
||||
Default Style
|
||||
|
||||
Block BlockTheme
|
||||
|
||||
BarChart BarChartTheme
|
||||
Gauge GaugeTheme
|
||||
Plot PlotTheme
|
||||
List ListTheme
|
||||
Paragraph ParagraphTheme
|
||||
PieChart PieChartTheme
|
||||
Sparkline SparklineTheme
|
||||
StackedBarChart StackedBarChartTheme
|
||||
Tab TabTheme
|
||||
Table TableTheme
|
||||
}
|
||||
|
||||
type BlockTheme struct {
|
||||
Title Style
|
||||
Border Style
|
||||
}
|
||||
|
||||
type BarChartTheme struct {
|
||||
Bars []Color
|
||||
Nums []Style
|
||||
Labels []Style
|
||||
}
|
||||
|
||||
type GaugeTheme struct {
|
||||
Bar Color
|
||||
Label Style
|
||||
}
|
||||
|
||||
type PlotTheme struct {
|
||||
Lines []Color
|
||||
Axes Color
|
||||
}
|
||||
|
||||
type ListTheme struct {
|
||||
Text Style
|
||||
}
|
||||
|
||||
type ParagraphTheme struct {
|
||||
Text Style
|
||||
}
|
||||
|
||||
type PieChartTheme struct {
|
||||
Slices []Color
|
||||
}
|
||||
|
||||
type SparklineTheme struct {
|
||||
Title Style
|
||||
Line Color
|
||||
}
|
||||
|
||||
type StackedBarChartTheme struct {
|
||||
Bars []Color
|
||||
Nums []Style
|
||||
Labels []Style
|
||||
}
|
||||
|
||||
type TabTheme struct {
|
||||
Active Style
|
||||
Inactive Style
|
||||
}
|
||||
|
||||
type TableTheme struct {
|
||||
Text Style
|
||||
}
|
||||
|
||||
// Theme holds the default Styles and Colors for all widgets.
|
||||
// You can set default widget Styles by modifying the Theme before creating the widgets.
|
||||
var Theme = RootTheme{
|
||||
Default: NewStyle(ColorWhite),
|
||||
|
||||
Block: BlockTheme{
|
||||
Title: NewStyle(ColorWhite),
|
||||
Border: NewStyle(ColorWhite),
|
||||
},
|
||||
|
||||
BarChart: BarChartTheme{
|
||||
Bars: StandardColors,
|
||||
Nums: StandardStyles,
|
||||
Labels: StandardStyles,
|
||||
},
|
||||
|
||||
Paragraph: ParagraphTheme{
|
||||
Text: NewStyle(ColorWhite),
|
||||
},
|
||||
|
||||
PieChart: PieChartTheme{
|
||||
Slices: StandardColors,
|
||||
},
|
||||
|
||||
List: ListTheme{
|
||||
Text: NewStyle(ColorWhite),
|
||||
},
|
||||
|
||||
StackedBarChart: StackedBarChartTheme{
|
||||
Bars: StandardColors,
|
||||
Nums: StandardStyles,
|
||||
Labels: StandardStyles,
|
||||
},
|
||||
|
||||
Gauge: GaugeTheme{
|
||||
Bar: ColorWhite,
|
||||
Label: NewStyle(ColorWhite),
|
||||
},
|
||||
|
||||
Sparkline: SparklineTheme{
|
||||
Title: NewStyle(ColorWhite),
|
||||
Line: ColorWhite,
|
||||
},
|
||||
|
||||
Plot: PlotTheme{
|
||||
Lines: StandardColors,
|
||||
Axes: ColorWhite,
|
||||
},
|
||||
|
||||
Table: TableTheme{
|
||||
Text: NewStyle(ColorWhite),
|
||||
},
|
||||
|
||||
Tab: TabTheme{
|
||||
Active: NewStyle(ColorRed),
|
||||
Inactive: NewStyle(ColorWhite),
|
||||
},
|
||||
}
|
||||
230
v3/utils.go
Normal file
230
v3/utils.go
Normal file
@@ -0,0 +1,230 @@
|
||||
// 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"
|
||||
"reflect"
|
||||
|
||||
rw "github.com/mattn/go-runewidth"
|
||||
wordwrap "github.com/mitchellh/go-wordwrap"
|
||||
)
|
||||
|
||||
// InterfaceSlice takes an []interface{} represented as an interface{} and converts it
|
||||
// https://stackoverflow.com/questions/12753805/type-converting-slices-of-interfaces-in-go
|
||||
func InterfaceSlice(slice interface{}) []interface{} {
|
||||
s := reflect.ValueOf(slice)
|
||||
if s.Kind() != reflect.Slice {
|
||||
panic("InterfaceSlice() given a non-slice type")
|
||||
}
|
||||
|
||||
ret := make([]interface{}, s.Len())
|
||||
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
ret[i] = s.Index(i).Interface()
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// TrimString trims a string to a max length and adds '…' to the end if it was trimmed.
|
||||
func TrimString(s string, w int) string {
|
||||
if w <= 0 {
|
||||
return ""
|
||||
}
|
||||
if rw.StringWidth(s) > w {
|
||||
return rw.Truncate(s, w, string(ELLIPSES))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func SelectColor(colors []Color, index int) Color {
|
||||
return colors[index%len(colors)]
|
||||
}
|
||||
|
||||
func SelectStyle(styles []Style, index int) Style {
|
||||
return styles[index%len(styles)]
|
||||
}
|
||||
|
||||
// Math ------------------------------------------------------------------------
|
||||
|
||||
func SumIntSlice(slice []int) int {
|
||||
sum := 0
|
||||
for _, val := range slice {
|
||||
sum += val
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func SumFloat64Slice(data []float64) float64 {
|
||||
sum := 0.0
|
||||
for _, v := range data {
|
||||
sum += v
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func GetMaxIntFromSlice(slice []int) (int, error) {
|
||||
if len(slice) == 0 {
|
||||
return 0, fmt.Errorf("cannot get max value from empty slice")
|
||||
}
|
||||
var max int
|
||||
for _, val := range slice {
|
||||
if val > max {
|
||||
max = val
|
||||
}
|
||||
}
|
||||
return max, nil
|
||||
}
|
||||
|
||||
func GetMaxFloat64FromSlice(slice []float64) (float64, error) {
|
||||
if len(slice) == 0 {
|
||||
return 0, fmt.Errorf("cannot get max value from empty slice")
|
||||
}
|
||||
var max float64
|
||||
for _, val := range slice {
|
||||
if val > max {
|
||||
max = val
|
||||
}
|
||||
}
|
||||
return max, nil
|
||||
}
|
||||
|
||||
func GetMaxFloat64From2dSlice(slices [][]float64) (float64, error) {
|
||||
if len(slices) == 0 {
|
||||
return 0, fmt.Errorf("cannot get max value from empty slice")
|
||||
}
|
||||
var max float64
|
||||
for _, slice := range slices {
|
||||
for _, val := range slice {
|
||||
if val > max {
|
||||
max = val
|
||||
}
|
||||
}
|
||||
}
|
||||
return max, nil
|
||||
}
|
||||
|
||||
func RoundFloat64(x float64) float64 {
|
||||
return math.Floor(x + 0.5)
|
||||
}
|
||||
|
||||
func FloorFloat64(x float64) float64 {
|
||||
return math.Floor(x)
|
||||
}
|
||||
|
||||
func AbsInt(x int) int {
|
||||
if x >= 0 {
|
||||
return x
|
||||
}
|
||||
return -x
|
||||
}
|
||||
|
||||
func MinFloat64(x, y float64) float64 {
|
||||
if x < y {
|
||||
return x
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
func MaxFloat64(x, y float64) float64 {
|
||||
if x > y {
|
||||
return x
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
func MaxInt(x, y int) int {
|
||||
if x > y {
|
||||
return x
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
func MinInt(x, y int) int {
|
||||
if x < y {
|
||||
return x
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
// []Cell ----------------------------------------------------------------------
|
||||
|
||||
// WrapCells takes []Cell and inserts Cells containing '\n' wherever a linebreak should go.
|
||||
func WrapCells(cells []Cell, width uint) []Cell {
|
||||
str := CellsToString(cells)
|
||||
wrapped := wordwrap.WrapString(str, width)
|
||||
wrappedCells := []Cell{}
|
||||
i := 0
|
||||
for _, _rune := range wrapped {
|
||||
if _rune == '\n' {
|
||||
wrappedCells = append(wrappedCells, Cell{_rune, StyleClear})
|
||||
} else {
|
||||
wrappedCells = append(wrappedCells, Cell{_rune, cells[i].Style})
|
||||
}
|
||||
i++
|
||||
}
|
||||
return wrappedCells
|
||||
}
|
||||
|
||||
func RunesToStyledCells(runes []rune, style Style) []Cell {
|
||||
cells := []Cell{}
|
||||
for _, _rune := range runes {
|
||||
cells = append(cells, Cell{_rune, style})
|
||||
}
|
||||
return cells
|
||||
}
|
||||
|
||||
func CellsToString(cells []Cell) string {
|
||||
runes := make([]rune, len(cells))
|
||||
for i, cell := range cells {
|
||||
runes[i] = cell.Rune
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
func TrimCells(cells []Cell, w int) []Cell {
|
||||
s := CellsToString(cells)
|
||||
s = TrimString(s, w)
|
||||
runes := []rune(s)
|
||||
newCells := []Cell{}
|
||||
for i, r := range runes {
|
||||
newCells = append(newCells, Cell{r, cells[i].Style})
|
||||
}
|
||||
return newCells
|
||||
}
|
||||
|
||||
func SplitCells(cells []Cell, r rune) [][]Cell {
|
||||
splitCells := [][]Cell{}
|
||||
temp := []Cell{}
|
||||
for _, cell := range cells {
|
||||
if cell.Rune == r {
|
||||
splitCells = append(splitCells, temp)
|
||||
temp = []Cell{}
|
||||
} else {
|
||||
temp = append(temp, cell)
|
||||
}
|
||||
}
|
||||
if len(temp) > 0 {
|
||||
splitCells = append(splitCells, temp)
|
||||
}
|
||||
return splitCells
|
||||
}
|
||||
|
||||
type CellWithX struct {
|
||||
X int
|
||||
Cell Cell
|
||||
}
|
||||
|
||||
func BuildCellWithXArray(cells []Cell) []CellWithX {
|
||||
cellWithXArray := make([]CellWithX, len(cells))
|
||||
index := 0
|
||||
for i, cell := range cells {
|
||||
cellWithXArray[i] = CellWithX{X: index, Cell: cell}
|
||||
index += rw.RuneWidth(cell.Rune)
|
||||
}
|
||||
return cellWithXArray
|
||||
}
|
||||
89
v3/widgets/barchart.go
Normal file
89
v3/widgets/barchart.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// 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 widgets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
rw "github.com/mattn/go-runewidth"
|
||||
|
||||
. "github.com/gizak/termui/v3"
|
||||
)
|
||||
|
||||
type BarChart struct {
|
||||
Block
|
||||
BarColors []Color
|
||||
LabelStyles []Style
|
||||
NumStyles []Style // only Fg and Modifier are used
|
||||
NumFormatter func(float64) string
|
||||
Data []float64
|
||||
Labels []string
|
||||
BarWidth int
|
||||
BarGap int
|
||||
MaxVal float64
|
||||
}
|
||||
|
||||
func NewBarChart() *BarChart {
|
||||
return &BarChart{
|
||||
Block: *NewBlock(),
|
||||
BarColors: Theme.BarChart.Bars,
|
||||
NumStyles: Theme.BarChart.Nums,
|
||||
LabelStyles: Theme.BarChart.Labels,
|
||||
NumFormatter: func(n float64) string { return fmt.Sprint(n) },
|
||||
BarGap: 1,
|
||||
BarWidth: 3,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *BarChart) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
maxVal := self.MaxVal
|
||||
if maxVal == 0 {
|
||||
maxVal, _ = GetMaxFloat64FromSlice(self.Data)
|
||||
}
|
||||
|
||||
barXCoordinate := self.Inner.Min.X
|
||||
|
||||
for i, data := range self.Data {
|
||||
// draw bar
|
||||
height := int((data / maxVal) * float64(self.Inner.Dy()-1))
|
||||
for x := barXCoordinate; x < MinInt(barXCoordinate+self.BarWidth, self.Inner.Max.X); x++ {
|
||||
for y := self.Inner.Max.Y - 2; y > (self.Inner.Max.Y-2)-height; y-- {
|
||||
c := NewCell(' ', NewStyle(ColorClear, SelectColor(self.BarColors, i)))
|
||||
buf.SetCell(c, image.Pt(x, y))
|
||||
}
|
||||
}
|
||||
|
||||
// draw label
|
||||
if i < len(self.Labels) {
|
||||
labelXCoordinate := barXCoordinate +
|
||||
int((float64(self.BarWidth) / 2)) -
|
||||
int((float64(rw.StringWidth(self.Labels[i])) / 2))
|
||||
buf.SetString(
|
||||
self.Labels[i],
|
||||
SelectStyle(self.LabelStyles, i),
|
||||
image.Pt(labelXCoordinate, self.Inner.Max.Y-1),
|
||||
)
|
||||
}
|
||||
|
||||
// draw number
|
||||
numberXCoordinate := barXCoordinate + int((float64(self.BarWidth) / 2))
|
||||
if numberXCoordinate <= self.Inner.Max.X {
|
||||
buf.SetString(
|
||||
self.NumFormatter(data),
|
||||
NewStyle(
|
||||
SelectStyle(self.NumStyles, i+1).Fg,
|
||||
SelectColor(self.BarColors, i),
|
||||
SelectStyle(self.NumStyles, i+1).Modifier,
|
||||
),
|
||||
image.Pt(numberXCoordinate, self.Inner.Max.Y-2),
|
||||
)
|
||||
}
|
||||
|
||||
barXCoordinate += (self.BarWidth + self.BarGap)
|
||||
}
|
||||
}
|
||||
57
v3/widgets/gauge.go
Normal file
57
v3/widgets/gauge.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// 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 widgets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
. "github.com/gizak/termui/v3"
|
||||
)
|
||||
|
||||
type Gauge struct {
|
||||
Block
|
||||
Percent int
|
||||
BarColor Color
|
||||
Label string
|
||||
LabelStyle Style
|
||||
}
|
||||
|
||||
func NewGauge() *Gauge {
|
||||
return &Gauge{
|
||||
Block: *NewBlock(),
|
||||
BarColor: Theme.Gauge.Bar,
|
||||
LabelStyle: Theme.Gauge.Label,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Gauge) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
label := self.Label
|
||||
if label == "" {
|
||||
label = fmt.Sprintf("%d%%", self.Percent)
|
||||
}
|
||||
|
||||
// plot bar
|
||||
barWidth := int((float64(self.Percent) / 100) * float64(self.Inner.Dx()))
|
||||
buf.Fill(
|
||||
NewCell(' ', NewStyle(ColorClear, self.BarColor)),
|
||||
image.Rect(self.Inner.Min.X, self.Inner.Min.Y, self.Inner.Min.X+barWidth, self.Inner.Max.Y),
|
||||
)
|
||||
|
||||
// plot label
|
||||
labelXCoordinate := self.Inner.Min.X + (self.Inner.Dx() / 2) - int(float64(len(label))/2)
|
||||
labelYCoordinate := self.Inner.Min.Y + ((self.Inner.Dy() - 1) / 2)
|
||||
if labelYCoordinate < self.Inner.Max.Y {
|
||||
for i, char := range label {
|
||||
style := self.LabelStyle
|
||||
if labelXCoordinate+i+1 <= self.Inner.Min.X+barWidth {
|
||||
style = NewStyle(self.BarColor, ColorClear, ModifierReverse)
|
||||
}
|
||||
buf.SetCell(NewCell(char, style), image.Pt(labelXCoordinate+i, labelYCoordinate))
|
||||
}
|
||||
}
|
||||
}
|
||||
204
v3/widgets/image.go
Normal file
204
v3/widgets/image.go
Normal file
@@ -0,0 +1,204 @@
|
||||
// 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 widgets
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
. "github.com/gizak/termui/v3"
|
||||
)
|
||||
|
||||
type Image struct {
|
||||
Block
|
||||
Image image.Image
|
||||
Monochrome bool
|
||||
MonochromeThreshold uint8
|
||||
MonochromeInvert bool
|
||||
}
|
||||
|
||||
func NewImage(img image.Image) *Image {
|
||||
return &Image{
|
||||
Block: *NewBlock(),
|
||||
MonochromeThreshold: 128,
|
||||
Image: img,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Image) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
if self.Image == nil {
|
||||
return
|
||||
}
|
||||
|
||||
bufWidth := self.Inner.Dx()
|
||||
bufHeight := self.Inner.Dy()
|
||||
imageWidth := self.Image.Bounds().Dx()
|
||||
imageHeight := self.Image.Bounds().Dy()
|
||||
|
||||
if self.Monochrome {
|
||||
if bufWidth > imageWidth/2 {
|
||||
bufWidth = imageWidth / 2
|
||||
}
|
||||
if bufHeight > imageHeight/2 {
|
||||
bufHeight = imageHeight / 2
|
||||
}
|
||||
for bx := 0; bx < bufWidth; bx++ {
|
||||
for by := 0; by < bufHeight; by++ {
|
||||
ul := self.colorAverage(
|
||||
2*bx*imageWidth/bufWidth/2,
|
||||
(2*bx+1)*imageWidth/bufWidth/2,
|
||||
2*by*imageHeight/bufHeight/2,
|
||||
(2*by+1)*imageHeight/bufHeight/2,
|
||||
)
|
||||
ur := self.colorAverage(
|
||||
(2*bx+1)*imageWidth/bufWidth/2,
|
||||
(2*bx+2)*imageWidth/bufWidth/2,
|
||||
2*by*imageHeight/bufHeight/2,
|
||||
(2*by+1)*imageHeight/bufHeight/2,
|
||||
)
|
||||
ll := self.colorAverage(
|
||||
2*bx*imageWidth/bufWidth/2,
|
||||
(2*bx+1)*imageWidth/bufWidth/2,
|
||||
(2*by+1)*imageHeight/bufHeight/2,
|
||||
(2*by+2)*imageHeight/bufHeight/2,
|
||||
)
|
||||
lr := self.colorAverage(
|
||||
(2*bx+1)*imageWidth/bufWidth/2,
|
||||
(2*bx+2)*imageWidth/bufWidth/2,
|
||||
(2*by+1)*imageHeight/bufHeight/2,
|
||||
(2*by+2)*imageHeight/bufHeight/2,
|
||||
)
|
||||
buf.SetCell(
|
||||
NewCell(blocksChar(ul, ur, ll, lr, self.MonochromeThreshold, self.MonochromeInvert)),
|
||||
image.Pt(self.Inner.Min.X+bx, self.Inner.Min.Y+by),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if bufWidth > imageWidth {
|
||||
bufWidth = imageWidth
|
||||
}
|
||||
if bufHeight > imageHeight {
|
||||
bufHeight = imageHeight
|
||||
}
|
||||
for bx := 0; bx < bufWidth; bx++ {
|
||||
for by := 0; by < bufHeight; by++ {
|
||||
c := self.colorAverage(
|
||||
bx*imageWidth/bufWidth,
|
||||
(bx+1)*imageWidth/bufWidth,
|
||||
by*imageHeight/bufHeight,
|
||||
(by+1)*imageHeight/bufHeight,
|
||||
)
|
||||
buf.SetCell(
|
||||
NewCell(c.ch(), NewStyle(c.fgColor(), ColorBlack)),
|
||||
image.Pt(self.Inner.Min.X+bx, self.Inner.Min.Y+by),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Image) colorAverage(x0, x1, y0, y1 int) colorAverager {
|
||||
var c colorAverager
|
||||
for x := x0; x < x1; x++ {
|
||||
for y := y0; y < y1; y++ {
|
||||
c = c.add(
|
||||
self.Image.At(
|
||||
x+self.Image.Bounds().Min.X,
|
||||
y+self.Image.Bounds().Min.Y,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
type colorAverager struct {
|
||||
rsum, gsum, bsum, asum, count uint64
|
||||
}
|
||||
|
||||
func (self colorAverager) add(col color.Color) colorAverager {
|
||||
r, g, b, a := col.RGBA()
|
||||
return colorAverager{
|
||||
rsum: self.rsum + uint64(r),
|
||||
gsum: self.gsum + uint64(g),
|
||||
bsum: self.bsum + uint64(b),
|
||||
asum: self.asum + uint64(a),
|
||||
count: self.count + 1,
|
||||
}
|
||||
}
|
||||
|
||||
func (self colorAverager) RGBA() (uint32, uint32, uint32, uint32) {
|
||||
if self.count == 0 {
|
||||
return 0, 0, 0, 0
|
||||
}
|
||||
return uint32(self.rsum/self.count) & 0xffff,
|
||||
uint32(self.gsum/self.count) & 0xffff,
|
||||
uint32(self.bsum/self.count) & 0xffff,
|
||||
uint32(self.asum/self.count) & 0xffff
|
||||
}
|
||||
|
||||
func (self colorAverager) fgColor() Color {
|
||||
return palette.Convert(self).(paletteColor).attribute
|
||||
}
|
||||
|
||||
func (self colorAverager) ch() rune {
|
||||
gray := color.GrayModel.Convert(self).(color.Gray).Y
|
||||
switch {
|
||||
case gray < 51:
|
||||
return SHADED_BLOCKS[0]
|
||||
case gray < 102:
|
||||
return SHADED_BLOCKS[1]
|
||||
case gray < 153:
|
||||
return SHADED_BLOCKS[2]
|
||||
case gray < 204:
|
||||
return SHADED_BLOCKS[3]
|
||||
default:
|
||||
return SHADED_BLOCKS[4]
|
||||
}
|
||||
}
|
||||
|
||||
func (self colorAverager) monochrome(threshold uint8, invert bool) bool {
|
||||
return self.count != 0 && (color.GrayModel.Convert(self).(color.Gray).Y < threshold != invert)
|
||||
}
|
||||
|
||||
type paletteColor struct {
|
||||
rgba color.RGBA
|
||||
attribute Color
|
||||
}
|
||||
|
||||
func (self paletteColor) RGBA() (uint32, uint32, uint32, uint32) {
|
||||
return self.rgba.RGBA()
|
||||
}
|
||||
|
||||
var palette = color.Palette([]color.Color{
|
||||
paletteColor{color.RGBA{0, 0, 0, 255}, ColorBlack},
|
||||
paletteColor{color.RGBA{255, 0, 0, 255}, ColorRed},
|
||||
paletteColor{color.RGBA{0, 255, 0, 255}, ColorGreen},
|
||||
paletteColor{color.RGBA{255, 255, 0, 255}, ColorYellow},
|
||||
paletteColor{color.RGBA{0, 0, 255, 255}, ColorBlue},
|
||||
paletteColor{color.RGBA{255, 0, 255, 255}, ColorMagenta},
|
||||
paletteColor{color.RGBA{0, 255, 255, 255}, ColorCyan},
|
||||
paletteColor{color.RGBA{255, 255, 255, 255}, ColorWhite},
|
||||
})
|
||||
|
||||
func blocksChar(ul, ur, ll, lr colorAverager, threshold uint8, invert bool) rune {
|
||||
index := 0
|
||||
if ul.monochrome(threshold, invert) {
|
||||
index |= 1
|
||||
}
|
||||
if ur.monochrome(threshold, invert) {
|
||||
index |= 2
|
||||
}
|
||||
if ll.monochrome(threshold, invert) {
|
||||
index |= 4
|
||||
}
|
||||
if lr.monochrome(threshold, invert) {
|
||||
index |= 8
|
||||
}
|
||||
return IRREGULAR_BLOCKS[index]
|
||||
}
|
||||
136
v3/widgets/list.go
Normal file
136
v3/widgets/list.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// 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 widgets
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
rw "github.com/mattn/go-runewidth"
|
||||
|
||||
. "github.com/gizak/termui/v3"
|
||||
)
|
||||
|
||||
type List struct {
|
||||
Block
|
||||
Rows []string
|
||||
WrapText bool
|
||||
TextStyle Style
|
||||
SelectedRow int
|
||||
topRow int
|
||||
SelectedRowStyle Style
|
||||
}
|
||||
|
||||
func NewList() *List {
|
||||
return &List{
|
||||
Block: *NewBlock(),
|
||||
TextStyle: Theme.List.Text,
|
||||
SelectedRowStyle: Theme.List.Text,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *List) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
point := self.Inner.Min
|
||||
|
||||
// adjusts view into widget
|
||||
if self.SelectedRow >= self.Inner.Dy()+self.topRow {
|
||||
self.topRow = self.SelectedRow - self.Inner.Dy() + 1
|
||||
} else if self.SelectedRow < self.topRow {
|
||||
self.topRow = self.SelectedRow
|
||||
}
|
||||
|
||||
// draw rows
|
||||
for row := self.topRow; row < len(self.Rows) && point.Y < self.Inner.Max.Y; row++ {
|
||||
cells := ParseStyles(self.Rows[row], self.TextStyle)
|
||||
if self.WrapText {
|
||||
cells = WrapCells(cells, uint(self.Inner.Dx()))
|
||||
}
|
||||
for j := 0; j < len(cells) && point.Y < self.Inner.Max.Y; j++ {
|
||||
style := cells[j].Style
|
||||
if row == self.SelectedRow {
|
||||
style = self.SelectedRowStyle
|
||||
}
|
||||
if cells[j].Rune == '\n' {
|
||||
point = image.Pt(self.Inner.Min.X, point.Y+1)
|
||||
} else {
|
||||
if point.X+1 == self.Inner.Max.X+1 && len(cells) > self.Inner.Dx() {
|
||||
buf.SetCell(NewCell(ELLIPSES, style), point.Add(image.Pt(-1, 0)))
|
||||
break
|
||||
} else {
|
||||
buf.SetCell(NewCell(cells[j].Rune, style), point)
|
||||
point = point.Add(image.Pt(rw.RuneWidth(cells[j].Rune), 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
point = image.Pt(self.Inner.Min.X, point.Y+1)
|
||||
}
|
||||
|
||||
// draw UP_ARROW if needed
|
||||
if self.topRow > 0 {
|
||||
buf.SetCell(
|
||||
NewCell(UP_ARROW, NewStyle(ColorWhite)),
|
||||
image.Pt(self.Inner.Max.X-1, self.Inner.Min.Y),
|
||||
)
|
||||
}
|
||||
|
||||
// draw DOWN_ARROW if needed
|
||||
if len(self.Rows) > int(self.topRow)+self.Inner.Dy() {
|
||||
buf.SetCell(
|
||||
NewCell(DOWN_ARROW, NewStyle(ColorWhite)),
|
||||
image.Pt(self.Inner.Max.X-1, self.Inner.Max.Y-1),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ScrollAmount scrolls by amount given. If amount is < 0, then scroll up.
|
||||
// There is no need to set self.topRow, as this will be set automatically when drawn,
|
||||
// since if the selected item is off screen then the topRow variable will change accordingly.
|
||||
func (self *List) ScrollAmount(amount int) {
|
||||
if len(self.Rows)-int(self.SelectedRow) <= amount {
|
||||
self.SelectedRow = len(self.Rows) - 1
|
||||
} else if int(self.SelectedRow)+amount < 0 {
|
||||
self.SelectedRow = 0
|
||||
} else {
|
||||
self.SelectedRow += amount
|
||||
}
|
||||
}
|
||||
|
||||
func (self *List) ScrollUp() {
|
||||
self.ScrollAmount(-1)
|
||||
}
|
||||
|
||||
func (self *List) ScrollDown() {
|
||||
self.ScrollAmount(1)
|
||||
}
|
||||
|
||||
func (self *List) ScrollPageUp() {
|
||||
// If an item is selected below top row, then go to the top row.
|
||||
if self.SelectedRow > self.topRow {
|
||||
self.SelectedRow = self.topRow
|
||||
} else {
|
||||
self.ScrollAmount(-self.Inner.Dy())
|
||||
}
|
||||
}
|
||||
|
||||
func (self *List) ScrollPageDown() {
|
||||
self.ScrollAmount(self.Inner.Dy())
|
||||
}
|
||||
|
||||
func (self *List) ScrollHalfPageUp() {
|
||||
self.ScrollAmount(-int(FloorFloat64(float64(self.Inner.Dy()) / 2)))
|
||||
}
|
||||
|
||||
func (self *List) ScrollHalfPageDown() {
|
||||
self.ScrollAmount(int(FloorFloat64(float64(self.Inner.Dy()) / 2)))
|
||||
}
|
||||
|
||||
func (self *List) ScrollTop() {
|
||||
self.SelectedRow = 0
|
||||
}
|
||||
|
||||
func (self *List) ScrollBottom() {
|
||||
self.SelectedRow = len(self.Rows) - 1
|
||||
}
|
||||
48
v3/widgets/paragraph.go
Normal file
48
v3/widgets/paragraph.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// 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 widgets
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
. "github.com/gizak/termui/v3"
|
||||
)
|
||||
|
||||
type Paragraph struct {
|
||||
Block
|
||||
Text string
|
||||
TextStyle Style
|
||||
WrapText bool
|
||||
}
|
||||
|
||||
func NewParagraph() *Paragraph {
|
||||
return &Paragraph{
|
||||
Block: *NewBlock(),
|
||||
TextStyle: Theme.Paragraph.Text,
|
||||
WrapText: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Paragraph) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
cells := ParseStyles(self.Text, self.TextStyle)
|
||||
if self.WrapText {
|
||||
cells = WrapCells(cells, uint(self.Inner.Dx()))
|
||||
}
|
||||
|
||||
rows := SplitCells(cells, '\n')
|
||||
|
||||
for y, row := range rows {
|
||||
if y+self.Inner.Min.Y >= self.Inner.Max.Y {
|
||||
break
|
||||
}
|
||||
row = TrimCells(row, self.Inner.Dx())
|
||||
for _, cx := range BuildCellWithXArray(row) {
|
||||
x, cell := cx.X, cx.Cell
|
||||
buf.SetCell(cell, image.Pt(x, y).Add(self.Inner.Min))
|
||||
}
|
||||
}
|
||||
}
|
||||
149
v3/widgets/piechart.go
Normal file
149
v3/widgets/piechart.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"image"
|
||||
"math"
|
||||
|
||||
. "github.com/gizak/termui/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
piechartOffsetUp = -.5 * math.Pi // the northward angle
|
||||
resolutionFactor = .0001 // circle resolution: precision vs. performance
|
||||
fullCircle = 2.0 * math.Pi // the full circle angle
|
||||
xStretch = 2.0 // horizontal adjustment
|
||||
)
|
||||
|
||||
// PieChartLabel callback
|
||||
type PieChartLabel func(dataIndex int, currentValue float64) string
|
||||
|
||||
type PieChart struct {
|
||||
Block
|
||||
Data []float64 // list of data items
|
||||
Colors []Color // colors to by cycled through
|
||||
LabelFormatter PieChartLabel // callback function for labels
|
||||
AngleOffset float64 // which angle to start drawing at? (see piechartOffsetUp)
|
||||
}
|
||||
|
||||
// NewPieChart Creates a new pie chart with reasonable defaults and no labels.
|
||||
func NewPieChart() *PieChart {
|
||||
return &PieChart{
|
||||
Block: *NewBlock(),
|
||||
Colors: Theme.PieChart.Slices,
|
||||
AngleOffset: piechartOffsetUp,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *PieChart) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
center := self.Inner.Min.Add(self.Inner.Size().Div(2))
|
||||
radius := MinFloat64(float64(self.Inner.Dx()/2/xStretch), float64(self.Inner.Dy()/2))
|
||||
|
||||
// compute slice sizes
|
||||
sum := SumFloat64Slice(self.Data)
|
||||
sliceSizes := make([]float64, len(self.Data))
|
||||
for i, v := range self.Data {
|
||||
sliceSizes[i] = v / sum * fullCircle
|
||||
}
|
||||
|
||||
borderCircle := &circle{center, radius}
|
||||
middleCircle := circle{Point: center, radius: radius / 2.0}
|
||||
|
||||
// draw sectors
|
||||
phi := self.AngleOffset
|
||||
for i, size := range sliceSizes {
|
||||
for j := 0.0; j < size; j += resolutionFactor {
|
||||
borderPoint := borderCircle.at(phi + j)
|
||||
line := line{P1: center, P2: borderPoint}
|
||||
line.draw(NewCell(SHADED_BLOCKS[1], NewStyle(SelectColor(self.Colors, i))), buf)
|
||||
}
|
||||
phi += size
|
||||
}
|
||||
|
||||
// draw labels
|
||||
if self.LabelFormatter != nil {
|
||||
phi = self.AngleOffset
|
||||
for i, size := range sliceSizes {
|
||||
labelPoint := middleCircle.at(phi + size/2.0)
|
||||
if len(self.Data) == 1 {
|
||||
labelPoint = center
|
||||
}
|
||||
buf.SetString(
|
||||
self.LabelFormatter(i, self.Data[i]),
|
||||
NewStyle(SelectColor(self.Colors, i)),
|
||||
image.Pt(labelPoint.X, labelPoint.Y),
|
||||
)
|
||||
phi += size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type circle struct {
|
||||
image.Point
|
||||
radius float64
|
||||
}
|
||||
|
||||
// computes the point at a given angle phi
|
||||
func (self circle) at(phi float64) image.Point {
|
||||
x := self.X + int(RoundFloat64(xStretch*self.radius*math.Cos(phi)))
|
||||
y := self.Y + int(RoundFloat64(self.radius*math.Sin(phi)))
|
||||
return image.Point{X: x, Y: y}
|
||||
}
|
||||
|
||||
// computes the perimeter of a circle
|
||||
func (self circle) perimeter() float64 {
|
||||
return 2.0 * math.Pi * self.radius
|
||||
}
|
||||
|
||||
// a line between two points
|
||||
type line struct {
|
||||
P1, P2 image.Point
|
||||
}
|
||||
|
||||
// draws the line
|
||||
func (self line) draw(cell Cell, buf *Buffer) {
|
||||
isLeftOf := func(p1, p2 image.Point) bool {
|
||||
return p1.X <= p2.X
|
||||
}
|
||||
isTopOf := func(p1, p2 image.Point) bool {
|
||||
return p1.Y <= p2.Y
|
||||
}
|
||||
p1, p2 := self.P1, self.P2
|
||||
buf.SetCell(NewCell('*', cell.Style), self.P2)
|
||||
width, height := self.size()
|
||||
if width > height { // paint left to right
|
||||
if !isLeftOf(p1, p2) {
|
||||
p1, p2 = p2, p1
|
||||
}
|
||||
flip := 1.0
|
||||
if !isTopOf(p1, p2) {
|
||||
flip = -1.0
|
||||
}
|
||||
for x := p1.X; x <= p2.X; x++ {
|
||||
ratio := float64(height) / float64(width)
|
||||
factor := float64(x - p1.X)
|
||||
y := ratio * factor * flip
|
||||
buf.SetCell(cell, image.Pt(x, int(RoundFloat64(y))+p1.Y))
|
||||
}
|
||||
} else { // paint top to bottom
|
||||
if !isTopOf(p1, p2) {
|
||||
p1, p2 = p2, p1
|
||||
}
|
||||
flip := 1.0
|
||||
if !isLeftOf(p1, p2) {
|
||||
flip = -1.0
|
||||
}
|
||||
for y := p1.Y; y <= p2.Y; y++ {
|
||||
ratio := float64(width) / float64(height)
|
||||
factor := float64(y - p1.Y)
|
||||
x := ratio * factor * flip
|
||||
buf.SetCell(cell, image.Pt(int(RoundFloat64(x))+p1.X, y))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// width and height of a line
|
||||
func (self line) size() (w, h int) {
|
||||
return AbsInt(self.P2.X - self.P1.X), AbsInt(self.P2.Y - self.P1.Y)
|
||||
}
|
||||
227
v3/widgets/plot.go
Normal file
227
v3/widgets/plot.go
Normal file
@@ -0,0 +1,227 @@
|
||||
// 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 widgets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
. "github.com/gizak/termui/v3"
|
||||
)
|
||||
|
||||
// Plot has two modes: line(default) and scatter.
|
||||
// Plot also has two marker types: braille(default) and dot.
|
||||
// A single braille character is a 2x4 grid of dots, so using braille
|
||||
// gives 2x X resolution and 4x Y resolution over dot mode.
|
||||
type Plot struct {
|
||||
Block
|
||||
|
||||
Data [][]float64
|
||||
DataLabels []string
|
||||
MaxVal float64
|
||||
|
||||
LineColors []Color
|
||||
AxesColor Color // TODO
|
||||
ShowAxes bool
|
||||
|
||||
Marker PlotMarker
|
||||
DotMarkerRune rune
|
||||
PlotType PlotType
|
||||
HorizontalScale int
|
||||
DrawDirection DrawDirection // TODO
|
||||
}
|
||||
|
||||
const (
|
||||
xAxisLabelsHeight = 1
|
||||
yAxisLabelsWidth = 4
|
||||
xAxisLabelsGap = 2
|
||||
yAxisLabelsGap = 1
|
||||
)
|
||||
|
||||
type PlotType uint
|
||||
|
||||
const (
|
||||
LineChart PlotType = iota
|
||||
ScatterPlot
|
||||
)
|
||||
|
||||
type PlotMarker uint
|
||||
|
||||
const (
|
||||
MarkerBraille PlotMarker = iota
|
||||
MarkerDot
|
||||
)
|
||||
|
||||
type DrawDirection uint
|
||||
|
||||
const (
|
||||
DrawLeft DrawDirection = iota
|
||||
DrawRight
|
||||
)
|
||||
|
||||
func NewPlot() *Plot {
|
||||
return &Plot{
|
||||
Block: *NewBlock(),
|
||||
LineColors: Theme.Plot.Lines,
|
||||
AxesColor: Theme.Plot.Axes,
|
||||
Marker: MarkerBraille,
|
||||
DotMarkerRune: DOT,
|
||||
Data: [][]float64{},
|
||||
HorizontalScale: 1,
|
||||
DrawDirection: DrawRight,
|
||||
ShowAxes: true,
|
||||
PlotType: LineChart,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Plot) renderBraille(buf *Buffer, drawArea image.Rectangle, maxVal float64) {
|
||||
canvas := NewCanvas()
|
||||
canvas.Rectangle = drawArea
|
||||
|
||||
switch self.PlotType {
|
||||
case ScatterPlot:
|
||||
for i, line := range self.Data {
|
||||
for j, val := range line {
|
||||
height := int((val / maxVal) * float64(drawArea.Dy()-1))
|
||||
canvas.SetPoint(
|
||||
image.Pt(
|
||||
(drawArea.Min.X+(j*self.HorizontalScale))*2,
|
||||
(drawArea.Max.Y-height-1)*4,
|
||||
),
|
||||
SelectColor(self.LineColors, i),
|
||||
)
|
||||
}
|
||||
}
|
||||
case LineChart:
|
||||
for i, line := range self.Data {
|
||||
previousHeight := int((line[1] / maxVal) * float64(drawArea.Dy()-1))
|
||||
for j, val := range line[1:] {
|
||||
height := int((val / maxVal) * float64(drawArea.Dy()-1))
|
||||
canvas.SetLine(
|
||||
image.Pt(
|
||||
(drawArea.Min.X+(j*self.HorizontalScale))*2,
|
||||
(drawArea.Max.Y-previousHeight-1)*4,
|
||||
),
|
||||
image.Pt(
|
||||
(drawArea.Min.X+((j+1)*self.HorizontalScale))*2,
|
||||
(drawArea.Max.Y-height-1)*4,
|
||||
),
|
||||
SelectColor(self.LineColors, i),
|
||||
)
|
||||
previousHeight = height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
canvas.Draw(buf)
|
||||
}
|
||||
|
||||
func (self *Plot) renderDot(buf *Buffer, drawArea image.Rectangle, maxVal float64) {
|
||||
switch self.PlotType {
|
||||
case ScatterPlot:
|
||||
for i, line := range self.Data {
|
||||
for j, val := range line {
|
||||
height := int((val / maxVal) * float64(drawArea.Dy()-1))
|
||||
point := image.Pt(drawArea.Min.X+(j*self.HorizontalScale), drawArea.Max.Y-1-height)
|
||||
if point.In(drawArea) {
|
||||
buf.SetCell(
|
||||
NewCell(self.DotMarkerRune, NewStyle(SelectColor(self.LineColors, i))),
|
||||
point,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
case LineChart:
|
||||
for i, line := range self.Data {
|
||||
for j := 0; j < len(line) && j*self.HorizontalScale < drawArea.Dx(); j++ {
|
||||
val := line[j]
|
||||
height := int((val / maxVal) * float64(drawArea.Dy()-1))
|
||||
buf.SetCell(
|
||||
NewCell(self.DotMarkerRune, NewStyle(SelectColor(self.LineColors, i))),
|
||||
image.Pt(drawArea.Min.X+(j*self.HorizontalScale), drawArea.Max.Y-1-height),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Plot) plotAxes(buf *Buffer, maxVal float64) {
|
||||
// draw origin cell
|
||||
buf.SetCell(
|
||||
NewCell(BOTTOM_LEFT, NewStyle(ColorWhite)),
|
||||
image.Pt(self.Inner.Min.X+yAxisLabelsWidth, self.Inner.Max.Y-xAxisLabelsHeight-1),
|
||||
)
|
||||
// draw x axis line
|
||||
for i := yAxisLabelsWidth + 1; i < self.Inner.Dx(); i++ {
|
||||
buf.SetCell(
|
||||
NewCell(HORIZONTAL_DASH, NewStyle(ColorWhite)),
|
||||
image.Pt(i+self.Inner.Min.X, self.Inner.Max.Y-xAxisLabelsHeight-1),
|
||||
)
|
||||
}
|
||||
// draw y axis line
|
||||
for i := 0; i < self.Inner.Dy()-xAxisLabelsHeight-1; i++ {
|
||||
buf.SetCell(
|
||||
NewCell(VERTICAL_DASH, NewStyle(ColorWhite)),
|
||||
image.Pt(self.Inner.Min.X+yAxisLabelsWidth, i+self.Inner.Min.Y),
|
||||
)
|
||||
}
|
||||
// draw x axis labels
|
||||
// draw 0
|
||||
buf.SetString(
|
||||
"0",
|
||||
NewStyle(ColorWhite),
|
||||
image.Pt(self.Inner.Min.X+yAxisLabelsWidth, self.Inner.Max.Y-1),
|
||||
)
|
||||
// draw rest
|
||||
for x := self.Inner.Min.X + yAxisLabelsWidth + (xAxisLabelsGap)*self.HorizontalScale + 1; x < self.Inner.Max.X-1; {
|
||||
label := fmt.Sprintf(
|
||||
"%d",
|
||||
(x-(self.Inner.Min.X+yAxisLabelsWidth)-1)/(self.HorizontalScale)+1,
|
||||
)
|
||||
buf.SetString(
|
||||
label,
|
||||
NewStyle(ColorWhite),
|
||||
image.Pt(x, self.Inner.Max.Y-1),
|
||||
)
|
||||
x += (len(label) + xAxisLabelsGap) * self.HorizontalScale
|
||||
}
|
||||
// draw y axis labels
|
||||
verticalScale := maxVal / float64(self.Inner.Dy()-xAxisLabelsHeight-1)
|
||||
for i := 0; i*(yAxisLabelsGap+1) < self.Inner.Dy()-1; i++ {
|
||||
buf.SetString(
|
||||
fmt.Sprintf("%.2f", float64(i)*verticalScale*(yAxisLabelsGap+1)),
|
||||
NewStyle(ColorWhite),
|
||||
image.Pt(self.Inner.Min.X, self.Inner.Max.Y-(i*(yAxisLabelsGap+1))-2),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Plot) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
maxVal := self.MaxVal
|
||||
if maxVal == 0 {
|
||||
maxVal, _ = GetMaxFloat64From2dSlice(self.Data)
|
||||
}
|
||||
|
||||
if self.ShowAxes {
|
||||
self.plotAxes(buf, maxVal)
|
||||
}
|
||||
|
||||
drawArea := self.Inner
|
||||
if self.ShowAxes {
|
||||
drawArea = image.Rect(
|
||||
self.Inner.Min.X+yAxisLabelsWidth+1, self.Inner.Min.Y,
|
||||
self.Inner.Max.X, self.Inner.Max.Y-xAxisLabelsHeight-1,
|
||||
)
|
||||
}
|
||||
|
||||
switch self.Marker {
|
||||
case MarkerBraille:
|
||||
self.renderBraille(buf, drawArea, maxVal)
|
||||
case MarkerDot:
|
||||
self.renderDot(buf, drawArea, maxVal)
|
||||
}
|
||||
}
|
||||
94
v3/widgets/sparkline.go
Normal file
94
v3/widgets/sparkline.go
Normal file
@@ -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 widgets
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
. "github.com/gizak/termui/v3"
|
||||
)
|
||||
|
||||
// Sparkline is like: ▅▆▂▂▅▇▂▂▃▆▆▆▅▃. The data points should be non-negative integers.
|
||||
type Sparkline struct {
|
||||
Data []float64
|
||||
Title string
|
||||
TitleStyle Style
|
||||
LineColor Color
|
||||
MaxVal float64
|
||||
MaxHeight int // TODO
|
||||
}
|
||||
|
||||
// SparklineGroup is a renderable widget which groups together the given sparklines.
|
||||
type SparklineGroup struct {
|
||||
Block
|
||||
Sparklines []*Sparkline
|
||||
}
|
||||
|
||||
// NewSparkline returns a unrenderable single sparkline that needs to be added to a SparklineGroup
|
||||
func NewSparkline() *Sparkline {
|
||||
return &Sparkline{
|
||||
TitleStyle: Theme.Sparkline.Title,
|
||||
LineColor: Theme.Sparkline.Line,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSparklineGroup(sls ...*Sparkline) *SparklineGroup {
|
||||
return &SparklineGroup{
|
||||
Block: *NewBlock(),
|
||||
Sparklines: sls,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *SparklineGroup) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
sparklineHeight := self.Inner.Dy() / len(self.Sparklines)
|
||||
|
||||
for i, sl := range self.Sparklines {
|
||||
heightOffset := (sparklineHeight * (i + 1))
|
||||
barHeight := sparklineHeight
|
||||
if i == len(self.Sparklines)-1 {
|
||||
heightOffset = self.Inner.Dy()
|
||||
barHeight = self.Inner.Dy() - (sparklineHeight * i)
|
||||
}
|
||||
if sl.Title != "" {
|
||||
barHeight--
|
||||
}
|
||||
|
||||
maxVal := sl.MaxVal
|
||||
if maxVal == 0 {
|
||||
maxVal, _ = GetMaxFloat64FromSlice(sl.Data)
|
||||
}
|
||||
|
||||
// draw line
|
||||
for j := 0; j < len(sl.Data) && j < self.Inner.Dx(); j++ {
|
||||
data := sl.Data[j]
|
||||
height := int((data / maxVal) * float64(barHeight))
|
||||
sparkChar := BARS[len(BARS)-1]
|
||||
for k := 0; k < height; k++ {
|
||||
buf.SetCell(
|
||||
NewCell(sparkChar, NewStyle(sl.LineColor)),
|
||||
image.Pt(j+self.Inner.Min.X, self.Inner.Min.Y-1+heightOffset-k),
|
||||
)
|
||||
}
|
||||
if height == 0 {
|
||||
sparkChar = BARS[1]
|
||||
buf.SetCell(
|
||||
NewCell(sparkChar, NewStyle(sl.LineColor)),
|
||||
image.Pt(j+self.Inner.Min.X, self.Inner.Min.Y-1+heightOffset),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if sl.Title != "" {
|
||||
// draw title
|
||||
buf.SetString(
|
||||
TrimString(sl.Title, self.Inner.Dx()),
|
||||
sl.TitleStyle,
|
||||
image.Pt(self.Inner.Min.X, self.Inner.Min.Y-1+heightOffset-barHeight),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
96
v3/widgets/stacked_barchart.go
Normal file
96
v3/widgets/stacked_barchart.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// 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 widgets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
rw "github.com/mattn/go-runewidth"
|
||||
|
||||
. "github.com/gizak/termui/v3"
|
||||
)
|
||||
|
||||
type StackedBarChart struct {
|
||||
Block
|
||||
BarColors []Color
|
||||
LabelStyles []Style
|
||||
NumStyles []Style // only Fg and Modifier are used
|
||||
NumFormatter func(float64) string
|
||||
Data [][]float64
|
||||
Labels []string
|
||||
BarWidth int
|
||||
BarGap int
|
||||
MaxVal float64
|
||||
}
|
||||
|
||||
func NewStackedBarChart() *StackedBarChart {
|
||||
return &StackedBarChart{
|
||||
Block: *NewBlock(),
|
||||
BarColors: Theme.StackedBarChart.Bars,
|
||||
LabelStyles: Theme.StackedBarChart.Labels,
|
||||
NumStyles: Theme.StackedBarChart.Nums,
|
||||
NumFormatter: func(n float64) string { return fmt.Sprint(n) },
|
||||
BarGap: 1,
|
||||
BarWidth: 3,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *StackedBarChart) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
maxVal := self.MaxVal
|
||||
if maxVal == 0 {
|
||||
for _, data := range self.Data {
|
||||
maxVal = MaxFloat64(maxVal, SumFloat64Slice(data))
|
||||
}
|
||||
}
|
||||
|
||||
barXCoordinate := self.Inner.Min.X
|
||||
|
||||
for i, bar := range self.Data {
|
||||
// draw stacked bars
|
||||
stackedBarYCoordinate := 0
|
||||
for j, data := range bar {
|
||||
// draw each stacked bar
|
||||
height := int((data / maxVal) * float64(self.Inner.Dy()-1))
|
||||
for x := barXCoordinate; x < MinInt(barXCoordinate+self.BarWidth, self.Inner.Max.X); x++ {
|
||||
for y := (self.Inner.Max.Y - 2) - stackedBarYCoordinate; y > (self.Inner.Max.Y-2)-stackedBarYCoordinate-height; y-- {
|
||||
c := NewCell(' ', NewStyle(ColorClear, SelectColor(self.BarColors, j)))
|
||||
buf.SetCell(c, image.Pt(x, y))
|
||||
}
|
||||
}
|
||||
|
||||
// draw number
|
||||
numberXCoordinate := barXCoordinate + int((float64(self.BarWidth) / 2)) - 1
|
||||
buf.SetString(
|
||||
self.NumFormatter(data),
|
||||
NewStyle(
|
||||
SelectStyle(self.NumStyles, j+1).Fg,
|
||||
SelectColor(self.BarColors, j),
|
||||
SelectStyle(self.NumStyles, j+1).Modifier,
|
||||
),
|
||||
image.Pt(numberXCoordinate, (self.Inner.Max.Y-2)-stackedBarYCoordinate),
|
||||
)
|
||||
|
||||
stackedBarYCoordinate += height
|
||||
}
|
||||
|
||||
// draw label
|
||||
if i < len(self.Labels) {
|
||||
labelXCoordinate := barXCoordinate + MaxInt(
|
||||
int((float64(self.BarWidth)/2))-int((float64(rw.StringWidth(self.Labels[i]))/2)),
|
||||
0,
|
||||
)
|
||||
buf.SetString(
|
||||
TrimString(self.Labels[i], self.BarWidth),
|
||||
SelectStyle(self.LabelStyles, i),
|
||||
image.Pt(labelXCoordinate, self.Inner.Max.Y-1),
|
||||
)
|
||||
}
|
||||
|
||||
barXCoordinate += (self.BarWidth + self.BarGap)
|
||||
}
|
||||
}
|
||||
136
v3/widgets/table.go
Normal file
136
v3/widgets/table.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// 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 widgets
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
. "github.com/gizak/termui/v3"
|
||||
)
|
||||
|
||||
/*Table is like:
|
||||
┌ Awesome Table ───────────────────────────────────────────────┐
|
||||
│ Col0 | Col1 | Col2 | Col3 | Col4 | Col5 | Col6 |
|
||||
│──────────────────────────────────────────────────────────────│
|
||||
│ Some Item #1 | AAA | 123 | CCCCC | EEEEE | GGGGG | IIIII |
|
||||
│──────────────────────────────────────────────────────────────│
|
||||
│ Some Item #2 | BBB | 456 | DDDDD | FFFFF | HHHHH | JJJJJ |
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
type Table struct {
|
||||
Block
|
||||
Rows [][]string
|
||||
ColumnWidths []int
|
||||
TextStyle Style
|
||||
RowSeparator bool
|
||||
TextAlignment Alignment
|
||||
RowStyles map[int]Style
|
||||
FillRow bool
|
||||
|
||||
// ColumnResizer is called on each Draw. Can be used for custom column sizing.
|
||||
ColumnResizer func()
|
||||
}
|
||||
|
||||
func NewTable() *Table {
|
||||
return &Table{
|
||||
Block: *NewBlock(),
|
||||
TextStyle: Theme.Table.Text,
|
||||
RowSeparator: true,
|
||||
RowStyles: make(map[int]Style),
|
||||
ColumnResizer: func() {},
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Table) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
self.ColumnResizer()
|
||||
|
||||
columnWidths := self.ColumnWidths
|
||||
if len(columnWidths) == 0 {
|
||||
columnCount := len(self.Rows[0])
|
||||
columnWidth := self.Inner.Dx() / columnCount
|
||||
for i := 0; i < columnCount; i++ {
|
||||
columnWidths = append(columnWidths, columnWidth)
|
||||
}
|
||||
}
|
||||
|
||||
yCoordinate := self.Inner.Min.Y
|
||||
|
||||
// draw rows
|
||||
for i := 0; i < len(self.Rows) && yCoordinate < self.Inner.Max.Y; i++ {
|
||||
row := self.Rows[i]
|
||||
colXCoordinate := self.Inner.Min.X
|
||||
|
||||
rowStyle := self.TextStyle
|
||||
// get the row style if one exists
|
||||
if style, ok := self.RowStyles[i]; ok {
|
||||
rowStyle = style
|
||||
}
|
||||
|
||||
if self.FillRow {
|
||||
blankCell := NewCell(' ', rowStyle)
|
||||
buf.Fill(blankCell, image.Rect(self.Inner.Min.X, yCoordinate, self.Inner.Max.X, yCoordinate+1))
|
||||
}
|
||||
|
||||
// draw row cells
|
||||
for j := 0; j < len(row); j++ {
|
||||
col := ParseStyles(row[j], rowStyle)
|
||||
// draw row cell
|
||||
if len(col) > columnWidths[j] || self.TextAlignment == AlignLeft {
|
||||
for _, cx := range BuildCellWithXArray(col) {
|
||||
k, cell := cx.X, cx.Cell
|
||||
if k == columnWidths[j] || colXCoordinate+k == self.Inner.Max.X {
|
||||
cell.Rune = ELLIPSES
|
||||
buf.SetCell(cell, image.Pt(colXCoordinate+k-1, yCoordinate))
|
||||
break
|
||||
} else {
|
||||
buf.SetCell(cell, image.Pt(colXCoordinate+k, yCoordinate))
|
||||
}
|
||||
}
|
||||
} else if self.TextAlignment == AlignCenter {
|
||||
xCoordinateOffset := (columnWidths[j] - len(col)) / 2
|
||||
stringXCoordinate := xCoordinateOffset + colXCoordinate
|
||||
for _, cx := range BuildCellWithXArray(col) {
|
||||
k, cell := cx.X, cx.Cell
|
||||
buf.SetCell(cell, image.Pt(stringXCoordinate+k, yCoordinate))
|
||||
}
|
||||
} else if self.TextAlignment == AlignRight {
|
||||
stringXCoordinate := MinInt(colXCoordinate+columnWidths[j], self.Inner.Max.X) - len(col)
|
||||
for _, cx := range BuildCellWithXArray(col) {
|
||||
k, cell := cx.X, cx.Cell
|
||||
buf.SetCell(cell, image.Pt(stringXCoordinate+k, yCoordinate))
|
||||
}
|
||||
}
|
||||
colXCoordinate += columnWidths[j] + 1
|
||||
}
|
||||
|
||||
// draw vertical separators
|
||||
separatorStyle := self.Block.BorderStyle
|
||||
|
||||
separatorXCoordinate := self.Inner.Min.X
|
||||
verticalCell := NewCell(VERTICAL_LINE, separatorStyle)
|
||||
for i, width := range columnWidths {
|
||||
if self.FillRow && i < len(columnWidths)-1 {
|
||||
verticalCell.Style.Bg = rowStyle.Bg
|
||||
} else {
|
||||
verticalCell.Style.Bg = self.Block.BorderStyle.Bg
|
||||
}
|
||||
|
||||
separatorXCoordinate += width
|
||||
buf.SetCell(verticalCell, image.Pt(separatorXCoordinate, yCoordinate))
|
||||
separatorXCoordinate++
|
||||
}
|
||||
|
||||
yCoordinate++
|
||||
|
||||
// draw horizontal separator
|
||||
horizontalCell := NewCell(HORIZONTAL_LINE, separatorStyle)
|
||||
if self.RowSeparator && yCoordinate < self.Inner.Max.Y && i != len(self.Rows)-1 {
|
||||
buf.Fill(horizontalCell, image.Rect(self.Inner.Min.X, yCoordinate, self.Inner.Max.X, yCoordinate+1))
|
||||
yCoordinate++
|
||||
}
|
||||
}
|
||||
}
|
||||
71
v3/widgets/tabs.go
Normal file
71
v3/widgets/tabs.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// 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 widgets
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
. "github.com/gizak/termui/v3"
|
||||
)
|
||||
|
||||
// TabPane is a renderable widget which can be used to conditionally render certain tabs/views.
|
||||
// TabPane shows a list of Tab names.
|
||||
// The currently selected tab can be found through the `ActiveTabIndex` field.
|
||||
type TabPane struct {
|
||||
Block
|
||||
TabNames []string
|
||||
ActiveTabIndex int
|
||||
ActiveTabStyle Style
|
||||
InactiveTabStyle Style
|
||||
}
|
||||
|
||||
func NewTabPane(names ...string) *TabPane {
|
||||
return &TabPane{
|
||||
Block: *NewBlock(),
|
||||
TabNames: names,
|
||||
ActiveTabStyle: Theme.Tab.Active,
|
||||
InactiveTabStyle: Theme.Tab.Inactive,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TabPane) FocusLeft() {
|
||||
if self.ActiveTabIndex > 0 {
|
||||
self.ActiveTabIndex--
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TabPane) FocusRight() {
|
||||
if self.ActiveTabIndex < len(self.TabNames)-1 {
|
||||
self.ActiveTabIndex++
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TabPane) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
xCoordinate := self.Inner.Min.X
|
||||
for i, name := range self.TabNames {
|
||||
ColorPair := self.InactiveTabStyle
|
||||
if i == self.ActiveTabIndex {
|
||||
ColorPair = self.ActiveTabStyle
|
||||
}
|
||||
buf.SetString(
|
||||
TrimString(name, self.Inner.Max.X-xCoordinate),
|
||||
ColorPair,
|
||||
image.Pt(xCoordinate, self.Inner.Min.Y),
|
||||
)
|
||||
|
||||
xCoordinate += 1 + len(name)
|
||||
|
||||
if i < len(self.TabNames)-1 && xCoordinate < self.Inner.Max.X {
|
||||
buf.SetCell(
|
||||
NewCell(VERTICAL_LINE, NewStyle(ColorWhite)),
|
||||
image.Pt(xCoordinate, self.Inner.Min.Y),
|
||||
)
|
||||
}
|
||||
|
||||
xCoordinate += 2
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user