Move source files into v3 directory

This commit is contained in:
Caleb Bassi
2019-06-30 15:06:06 -07:00
parent f08e81d72a
commit 12333d6a7e
30 changed files with 0 additions and 0 deletions

9
v3/alignment.go Normal file
View File

@@ -0,0 +1,9 @@
package termui
type Alignment uint
const (
AlignLeft Alignment = iota
AlignCenter
AlignRight
)

35
v3/backend.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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),
)
}
}
}

View 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
View 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
View 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
}
}