Merge branch 'v2'

This commit is contained in:
gizak 2015-10-22 11:17:20 -04:00
commit ad8b8b432b
62 changed files with 2089 additions and 954 deletions

2
.gitignore vendored
View File

@ -22,4 +22,4 @@ _testmain.go
*.exe
*.test
*.prof
/.DS_Store
.DS_Store

View File

@ -117,6 +117,11 @@ The `helloworld` color scheme drops in some colors!
<img src="./example/list.png" alt="list" type="image/png" width="200">
#### Colored List
[demo code](https://github.com/gizak/termui/blob/master/example/coloredList.go)
TODO: Image (let's wait until the implementation is finished).
#### Gauge
[demo code](https://github.com/gizak/termui/blob/master/example/gauge.go)

View File

@ -9,18 +9,15 @@ package main
import "github.com/gizak/termui"
func main() {
err := termui.Init()
if err != nil {
if termui.Init() != nil {
panic(err)
}
defer termui.Close()
termui.UseTheme("helloworld")
bc := termui.NewBarChart()
data := []int{3, 2, 5, 3, 9, 5, 3, 2, 5, 8, 3, 2, 4, 5, 3, 2, 5, 7, 5, 3, 2, 6, 7, 4, 6, 3, 6, 7, 8, 3, 6, 4, 5, 3, 2, 4, 6, 4, 8, 5, 9, 4, 3, 6, 5, 3, 6}
bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"}
bc.Border.Label = "Bar Chart"
bc.BorderLabel = "Bar Chart"
bc.Data = data
bc.Width = 26
bc.Height = 10
@ -31,5 +28,9 @@ func main() {
termui.Render(bc)
<-termui.EventCh()
termui.Handle("/sys/kbd/q", func(termui.Event) {
termui.StopLoop()
})
termui.Loop()
}

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 443 KiB

After

Width:  |  Height:  |  Size: 443 KiB

View File

@ -9,11 +9,8 @@ package main
import ui "github.com/gizak/termui"
import "math"
import "time"
func main() {
err := ui.Init()
if err != nil {
if err := ui.Init(); err != nil {
panic(err)
}
defer ui.Close()
@ -22,14 +19,22 @@ func main() {
p.Height = 3
p.Width = 50
p.TextFgColor = ui.ColorWhite
p.Border.Label = "Text Box"
p.Border.FgColor = ui.ColorCyan
p.BorderLabel = "Text Box"
p.BorderFg = ui.ColorCyan
p.Handle("/timer/1s", func(e ui.Event) {
cnt := e.Data.(ui.EvtTimer)
if cnt.Count%2 == 0 {
p.TextFgColor = ui.ColorRed
} else {
p.TextFgColor = ui.ColorWhite
}
})
strs := []string{"[0] gizak/termui", "[1] editbox.go", "[2] iterrupt.go", "[3] keyboard.go", "[4] output.go", "[5] random_out.go", "[6] dashboard.go", "[7] nsf/termbox-go"}
list := ui.NewList()
list.Items = strs
list.ItemFgColor = ui.ColorYellow
list.Border.Label = "List"
list.BorderLabel = "List"
list.Height = 7
list.Width = 25
list.Y = 4
@ -39,10 +44,10 @@ func main() {
g.Width = 50
g.Height = 3
g.Y = 11
g.Border.Label = "Gauge"
g.BorderLabel = "Gauge"
g.BarColor = ui.ColorRed
g.Border.FgColor = ui.ColorWhite
g.Border.LabelFgColor = ui.ColorCyan
g.BorderFg = ui.ColorWhite
g.BorderLabelFg = ui.ColorCyan
spark := ui.Sparkline{}
spark.Height = 1
@ -62,7 +67,7 @@ func main() {
sp := ui.NewSparklines(spark, spark1)
sp.Width = 25
sp.Height = 7
sp.Border.Label = "Sparkline"
sp.BorderLabel = "Sparkline"
sp.Y = 4
sp.X = 25
@ -76,7 +81,7 @@ func main() {
})()
lc := ui.NewLineChart()
lc.Border.Label = "dot-mode Line Chart"
lc.BorderLabel = "dot-mode Line Chart"
lc.Data = sinps
lc.Width = 50
lc.Height = 11
@ -89,7 +94,7 @@ func main() {
bc := ui.NewBarChart()
bcdata := []int{3, 2, 5, 3, 9, 5, 3, 2, 5, 8, 3, 2, 4, 5, 3, 2, 5, 7, 5, 3, 2, 6, 7, 4, 6, 3, 6, 7, 8, 3, 6, 4, 5, 3, 2, 4, 6, 4, 8, 5, 9, 4, 3, 6, 5, 3, 6}
bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"}
bc.Border.Label = "Bar Chart"
bc.BorderLabel = "Bar Chart"
bc.Width = 26
bc.Height = 10
bc.X = 51
@ -99,7 +104,7 @@ func main() {
bc.NumColor = ui.ColorBlack
lc1 := ui.NewLineChart()
lc1.Border.Label = "braille-mode Line Chart"
lc1.BorderLabel = "braille-mode Line Chart"
lc1.Data = sinps
lc1.Width = 26
lc1.Height = 11
@ -109,7 +114,7 @@ func main() {
lc1.LineColor = ui.ColorYellow | ui.AttrBold
p1 := ui.NewPar("Hey!\nI am a borderless block!")
p1.HasBorder = false
p1.Border = false
p1.Width = 26
p1.Height = 2
p1.TextFgColor = ui.ColorMagenta
@ -126,23 +131,12 @@ func main() {
bc.Data = bcdata[t/2%10:]
ui.Render(p, list, g, sp, lc, bc, lc1, p1)
}
evt := ui.EventCh()
i := 0
for {
select {
case e := <-evt:
if e.Type == ui.EventKey && e.Ch == 'q' {
return
}
default:
draw(i)
i++
if i == 102 {
return
}
time.Sleep(time.Second / 2)
}
}
ui.Handle("/sys/kbd/q", func(ui.Event) {
ui.StopLoop()
})
ui.Handle("/timer/1s", func(e ui.Event) {
t := e.Data.(ui.EvtTimer)
draw(int(t.Count))
})
ui.Loop()
}

View File

@ -15,16 +15,23 @@ func main() {
}
defer termui.Close()
termui.UseTheme("helloworld")
//termui.UseTheme("helloworld")
g0 := termui.NewGauge()
g0.Percent = 40
g0.Width = 50
g0.Height = 3
g0.Border.Label = "Slim Gauge"
g0.BorderLabel = "Slim Gauge"
g0.BarColor = termui.ColorRed
g0.Border.FgColor = termui.ColorWhite
g0.Border.LabelFgColor = termui.ColorCyan
g0.BorderFg = termui.ColorWhite
g0.BorderLabelFg = termui.ColorCyan
gg := termui.NewBlock()
gg.Width = 50
gg.Height = 5
gg.Y = 12
gg.BorderLabel = "TEST"
gg.Align()
g2 := termui.NewGauge()
g2.Percent = 60
@ -32,27 +39,27 @@ func main() {
g2.Height = 3
g2.PercentColor = termui.ColorBlue
g2.Y = 3
g2.Border.Label = "Slim Gauge"
g2.BorderLabel = "Slim Gauge"
g2.BarColor = termui.ColorYellow
g2.Border.FgColor = termui.ColorWhite
g2.BorderFg = termui.ColorWhite
g1 := termui.NewGauge()
g1.Percent = 30
g1.Width = 50
g1.Height = 5
g1.Y = 6
g1.Border.Label = "Big Gauge"
g1.BorderLabel = "Big Gauge"
g1.PercentColor = termui.ColorYellow
g1.BarColor = termui.ColorGreen
g1.Border.FgColor = termui.ColorWhite
g1.Border.LabelFgColor = termui.ColorMagenta
g1.BorderFg = termui.ColorWhite
g1.BorderLabelFg = termui.ColorMagenta
g3 := termui.NewGauge()
g3.Percent = 50
g3.Width = 50
g3.Height = 3
g3.Y = 11
g3.Border.Label = "Gauge with custom label"
g3.BorderLabel = "Gauge with custom label"
g3.Label = "{{percent}}% (100MBs free)"
g3.LabelAlign = termui.AlignRight
@ -69,5 +76,9 @@ func main() {
termui.Render(g0, g1, g2, g3, g4)
<-termui.EventCh()
termui.Handle("/sys/kbd/q", func(termui.Event) {
termui.StopLoop()
})
termui.Loop()
}

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

Before

Width:  |  Height:  |  Size: 782 KiB

After

Width:  |  Height:  |  Size: 782 KiB

View File

@ -7,12 +7,11 @@
package main
import ui "github.com/gizak/termui"
import "math"
import "time"
func main() {
err := ui.Init()
if err != nil {
if err := ui.Init(); err != nil {
panic(err)
}
defer ui.Close()
@ -33,8 +32,6 @@ func main() {
return ps
})()
ui.UseTheme("helloworld")
spark := ui.Sparkline{}
spark.Height = 8
spdata := sinpsint
@ -44,10 +41,10 @@ func main() {
sp := ui.NewSparklines(spark)
sp.Height = 11
sp.Border.Label = "Sparkline"
sp.BorderLabel = "Sparkline"
lc := ui.NewLineChart()
lc.Border.Label = "braille-mode Line Chart"
lc.BorderLabel = "braille-mode Line Chart"
lc.Data = sinps
lc.Height = 11
lc.AxesColor = ui.ColorWhite
@ -56,15 +53,16 @@ func main() {
gs := make([]*ui.Gauge, 3)
for i := range gs {
gs[i] = ui.NewGauge()
//gs[i].LabelAlign = ui.AlignCenter
gs[i].Height = 2
gs[i].HasBorder = false
gs[i].Border = false
gs[i].Percent = i * 10
gs[i].PaddingBottom = 1
gs[i].BarColor = ui.ColorRed
}
ls := ui.NewList()
ls.HasBorder = false
ls.Border = false
ls.Items = []string{
"[1] Downloading File 1",
"", // == \newline
@ -76,7 +74,7 @@ func main() {
par := ui.NewPar("<> This row has 3 columns\n<- Widgets can be stacked up like left side\n<- Stacked widgets are treated as a single widget")
par.Height = 5
par.Border.Label = "Demonstration"
par.BorderLabel = "Demonstration"
// build layout
ui.Body.AddRows(
@ -91,44 +89,33 @@ func main() {
// calculate layout
ui.Body.Align()
done := make(chan bool)
redraw := make(chan bool)
update := func() {
for i := 0; i < 103; i++ {
for _, g := range gs {
g.Percent = (g.Percent + 3) % 100
}
sp.Lines[0].Data = spdata[:100+i]
lc.Data = sinps[2*i:]
time.Sleep(time.Second / 2)
redraw <- true
}
done <- true
}
evt := ui.EventCh()
ui.Render(ui.Body)
go update()
for {
select {
case e := <-evt:
if e.Type == ui.EventKey && e.Ch == 'q' {
return
}
if e.Type == ui.EventResize {
ui.Body.Width = ui.TermWidth()
ui.Body.Align()
go func() { redraw <- true }()
}
case <-done:
ui.Handle("/sys/kbd/q", func(ui.Event) {
ui.StopLoop()
})
ui.Handle("/timer/1s", func(e ui.Event) {
t := e.Data.(ui.EvtTimer)
i := t.Count
if i > 103 {
ui.StopLoop()
return
case <-redraw:
ui.Render(ui.Body)
}
}
for _, g := range gs {
g.Percent = (g.Percent + 3) % 100
}
sp.Lines[0].Data = spdata[:100+i]
lc.Data = sinps[2*i:]
ui.Render(ui.Body)
})
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
ui.Body.Width = ui.TermWidth()
ui.Body.Align()
ui.Render(ui.Body)
})
ui.Loop()
}

View File

@ -19,7 +19,7 @@ func main() {
}
defer termui.Close()
termui.UseTheme("helloworld")
//termui.UseTheme("helloworld")
sinps := (func() []float64 {
n := 220
@ -31,7 +31,7 @@ func main() {
})()
lc0 := termui.NewLineChart()
lc0.Border.Label = "braille-mode Line Chart"
lc0.BorderLabel = "braille-mode Line Chart"
lc0.Data = sinps
lc0.Width = 50
lc0.Height = 12
@ -41,7 +41,7 @@ func main() {
lc0.LineColor = termui.ColorGreen | termui.AttrBold
lc1 := termui.NewLineChart()
lc1.Border.Label = "dot-mode Line Chart"
lc1.BorderLabel = "dot-mode Line Chart"
lc1.Mode = "dot"
lc1.Data = sinps
lc1.Width = 26
@ -52,7 +52,7 @@ func main() {
lc1.LineColor = termui.ColorYellow | termui.AttrBold
lc2 := termui.NewLineChart()
lc2.Border.Label = "dot-mode Line Chart"
lc2.BorderLabel = "dot-mode Line Chart"
lc2.Mode = "dot"
lc2.Data = sinps[4:]
lc2.Width = 77
@ -63,6 +63,9 @@ func main() {
lc2.LineColor = termui.ColorCyan | termui.AttrBold
termui.Render(lc0, lc1, lc2)
termui.Handle("/sys/kbd/q", func(termui.Event) {
termui.StopLoop()
})
termui.Loop()
<-termui.EventCh()
}

View File

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 136 KiB

View File

@ -15,13 +15,13 @@ func main() {
}
defer termui.Close()
termui.UseTheme("helloworld")
//termui.UseTheme("helloworld")
strs := []string{
"[0] github.com/gizak/termui",
"[1] 你好,世界",
"[2] こんにちは世界",
"[3] keyboard.go",
"[1] [你好,世界](fg-blue)",
"[2] [こんにちは世界](fg-red)",
"[3] [color output](fg-white,bg-green)",
"[4] output.go",
"[5] random_out.go",
"[6] dashboard.go",
@ -30,12 +30,15 @@ func main() {
ls := termui.NewList()
ls.Items = strs
ls.ItemFgColor = termui.ColorYellow
ls.Border.Label = "List"
ls.BorderLabel = "List"
ls.Height = 7
ls.Width = 25
ls.Y = 0
termui.Render(ls)
termui.Handle("/sys/kbd/q", func(termui.Event) {
termui.StopLoop()
})
termui.Loop()
<-termui.EventCh()
}

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -15,7 +15,7 @@ func main() {
}
defer termui.Close()
termui.UseTheme("helloworld")
//termui.UseTheme("helloworld")
bc := termui.NewMBarChart()
math := []int{90, 85, 90, 80}
@ -27,10 +27,10 @@ func main() {
bc.Data[2] = science
bc.Data[3] = compsci
studentsName := []string{"Ken", "Rob", "Dennis", "Linus"}
bc.Border.Label = "Student's Marks X-Axis=Name Y-Axis=Marks[Math,English,Science,ComputerScience] in %"
bc.BorderLabel = "Student's Marks X-Axis=Name Y-Axis=Marks[Math,English,Science,ComputerScience] in %"
bc.Width = 100
bc.Height = 50
bc.Y = 10
bc.Height = 30
bc.Y = 0
bc.BarWidth = 10
bc.DataLabels = studentsName
bc.ShowScale = true //Show y_axis scale value (min and max)
@ -46,5 +46,9 @@ func main() {
termui.Render(bc)
<-termui.EventCh()
termui.Handle("/sys/kbd/q", func(termui.Event) {
termui.StopLoop()
})
termui.Loop()
}

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -15,34 +15,38 @@ func main() {
}
defer termui.Close()
termui.UseTheme("helloworld")
//termui.UseTheme("helloworld")
par0 := termui.NewPar("Borderless Text")
par0.Height = 1
par0.Width = 20
par0.Y = 1
par0.HasBorder = false
par0.Border = false
par1 := termui.NewPar("你好,世界。")
par1.Height = 3
par1.Width = 17
par1.X = 20
par1.Border.Label = "标签"
par1.BorderLabel = "标签"
par2 := termui.NewPar("Simple text\nwith label. It can be multilined with \\n or break automatically")
par2 := termui.NewPar("Simple colored text\nwith label. It [can be](fg-red) multilined with \\n or [break automatically](fg-red,fg-bold)")
par2.Height = 5
par2.Width = 37
par2.Y = 4
par2.Border.Label = "Multiline"
par2.Border.FgColor = termui.ColorYellow
par2.BorderLabel = "Multiline"
par2.BorderFg = termui.ColorYellow
par3 := termui.NewPar("Long text with label and it is auto trimmed.")
par3.Height = 3
par3.Width = 37
par3.Y = 9
par3.Border.Label = "Auto Trim"
par3.BorderLabel = "Auto Trim"
termui.Render(par0, par1, par2, par3)
<-termui.EventCh()
termui.Handle("/sys/kbd/q", func(termui.Event) {
termui.StopLoop()
})
termui.Loop()
}

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -15,7 +15,7 @@ func main() {
}
defer termui.Close()
termui.UseTheme("helloworld")
//termui.UseTheme("helloworld")
data := []int{4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6}
spl0 := termui.NewSparkline()
@ -27,7 +27,7 @@ func main() {
spls0 := termui.NewSparklines(spl0)
spls0.Height = 2
spls0.Width = 20
spls0.HasBorder = false
spls0.Border = false
spl1 := termui.NewSparkline()
spl1.Data = data
@ -44,7 +44,7 @@ func main() {
spls1.Height = 8
spls1.Width = 20
spls1.Y = 3
spls1.Border.Label = "Group Sparklines"
spls1.BorderLabel = "Group Sparklines"
spl3 := termui.NewSparkline()
spl3.Data = data
@ -55,11 +55,15 @@ func main() {
spls2 := termui.NewSparklines(spl3)
spls2.Height = 11
spls2.Width = 30
spls2.Border.FgColor = termui.ColorCyan
spls2.BorderFg = termui.ColorCyan
spls2.X = 21
spls2.Border.Label = "Tweeked Sparkline"
spls2.BorderLabel = "Tweeked Sparkline"
termui.Render(spls0, spls1, spls2)
<-termui.EventCh()
termui.Handle("/sys/kbd/q", func(termui.Event) {
termui.StopLoop()
})
termui.Loop()
}

View File

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

View File

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View File

@ -39,16 +39,16 @@ type BarChart struct {
// NewBarChart returns a new *BarChart with current theme.
func NewBarChart() *BarChart {
bc := &BarChart{Block: *NewBlock()}
bc.BarColor = theme.BarChartBar
bc.NumColor = theme.BarChartNum
bc.TextColor = theme.BarChartText
bc.BarColor = ThemeAttr("barchart.bar.bg")
bc.NumColor = ThemeAttr("barchart.num.fg")
bc.TextColor = ThemeAttr("barchart.text.fg")
bc.BarGap = 1
bc.BarWidth = 3
return bc
}
func (bc *BarChart) layout() {
bc.numBar = bc.innerWidth / (bc.BarGap + bc.BarWidth)
bc.numBar = bc.innerArea.Dx() / (bc.BarGap + bc.BarWidth)
bc.labels = make([][]rune, bc.numBar)
bc.dataNum = make([][]rune, len(bc.Data))
@ -69,7 +69,7 @@ func (bc *BarChart) layout() {
bc.max = bc.Data[i]
}
}
bc.scale = float64(bc.max) / float64(bc.innerHeight-1)
bc.scale = float64(bc.max) / float64(bc.innerArea.Dy()-1)
}
func (bc *BarChart) SetMax(max int) {
@ -80,8 +80,8 @@ func (bc *BarChart) SetMax(max int) {
}
// Buffer implements Bufferer interface.
func (bc *BarChart) Buffer() []Point {
ps := bc.Block.Buffer()
func (bc *BarChart) Buffer() Buffer {
buf := bc.Block.Buffer()
bc.layout()
for i := 0; i < bc.numBar && i < len(bc.Data) && i < len(bc.DataLabels); i++ {
@ -90,46 +90,49 @@ func (bc *BarChart) Buffer() []Point {
// plot bar
for j := 0; j < bc.BarWidth; j++ {
for k := 0; k < h; k++ {
p := Point{}
p.Ch = ' '
p.Bg = bc.BarColor
if bc.BarColor == ColorDefault { // when color is default, space char treated as transparent!
p.Bg |= AttrReverse
c := Cell{
Ch: ' ',
Bg: bc.BarColor,
}
p.X = bc.innerX + i*(bc.BarWidth+bc.BarGap) + j
p.Y = bc.innerY + bc.innerHeight - 2 - k
ps = append(ps, p)
if bc.BarColor == ColorDefault { // when color is default, space char treated as transparent!
c.Bg |= AttrReverse
}
x := bc.innerArea.Min.X + i*(bc.BarWidth+bc.BarGap) + j
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2 - k
buf.Set(x, y, c)
}
}
// plot text
for j, k := 0, 0; j < len(bc.labels[i]); j++ {
w := charWidth(bc.labels[i][j])
p := Point{}
p.Ch = bc.labels[i][j]
p.Bg = bc.BgColor
p.Fg = bc.TextColor
p.Y = bc.innerY + bc.innerHeight - 1
p.X = bc.innerX + oftX + k
ps = append(ps, p)
c := Cell{
Ch: bc.labels[i][j],
Bg: bc.Bg,
Fg: bc.TextColor,
}
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 1
x := bc.innerArea.Min.X + oftX + k
buf.Set(x, y, c)
k += w
}
// plot num
for j := 0; j < len(bc.dataNum[i]); j++ {
p := Point{}
p.Ch = bc.dataNum[i][j]
p.Fg = bc.NumColor
p.Bg = bc.BarColor
c := Cell{
Ch: bc.dataNum[i][j],
Fg: bc.NumColor,
Bg: bc.BarColor,
}
if bc.BarColor == ColorDefault { // the same as above
p.Bg |= AttrReverse
c.Bg |= AttrReverse
}
if h == 0 {
p.Bg = bc.BgColor
c.Bg = bc.Bg
}
p.X = bc.innerX + oftX + (bc.BarWidth-len(bc.dataNum[i]))/2 + j
p.Y = bc.innerY + bc.innerHeight - 2
ps = append(ps, p)
x := bc.innerArea.Min.X + oftX + (bc.BarWidth-len(bc.dataNum[i]))/2 + j
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2
buf.Set(x, y, c)
}
}
return bc.Block.chopOverflow(ps)
return buf
}

282
block.go
View File

@ -4,163 +4,237 @@
package termui
import "image"
// Hline is a horizontal line.
type Hline struct {
X int
Y int
Len int
Fg Attribute
Bg Attribute
}
// Vline is a vertical line.
type Vline struct {
X int
Y int
Len int
Fg Attribute
Bg Attribute
}
// Buffer draws a horizontal line.
func (l Hline) Buffer() Buffer {
if l.Len <= 0 {
return NewBuffer()
}
return NewFilledBuffer(l.X, l.Y, l.X+l.Len, l.Y+1, HORIZONTAL_LINE, l.Fg, l.Bg)
}
// Buffer draws a vertical line.
func (l Vline) Buffer() Buffer {
if l.Len <= 0 {
return NewBuffer()
}
return NewFilledBuffer(l.X, l.Y, l.X+1, l.Y+l.Len, VERTICAL_LINE, l.Fg, l.Bg)
}
// Buffer draws a box border.
func (b Block) drawBorder(buf Buffer) {
if !b.Border {
return
}
min := b.area.Min
max := b.area.Max
x0 := min.X
y0 := min.Y
x1 := max.X - 1
y1 := max.Y - 1
// draw lines
if b.BorderTop {
buf.Merge(Hline{x0, y0, x1 - x0, b.BorderFg, b.BorderBg}.Buffer())
}
if b.BorderBottom {
buf.Merge(Hline{x0, y1, x1 - x0, b.BorderFg, b.BorderBg}.Buffer())
}
if b.BorderLeft {
buf.Merge(Vline{x0, y0, y1 - y0, b.BorderFg, b.BorderBg}.Buffer())
}
if b.BorderRight {
buf.Merge(Vline{x1, y0, y1 - y0, b.BorderFg, b.BorderBg}.Buffer())
}
// draw corners
if b.BorderTop && b.BorderLeft && b.area.Dx() > 0 && b.area.Dy() > 0 {
buf.Set(x0, y0, Cell{TOP_LEFT, b.BorderFg, b.BorderBg})
}
if b.BorderTop && b.BorderRight && b.area.Dx() > 1 && b.area.Dy() > 0 {
buf.Set(x1, y0, Cell{TOP_RIGHT, b.BorderFg, b.BorderBg})
}
if b.BorderBottom && b.BorderLeft && b.area.Dx() > 0 && b.area.Dy() > 1 {
buf.Set(x0, y1, Cell{BOTTOM_LEFT, b.BorderFg, b.BorderBg})
}
if b.BorderBottom && b.BorderRight && b.area.Dx() > 1 && b.area.Dy() > 1 {
buf.Set(x1, y1, Cell{BOTTOM_RIGHT, b.BorderFg, b.BorderBg})
}
}
func (b Block) drawBorderLabel(buf Buffer) {
maxTxtW := b.area.Dx() - 2
tx := DTrimTxCls(DefaultTxBuilder.Build(b.BorderLabel, b.BorderLabelFg, b.BorderLabelBg), maxTxtW)
for i, w := 0, 0; i < len(tx); i++ {
buf.Set(b.area.Min.X+1+w, b.area.Min.Y, tx[i])
w += tx[i].Width()
}
}
// Block is a base struct for all other upper level widgets,
// consider it as css: display:block.
// Normally you do not need to create it manually.
type Block struct {
area image.Rectangle
innerArea image.Rectangle
X int
Y int
Border labeledBorder
IsDisplay bool
HasBorder bool
BgColor Attribute
Border bool
BorderFg Attribute
BorderBg Attribute
BorderLeft bool
BorderRight bool
BorderTop bool
BorderBottom bool
BorderLabel string
BorderLabelFg Attribute
BorderLabelBg Attribute
Display bool
Bg Attribute
Width int
Height int
innerWidth int
innerHeight int
innerX int
innerY int
PaddingTop int
PaddingBottom int
PaddingLeft int
PaddingRight int
id string
Float Align
}
// NewBlock returns a *Block which inherits styles from current theme.
func NewBlock() *Block {
d := Block{}
d.IsDisplay = true
d.HasBorder = theme.HasBorder
d.Border.BgColor = theme.BorderBg
d.Border.FgColor = theme.BorderFg
d.Border.LabelBgColor = theme.BorderLabelTextBg
d.Border.LabelFgColor = theme.BorderLabelTextFg
d.BgColor = theme.BlockBg
d.Width = 2
d.Height = 2
return &d
b := Block{}
b.Display = true
b.Border = true
b.BorderLeft = true
b.BorderRight = true
b.BorderTop = true
b.BorderBottom = true
b.BorderBg = ThemeAttr("border.bg")
b.BorderFg = ThemeAttr("border.fg")
b.BorderLabelBg = ThemeAttr("label.bg")
b.BorderLabelFg = ThemeAttr("label.fg")
b.Bg = ThemeAttr("block.bg")
b.Width = 2
b.Height = 2
b.id = GenId()
b.Float = AlignNone
return &b
}
// compute box model
func (d *Block) align() {
d.innerWidth = d.Width - d.PaddingLeft - d.PaddingRight
d.innerHeight = d.Height - d.PaddingTop - d.PaddingBottom
d.innerX = d.X + d.PaddingLeft
d.innerY = d.Y + d.PaddingTop
func (b Block) Id() string {
return b.id
}
if d.HasBorder {
d.innerHeight -= 2
d.innerWidth -= 2
d.Border.X = d.X
d.Border.Y = d.Y
d.Border.Width = d.Width
d.Border.Height = d.Height
d.innerX++
d.innerY++
}
// Align computes box model
func (b *Block) Align() {
// outer
b.area.Min.X = 0
b.area.Min.Y = 0
b.area.Max.X = b.Width
b.area.Max.Y = b.Height
if d.innerHeight < 0 {
d.innerHeight = 0
}
if d.innerWidth < 0 {
d.innerWidth = 0
}
// float
b.area = AlignArea(TermRect(), b.area, b.Float)
b.area = MoveArea(b.area, b.X, b.Y)
// inner
b.innerArea.Min.X = b.area.Min.X + b.PaddingLeft
b.innerArea.Min.Y = b.area.Min.Y + b.PaddingTop
b.innerArea.Max.X = b.area.Max.X - b.PaddingRight
b.innerArea.Max.Y = b.area.Max.Y - b.PaddingBottom
if b.Border {
if b.BorderLeft {
b.innerArea.Min.X++
}
if b.BorderRight {
b.innerArea.Max.X--
}
if b.BorderTop {
b.innerArea.Min.Y++
}
if b.BorderBottom {
b.innerArea.Max.Y--
}
}
}
// InnerBounds returns the internal bounds of the block after aligning and
// calculating the padding and border, if any.
func (d *Block) InnerBounds() (x, y, width, height int) {
d.align()
return d.innerX, d.innerY, d.innerWidth, d.innerHeight
func (b *Block) InnerBounds() image.Rectangle {
b.Align()
return b.innerArea
}
// Buffer implements Bufferer interface.
// Draw background and border (if any).
func (d *Block) Buffer() []Point {
d.align()
func (b *Block) Buffer() Buffer {
b.Align()
ps := []Point{}
if !d.IsDisplay {
return ps
}
buf := NewBuffer()
buf.SetArea(b.area)
buf.Fill(' ', ColorDefault, b.Bg)
if d.HasBorder {
ps = d.Border.Buffer()
}
b.drawBorder(buf)
b.drawBorderLabel(buf)
for i := 0; i < d.innerWidth; i++ {
for j := 0; j < d.innerHeight; j++ {
p := Point{}
p.X = d.X + i
p.Y = d.Y + j
if d.HasBorder {
p.X++
p.Y++
}
p.Ch = ' '
p.Bg = d.BgColor
ps = append(ps, p)
}
}
return ps
return buf
}
// GetHeight implements GridBufferer.
// It returns current height of the block.
func (d Block) GetHeight() int {
return d.Height
func (b Block) GetHeight() int {
return b.Height
}
// SetX implements GridBufferer interface, which sets block's x position.
func (d *Block) SetX(x int) {
d.X = x
func (b *Block) SetX(x int) {
b.X = x
}
// SetY implements GridBufferer interface, it sets y position for block.
func (d *Block) SetY(y int) {
d.Y = y
func (b *Block) SetY(y int) {
b.Y = y
}
// SetWidth implements GridBuffer interface, it sets block's width.
func (d *Block) SetWidth(w int) {
d.Width = w
}
// chop the overflow parts
func (d *Block) chopOverflow(ps []Point) []Point {
nps := make([]Point, 0, len(ps))
x := d.X
y := d.Y
w := d.Width
h := d.Height
for _, v := range ps {
if v.X >= x &&
v.X < x+w &&
v.Y >= y &&
v.Y < y+h {
nps = append(nps, v)
}
}
return nps
func (b *Block) SetWidth(w int) {
b.Width = w
}
func (b Block) InnerWidth() int {
return b.innerWidth
return b.innerArea.Dx()
}
func (b Block) InnerHeight() int {
return b.innerHeight
return b.innerArea.Dy()
}
func (b Block) InnerX() int {
return b.innerX
return b.innerArea.Min.X
}
func (b Block) InnerY() int {
return b.innerY
}
func (b *Block) Align() {
b.align()
}
func (b Block) InnerY() int { return b.innerArea.Min.Y }

View File

@ -1,8 +1,25 @@
package termui
import "testing"
import (
"testing"
)
func TestBlockFloat(t *testing.T) {
Init()
defer Close()
b := NewBlock()
b.X = 10
b.Y = 20
b.Float = AlignCenter
b.Align()
}
func TestBlockInnerBounds(t *testing.T) {
Init()
defer Close()
func TestBlock_InnerBounds(t *testing.T) {
b := NewBlock()
b.X = 10
b.Y = 11
@ -11,12 +28,17 @@ func TestBlock_InnerBounds(t *testing.T) {
assert := func(name string, x, y, w, h int) {
t.Log(name)
cx, cy, cw, ch := b.InnerBounds()
area := b.InnerBounds()
cx := area.Min.X
cy := area.Min.Y
cw := area.Dx()
ch := area.Dy()
if cx != x {
t.Errorf("expected x to be %d but got %d", x, cx)
}
if cy != y {
t.Errorf("expected y to be %d but got %d", y, cy)
t.Errorf("expected y to be %d but got %d\n%+v", y, cy, area)
}
if cw != w {
t.Errorf("expected width to be %d but got %d", w, cw)
@ -26,10 +48,10 @@ func TestBlock_InnerBounds(t *testing.T) {
}
}
b.HasBorder = false
b.Border = false
assert("no border, no padding", 10, 11, 12, 13)
b.HasBorder = true
b.Border = true
assert("border, no padding", 11, 12, 10, 11)
b.PaddingBottom = 2

117
box.go
View File

@ -1,117 +0,0 @@
// Copyright 2015 Zack Guo <gizak@icloud.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 border struct {
X int
Y int
Width int
Height int
FgColor Attribute
BgColor Attribute
}
type hline struct {
X int
Y int
Length int
FgColor Attribute
BgColor Attribute
}
type vline struct {
X int
Y int
Length int
FgColor Attribute
BgColor Attribute
}
// Draw a horizontal line.
func (l hline) Buffer() []Point {
pts := make([]Point, l.Length)
for i := 0; i < l.Length; i++ {
pts[i].X = l.X + i
pts[i].Y = l.Y
pts[i].Ch = HORIZONTAL_LINE
pts[i].Bg = l.BgColor
pts[i].Fg = l.FgColor
}
return pts
}
// Draw a vertical line.
func (l vline) Buffer() []Point {
pts := make([]Point, l.Length)
for i := 0; i < l.Length; i++ {
pts[i].X = l.X
pts[i].Y = l.Y + i
pts[i].Ch = VERTICAL_LINE
pts[i].Bg = l.BgColor
pts[i].Fg = l.FgColor
}
return pts
}
// Draw a box border.
func (b border) Buffer() []Point {
if b.Width < 2 || b.Height < 2 {
return nil
}
pts := make([]Point, 2*b.Width+2*b.Height-4)
pts[0].X = b.X
pts[0].Y = b.Y
pts[0].Fg = b.FgColor
pts[0].Bg = b.BgColor
pts[0].Ch = TOP_LEFT
pts[1].X = b.X + b.Width - 1
pts[1].Y = b.Y
pts[1].Fg = b.FgColor
pts[1].Bg = b.BgColor
pts[1].Ch = TOP_RIGHT
pts[2].X = b.X
pts[2].Y = b.Y + b.Height - 1
pts[2].Fg = b.FgColor
pts[2].Bg = b.BgColor
pts[2].Ch = BOTTOM_LEFT
pts[3].X = b.X + b.Width - 1
pts[3].Y = b.Y + b.Height - 1
pts[3].Fg = b.FgColor
pts[3].Bg = b.BgColor
pts[3].Ch = BOTTOM_RIGHT
copy(pts[4:], (hline{b.X + 1, b.Y, b.Width - 2, b.FgColor, b.BgColor}).Buffer())
copy(pts[4+b.Width-2:], (hline{b.X + 1, b.Y + b.Height - 1, b.Width - 2, b.FgColor, b.BgColor}).Buffer())
copy(pts[4+2*b.Width-4:], (vline{b.X, b.Y + 1, b.Height - 2, b.FgColor, b.BgColor}).Buffer())
copy(pts[4+2*b.Width-4+b.Height-2:], (vline{b.X + b.Width - 1, b.Y + 1, b.Height - 2, b.FgColor, b.BgColor}).Buffer())
return pts
}
type labeledBorder struct {
border
Label string
LabelFgColor Attribute
LabelBgColor Attribute
}
// Draw a box border with label.
func (lb labeledBorder) Buffer() []Point {
ps := lb.border.Buffer()
maxTxtW := lb.Width - 2
rs := trimStr2Runes(lb.Label, maxTxtW)
for i, j, w := 0, 0, 0; i < len(rs); i++ {
w = charWidth(rs[i])
ps = append(ps, newPointWithAttrs(rs[i], lb.X+1+j, lb.Y, lb.LabelFgColor, lb.LabelBgColor))
j += w
}
return ps
}

106
buffer.go Normal file
View File

@ -0,0 +1,106 @@
// Copyright 2015 Zack Guo <gizak@icloud.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import "image"
// Cell is a rune with assigned Fg and Bg
type Cell struct {
Ch rune
Fg Attribute
Bg Attribute
}
// Buffer is a renderable rectangle cell data container.
type Buffer struct {
Area image.Rectangle // selected drawing area
CellMap map[image.Point]Cell
}
// At returns the cell at (x,y).
func (b Buffer) At(x, y int) Cell {
return b.CellMap[image.Pt(x, y)]
}
// Set assigns a char to (x,y)
func (b Buffer) Set(x, y int, c Cell) {
b.CellMap[image.Pt(x, y)] = c
}
// Bounds returns the domain for which At can return non-zero color.
func (b Buffer) Bounds() image.Rectangle {
x0, y0, x1, y1 := 0, 0, 0, 0
for p := range b.CellMap {
if p.X > x1 {
x1 = p.X
}
if p.X < x0 {
x0 = p.X
}
if p.Y > y1 {
y1 = p.Y
}
if p.Y < y0 {
y0 = p.Y
}
}
return image.Rect(x0, y0, x1, y1)
}
// SetArea assigns a new rect area to Buffer b.
func (b *Buffer) SetArea(r image.Rectangle) {
b.Area.Max = r.Max
b.Area.Min = r.Min
}
// Sync sets drawing area to the buffer's bound
func (b Buffer) Sync() {
b.SetArea(b.Bounds())
}
// NewCell returns a new cell
func NewCell(ch rune, fg, bg Attribute) Cell {
return Cell{ch, fg, bg}
}
// Merge merges bs Buffers onto b
func (b *Buffer) Merge(bs ...Buffer) {
for _, buf := range bs {
for p, v := range buf.CellMap {
b.Set(p.X, p.Y, v)
}
b.SetArea(b.Area.Union(buf.Area))
}
}
// NewBuffer returns a new Buffer
func NewBuffer() Buffer {
return Buffer{
CellMap: make(map[image.Point]Cell),
Area: image.Rectangle{}}
}
// Fill fills the Buffer b with ch,fg and bg.
func (b Buffer) Fill(ch rune, fg, bg Attribute) {
for x := b.Area.Min.X; x < b.Area.Max.X; x++ {
for y := b.Area.Min.Y; y < b.Area.Max.Y; y++ {
b.Set(x, y, Cell{ch, fg, bg})
}
}
}
// NewFilledBuffer returns a new Buffer filled with ch, fb and bg.
func NewFilledBuffer(x0, y0, x1, y1 int, ch rune, fg, bg Attribute) Buffer {
buf := NewBuffer()
buf.Area.Min = image.Pt(x0, y0)
buf.Area.Max = image.Pt(x1, y1)
for x := buf.Area.Min.X; x < buf.Area.Max.X; x++ {
for y := buf.Area.Min.Y; y < buf.Area.Max.Y; y++ {
buf.Set(x, y, Cell{ch, fg, bg})
}
}
return buf
}

19
buffer_test.go Normal file
View File

@ -0,0 +1,19 @@
package termui
import (
"image"
"testing"
)
func TestBufferUnion(t *testing.T) {
b0 := NewBuffer()
b1 := NewBuffer()
b1.Area.Max.X = 100
b1.Area.Max.Y = 100
b0.Area.Max.X = 50
b0.Merge(b1)
if b0.Area.Max.X != 100 {
t.Errorf("Buffer.Merge unions Area failed: should:%v, actual %v,%v", image.Rect(0, 0, 50, 0).Union(image.Rect(0, 0, 100, 100)), b1.Area, b0.Area)
}
}

View File

@ -63,12 +63,10 @@ func (c Canvas) Unset(x, y int) {
}
// Buffer returns un-styled points
func (c Canvas) Buffer() []Point {
ps := make([]Point, len(c))
i := 0
func (c Canvas) Buffer() Buffer {
buf := NewBuffer()
for k, v := range c {
ps[i] = newPoint(v+brailleBase, k[0], k[1])
i++
buf.Set(k[0], k[1], Cell{Ch: v + brailleBase})
}
return ps
return buf
}

View File

@ -1,3 +1,5 @@
// +build ignore
package termui
import (
@ -47,9 +49,5 @@ func TestCanvasBuffer(t *testing.T) {
c.Set(8, 1)
c.Set(9, 0)
bufs := c.Buffer()
rs := make([]rune, len(bufs))
for i, v := range bufs {
rs[i] = v.Ch
}
spew.Dump(string(rs))
spew.Dump(bufs)
}

113
debug/debuger.go Normal file
View File

@ -0,0 +1,113 @@
package debug
import (
"fmt"
"net/http"
"golang.org/x/net/websocket"
)
type Server struct {
Port string
Addr string
Path string
Msg chan string
chs []chan string
}
type Client struct {
Port string
Addr string
Path string
ws *websocket.Conn
}
var defaultPort = ":8080"
func NewServer() *Server {
return &Server{
Port: defaultPort,
Addr: "localhost",
Path: "/echo",
Msg: make(chan string),
chs: make([]chan string, 0),
}
}
func NewClient() Client {
return Client{
Port: defaultPort,
Addr: "localhost",
Path: "/echo",
}
}
func (c Client) ConnectAndListen() error {
ws, err := websocket.Dial("ws://"+c.Addr+c.Port+c.Path, "", "http://"+c.Addr)
if err != nil {
return err
}
defer ws.Close()
var m string
for {
err := websocket.Message.Receive(ws, &m)
if err != nil {
fmt.Print(err)
return err
}
fmt.Print(m)
}
}
func (s *Server) ListenAndServe() error {
http.Handle(s.Path, websocket.Handler(func(ws *websocket.Conn) {
defer ws.Close()
mc := make(chan string)
s.chs = append(s.chs, mc)
for m := range mc {
websocket.Message.Send(ws, m)
}
}))
go func() {
for msg := range s.Msg {
for _, c := range s.chs {
go func(a chan string) {
a <- msg
}(c)
}
}
}()
return http.ListenAndServe(s.Port, nil)
}
func (s *Server) Log(msg string) {
go func() { s.Msg <- msg }()
}
func (s *Server) Logf(format string, a ...interface{}) {
s.Log(fmt.Sprintf(format, a...))
}
var DefaultServer = NewServer()
var DefaultClient = NewClient()
func ListenAndServe() error {
return DefaultServer.ListenAndServe()
}
func ConnectAndListen() error {
return DefaultClient.ConnectAndListen()
}
func Log(msg string) {
DefaultServer.Log(msg)
}
func Logf(format string, a ...interface{}) {
DefaultServer.Logf(format, a...)
}

443
events.go
View File

@ -8,166 +8,313 @@
package termui
import "github.com/nsf/termbox-go"
import (
"path"
"strconv"
"sync"
"time"
/***********************************termbox-go**************************************/
type (
EventType uint8
Modifier uint8
Key uint16
"github.com/nsf/termbox-go"
)
// This type represents a termbox event. The 'Mod', 'Key' and 'Ch' fields are
// valid if 'Type' is EventKey. The 'Width' and 'Height' fields are valid if
// 'Type' is EventResize. The 'Err' field is valid if 'Type' is EventError.
type Event struct {
Type EventType // one of Event* constants
Mod Modifier // one of Mod* constants or 0
Key Key // one of Key* constants, invalid if 'Ch' is not 0
Ch rune // a unicode character
Width int // width of the screen
Height int // height of the screen
Err error // error in case if input failed
MouseX int // x coord of mouse
MouseY int // y coord of mouse
N int // number of bytes written when getting a raw event
Type string
Path string
From string
To string
Data interface{}
Time int64
}
const (
KeyF1 Key = 0xFFFF - iota
KeyF2
KeyF3
KeyF4
KeyF5
KeyF6
KeyF7
KeyF8
KeyF9
KeyF10
KeyF11
KeyF12
KeyInsert
KeyDelete
KeyHome
KeyEnd
KeyPgup
KeyPgdn
KeyArrowUp
KeyArrowDown
KeyArrowLeft
KeyArrowRight
key_min // see terminfo
MouseLeft
MouseMiddle
MouseRight
)
var sysEvtChs []chan Event
const (
KeyCtrlTilde Key = 0x00
KeyCtrl2 Key = 0x00
KeyCtrlSpace Key = 0x00
KeyCtrlA Key = 0x01
KeyCtrlB Key = 0x02
KeyCtrlC Key = 0x03
KeyCtrlD Key = 0x04
KeyCtrlE Key = 0x05
KeyCtrlF Key = 0x06
KeyCtrlG Key = 0x07
KeyBackspace Key = 0x08
KeyCtrlH Key = 0x08
KeyTab Key = 0x09
KeyCtrlI Key = 0x09
KeyCtrlJ Key = 0x0A
KeyCtrlK Key = 0x0B
KeyCtrlL Key = 0x0C
KeyEnter Key = 0x0D
KeyCtrlM Key = 0x0D
KeyCtrlN Key = 0x0E
KeyCtrlO Key = 0x0F
KeyCtrlP Key = 0x10
KeyCtrlQ Key = 0x11
KeyCtrlR Key = 0x12
KeyCtrlS Key = 0x13
KeyCtrlT Key = 0x14
KeyCtrlU Key = 0x15
KeyCtrlV Key = 0x16
KeyCtrlW Key = 0x17
KeyCtrlX Key = 0x18
KeyCtrlY Key = 0x19
KeyCtrlZ Key = 0x1A
KeyEsc Key = 0x1B
KeyCtrlLsqBracket Key = 0x1B
KeyCtrl3 Key = 0x1B
KeyCtrl4 Key = 0x1C
KeyCtrlBackslash Key = 0x1C
KeyCtrl5 Key = 0x1D
KeyCtrlRsqBracket Key = 0x1D
KeyCtrl6 Key = 0x1E
KeyCtrl7 Key = 0x1F
KeyCtrlSlash Key = 0x1F
KeyCtrlUnderscore Key = 0x1F
KeySpace Key = 0x20
KeyBackspace2 Key = 0x7F
KeyCtrl8 Key = 0x7F
)
// Alt modifier constant, see Event.Mod field and SetInputMode function.
const (
ModAlt Modifier = 0x01
)
// Event type. See Event.Type field.
const (
EventKey EventType = iota
EventResize
EventMouse
EventError
EventInterrupt
EventRaw
EventNone
)
/**************************************end**************************************/
// convert termbox.Event to termui.Event
func uiEvt(e termbox.Event) Event {
event := Event{}
event.Type = EventType(e.Type)
event.Mod = Modifier(e.Mod)
event.Key = Key(e.Key)
event.Ch = e.Ch
event.Width = e.Width
event.Height = e.Height
event.Err = e.Err
event.MouseX = e.MouseX
event.MouseY = e.MouseY
event.N = e.N
return event
type EvtKbd struct {
KeyStr string
}
var evtChs = make([]chan Event, 0)
func evtKbd(e termbox.Event) EvtKbd {
ek := EvtKbd{}
// EventCh returns an output-only event channel.
// This function can be called many times (multiplexer).
func EventCh() <-chan Event {
out := make(chan Event)
evtChs = append(evtChs, out)
return out
}
k := string(e.Ch)
pre := ""
mod := ""
// turn on event listener
func evtListen() {
go func() {
for {
e := termbox.PollEvent()
// dispatch
for _, c := range evtChs {
go func(ch chan Event) {
ch <- uiEvt(e)
}(c)
if e.Mod == termbox.ModAlt {
mod = "M-"
}
if e.Ch == 0 {
if e.Key > 0xFFFF-12 {
k = "<f" + strconv.Itoa(0xFFFF-int(e.Key)+1) + ">"
} else if e.Key > 0xFFFF-25 {
ks := []string{"<insert>", "<delete>", "<home>", "<end>", "<previous>", "<next>", "<up>", "<down>", "<left>", "<right>"}
k = ks[0xFFFF-int(e.Key)-12]
}
if e.Key <= 0x7F {
pre = "C-"
k = string('a' - 1 + int(e.Key))
kmap := map[termbox.Key][2]string{
termbox.KeyCtrlSpace: {"C-", "<space>"},
termbox.KeyBackspace: {"", "<backspace>"},
termbox.KeyTab: {"", "<tab>"},
termbox.KeyEnter: {"", "<enter>"},
termbox.KeyEsc: {"", "<escape>"},
termbox.KeyCtrlBackslash: {"C-", "\\"},
termbox.KeyCtrlSlash: {"C-", "/"},
termbox.KeySpace: {"", "<space>"},
termbox.KeyCtrl8: {"C-", "8"},
}
if sk, ok := kmap[e.Key]; ok {
pre = sk[0]
k = sk[1]
}
}
}
ek.KeyStr = pre + mod + k
return ek
}
func crtTermboxEvt(e termbox.Event) Event {
systypemap := map[termbox.EventType]string{
termbox.EventKey: "keyboard",
termbox.EventResize: "window",
termbox.EventMouse: "mouse",
termbox.EventError: "error",
termbox.EventInterrupt: "interrupt",
}
ne := Event{From: "/sys", Time: time.Now().Unix()}
typ := e.Type
ne.Type = systypemap[typ]
switch typ {
case termbox.EventKey:
kbd := evtKbd(e)
ne.Path = "/sys/kbd/" + kbd.KeyStr
ne.Data = kbd
case termbox.EventResize:
wnd := EvtWnd{}
wnd.Width = e.Width
wnd.Height = e.Height
ne.Path = "/sys/wnd/resize"
ne.Data = wnd
case termbox.EventError:
err := EvtErr(e.Err)
ne.Path = "/sys/err"
ne.Data = err
case termbox.EventMouse:
m := EvtMouse{}
m.X = e.MouseX
m.Y = e.MouseY
ne.Path = "/sys/mouse"
ne.Data = m
}
return ne
}
type EvtWnd struct {
Width int
Height int
}
type EvtMouse struct {
X int
Y int
Press string
}
type EvtErr error
func hookTermboxEvt() {
for {
e := termbox.PollEvent()
for _, c := range sysEvtChs {
go func(ch chan Event) {
ch <- crtTermboxEvt(e)
}(c)
}
}
}
func NewSysEvtCh() chan Event {
ec := make(chan Event)
sysEvtChs = append(sysEvtChs, ec)
return ec
}
var DefaultEvtStream = NewEvtStream()
type EvtStream struct {
sync.RWMutex
srcMap map[string]chan Event
stream chan Event
wg sync.WaitGroup
sigStopLoop chan Event
Handlers map[string]func(Event)
hook func(Event)
}
func NewEvtStream() *EvtStream {
return &EvtStream{
srcMap: make(map[string]chan Event),
stream: make(chan Event),
Handlers: make(map[string]func(Event)),
sigStopLoop: make(chan Event),
}
}
func (es *EvtStream) Init() {
es.Merge("internal", es.sigStopLoop)
go func() {
es.wg.Wait()
close(es.stream)
}()
}
func cleanPath(p string) string {
if p == "" {
return "/"
}
if p[0] != '/' {
p = "/" + p
}
return path.Clean(p)
}
func isPathMatch(pattern, path string) bool {
if len(pattern) == 0 {
return false
}
n := len(pattern)
return len(path) >= n && path[0:n] == pattern
}
func (es *EvtStream) Merge(name string, ec chan Event) {
es.Lock()
defer es.Unlock()
es.wg.Add(1)
es.srcMap[name] = ec
go func(a chan Event) {
for n := range a {
n.From = name
es.stream <- n
}
es.wg.Done()
}(ec)
}
func (es *EvtStream) Handle(path string, handler func(Event)) {
es.Handlers[cleanPath(path)] = handler
}
func findMatch(mux map[string]func(Event), path string) string {
n := -1
pattern := ""
for m := range mux {
if !isPathMatch(m, path) {
continue
}
if len(m) > n {
pattern = m
n = len(m)
}
}
return pattern
}
func (es *EvtStream) match(path string) string {
return findMatch(es.Handlers, path)
}
func (es *EvtStream) Hook(f func(Event)) {
es.hook = f
}
func (es *EvtStream) Loop() {
for e := range es.stream {
switch e.Path {
case "/sig/stoploop":
return
}
go func(a Event) {
es.RLock()
defer es.RUnlock()
if pattern := es.match(a.Path); pattern != "" {
es.Handlers[pattern](a)
}
}(e)
if es.hook != nil {
es.hook(e)
}
}
}
func (es *EvtStream) StopLoop() {
go func() {
e := Event{
Path: "/sig/stoploop",
}
es.sigStopLoop <- e
}()
}
func Merge(name string, ec chan Event) {
DefaultEvtStream.Merge(name, ec)
}
func Handle(path string, handler func(Event)) {
DefaultEvtStream.Handle(path, handler)
}
func Loop() {
DefaultEvtStream.Loop()
}
func StopLoop() {
DefaultEvtStream.StopLoop()
}
type EvtTimer struct {
Duration time.Duration
Count uint64
}
func NewTimerCh(du time.Duration) chan Event {
t := make(chan Event)
go func(a chan Event) {
n := uint64(0)
for {
n++
time.Sleep(du)
e := Event{}
e.Type = "timer"
e.Path = "/timer/" + du.String()
e.Time = time.Now().Unix()
e.Data = EvtTimer{
Duration: du,
Count: n,
}
t <- e
}
}(t)
return t
}
var DefualtHandler = func(e Event) {
}
var usrEvtCh = make(chan Event)
func SendCustomEvt(path string, data interface{}) {
e := Event{}
e.Path = path
e.Data = data
e.Time = time.Now().Unix()
usrEvtCh <- e
}

View File

@ -8,21 +8,34 @@
package termui
import (
"errors"
"testing"
import "testing"
termbox "github.com/nsf/termbox-go"
"github.com/stretchr/testify/assert"
)
var ps = []string{
"",
"/",
"/a",
"/b",
"/a/c",
"/a/b",
"/a/b/c",
"/a/b/c/d",
"/a/b/c/d/"}
type boxEvent termbox.Event
func TestMatchScore(t *testing.T) {
chk := func(a, b string, s bool) {
if c := isPathMatch(a, b); c != s {
t.Errorf("\na:%s\nb:%s\nshould:%t\nactual:%t", a, b, s, c)
}
}
chk(ps[1], ps[1], true)
chk(ps[1], ps[2], true)
chk(ps[2], ps[1], false)
chk(ps[4], ps[1], false)
chk(ps[6], ps[2], false)
chk(ps[4], ps[5], false)
}
func TestCrtEvt(t *testing.T) {
func TestUiEvt(t *testing.T) {
err := errors.New("This is a mock error")
event := boxEvent{3, 5, 2, 'H', 200, 500, err, 50, 30, 2}
expetced := Event{3, 5, 2, 'H', 200, 500, err, 50, 30, 2}
// We need to do that ugly casting so that vet does not complain
assert.Equal(t, uiEvt(termbox.Event(event)), expetced)
}

View File

@ -21,17 +21,7 @@ import (
g.PercentColor = termui.ColorBlue
*/
// Align is the position of the gauge's label.
type Align int
// All supported positions.
const (
AlignLeft Align = iota
AlignCenter
AlignRight
)
const uint16max = ^uint16(0)
const ColorUndef Attribute = Attribute(^uint16(0))
type Gauge struct {
Block
@ -47,11 +37,11 @@ type Gauge struct {
func NewGauge() *Gauge {
g := &Gauge{
Block: *NewBlock(),
PercentColor: theme.GaugePercent,
BarColor: theme.GaugeBar,
PercentColor: ThemeAttr("gauge.percent.fg"),
BarColor: ThemeAttr("gauge.bar.bg"),
Label: "{{percent}}%",
LabelAlign: AlignCenter,
PercentColorHighlighted: Attribute(uint16max),
PercentColorHighlighted: ColorUndef,
}
g.Width = 12
@ -60,28 +50,26 @@ func NewGauge() *Gauge {
}
// Buffer implements Bufferer interface.
func (g *Gauge) Buffer() []Point {
ps := g.Block.Buffer()
func (g *Gauge) Buffer() Buffer {
buf := g.Block.Buffer()
// plot bar
w := g.Percent * g.innerWidth / 100
for i := 0; i < g.innerHeight; i++ {
w := g.Percent * g.innerArea.Dx() / 100
for i := 0; i < g.innerArea.Dy(); i++ {
for j := 0; j < w; j++ {
p := Point{}
p.X = g.innerX + j
p.Y = g.innerY + i
p.Ch = ' '
p.Bg = g.BarColor
if p.Bg == ColorDefault {
p.Bg |= AttrReverse
c := Cell{}
c.Ch = ' '
c.Bg = g.BarColor
if c.Bg == ColorDefault {
c.Bg |= AttrReverse
}
ps = append(ps, p)
buf.Set(g.innerArea.Min.X+j, g.innerArea.Min.Y+i, c)
}
}
// plot percentage
s := strings.Replace(g.Label, "{{percent}}", strconv.Itoa(g.Percent), -1)
pry := g.innerY + g.innerHeight/2
pry := g.innerArea.Min.Y + g.innerArea.Dy()/2
rs := str2runes(s)
var pos int
switch g.LabelAlign {
@ -89,33 +77,33 @@ func (g *Gauge) Buffer() []Point {
pos = 0
case AlignCenter:
pos = (g.innerWidth - strWidth(s)) / 2
pos = (g.innerArea.Dx() - strWidth(s)) / 2
case AlignRight:
pos = g.innerWidth - strWidth(s)
pos = g.innerArea.Dx() - strWidth(s) - 1
}
pos += g.innerX
pos += g.innerArea.Min.X
for i, v := range rs {
p := Point{}
p.X = 1 + pos + i
p.Y = pry
p.Ch = v
p.Fg = g.PercentColor
if w+g.innerX > pos+i {
p.Bg = g.BarColor
if p.Bg == ColorDefault {
p.Bg |= AttrReverse
}
if g.PercentColorHighlighted != Attribute(uint16max) {
p.Fg = g.PercentColorHighlighted
}
} else {
p.Bg = g.Block.BgColor
c := Cell{
Ch: v,
Fg: g.PercentColor,
}
ps = append(ps, p)
if w+g.innerArea.Min.X > pos+i {
c.Bg = g.BarColor
if c.Bg == ColorDefault {
c.Bg |= AttrReverse
}
if g.PercentColorHighlighted != ColorUndef {
c.Fg = g.PercentColorHighlighted
}
} else {
c.Bg = g.Block.Bg
}
buf.Set(1+pos+i, pry, c)
}
return g.Block.chopOverflow(ps)
return buf
}

18
grid.go
View File

@ -160,8 +160,8 @@ func (r *Row) SetWidth(w int) {
// Buffer implements Bufferer interface,
// recursively merge all widgets buffer
func (r *Row) Buffer() []Point {
merged := []Point{}
func (r *Row) Buffer() Buffer {
merged := NewBuffer()
if r.isRenderableLeaf() {
return r.Widget.Buffer()
@ -169,13 +169,13 @@ func (r *Row) Buffer() []Point {
// for those are not leaves but have a renderable widget
if r.Widget != nil {
merged = append(merged, r.Widget.Buffer()...)
merged.Merge(r.Widget.Buffer())
}
// collect buffer from children
if !r.isLeaf() {
for _, c := range r.Cols {
merged = append(merged, c.Buffer()...)
merged.Merge(c.Buffer())
}
}
@ -267,13 +267,13 @@ func (g *Grid) Align() {
}
// Buffer implments Bufferer interface.
func (g Grid) Buffer() []Point {
ps := []Point{}
func (g Grid) Buffer() Buffer {
buf := NewBuffer()
for _, r := range g.Rows {
ps = append(ps, r.Buffer()...)
buf.Merge(r.Buffer())
}
return ps
return buf
}
// Body corresponds to the entire terminal display region.
var Body *Grid

View File

@ -13,13 +13,13 @@ import (
var r *Row
func TestRowWidth(t *testing.T) {
p0 := NewPar("p0")
p0 := NewBlock()
p0.Height = 1
p1 := NewPar("p1")
p1 := NewBlock()
p1.Height = 1
p2 := NewPar("p2")
p2 := NewBlock()
p2.Height = 1
p3 := NewPar("p3")
p3 := NewBlock()
p3.Height = 1
/* test against tree:
@ -34,24 +34,6 @@ func TestRowWidth(t *testing.T) {
/
1100:w
*/
/*
r = &row{
Span: 12,
Cols: []*row{
&row{Widget: p0, Span: 6},
&row{
Span: 6,
Cols: []*row{
&row{Widget: p1, Span: 6},
&row{
Span: 6,
Cols: []*row{
&row{
Span: 12,
Widget: p2,
Cols: []*row{
&row{Span: 12, Widget: p3}}}}}}}}}
*/
r = NewRow(
NewCol(6, 0, p0),

154
helper.go
View File

@ -4,7 +4,12 @@
package termui
import tm "github.com/nsf/termbox-go"
import (
"regexp"
"strings"
tm "github.com/nsf/termbox-go"
)
import rw "github.com/mattn/go-runewidth"
/* ---------------Port from termbox-go --------------------- */
@ -12,6 +17,7 @@ import rw "github.com/mattn/go-runewidth"
// Attribute is printable cell's color and style.
type Attribute uint16
// 8 basic clolrs
const (
ColorDefault Attribute = iota
ColorBlack
@ -24,7 +30,10 @@ const (
ColorWhite
)
const NumberofColors = 8 //Have a constant that defines number of colors
//Have a constant that defines number of colors
const NumberofColors = 8
// Text style
const (
AttrBold Attribute = 1 << (iota + 9)
AttrUnderline
@ -46,15 +55,39 @@ func str2runes(s string) []rune {
return []rune(s)
}
// Here for backwards-compatibility.
func trimStr2Runes(s string, w int) []rune {
return TrimStr2Runes(s, w)
}
// TrimStr2Runes trims string to w[-1 rune], appends …, and returns the runes
// of that string if string is grather then n. If string is small then w,
// return the runes.
func TrimStr2Runes(s string, w int) []rune {
if w <= 0 {
return []rune{}
}
sw := rw.StringWidth(s)
if sw > w {
return []rune(rw.Truncate(s, w, dot))
}
return str2runes(s) //[]rune(rw.Truncate(s, w, ""))
return str2runes(s)
}
// TrimStrIfAppropriate trim string to "s[:-1] + …"
// if string > width otherwise return string
func TrimStrIfAppropriate(s string, w int) string {
if w <= 0 {
return ""
}
sw := rw.StringWidth(s)
if sw > w {
return rw.Truncate(s, w, dot)
}
return s
}
func strWidth(s string) int {
@ -64,3 +97,118 @@ func strWidth(s string) int {
func charWidth(ch rune) int {
return rw.RuneWidth(ch)
}
var whiteSpaceRegex = regexp.MustCompile(`\s`)
// StringToAttribute converts text to a termui attribute. You may specifiy more
// then one attribute like that: "BLACK, BOLD, ...". All whitespaces
// are ignored.
func StringToAttribute(text string) Attribute {
text = whiteSpaceRegex.ReplaceAllString(strings.ToLower(text), "")
attributes := strings.Split(text, ",")
result := Attribute(0)
for _, theAttribute := range attributes {
var match Attribute
switch theAttribute {
case "reset", "default":
match = ColorDefault
case "black":
match = ColorBlack
case "red":
match = ColorRed
case "green":
match = ColorGreen
case "yellow":
match = ColorYellow
case "blue":
match = ColorBlue
case "magenta":
match = ColorMagenta
case "cyan":
match = ColorCyan
case "white":
match = ColorWhite
case "bold":
match = AttrBold
case "underline":
match = AttrUnderline
case "reverse":
match = AttrReverse
}
result |= match
}
return result
}
// TextCells returns a coloured text cells []Cell
func TextCells(s string, fg, bg Attribute) []Cell {
cs := make([]Cell, 0, len(s))
// sequence := MarkdownTextRendererFactory{}.TextRenderer(s).Render(fg, bg)
// runes := []rune(sequence.NormalizedText)
runes := str2runes(s)
for n := range runes {
// point, _ := sequence.PointAt(n, 0, 0)
// cs = append(cs, Cell{point.Ch, point.Fg, point.Bg})
cs = append(cs, Cell{runes[n], fg, bg})
}
return cs
}
// Width returns the actual screen space the cell takes (usually 1 or 2).
func (c Cell) Width() int {
return charWidth(c.Ch)
}
// Copy return a copy of c
func (c Cell) Copy() Cell {
return c
}
// TrimTxCells trims the overflowed text cells sequence.
func TrimTxCells(cs []Cell, w int) []Cell {
if len(cs) <= w {
return cs
}
return cs[:w]
}
// DTrimTxCls trims the overflowed text cells sequence and append dots at the end.
func DTrimTxCls(cs []Cell, w int) []Cell {
l := len(cs)
if l <= 0 {
return []Cell{}
}
rt := make([]Cell, 0, w)
csw := 0
for i := 0; i < l && csw <= w; i++ {
c := cs[i]
cw := c.Width()
if cw+csw < w {
rt = append(rt, c)
csw += cw
} else {
rt = append(rt, Cell{'…', c.Fg, c.Bg})
break
}
}
return rt
}

View File

@ -7,22 +7,20 @@ package termui
import (
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/stretchr/testify/assert"
)
func TestStr2Rune(t *testing.T) {
s := "你好,世界."
rs := str2runes(s)
if len(rs) != 6 {
t.Error()
t.Error(t)
}
}
func TestWidth(t *testing.T) {
s0 := "つのだ☆HIRO"
s1 := "11111111111"
spew.Dump(s0)
spew.Dump(s1)
// above not align for setting East Asian Ambiguous to wide!!
if strWidth(s0) != strWidth(s1) {
@ -56,3 +54,17 @@ func TestTrim(t *testing.T) {
t.Error("avoid trim failed")
}
}
func TestTrimStrIfAppropriate_NoTrim(t *testing.T) {
assert.Equal(t, "hello", TrimStrIfAppropriate("hello", 5))
}
func TestTrimStrIfAppropriate(t *testing.T) {
assert.Equal(t, "hel…", TrimStrIfAppropriate("hello", 4))
assert.Equal(t, "h…", TrimStrIfAppropriate("hello", 2))
}
func TestStringToAttribute(t *testing.T) {
assert.Equal(t, ColorRed, StringToAttribute("ReD"))
assert.Equal(t, ColorRed|AttrBold, StringToAttribute("RED, bold"))
}

View File

@ -74,8 +74,8 @@ type LineChart struct {
// NewLineChart returns a new LineChart with current theme.
func NewLineChart() *LineChart {
lc := &LineChart{Block: *NewBlock()}
lc.AxesColor = theme.LineChartAxes
lc.LineColor = theme.LineChartLine
lc.AxesColor = ThemeAttr("linechart.axes.fg")
lc.LineColor = ThemeAttr("linechart.line.fg")
lc.Mode = "braille"
lc.DotStyle = '•'
lc.axisXLebelGap = 2
@ -87,8 +87,8 @@ func NewLineChart() *LineChart {
// one cell contains two data points
// so the capicity is 2x as dot-mode
func (lc *LineChart) renderBraille() []Point {
ps := []Point{}
func (lc *LineChart) renderBraille() Buffer {
buf := NewBuffer()
// return: b -> which cell should the point be in
// m -> in the cell, divided into 4 equal height levels, which subcell?
@ -104,44 +104,48 @@ func (lc *LineChart) renderBraille() []Point {
b1, m1 := getPos(lc.Data[2*i+1])
if b0 == b1 {
p := Point{}
p.Ch = braillePatterns[[2]int{m0, m1}]
p.Bg = lc.BgColor
p.Fg = lc.LineColor
p.Y = lc.innerY + lc.innerHeight - 3 - b0
p.X = lc.innerX + lc.labelYSpace + 1 + i
ps = append(ps, p)
c := Cell{
Ch: braillePatterns[[2]int{m0, m1}],
Bg: lc.Bg,
Fg: lc.LineColor,
}
y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
x := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
buf.Set(x, y, c)
} else {
p0 := newPointWithAttrs(lSingleBraille[m0],
lc.innerX+lc.labelYSpace+1+i,
lc.innerY+lc.innerHeight-3-b0,
lc.LineColor,
lc.BgColor)
p1 := newPointWithAttrs(rSingleBraille[m1],
lc.innerX+lc.labelYSpace+1+i,
lc.innerY+lc.innerHeight-3-b1,
lc.LineColor,
lc.BgColor)
ps = append(ps, p0, p1)
c0 := Cell{Ch: lSingleBraille[m0],
Fg: lc.LineColor,
Bg: lc.Bg}
x0 := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
y0 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
buf.Set(x0, y0, c0)
c1 := Cell{Ch: rSingleBraille[m1],
Fg: lc.LineColor,
Bg: lc.Bg}
x1 := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
y1 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b1
buf.Set(x1, y1, c1)
}
}
return ps
return buf
}
func (lc *LineChart) renderDot() []Point {
ps := []Point{}
func (lc *LineChart) renderDot() Buffer {
buf := NewBuffer()
for i := 0; i < len(lc.Data) && i < lc.axisXWidth; i++ {
p := Point{}
p.Ch = lc.DotStyle
p.Fg = lc.LineColor
p.Bg = lc.BgColor
p.X = lc.innerX + lc.labelYSpace + 1 + i
p.Y = lc.innerY + lc.innerHeight - 3 - int((lc.Data[i]-lc.bottomValue)/lc.scale+0.5)
ps = append(ps, p)
c := Cell{
Ch: lc.DotStyle,
Fg: lc.LineColor,
Bg: lc.Bg,
}
x := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - int((lc.Data[i]-lc.bottomValue)/lc.scale+0.5)
buf.Set(x, y, c)
}
return ps
return buf
}
func (lc *LineChart) calcLabelX() {
@ -220,9 +224,9 @@ func (lc *LineChart) calcLayout() {
lc.maxY = lc.Data[0]
// valid visible range
vrange := lc.innerWidth
vrange := lc.innerArea.Dx()
if lc.Mode == "braille" {
vrange = 2 * lc.innerWidth
vrange = 2 * lc.innerArea.Dx()
}
if vrange > len(lc.Data) {
vrange = len(lc.Data)
@ -247,40 +251,30 @@ func (lc *LineChart) calcLayout() {
lc.topValue = lc.maxY + 0.2*span
}
lc.axisYHeight = lc.innerHeight - 2
lc.axisYHeight = lc.innerArea.Dy() - 2
lc.calcLabelY()
lc.axisXWidth = lc.innerWidth - 1 - lc.labelYSpace
lc.axisXWidth = lc.innerArea.Dx() - 1 - lc.labelYSpace
lc.calcLabelX()
lc.drawingX = lc.innerX + 1 + lc.labelYSpace
lc.drawingY = lc.innerY
lc.drawingX = lc.innerArea.Min.X + 1 + lc.labelYSpace
lc.drawingY = lc.innerArea.Min.Y
}
func (lc *LineChart) plotAxes() []Point {
origY := lc.innerY + lc.innerHeight - 2
origX := lc.innerX + lc.labelYSpace
func (lc *LineChart) plotAxes() Buffer {
buf := NewBuffer()
ps := []Point{newPointWithAttrs(ORIGIN, origX, origY, lc.AxesColor, lc.BgColor)}
origY := lc.innerArea.Min.Y + lc.innerArea.Dy() - 2
origX := lc.innerArea.Min.X + lc.labelYSpace
buf.Set(origX, origY, Cell{Ch: ORIGIN, Fg: lc.AxesColor, Bg: lc.Bg})
for x := origX + 1; x < origX+lc.axisXWidth; x++ {
p := Point{}
p.X = x
p.Y = origY
p.Bg = lc.BgColor
p.Fg = lc.AxesColor
p.Ch = HDASH
ps = append(ps, p)
buf.Set(x, origY, Cell{Ch: HDASH, Fg: lc.AxesColor, Bg: lc.Bg})
}
for dy := 1; dy <= lc.axisYHeight; dy++ {
p := Point{}
p.X = origX
p.Y = origY - dy
p.Bg = lc.BgColor
p.Fg = lc.AxesColor
p.Ch = VDASH
ps = append(ps, p)
buf.Set(origX, origY-dy, Cell{Ch: VDASH, Fg: lc.AxesColor, Bg: lc.Bg})
}
// x label
@ -290,13 +284,14 @@ func (lc *LineChart) plotAxes() []Point {
break
}
for j, r := range rs {
p := Point{}
p.Ch = r
p.Fg = lc.AxesColor
p.Bg = lc.BgColor
p.X = origX + oft + j
p.Y = lc.innerY + lc.innerHeight - 1
ps = append(ps, p)
c := Cell{
Ch: r,
Fg: lc.AxesColor,
Bg: lc.Bg,
}
x := origX + oft + j
y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 1
buf.Set(x, y, c)
}
oft += len(rs) + lc.axisXLebelGap
}
@ -304,33 +299,31 @@ func (lc *LineChart) plotAxes() []Point {
// y labels
for i, rs := range lc.labelY {
for j, r := range rs {
p := Point{}
p.Ch = r
p.Fg = lc.AxesColor
p.Bg = lc.BgColor
p.X = lc.innerX + j
p.Y = origY - i*(lc.axisYLebelGap+1)
ps = append(ps, p)
buf.Set(
lc.innerArea.Min.X+j,
origY-i*(lc.axisYLebelGap+1),
Cell{Ch: r, Fg: lc.AxesColor, Bg: lc.Bg})
}
}
return ps
return buf
}
// Buffer implements Bufferer interface.
func (lc *LineChart) Buffer() []Point {
ps := lc.Block.Buffer()
func (lc *LineChart) Buffer() Buffer {
buf := lc.Block.Buffer()
if lc.Data == nil || len(lc.Data) == 0 {
return ps
return buf
}
lc.calcLayout()
ps = append(ps, lc.plotAxes()...)
buf.Merge(lc.plotAxes())
if lc.Mode == "dot" {
ps = append(ps, lc.renderDot()...)
buf.Merge(lc.renderDot())
} else {
ps = append(ps, lc.renderBraille()...)
buf.Merge(lc.renderBraille())
}
return lc.Block.chopOverflow(ps)
return buf
}

51
list.go
View File

@ -41,64 +41,49 @@ type List struct {
func NewList() *List {
l := &List{Block: *NewBlock()}
l.Overflow = "hidden"
l.ItemFgColor = theme.ListItemFg
l.ItemBgColor = theme.ListItemBg
l.ItemFgColor = ThemeAttr("list.item.fg")
l.ItemBgColor = ThemeAttr("list.item.bg")
return l
}
// Buffer implements Bufferer interface.
func (l *List) Buffer() []Point {
ps := l.Block.Buffer()
func (l *List) Buffer() Buffer {
buf := l.Block.Buffer()
switch l.Overflow {
case "wrap":
rs := str2runes(strings.Join(l.Items, "\n"))
cs := DefaultTxBuilder.Build(strings.Join(l.Items, "\n"), l.ItemFgColor, l.ItemBgColor)
i, j, k := 0, 0, 0
for i < l.innerHeight && k < len(rs) {
w := charWidth(rs[k])
if rs[k] == '\n' || j+w > l.innerWidth {
for i < l.innerArea.Dy() && k < len(cs) {
w := cs[k].Width()
if cs[k].Ch == '\n' || j+w > l.innerArea.Dx() {
i++
j = 0
if rs[k] == '\n' {
if cs[k].Ch == '\n' {
k++
}
continue
}
pi := Point{}
pi.X = l.innerX + j
pi.Y = l.innerY + i
buf.Set(l.innerArea.Min.X+j, l.innerArea.Min.Y+i, cs[k])
pi.Ch = rs[k]
pi.Bg = l.ItemBgColor
pi.Fg = l.ItemFgColor
ps = append(ps, pi)
k++
j++
}
case "hidden":
trimItems := l.Items
if len(trimItems) > l.innerHeight {
trimItems = trimItems[:l.innerHeight]
if len(trimItems) > l.innerArea.Dy() {
trimItems = trimItems[:l.innerArea.Dy()]
}
for i, v := range trimItems {
rs := trimStr2Runes(v, l.innerWidth)
cs := DTrimTxCls(DefaultTxBuilder.Build(v, l.ItemFgColor, l.ItemBgColor), l.innerArea.Dx())
j := 0
for _, vv := range rs {
w := charWidth(vv)
p := Point{}
p.X = l.innerX + j
p.Y = l.innerY + i
p.Ch = vv
p.Bg = l.ItemBgColor
p.Fg = l.ItemFgColor
ps = append(ps, p)
for _, vv := range cs {
w := vv.Width()
buf.Set(l.innerArea.Min.X+j, l.innerArea.Min.Y+i, vv)
j += w
}
}
}
return l.Block.chopOverflow(ps)
return buf
}

View File

@ -48,16 +48,16 @@ type MBarChart struct {
// NewBarChart returns a new *BarChart with current theme.
func NewMBarChart() *MBarChart {
bc := &MBarChart{Block: *NewBlock()}
bc.BarColor[0] = theme.MBarChartBar
bc.NumColor[0] = theme.MBarChartNum
bc.TextColor = theme.MBarChartText
bc.BarColor[0] = ThemeAttr("mbarchart.bar.bg")
bc.NumColor[0] = ThemeAttr("mbarchart.num.fg")
bc.TextColor = ThemeAttr("mbarchart.text.fg")
bc.BarGap = 1
bc.BarWidth = 3
return bc
}
func (bc *MBarChart) layout() {
bc.numBar = bc.innerWidth / (bc.BarGap + bc.BarWidth)
bc.numBar = bc.innerArea.Dx() / (bc.BarGap + bc.BarWidth)
bc.labels = make([][]rune, bc.numBar)
DataLen := 0
LabelLen := len(bc.DataLabels)
@ -129,9 +129,9 @@ func (bc *MBarChart) layout() {
if bc.ShowScale {
s := fmt.Sprintf("%d", bc.max)
bc.maxScale = trimStr2Runes(s, len(s))
bc.scale = float64(bc.max) / float64(bc.innerHeight-2)
bc.scale = float64(bc.max) / float64(bc.innerArea.Dy()-2)
} else {
bc.scale = float64(bc.max) / float64(bc.innerHeight-1)
bc.scale = float64(bc.max) / float64(bc.innerArea.Dy()-1)
}
}
@ -144,8 +144,8 @@ func (bc *MBarChart) SetMax(max int) {
}
// Buffer implements Bufferer interface.
func (bc *MBarChart) Buffer() []Point {
ps := bc.Block.Buffer()
func (bc *MBarChart) Buffer() Buffer {
buf := bc.Block.Buffer()
bc.layout()
var oftX int
@ -157,15 +157,17 @@ func (bc *MBarChart) Buffer() []Point {
// plot bars
for j := 0; j < bc.BarWidth; j++ {
for k := 0; k < h; k++ {
p := Point{}
p.Ch = ' '
p.Bg = bc.BarColor[i1]
if bc.BarColor[i1] == ColorDefault { // when color is default, space char treated as transparent!
p.Bg |= AttrReverse
c := Cell{
Ch: ' ',
Bg: bc.BarColor[i1],
}
p.X = bc.innerX + i*(bc.BarWidth+bc.BarGap) + j
p.Y = bc.innerY + bc.innerHeight - 2 - k - ph
ps = append(ps, p)
if bc.BarColor[i1] == ColorDefault { // when color is default, space char treated as transparent!
c.Bg |= AttrReverse
}
x := bc.innerArea.Min.X + i*(bc.BarWidth+bc.BarGap) + j
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2 - k - ph
buf.Set(x, y, c)
}
}
ph += h
@ -173,13 +175,14 @@ func (bc *MBarChart) Buffer() []Point {
// plot text
for j, k := 0, 0; j < len(bc.labels[i]); j++ {
w := charWidth(bc.labels[i][j])
p := Point{}
p.Ch = bc.labels[i][j]
p.Bg = bc.BgColor
p.Fg = bc.TextColor
p.Y = bc.innerY + bc.innerHeight - 1
p.X = bc.innerX + oftX + ((bc.BarWidth - len(bc.labels[i])) / 2) + k
ps = append(ps, p)
c := Cell{
Ch: bc.labels[i][j],
Bg: bc.Bg,
Fg: bc.TextColor,
}
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 1
x := bc.innerArea.Max.X + oftX + ((bc.BarWidth - len(bc.labels[i])) / 2) + k
buf.Set(x, y, c)
k += w
}
// plot num
@ -187,19 +190,20 @@ func (bc *MBarChart) Buffer() []Point {
for i1 := 0; i1 < bc.numStack; i1++ {
h := int(float64(bc.Data[i1][i]) / bc.scale)
for j := 0; j < len(bc.dataNum[i1][i]) && h > 0; j++ {
p := Point{}
p.Ch = bc.dataNum[i1][i][j]
p.Fg = bc.NumColor[i1]
p.Bg = bc.BarColor[i1]
c := Cell{
Ch: bc.dataNum[i1][i][j],
Fg: bc.NumColor[i1],
Bg: bc.BarColor[i1],
}
if bc.BarColor[i1] == ColorDefault { // the same as above
p.Bg |= AttrReverse
c.Bg |= AttrReverse
}
if h == 0 {
p.Bg = bc.BgColor
c.Bg = bc.Bg
}
p.X = bc.innerX + oftX + (bc.BarWidth-len(bc.dataNum[i1][i]))/2 + j
p.Y = bc.innerY + bc.innerHeight - 2 - ph
ps = append(ps, p)
x := bc.innerArea.Min.X + oftX + (bc.BarWidth-len(bc.dataNum[i1][i]))/2 + j
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2 - ph
buf.Set(x, y, c)
}
ph += h
}
@ -208,26 +212,31 @@ func (bc *MBarChart) Buffer() []Point {
if bc.ShowScale {
//Currently bar graph only supprts data range from 0 to MAX
//Plot 0
p := Point{}
p.Ch = '0'
p.Bg = bc.BgColor
p.Fg = bc.TextColor
p.Y = bc.innerY + bc.innerHeight - 2
p.X = bc.X
ps = append(ps, p)
c := Cell{
Ch: '0',
Bg: bc.Bg,
Fg: bc.TextColor,
}
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2
x := bc.X
buf.Set(x, y, c)
//Plot the maximum sacle value
for i := 0; i < len(bc.maxScale); i++ {
p := Point{}
p.Ch = bc.maxScale[i]
p.Bg = bc.BgColor
p.Fg = bc.TextColor
p.Y = bc.innerY
p.X = bc.X + i
ps = append(ps, p)
c := Cell{
Ch: bc.maxScale[i],
Bg: bc.Bg,
Fg: bc.TextColor,
}
y := bc.innerArea.Min.Y
x := bc.X + i
buf.Set(x, y, c)
}
}
return bc.Block.chopOverflow(ps)
return buf
}

71
p.go
View File

@ -1,71 +0,0 @@
// Copyright 2015 Zack Guo <gizak@icloud.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
// Par displays a paragraph.
/*
par := termui.NewPar("Simple Text")
par.Height = 3
par.Width = 17
par.Border.Label = "Label"
*/
type Par struct {
Block
Text string
TextFgColor Attribute
TextBgColor Attribute
}
// NewPar returns a new *Par with given text as its content.
func NewPar(s string) *Par {
return &Par{
Block: *NewBlock(),
Text: s,
TextFgColor: theme.ParTextFg,
TextBgColor: theme.ParTextBg}
}
// Buffer implements Bufferer interface.
func (p *Par) Buffer() []Point {
ps := p.Block.Buffer()
rs := str2runes(p.Text)
i, j, k := 0, 0, 0
for i < p.innerHeight && k < len(rs) {
// the width of char is about to print
w := charWidth(rs[k])
if rs[k] == '\n' || j+w > p.innerWidth {
i++
j = 0 // set x = 0
if rs[k] == '\n' {
k++
}
if i >= p.innerHeight {
ps = append(ps, newPointWithAttrs('…',
p.innerX+p.innerWidth-1,
p.innerY+p.innerHeight-1,
p.TextFgColor, p.TextBgColor))
break
}
continue
}
pi := Point{}
pi.X = p.innerX + j
pi.Y = p.innerY + i
pi.Ch = rs[k]
pi.Bg = p.TextBgColor
pi.Fg = p.TextFgColor
ps = append(ps, pi)
k++
j += w
}
return p.Block.chopOverflow(ps)
}

64
par.go Normal file
View File

@ -0,0 +1,64 @@
// Copyright 2015 Zack Guo <gizak@icloud.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
// Par displays a paragraph.
/*
par := termui.NewPar("Simple Text")
par.Height = 3
par.Width = 17
par.Border.Label = "Label"
*/
type Par struct {
Block
Text string
TextFgColor Attribute
TextBgColor Attribute
}
// NewPar returns a new *Par with given text as its content.
func NewPar(s string) *Par {
return &Par{
Block: *NewBlock(),
Text: s,
TextFgColor: ThemeAttr("par.text.fg"),
TextBgColor: ThemeAttr("par.text.bg"),
}
}
// Buffer implements Bufferer interface.
func (p *Par) Buffer() Buffer {
buf := p.Block.Buffer()
fg, bg := p.TextFgColor, p.TextBgColor
cs := DefaultTxBuilder.Build(p.Text, fg, bg)
y, x, n := 0, 0, 0
for y < p.innerArea.Dy() && n < len(cs) {
w := cs[n].Width()
if cs[n].Ch == '\n' || x+w > p.innerArea.Dx() {
y++
x = 0 // set x = 0
if cs[n].Ch == '\n' {
n++
}
if y >= p.innerArea.Dy() {
buf.Set(p.innerArea.Min.X+p.innerArea.Dx()-1,
p.innerArea.Min.Y+p.innerArea.Dy()-1,
Cell{Ch: '…', Fg: p.TextFgColor, Bg: p.TextBgColor})
break
}
continue
}
buf.Set(p.innerArea.Min.X+x, p.innerArea.Min.Y+y, cs[n])
n++
x += w
}
return buf
}

View File

@ -4,17 +4,17 @@ import "testing"
func TestPar_NoBorderBackground(t *testing.T) {
par := NewPar("a")
par.HasBorder = false
par.BgColor = ColorBlue
par.Border = false
par.Bg = ColorBlue
par.TextBgColor = ColorBlue
par.Width = 2
par.Height = 2
pts := par.Buffer()
for _, p := range pts {
for _, p := range pts.CellMap {
t.Log(p)
if p.Bg != par.BgColor {
t.Errorf("expected color to be %v but got %v", par.BgColor, p.Bg)
if p.Bg != par.Bg {
t.Errorf("expected color to be %v but got %v", par.Bg, p.Bg)
}
}
}

View File

@ -1,28 +0,0 @@
// Copyright 2015 Zack Guo <gizak@icloud.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
// Point stands for a single cell in terminal.
type Point struct {
Ch rune
Bg Attribute
Fg Attribute
X int
Y int
}
func newPoint(c rune, x, y int) (p Point) {
p.Ch = c
p.X = x
p.Y = y
return
}
func newPointWithAttrs(c rune, x, y int, fg, bg Attribute) Point {
p := newPoint(c, x, y)
p.Bg = bg
p.Fg = fg
return p
}

71
pos.go Normal file
View File

@ -0,0 +1,71 @@
package termui
import "image"
// Align is the position of the gauge's label.
type Align uint
// All supported positions.
const (
AlignNone Align = 0
AlignLeft Align = 1 << iota
AlignRight
AlignBottom
AlignTop
AlignCenterVertical
AlignCenterHorizontal
AlignCenter = AlignCenterVertical | AlignCenterHorizontal
)
func AlignArea(parent, child image.Rectangle, a Align) image.Rectangle {
w, h := child.Dx(), child.Dy()
// parent center
pcx, pcy := parent.Min.X+parent.Dx()/2, parent.Min.Y+parent.Dy()/2
// child center
ccx, ccy := child.Min.X+child.Dx()/2, child.Min.Y+child.Dy()/2
if a&AlignLeft == AlignLeft {
child.Min.X = parent.Min.X
child.Max.X = child.Min.X + w
}
if a&AlignRight == AlignRight {
child.Max.X = parent.Max.X
child.Min.X = child.Max.X - w
}
if a&AlignBottom == AlignBottom {
child.Max.Y = parent.Max.Y
child.Min.Y = child.Max.Y - h
}
if a&AlignTop == AlignRight {
child.Min.Y = parent.Min.Y
child.Max.Y = child.Min.Y + h
}
if a&AlignCenterHorizontal == AlignCenterHorizontal {
child.Min.X += pcx - ccx
child.Max.X = child.Min.X + w
}
if a&AlignCenterVertical == AlignCenterVertical {
child.Min.Y += pcy - ccy
child.Max.Y = child.Min.Y + h
}
return child
}
func MoveArea(a image.Rectangle, dx, dy int) image.Rectangle {
a.Min.X += dx
a.Max.X += dx
a.Min.Y += dy
a.Max.Y += dy
return a
}
func TermRect() image.Rectangle {
return image.Rect(0, 0, TermWidth(), TermHeight())
}

34
pos_test.go Normal file
View File

@ -0,0 +1,34 @@
package termui
import (
"image"
"testing"
)
func TestAlignArea(t *testing.T) {
p := image.Rect(0, 0, 100, 100)
c := image.Rect(10, 10, 20, 20)
nc := AlignArea(p, c, AlignLeft)
if nc.Min.X != 0 || nc.Max.Y != 20 {
t.Errorf("AlignLeft failed:\n%+v", nc)
}
nc = AlignArea(p, c, AlignCenter)
if nc.Min.X != 45 || nc.Max.Y != 55 {
t.Error("AlignCenter failed")
}
nc = AlignArea(p, c, AlignBottom|AlignRight)
if nc.Min.X != 90 || nc.Max.Y != 100 {
t.Errorf("AlignBottom|AlignRight failed\n%+v", nc)
}
}
func TestMoveArea(t *testing.T) {
a := image.Rect(10, 10, 20, 20)
a = MoveArea(a, 5, 10)
if a.Min.X != 15 || a.Min.Y != 20 || a.Max.X != 25 || a.Max.Y != 30 {
t.Error("MoveArea failed")
}
}

View File

@ -4,26 +4,54 @@
package termui
import tm "github.com/nsf/termbox-go"
import (
"time"
tm "github.com/nsf/termbox-go"
)
// Bufferer should be implemented by all renderable components.
type Bufferer interface {
Buffer() []Point
Buffer() Buffer
}
// Init initializes termui library. This function should be called before any others.
// After initialization, the library must be finalized by 'Close' function.
func Init() error {
if err := tm.Init(); err != nil {
return err
}
sysEvtChs = make([]chan Event, 0)
go hookTermboxEvt()
renderJobs = make(chan []Bufferer)
go func() {
for bs := range renderJobs {
render(bs...)
}
}()
Body = NewGrid()
Body.X = 0
Body.Y = 0
Body.BgColor = theme.BodyBg
defer func() {
w, _ := tm.Size()
Body.Width = w
evtListen()
}()
return tm.Init()
Body.BgColor = ThemeAttr("bg")
Body.Width = TermWidth()
DefaultEvtStream.Init()
DefaultEvtStream.Merge("termbox", NewSysEvtCh())
DefaultEvtStream.Merge("timer", NewTimerCh(time.Second))
DefaultEvtStream.Merge("custom", usrEvtCh)
DefaultEvtStream.Handle("/", DefualtHandler)
DefaultEvtStream.Handle("/sys/wnd/resize", func(e Event) {
w := e.Data.(EvtWnd)
Body.Width = w.Width
})
DefaultWgtMgr = NewWgtMgr()
DefaultEvtStream.Hook(DefaultWgtMgr.WgtHandlersHook())
return nil
}
// Close finalizes termui library,
@ -48,13 +76,24 @@ func TermHeight() int {
// Render renders all Bufferer in the given order from left to right,
// right could overlap on left ones.
func Render(rs ...Bufferer) {
tm.Clear(tm.ColorDefault, toTmAttr(theme.BodyBg))
for _, r := range rs {
buf := r.Buffer()
for _, v := range buf {
tm.SetCell(v.X, v.Y, v.Ch, toTmAttr(v.Fg), toTmAttr(v.Bg))
func render(bs ...Bufferer) {
// set tm bg
tm.Clear(tm.ColorDefault, toTmAttr(ThemeAttr("bg")))
for _, b := range bs {
buf := b.Buffer()
// set cels in buf
for p, c := range buf.CellMap {
if p.In(buf.Area) {
tm.SetCell(p.X, p.Y, c.Ch, toTmAttr(c.Fg), toTmAttr(c.Bg))
}
}
}
// render
tm.Flush()
}
var renderJobs chan []Bufferer
func Render(bs ...Bufferer) {
go func() { renderJobs <- bs }()
}

View File

@ -49,8 +49,8 @@ func (s *Sparklines) Add(sl Sparkline) {
func NewSparkline() Sparkline {
return Sparkline{
Height: 1,
TitleColor: theme.SparklineTitle,
LineColor: theme.SparklineLine}
TitleColor: ThemeAttr("sparkline.title.fg"),
LineColor: ThemeAttr("sparkline.line.fg")}
}
// NewSparklines return a new *Spaklines with given Sparkline(s), you can always add a new Sparkline later.
@ -67,13 +67,13 @@ func (sl *Sparklines) update() {
sl.Lines[i].displayHeight = v.Height + 1
}
}
sl.displayWidth = sl.innerWidth
sl.displayWidth = sl.innerArea.Dx()
// get how many lines gotta display
h := 0
sl.displayLines = 0
for _, v := range sl.Lines {
if h+v.displayHeight <= sl.innerHeight {
if h+v.displayHeight <= sl.innerArea.Dy() {
sl.displayLines++
} else {
break
@ -96,8 +96,8 @@ func (sl *Sparklines) update() {
}
// Buffer implements Bufferer interface.
func (sl *Sparklines) Buffer() []Point {
ps := sl.Block.Buffer()
func (sl *Sparklines) Buffer() Buffer {
buf := sl.Block.Buffer()
sl.update()
oftY := 0
@ -105,22 +105,23 @@ func (sl *Sparklines) Buffer() []Point {
l := sl.Lines[i]
data := l.Data
if len(data) > sl.innerWidth {
data = data[len(data)-sl.innerWidth:]
if len(data) > sl.innerArea.Dx() {
data = data[len(data)-sl.innerArea.Dx():]
}
if l.Title != "" {
rs := trimStr2Runes(l.Title, sl.innerWidth)
rs := trimStr2Runes(l.Title, sl.innerArea.Dx())
oftX := 0
for _, v := range rs {
w := charWidth(v)
p := Point{}
p.Ch = v
p.Fg = l.TitleColor
p.Bg = sl.BgColor
p.X = sl.innerX + oftX
p.Y = sl.innerY + oftY
ps = append(ps, p)
c := Cell{
Ch: v,
Fg: l.TitleColor,
Bg: sl.Bg,
}
x := sl.innerArea.Min.X + oftX
y := sl.innerArea.Min.Y + oftY
buf.Set(x, y, c)
oftX += w
}
}
@ -130,27 +131,30 @@ func (sl *Sparklines) Buffer() []Point {
barCnt := h / 8
barMod := h % 8
for jj := 0; jj < barCnt; jj++ {
p := Point{}
p.X = sl.innerX + j
p.Y = sl.innerY + oftY + l.Height - jj
p.Ch = ' ' // => sparks[7]
p.Bg = l.LineColor
c := Cell{
Ch: ' ', // => sparks[7]
Bg: l.LineColor,
}
x := sl.innerArea.Min.X + j
y := sl.innerArea.Min.Y + oftY + l.Height - jj
//p.Bg = sl.BgColor
ps = append(ps, p)
buf.Set(x, y, c)
}
if barMod != 0 {
p := Point{}
p.X = sl.innerX + j
p.Y = sl.innerY + oftY + l.Height - barCnt
p.Ch = sparks[barMod-1]
p.Fg = l.LineColor
p.Bg = sl.BgColor
ps = append(ps, p)
c := Cell{
Ch: sparks[barMod-1],
Fg: l.LineColor,
Bg: sl.Bg,
}
x := sl.innerArea.Min.X + j
y := sl.innerArea.Min.Y + oftY + l.Height - barCnt
buf.Set(x, y, c)
}
}
oftY += l.displayHeight
}
return sl.Block.chopOverflow(ps)
return buf
}

62
test/runtest.go Normal file
View File

@ -0,0 +1,62 @@
package main
import (
"fmt"
"os"
"github.com/gizak/termui"
"github.com/gizak/termui/debug"
)
func main() {
// run as client
if len(os.Args) > 1 {
fmt.Print(debug.ConnectAndListen())
return
}
// run as server
go func() { panic(debug.ListenAndServe()) }()
if err := termui.Init(); err != nil {
panic(err)
}
defer termui.Close()
//termui.UseTheme("helloworld")
b := termui.NewBlock()
b.Width = 20
b.Height = 20
b.Float = termui.AlignCenter
b.BorderLabel = "[HELLO](fg-red,bg-white) [WORLD](fg-blue,bg-green)"
termui.Render(b)
termui.Handle("/sys", func(e termui.Event) {
k, ok := e.Data.(termui.EvtKbd)
debug.Logf("->%v\n", e)
if ok && k.KeyStr == "q" {
termui.StopLoop()
}
})
termui.Handle(("/usr"), func(e termui.Event) {
debug.Logf("->%v\n", e)
})
termui.Handle("/timer/1s", func(e termui.Event) {
t := e.Data.(termui.EvtTimer)
termui.SendCustomEvt("/usr/t", t.Count)
if t.Count%2 == 0 {
b.BorderLabel = "[HELLO](fg-red,bg-green) [WORLD](fg-blue,bg-white)"
} else {
b.BorderLabel = "[HELLO](fg-blue,bg-white) [WORLD](fg-red,bg-green)"
}
termui.Render(b)
})
termui.Loop()
}

210
textbuilder.go Normal file
View File

@ -0,0 +1,210 @@
package termui
import (
"regexp"
"strings"
)
// TextBuilder is a minial interface to produce text []Cell using sepcific syntax (markdown).
type TextBuilder interface {
Build(s string, fg, bg Attribute) []Cell
}
// DefaultTxBuilder is set to be MarkdownTxBuilder.
var DefaultTxBuilder = NewMarkdownTxBuilder()
// MarkdownTxBuilder implements TextBuilder interface, using markdown syntax.
type MarkdownTxBuilder struct {
baseFg Attribute
baseBg Attribute
plainTx []rune
markers []marker
}
type marker struct {
st int
ed int
fg Attribute
bg Attribute
}
var colorMap = map[string]Attribute{
"red": ColorRed,
"blue": ColorBlue,
"black": ColorBlack,
"cyan": ColorCyan,
"white": ColorWhite,
"default": ColorDefault,
"green": ColorGreen,
"magenta": ColorMagenta,
}
var attrMap = map[string]Attribute{
"bold": AttrBold,
"underline": AttrUnderline,
"reverse": AttrReverse,
}
func rmSpc(s string) string {
reg := regexp.MustCompile(`\s+`)
return reg.ReplaceAllString(s, "")
}
// readAttr translates strings like `fg-red,fg-bold,bg-white` to fg and bg Attribute
func (mtb MarkdownTxBuilder) readAttr(s string) (Attribute, Attribute) {
fg := mtb.baseFg
bg := mtb.baseBg
updateAttr := func(a Attribute, attrs []string) Attribute {
for _, s := range attrs {
// replace the color
if c, ok := colorMap[s]; ok {
a &= 0xFF00 // erase clr 0 ~ 8 bits
a |= c // set clr
}
// add attrs
if c, ok := attrMap[s]; ok {
a |= c
}
}
return a
}
ss := strings.Split(s, ",")
fgs := []string{}
bgs := []string{}
for _, v := range ss {
subs := strings.Split(v, "-")
if len(subs) > 1 {
if subs[0] == "fg" {
fgs = append(fgs, subs[1])
}
if subs[0] == "bg" {
bgs = append(bgs, subs[1])
}
}
}
fg = updateAttr(fg, fgs)
bg = updateAttr(bg, bgs)
return fg, bg
}
func (mtb *MarkdownTxBuilder) reset() {
mtb.plainTx = []rune{}
mtb.markers = []marker{}
}
// parse streams and parses text into normalized text and render sequence.
func (mtb *MarkdownTxBuilder) parse(str string) {
rs := str2runes(str)
normTx := []rune{}
square := []rune{}
brackt := []rune{}
accSquare := false
accBrackt := false
cntSquare := 0
reset := func() {
square = []rune{}
brackt = []rune{}
accSquare = false
accBrackt = false
cntSquare = 0
}
// pipe stacks into normTx and clear
rollback := func() {
normTx = append(normTx, square...)
normTx = append(normTx, brackt...)
reset()
}
// chop first and last
chop := func(s []rune) []rune {
return s[1 : len(s)-1]
}
for i, r := range rs {
switch {
// stacking brackt
case accBrackt:
brackt = append(brackt, r)
if ')' == r {
fg, bg := mtb.readAttr(string(chop(brackt)))
st := len(normTx)
ed := len(normTx) + len(square) - 2
mtb.markers = append(mtb.markers, marker{st, ed, fg, bg})
normTx = append(normTx, chop(square)...)
reset()
} else if i+1 == len(rs) {
rollback()
}
// stacking square
case accSquare:
switch {
// squares closed and followed by a '('
case cntSquare == 0 && '(' == r:
accBrackt = true
brackt = append(brackt, '(')
// squares closed but not followed by a '('
case cntSquare == 0:
rollback()
if '[' == r {
accSquare = true
cntSquare = 1
brackt = append(brackt, '[')
} else {
normTx = append(normTx, r)
}
// hit the end
case i+1 == len(rs):
square = append(square, r)
rollback()
case '[' == r:
cntSquare++
square = append(square, '[')
case ']' == r:
cntSquare--
square = append(square, ']')
// normal char
default:
square = append(square, r)
}
// stacking normTx
default:
if '[' == r {
accSquare = true
cntSquare = 1
square = append(square, '[')
} else {
normTx = append(normTx, r)
}
}
}
mtb.plainTx = normTx
}
// Build implements TextBuilder interface.
func (mtb MarkdownTxBuilder) Build(s string, fg, bg Attribute) []Cell {
mtb.baseFg = fg
mtb.baseBg = bg
mtb.reset()
mtb.parse(s)
cs := make([]Cell, len(mtb.plainTx))
for i := range cs {
cs[i] = Cell{Ch: mtb.plainTx[i], Fg: fg, Bg: bg}
}
for _, mrk := range mtb.markers {
for i := mrk.st; i < mrk.ed; i++ {
cs[i].Fg = mrk.fg
cs[i].Bg = mrk.bg
}
}
return cs
}
// NewMarkdownTxBuilder returns a TextBuilder employing markdown syntax.
func NewMarkdownTxBuilder() TextBuilder {
return MarkdownTxBuilder{}
}

66
textbuilder_test.go Normal file
View File

@ -0,0 +1,66 @@
package termui
import "testing"
func TestReadAttr(t *testing.T) {
m := MarkdownTxBuilder{}
m.baseFg = ColorCyan | AttrUnderline
m.baseBg = ColorBlue | AttrBold
fg, bg := m.readAttr("fg-red,bg-reverse")
if fg != ColorRed|AttrUnderline || bg != ColorBlue|AttrBold|AttrReverse {
t.Error("readAttr failed")
}
}
func TestMTBParse(t *testing.T) {
/*
str := func(cs []Cell) string {
rs := make([]rune, len(cs))
for i := range cs {
rs[i] = cs[i].Ch
}
return string(rs)
}
*/
tbls := [][]string{
{"hello world", "hello world"},
{"[hello](fg-red) world", "hello world"},
{"[[hello]](bg-red) world", "[hello] world"},
{"[1] hello world", "[1] hello world"},
{"[[1]](bg-white) [hello] world", "[1] [hello] world"},
{"[hello world]", "[hello world]"},
{"", ""},
{"[hello world)", "[hello world)"},
{"[0] [hello](bg-red)[ world](fg-blue)!", "[0] hello world!"},
}
m := MarkdownTxBuilder{}
m.baseFg = ColorWhite
m.baseBg = ColorDefault
for _, s := range tbls {
m.reset()
m.parse(s[0])
res := string(m.plainTx)
if s[1] != res {
t.Errorf("\ninput :%s\nshould:%s\noutput:%s", s[0], s[1], res)
}
}
m.reset()
m.parse("[0] [hello](bg-red)[ world](fg-blue)")
if len(m.markers) != 2 &&
m.markers[0].st == 4 &&
m.markers[0].ed == 11 &&
m.markers[0].fg == ColorWhite &&
m.markers[0].bg == ColorRed {
t.Error("markers dismatch")
}
m2 := NewMarkdownTxBuilder()
cs := m2.Build("[0] [hellob-e) wrd]fgblue)!", ColorWhite, ColorBlack)
cs = m2.Build("[0] [hello](bg-red) [world](fg-blue)!", ColorWhite, ColorBlack)
if cs[4].Ch != 'h' && cs[4].Bg != ColorRed && cs[4].Fg != ColorWhite {
t.Error("dismatch in Build")
}
}

View File

@ -4,6 +4,9 @@
package termui
import "strings"
/*
// A ColorScheme represents the current look-and-feel of the dashboard.
type ColorScheme struct {
BodyBg Attribute
@ -84,3 +87,54 @@ func UseTheme(th string) {
theme = themeDefault
}
}
*/
var ColorMap = map[string]Attribute{
"fg": ColorWhite,
"bg": ColorDefault,
"border.fg": ColorWhite,
"label.fg": ColorGreen,
"par.fg": ColorYellow,
"par.label.bg": ColorWhite,
}
func ThemeAttr(name string) Attribute {
return lookUpAttr(ColorMap, name)
}
func lookUpAttr(clrmap map[string]Attribute, name string) Attribute {
a, ok := clrmap[name]
if ok {
return a
}
ns := strings.Split(name, ".")
for i := range ns {
nn := strings.Join(ns[i:len(ns)], ".")
a, ok = ColorMap[nn]
if ok {
break
}
}
return a
}
// 0<=r,g,b <= 5
func ColorRGB(r, g, b int) Attribute {
within := func(n int) int {
if n < 0 {
return 0
}
if n > 5 {
return 5
}
return n
}
r, b, g = within(r), within(b), within(g)
return Attribute(0x0f + 36*r + 6*g + b)
}

31
theme_test.go Normal file
View File

@ -0,0 +1,31 @@
package termui
import "testing"
var cmap = map[string]Attribute{
"fg": ColorWhite,
"bg": ColorDefault,
"border.fg": ColorWhite,
"label.fg": ColorGreen,
"par.fg": ColorYellow,
"par.label.bg": ColorWhite,
}
func TestLoopUpAttr(t *testing.T) {
tbl := []struct {
name string
should Attribute
}{
{"par.label.bg", ColorWhite},
{"par.label.fg", ColorGreen},
{"par.bg", ColorDefault},
{"bar.border.fg", ColorWhite},
{"bar.label.bg", ColorDefault},
}
for _, v := range tbl {
if lookUpAttr(cmap, v.name) != v.should {
t.Error(v.name)
}
}
}

90
widget.go Normal file
View File

@ -0,0 +1,90 @@
package termui
import (
"fmt"
"sync"
)
// event mixins
type WgtMgr map[string]WgtInfo
type WgtInfo struct {
Handlers map[string]func(Event)
WgtRef Widget
Id string
}
type Widget interface {
Id() string
}
func NewWgtInfo(wgt Widget) WgtInfo {
return WgtInfo{
Handlers: make(map[string]func(Event)),
WgtRef: wgt,
Id: wgt.Id(),
}
}
func NewWgtMgr() WgtMgr {
wm := WgtMgr(make(map[string]WgtInfo))
return wm
}
func (wm WgtMgr) AddWgt(wgt Widget) {
wm[wgt.Id()] = NewWgtInfo(wgt)
}
func (wm WgtMgr) RmWgt(wgt Widget) {
wm.RmWgtById(wgt.Id())
}
func (wm WgtMgr) RmWgtById(id string) {
delete(wm, id)
}
func (wm WgtMgr) AddWgtHandler(id, path string, h func(Event)) {
if w, ok := wm[id]; ok {
w.Handlers[path] = h
}
}
func (wm WgtMgr) RmWgtHandler(id, path string) {
if w, ok := wm[id]; ok {
delete(w.Handlers, path)
}
}
var counter struct {
sync.RWMutex
count int
}
func GenId() string {
counter.Lock()
defer counter.Unlock()
counter.count += 1
return fmt.Sprintf("%d", counter.count)
}
func (wm WgtMgr) WgtHandlersHook() func(Event) {
return func(e Event) {
for _, v := range wm {
if k := findMatch(v.Handlers, e.Path); k != "" {
v.Handlers[k](e)
}
}
}
}
var DefaultWgtMgr WgtMgr
func (b *Block) Handle(path string, handler func(Event)) {
if _, ok := DefaultWgtMgr[b.Id()]; !ok {
DefaultWgtMgr.AddWgt(b)
}
DefaultWgtMgr.AddWgtHandler(b.Id(), path, handler)
}