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

View File

@ -15,16 +15,23 @@ func main() {
} }
defer termui.Close() defer termui.Close()
termui.UseTheme("helloworld") //termui.UseTheme("helloworld")
g0 := termui.NewGauge() g0 := termui.NewGauge()
g0.Percent = 40 g0.Percent = 40
g0.Width = 50 g0.Width = 50
g0.Height = 3 g0.Height = 3
g0.Border.Label = "Slim Gauge" g0.BorderLabel = "Slim Gauge"
g0.BarColor = termui.ColorRed g0.BarColor = termui.ColorRed
g0.Border.FgColor = termui.ColorWhite g0.BorderFg = termui.ColorWhite
g0.Border.LabelFgColor = termui.ColorCyan 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 := termui.NewGauge()
g2.Percent = 60 g2.Percent = 60
@ -32,27 +39,27 @@ func main() {
g2.Height = 3 g2.Height = 3
g2.PercentColor = termui.ColorBlue g2.PercentColor = termui.ColorBlue
g2.Y = 3 g2.Y = 3
g2.Border.Label = "Slim Gauge" g2.BorderLabel = "Slim Gauge"
g2.BarColor = termui.ColorYellow g2.BarColor = termui.ColorYellow
g2.Border.FgColor = termui.ColorWhite g2.BorderFg = termui.ColorWhite
g1 := termui.NewGauge() g1 := termui.NewGauge()
g1.Percent = 30 g1.Percent = 30
g1.Width = 50 g1.Width = 50
g1.Height = 5 g1.Height = 5
g1.Y = 6 g1.Y = 6
g1.Border.Label = "Big Gauge" g1.BorderLabel = "Big Gauge"
g1.PercentColor = termui.ColorYellow g1.PercentColor = termui.ColorYellow
g1.BarColor = termui.ColorGreen g1.BarColor = termui.ColorGreen
g1.Border.FgColor = termui.ColorWhite g1.BorderFg = termui.ColorWhite
g1.Border.LabelFgColor = termui.ColorMagenta g1.BorderLabelFg = termui.ColorMagenta
g3 := termui.NewGauge() g3 := termui.NewGauge()
g3.Percent = 50 g3.Percent = 50
g3.Width = 50 g3.Width = 50
g3.Height = 3 g3.Height = 3
g3.Y = 11 g3.Y = 11
g3.Border.Label = "Gauge with custom label" g3.BorderLabel = "Gauge with custom label"
g3.Label = "{{percent}}% (100MBs free)" g3.Label = "{{percent}}% (100MBs free)"
g3.LabelAlign = termui.AlignRight g3.LabelAlign = termui.AlignRight
@ -69,5 +76,9 @@ func main() {
termui.Render(g0, g1, g2, g3, g4) 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 package main
import ui "github.com/gizak/termui" import ui "github.com/gizak/termui"
import "math" import "math"
import "time"
func main() { func main() {
err := ui.Init() if err := ui.Init(); err != nil {
if err != nil {
panic(err) panic(err)
} }
defer ui.Close() defer ui.Close()
@ -33,8 +32,6 @@ func main() {
return ps return ps
})() })()
ui.UseTheme("helloworld")
spark := ui.Sparkline{} spark := ui.Sparkline{}
spark.Height = 8 spark.Height = 8
spdata := sinpsint spdata := sinpsint
@ -44,10 +41,10 @@ func main() {
sp := ui.NewSparklines(spark) sp := ui.NewSparklines(spark)
sp.Height = 11 sp.Height = 11
sp.Border.Label = "Sparkline" sp.BorderLabel = "Sparkline"
lc := ui.NewLineChart() lc := ui.NewLineChart()
lc.Border.Label = "braille-mode Line Chart" lc.BorderLabel = "braille-mode Line Chart"
lc.Data = sinps lc.Data = sinps
lc.Height = 11 lc.Height = 11
lc.AxesColor = ui.ColorWhite lc.AxesColor = ui.ColorWhite
@ -56,15 +53,16 @@ func main() {
gs := make([]*ui.Gauge, 3) gs := make([]*ui.Gauge, 3)
for i := range gs { for i := range gs {
gs[i] = ui.NewGauge() gs[i] = ui.NewGauge()
//gs[i].LabelAlign = ui.AlignCenter
gs[i].Height = 2 gs[i].Height = 2
gs[i].HasBorder = false gs[i].Border = false
gs[i].Percent = i * 10 gs[i].Percent = i * 10
gs[i].PaddingBottom = 1 gs[i].PaddingBottom = 1
gs[i].BarColor = ui.ColorRed gs[i].BarColor = ui.ColorRed
} }
ls := ui.NewList() ls := ui.NewList()
ls.HasBorder = false ls.Border = false
ls.Items = []string{ ls.Items = []string{
"[1] Downloading File 1", "[1] Downloading File 1",
"", // == \newline "", // == \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 := 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.Height = 5
par.Border.Label = "Demonstration" par.BorderLabel = "Demonstration"
// build layout // build layout
ui.Body.AddRows( ui.Body.AddRows(
@ -91,44 +89,33 @@ func main() {
// calculate layout // calculate layout
ui.Body.Align() 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) ui.Render(ui.Body)
go update()
for { ui.Handle("/sys/kbd/q", func(ui.Event) {
select { ui.StopLoop()
case e := <-evt: })
if e.Type == ui.EventKey && e.Ch == 'q' { ui.Handle("/timer/1s", func(e ui.Event) {
return t := e.Data.(ui.EvtTimer)
} i := t.Count
if e.Type == ui.EventResize { if i > 103 {
ui.Body.Width = ui.TermWidth() ui.StopLoop()
ui.Body.Align()
go func() { redraw <- true }()
}
case <-done:
return 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() defer termui.Close()
termui.UseTheme("helloworld") //termui.UseTheme("helloworld")
sinps := (func() []float64 { sinps := (func() []float64 {
n := 220 n := 220
@ -31,7 +31,7 @@ func main() {
})() })()
lc0 := termui.NewLineChart() lc0 := termui.NewLineChart()
lc0.Border.Label = "braille-mode Line Chart" lc0.BorderLabel = "braille-mode Line Chart"
lc0.Data = sinps lc0.Data = sinps
lc0.Width = 50 lc0.Width = 50
lc0.Height = 12 lc0.Height = 12
@ -41,7 +41,7 @@ func main() {
lc0.LineColor = termui.ColorGreen | termui.AttrBold lc0.LineColor = termui.ColorGreen | termui.AttrBold
lc1 := termui.NewLineChart() lc1 := termui.NewLineChart()
lc1.Border.Label = "dot-mode Line Chart" lc1.BorderLabel = "dot-mode Line Chart"
lc1.Mode = "dot" lc1.Mode = "dot"
lc1.Data = sinps lc1.Data = sinps
lc1.Width = 26 lc1.Width = 26
@ -52,7 +52,7 @@ func main() {
lc1.LineColor = termui.ColorYellow | termui.AttrBold lc1.LineColor = termui.ColorYellow | termui.AttrBold
lc2 := termui.NewLineChart() lc2 := termui.NewLineChart()
lc2.Border.Label = "dot-mode Line Chart" lc2.BorderLabel = "dot-mode Line Chart"
lc2.Mode = "dot" lc2.Mode = "dot"
lc2.Data = sinps[4:] lc2.Data = sinps[4:]
lc2.Width = 77 lc2.Width = 77
@ -63,6 +63,9 @@ func main() {
lc2.LineColor = termui.ColorCyan | termui.AttrBold lc2.LineColor = termui.ColorCyan | termui.AttrBold
termui.Render(lc0, lc1, lc2) 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() defer termui.Close()
termui.UseTheme("helloworld") //termui.UseTheme("helloworld")
strs := []string{ strs := []string{
"[0] github.com/gizak/termui", "[0] github.com/gizak/termui",
"[1] 你好,世界", "[1] [你好,世界](fg-blue)",
"[2] こんにちは世界", "[2] [こんにちは世界](fg-red)",
"[3] keyboard.go", "[3] [color output](fg-white,bg-green)",
"[4] output.go", "[4] output.go",
"[5] random_out.go", "[5] random_out.go",
"[6] dashboard.go", "[6] dashboard.go",
@ -30,12 +30,15 @@ func main() {
ls := termui.NewList() ls := termui.NewList()
ls.Items = strs ls.Items = strs
ls.ItemFgColor = termui.ColorYellow ls.ItemFgColor = termui.ColorYellow
ls.Border.Label = "List" ls.BorderLabel = "List"
ls.Height = 7 ls.Height = 7
ls.Width = 25 ls.Width = 25
ls.Y = 0 ls.Y = 0
termui.Render(ls) 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() defer termui.Close()
termui.UseTheme("helloworld") //termui.UseTheme("helloworld")
bc := termui.NewMBarChart() bc := termui.NewMBarChart()
math := []int{90, 85, 90, 80} math := []int{90, 85, 90, 80}
@ -27,10 +27,10 @@ func main() {
bc.Data[2] = science bc.Data[2] = science
bc.Data[3] = compsci bc.Data[3] = compsci
studentsName := []string{"Ken", "Rob", "Dennis", "Linus"} 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.Width = 100
bc.Height = 50 bc.Height = 30
bc.Y = 10 bc.Y = 0
bc.BarWidth = 10 bc.BarWidth = 10
bc.DataLabels = studentsName bc.DataLabels = studentsName
bc.ShowScale = true //Show y_axis scale value (min and max) bc.ShowScale = true //Show y_axis scale value (min and max)
@ -46,5 +46,9 @@ func main() {
termui.Render(bc) 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() defer termui.Close()
termui.UseTheme("helloworld") //termui.UseTheme("helloworld")
par0 := termui.NewPar("Borderless Text") par0 := termui.NewPar("Borderless Text")
par0.Height = 1 par0.Height = 1
par0.Width = 20 par0.Width = 20
par0.Y = 1 par0.Y = 1
par0.HasBorder = false par0.Border = false
par1 := termui.NewPar("你好,世界。") par1 := termui.NewPar("你好,世界。")
par1.Height = 3 par1.Height = 3
par1.Width = 17 par1.Width = 17
par1.X = 20 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.Height = 5
par2.Width = 37 par2.Width = 37
par2.Y = 4 par2.Y = 4
par2.Border.Label = "Multiline" par2.BorderLabel = "Multiline"
par2.Border.FgColor = termui.ColorYellow par2.BorderFg = termui.ColorYellow
par3 := termui.NewPar("Long text with label and it is auto trimmed.") par3 := termui.NewPar("Long text with label and it is auto trimmed.")
par3.Height = 3 par3.Height = 3
par3.Width = 37 par3.Width = 37
par3.Y = 9 par3.Y = 9
par3.Border.Label = "Auto Trim" par3.BorderLabel = "Auto Trim"
termui.Render(par0, par1, par2, par3) 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() 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} 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() spl0 := termui.NewSparkline()
@ -27,7 +27,7 @@ func main() {
spls0 := termui.NewSparklines(spl0) spls0 := termui.NewSparklines(spl0)
spls0.Height = 2 spls0.Height = 2
spls0.Width = 20 spls0.Width = 20
spls0.HasBorder = false spls0.Border = false
spl1 := termui.NewSparkline() spl1 := termui.NewSparkline()
spl1.Data = data spl1.Data = data
@ -44,7 +44,7 @@ func main() {
spls1.Height = 8 spls1.Height = 8
spls1.Width = 20 spls1.Width = 20
spls1.Y = 3 spls1.Y = 3
spls1.Border.Label = "Group Sparklines" spls1.BorderLabel = "Group Sparklines"
spl3 := termui.NewSparkline() spl3 := termui.NewSparkline()
spl3.Data = data spl3.Data = data
@ -55,11 +55,15 @@ func main() {
spls2 := termui.NewSparklines(spl3) spls2 := termui.NewSparklines(spl3)
spls2.Height = 11 spls2.Height = 11
spls2.Width = 30 spls2.Width = 30
spls2.Border.FgColor = termui.ColorCyan spls2.BorderFg = termui.ColorCyan
spls2.X = 21 spls2.X = 21
spls2.Border.Label = "Tweeked Sparkline" spls2.BorderLabel = "Tweeked Sparkline"
termui.Render(spls0, spls1, spls2) 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. // NewBarChart returns a new *BarChart with current theme.
func NewBarChart() *BarChart { func NewBarChart() *BarChart {
bc := &BarChart{Block: *NewBlock()} bc := &BarChart{Block: *NewBlock()}
bc.BarColor = theme.BarChartBar bc.BarColor = ThemeAttr("barchart.bar.bg")
bc.NumColor = theme.BarChartNum bc.NumColor = ThemeAttr("barchart.num.fg")
bc.TextColor = theme.BarChartText bc.TextColor = ThemeAttr("barchart.text.fg")
bc.BarGap = 1 bc.BarGap = 1
bc.BarWidth = 3 bc.BarWidth = 3
return bc return bc
} }
func (bc *BarChart) layout() { 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.labels = make([][]rune, bc.numBar)
bc.dataNum = make([][]rune, len(bc.Data)) bc.dataNum = make([][]rune, len(bc.Data))
@ -69,7 +69,7 @@ func (bc *BarChart) layout() {
bc.max = bc.Data[i] 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) { func (bc *BarChart) SetMax(max int) {
@ -80,8 +80,8 @@ func (bc *BarChart) SetMax(max int) {
} }
// Buffer implements Bufferer interface. // Buffer implements Bufferer interface.
func (bc *BarChart) Buffer() []Point { func (bc *BarChart) Buffer() Buffer {
ps := bc.Block.Buffer() buf := bc.Block.Buffer()
bc.layout() bc.layout()
for i := 0; i < bc.numBar && i < len(bc.Data) && i < len(bc.DataLabels); i++ { 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 // plot bar
for j := 0; j < bc.BarWidth; j++ { for j := 0; j < bc.BarWidth; j++ {
for k := 0; k < h; k++ { for k := 0; k < h; k++ {
p := Point{} c := Cell{
p.Ch = ' ' Ch: ' ',
p.Bg = bc.BarColor Bg: bc.BarColor,
if bc.BarColor == ColorDefault { // when color is default, space char treated as transparent!
p.Bg |= AttrReverse
} }
p.X = bc.innerX + i*(bc.BarWidth+bc.BarGap) + j if bc.BarColor == ColorDefault { // when color is default, space char treated as transparent!
p.Y = bc.innerY + bc.innerHeight - 2 - k c.Bg |= AttrReverse
ps = append(ps, p) }
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 // plot text
for j, k := 0, 0; j < len(bc.labels[i]); j++ { for j, k := 0, 0; j < len(bc.labels[i]); j++ {
w := charWidth(bc.labels[i][j]) w := charWidth(bc.labels[i][j])
p := Point{} c := Cell{
p.Ch = bc.labels[i][j] Ch: bc.labels[i][j],
p.Bg = bc.BgColor Bg: bc.Bg,
p.Fg = bc.TextColor Fg: bc.TextColor,
p.Y = bc.innerY + bc.innerHeight - 1 }
p.X = bc.innerX + oftX + k y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 1
ps = append(ps, p) x := bc.innerArea.Min.X + oftX + k
buf.Set(x, y, c)
k += w k += w
} }
// plot num // plot num
for j := 0; j < len(bc.dataNum[i]); j++ { for j := 0; j < len(bc.dataNum[i]); j++ {
p := Point{} c := Cell{
p.Ch = bc.dataNum[i][j] Ch: bc.dataNum[i][j],
p.Fg = bc.NumColor Fg: bc.NumColor,
p.Bg = bc.BarColor Bg: bc.BarColor,
}
if bc.BarColor == ColorDefault { // the same as above if bc.BarColor == ColorDefault { // the same as above
p.Bg |= AttrReverse c.Bg |= AttrReverse
} }
if h == 0 { if h == 0 {
p.Bg = bc.BgColor c.Bg = bc.Bg
} }
p.X = bc.innerX + oftX + (bc.BarWidth-len(bc.dataNum[i]))/2 + j x := bc.innerArea.Min.X + oftX + (bc.BarWidth-len(bc.dataNum[i]))/2 + j
p.Y = bc.innerY + bc.innerHeight - 2 y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2
ps = append(ps, p) buf.Set(x, y, c)
} }
} }
return bc.Block.chopOverflow(ps) return buf
} }

282
block.go
View File

@ -4,163 +4,237 @@
package termui 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, // Block is a base struct for all other upper level widgets,
// consider it as css: display:block. // consider it as css: display:block.
// Normally you do not need to create it manually. // Normally you do not need to create it manually.
type Block struct { type Block struct {
area image.Rectangle
innerArea image.Rectangle
X int X int
Y int Y int
Border labeledBorder Border bool
IsDisplay bool BorderFg Attribute
HasBorder bool BorderBg Attribute
BgColor Attribute BorderLeft bool
BorderRight bool
BorderTop bool
BorderBottom bool
BorderLabel string
BorderLabelFg Attribute
BorderLabelBg Attribute
Display bool
Bg Attribute
Width int Width int
Height int Height int
innerWidth int
innerHeight int
innerX int
innerY int
PaddingTop int PaddingTop int
PaddingBottom int PaddingBottom int
PaddingLeft int PaddingLeft int
PaddingRight int PaddingRight int
id string
Float Align
} }
// NewBlock returns a *Block which inherits styles from current theme. // NewBlock returns a *Block which inherits styles from current theme.
func NewBlock() *Block { func NewBlock() *Block {
d := Block{} b := Block{}
d.IsDisplay = true b.Display = true
d.HasBorder = theme.HasBorder b.Border = true
d.Border.BgColor = theme.BorderBg b.BorderLeft = true
d.Border.FgColor = theme.BorderFg b.BorderRight = true
d.Border.LabelBgColor = theme.BorderLabelTextBg b.BorderTop = true
d.Border.LabelFgColor = theme.BorderLabelTextFg b.BorderBottom = true
d.BgColor = theme.BlockBg b.BorderBg = ThemeAttr("border.bg")
d.Width = 2 b.BorderFg = ThemeAttr("border.fg")
d.Height = 2 b.BorderLabelBg = ThemeAttr("label.bg")
return &d 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 (b Block) Id() string {
func (d *Block) align() { return b.id
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
if d.HasBorder { // Align computes box model
d.innerHeight -= 2 func (b *Block) Align() {
d.innerWidth -= 2 // outer
d.Border.X = d.X b.area.Min.X = 0
d.Border.Y = d.Y b.area.Min.Y = 0
d.Border.Width = d.Width b.area.Max.X = b.Width
d.Border.Height = d.Height b.area.Max.Y = b.Height
d.innerX++
d.innerY++
}
if d.innerHeight < 0 { // float
d.innerHeight = 0 b.area = AlignArea(TermRect(), b.area, b.Float)
} b.area = MoveArea(b.area, b.X, b.Y)
if d.innerWidth < 0 {
d.innerWidth = 0
}
// 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 // InnerBounds returns the internal bounds of the block after aligning and
// calculating the padding and border, if any. // calculating the padding and border, if any.
func (d *Block) InnerBounds() (x, y, width, height int) { func (b *Block) InnerBounds() image.Rectangle {
d.align() b.Align()
return d.innerX, d.innerY, d.innerWidth, d.innerHeight return b.innerArea
} }
// Buffer implements Bufferer interface. // Buffer implements Bufferer interface.
// Draw background and border (if any). // Draw background and border (if any).
func (d *Block) Buffer() []Point { func (b *Block) Buffer() Buffer {
d.align() b.Align()
ps := []Point{} buf := NewBuffer()
if !d.IsDisplay { buf.SetArea(b.area)
return ps buf.Fill(' ', ColorDefault, b.Bg)
}
if d.HasBorder { b.drawBorder(buf)
ps = d.Border.Buffer() b.drawBorderLabel(buf)
}
for i := 0; i < d.innerWidth; i++ { return buf
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
} }
// GetHeight implements GridBufferer. // GetHeight implements GridBufferer.
// It returns current height of the block. // It returns current height of the block.
func (d Block) GetHeight() int { func (b Block) GetHeight() int {
return d.Height return b.Height
} }
// SetX implements GridBufferer interface, which sets block's x position. // SetX implements GridBufferer interface, which sets block's x position.
func (d *Block) SetX(x int) { func (b *Block) SetX(x int) {
d.X = x b.X = x
} }
// SetY implements GridBufferer interface, it sets y position for block. // SetY implements GridBufferer interface, it sets y position for block.
func (d *Block) SetY(y int) { func (b *Block) SetY(y int) {
d.Y = y b.Y = y
} }
// SetWidth implements GridBuffer interface, it sets block's width. // SetWidth implements GridBuffer interface, it sets block's width.
func (d *Block) SetWidth(w int) { func (b *Block) SetWidth(w int) {
d.Width = w b.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) InnerWidth() int { func (b Block) InnerWidth() int {
return b.innerWidth return b.innerArea.Dx()
} }
func (b Block) InnerHeight() int { func (b Block) InnerHeight() int {
return b.innerHeight return b.innerArea.Dy()
} }
func (b Block) InnerX() int { func (b Block) InnerX() int {
return b.innerX return b.innerArea.Min.X
} }
func (b Block) InnerY() int { func (b Block) InnerY() int { return b.innerArea.Min.Y }
return b.innerY
}
func (b *Block) Align() {
b.align()
}

View File

@ -1,8 +1,25 @@
package termui 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 := NewBlock()
b.X = 10 b.X = 10
b.Y = 11 b.Y = 11
@ -11,12 +28,17 @@ func TestBlock_InnerBounds(t *testing.T) {
assert := func(name string, x, y, w, h int) { assert := func(name string, x, y, w, h int) {
t.Log(name) 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 { if cx != x {
t.Errorf("expected x to be %d but got %d", x, cx) t.Errorf("expected x to be %d but got %d", x, cx)
} }
if cy != y { 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 { if cw != w {
t.Errorf("expected width to be %d but got %d", w, cw) 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) assert("no border, no padding", 10, 11, 12, 13)
b.HasBorder = true b.Border = true
assert("border, no padding", 11, 12, 10, 11) assert("border, no padding", 11, 12, 10, 11)
b.PaddingBottom = 2 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 // Buffer returns un-styled points
func (c Canvas) Buffer() []Point { func (c Canvas) Buffer() Buffer {
ps := make([]Point, len(c)) buf := NewBuffer()
i := 0
for k, v := range c { for k, v := range c {
ps[i] = newPoint(v+brailleBase, k[0], k[1]) buf.Set(k[0], k[1], Cell{Ch: v + brailleBase})
i++
} }
return ps return buf
} }

View File

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

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 package termui
import "github.com/nsf/termbox-go" import (
"path"
"strconv"
"sync"
"time"
/***********************************termbox-go**************************************/ "github.com/nsf/termbox-go"
type (
EventType uint8
Modifier uint8
Key uint16
) )
// This type represents a termbox event. The 'Mod', 'Key' and 'Ch' fields are
// valid if 'Type' is EventKey. The 'Width' and 'Height' fields are valid if
// 'Type' is EventResize. The 'Err' field is valid if 'Type' is EventError.
type Event struct { type Event struct {
Type EventType // one of Event* constants Type string
Mod Modifier // one of Mod* constants or 0 Path string
Key Key // one of Key* constants, invalid if 'Ch' is not 0 From string
Ch rune // a unicode character To string
Width int // width of the screen Data interface{}
Height int // height of the screen Time int64
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
} }
const ( var sysEvtChs []chan Event
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
)
const ( type EvtKbd struct {
KeyCtrlTilde Key = 0x00 KeyStr string
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
} }
var evtChs = make([]chan Event, 0) func evtKbd(e termbox.Event) EvtKbd {
ek := EvtKbd{}
// EventCh returns an output-only event channel. k := string(e.Ch)
// This function can be called many times (multiplexer). pre := ""
func EventCh() <-chan Event { mod := ""
out := make(chan Event)
evtChs = append(evtChs, out)
return out
}
// turn on event listener if e.Mod == termbox.ModAlt {
func evtListen() { mod = "M-"
go func() { }
for { if e.Ch == 0 {
e := termbox.PollEvent() if e.Key > 0xFFFF-12 {
// dispatch k = "<f" + strconv.Itoa(0xFFFF-int(e.Key)+1) + ">"
for _, c := range evtChs { } else if e.Key > 0xFFFF-25 {
go func(ch chan Event) { ks := []string{"<insert>", "<delete>", "<home>", "<end>", "<previous>", "<next>", "<up>", "<down>", "<left>", "<right>"}
ch <- uiEvt(e) k = ks[0xFFFF-int(e.Key)-12]
}(c) }
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 package termui
import ( import "testing"
"errors"
"testing"
termbox "github.com/nsf/termbox-go" var ps = []string{
"github.com/stretchr/testify/assert" "",
) "/",
"/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 g.PercentColor = termui.ColorBlue
*/ */
// Align is the position of the gauge's label. const ColorUndef Attribute = Attribute(^uint16(0))
type Align int
// All supported positions.
const (
AlignLeft Align = iota
AlignCenter
AlignRight
)
const uint16max = ^uint16(0)
type Gauge struct { type Gauge struct {
Block Block
@ -47,11 +37,11 @@ type Gauge struct {
func NewGauge() *Gauge { func NewGauge() *Gauge {
g := &Gauge{ g := &Gauge{
Block: *NewBlock(), Block: *NewBlock(),
PercentColor: theme.GaugePercent, PercentColor: ThemeAttr("gauge.percent.fg"),
BarColor: theme.GaugeBar, BarColor: ThemeAttr("gauge.bar.bg"),
Label: "{{percent}}%", Label: "{{percent}}%",
LabelAlign: AlignCenter, LabelAlign: AlignCenter,
PercentColorHighlighted: Attribute(uint16max), PercentColorHighlighted: ColorUndef,
} }
g.Width = 12 g.Width = 12
@ -60,28 +50,26 @@ func NewGauge() *Gauge {
} }
// Buffer implements Bufferer interface. // Buffer implements Bufferer interface.
func (g *Gauge) Buffer() []Point { func (g *Gauge) Buffer() Buffer {
ps := g.Block.Buffer() buf := g.Block.Buffer()
// plot bar // plot bar
w := g.Percent * g.innerWidth / 100 w := g.Percent * g.innerArea.Dx() / 100
for i := 0; i < g.innerHeight; i++ { for i := 0; i < g.innerArea.Dy(); i++ {
for j := 0; j < w; j++ { for j := 0; j < w; j++ {
p := Point{} c := Cell{}
p.X = g.innerX + j c.Ch = ' '
p.Y = g.innerY + i c.Bg = g.BarColor
p.Ch = ' ' if c.Bg == ColorDefault {
p.Bg = g.BarColor c.Bg |= AttrReverse
if p.Bg == ColorDefault {
p.Bg |= AttrReverse
} }
ps = append(ps, p) buf.Set(g.innerArea.Min.X+j, g.innerArea.Min.Y+i, c)
} }
} }
// plot percentage // plot percentage
s := strings.Replace(g.Label, "{{percent}}", strconv.Itoa(g.Percent), -1) 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) rs := str2runes(s)
var pos int var pos int
switch g.LabelAlign { switch g.LabelAlign {
@ -89,33 +77,33 @@ func (g *Gauge) Buffer() []Point {
pos = 0 pos = 0
case AlignCenter: case AlignCenter:
pos = (g.innerWidth - strWidth(s)) / 2 pos = (g.innerArea.Dx() - strWidth(s)) / 2
case AlignRight: 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 { for i, v := range rs {
p := Point{} c := Cell{
p.X = 1 + pos + i Ch: v,
p.Y = pry Fg: g.PercentColor,
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
} }
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, // Buffer implements Bufferer interface,
// recursively merge all widgets buffer // recursively merge all widgets buffer
func (r *Row) Buffer() []Point { func (r *Row) Buffer() Buffer {
merged := []Point{} merged := NewBuffer()
if r.isRenderableLeaf() { if r.isRenderableLeaf() {
return r.Widget.Buffer() return r.Widget.Buffer()
@ -169,13 +169,13 @@ func (r *Row) Buffer() []Point {
// for those are not leaves but have a renderable widget // for those are not leaves but have a renderable widget
if r.Widget != nil { if r.Widget != nil {
merged = append(merged, r.Widget.Buffer()...) merged.Merge(r.Widget.Buffer())
} }
// collect buffer from children // collect buffer from children
if !r.isLeaf() { if !r.isLeaf() {
for _, c := range r.Cols { 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. // Buffer implments Bufferer interface.
func (g Grid) Buffer() []Point { func (g Grid) Buffer() Buffer {
ps := []Point{} buf := NewBuffer()
for _, r := range g.Rows { 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 var Body *Grid

View File

@ -13,13 +13,13 @@ import (
var r *Row var r *Row
func TestRowWidth(t *testing.T) { func TestRowWidth(t *testing.T) {
p0 := NewPar("p0") p0 := NewBlock()
p0.Height = 1 p0.Height = 1
p1 := NewPar("p1") p1 := NewBlock()
p1.Height = 1 p1.Height = 1
p2 := NewPar("p2") p2 := NewBlock()
p2.Height = 1 p2.Height = 1
p3 := NewPar("p3") p3 := NewBlock()
p3.Height = 1 p3.Height = 1
/* test against tree: /* test against tree:
@ -34,24 +34,6 @@ func TestRowWidth(t *testing.T) {
/ /
1100:w 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( r = NewRow(
NewCol(6, 0, p0), NewCol(6, 0, p0),

154
helper.go
View File

@ -4,7 +4,12 @@
package termui 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" import rw "github.com/mattn/go-runewidth"
/* ---------------Port from termbox-go --------------------- */ /* ---------------Port from termbox-go --------------------- */
@ -12,6 +17,7 @@ import rw "github.com/mattn/go-runewidth"
// Attribute is printable cell's color and style. // Attribute is printable cell's color and style.
type Attribute uint16 type Attribute uint16
// 8 basic clolrs
const ( const (
ColorDefault Attribute = iota ColorDefault Attribute = iota
ColorBlack ColorBlack
@ -24,7 +30,10 @@ const (
ColorWhite 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 ( const (
AttrBold Attribute = 1 << (iota + 9) AttrBold Attribute = 1 << (iota + 9)
AttrUnderline AttrUnderline
@ -46,15 +55,39 @@ func str2runes(s string) []rune {
return []rune(s) return []rune(s)
} }
// Here for backwards-compatibility.
func trimStr2Runes(s string, w int) []rune { 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 { if w <= 0 {
return []rune{} return []rune{}
} }
sw := rw.StringWidth(s) sw := rw.StringWidth(s)
if sw > w { if sw > w {
return []rune(rw.Truncate(s, w, dot)) 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 { func strWidth(s string) int {
@ -64,3 +97,118 @@ func strWidth(s string) int {
func charWidth(ch rune) int { func charWidth(ch rune) int {
return rw.RuneWidth(ch) 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 ( import (
"testing" "testing"
"github.com/davecgh/go-spew/spew" "github.com/stretchr/testify/assert"
) )
func TestStr2Rune(t *testing.T) { func TestStr2Rune(t *testing.T) {
s := "你好,世界." s := "你好,世界."
rs := str2runes(s) rs := str2runes(s)
if len(rs) != 6 { if len(rs) != 6 {
t.Error() t.Error(t)
} }
} }
func TestWidth(t *testing.T) { func TestWidth(t *testing.T) {
s0 := "つのだ☆HIRO" s0 := "つのだ☆HIRO"
s1 := "11111111111" s1 := "11111111111"
spew.Dump(s0)
spew.Dump(s1)
// above not align for setting East Asian Ambiguous to wide!! // above not align for setting East Asian Ambiguous to wide!!
if strWidth(s0) != strWidth(s1) { if strWidth(s0) != strWidth(s1) {
@ -56,3 +54,17 @@ func TestTrim(t *testing.T) {
t.Error("avoid trim failed") 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. // NewLineChart returns a new LineChart with current theme.
func NewLineChart() *LineChart { func NewLineChart() *LineChart {
lc := &LineChart{Block: *NewBlock()} lc := &LineChart{Block: *NewBlock()}
lc.AxesColor = theme.LineChartAxes lc.AxesColor = ThemeAttr("linechart.axes.fg")
lc.LineColor = theme.LineChartLine lc.LineColor = ThemeAttr("linechart.line.fg")
lc.Mode = "braille" lc.Mode = "braille"
lc.DotStyle = '•' lc.DotStyle = '•'
lc.axisXLebelGap = 2 lc.axisXLebelGap = 2
@ -87,8 +87,8 @@ func NewLineChart() *LineChart {
// one cell contains two data points // one cell contains two data points
// so the capicity is 2x as dot-mode // so the capicity is 2x as dot-mode
func (lc *LineChart) renderBraille() []Point { func (lc *LineChart) renderBraille() Buffer {
ps := []Point{} buf := NewBuffer()
// return: b -> which cell should the point be in // return: b -> which cell should the point be in
// m -> in the cell, divided into 4 equal height levels, which subcell? // 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]) b1, m1 := getPos(lc.Data[2*i+1])
if b0 == b1 { if b0 == b1 {
p := Point{} c := Cell{
p.Ch = braillePatterns[[2]int{m0, m1}] Ch: braillePatterns[[2]int{m0, m1}],
p.Bg = lc.BgColor Bg: lc.Bg,
p.Fg = lc.LineColor Fg: lc.LineColor,
p.Y = lc.innerY + lc.innerHeight - 3 - b0 }
p.X = lc.innerX + lc.labelYSpace + 1 + i y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
ps = append(ps, p) x := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
buf.Set(x, y, c)
} else { } else {
p0 := newPointWithAttrs(lSingleBraille[m0], c0 := Cell{Ch: lSingleBraille[m0],
lc.innerX+lc.labelYSpace+1+i, Fg: lc.LineColor,
lc.innerY+lc.innerHeight-3-b0, Bg: lc.Bg}
lc.LineColor, x0 := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
lc.BgColor) y0 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
p1 := newPointWithAttrs(rSingleBraille[m1], buf.Set(x0, y0, c0)
lc.innerX+lc.labelYSpace+1+i,
lc.innerY+lc.innerHeight-3-b1, c1 := Cell{Ch: rSingleBraille[m1],
lc.LineColor, Fg: lc.LineColor,
lc.BgColor) Bg: lc.Bg}
ps = append(ps, p0, p1) 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 { func (lc *LineChart) renderDot() Buffer {
ps := []Point{} buf := NewBuffer()
for i := 0; i < len(lc.Data) && i < lc.axisXWidth; i++ { for i := 0; i < len(lc.Data) && i < lc.axisXWidth; i++ {
p := Point{} c := Cell{
p.Ch = lc.DotStyle Ch: lc.DotStyle,
p.Fg = lc.LineColor Fg: lc.LineColor,
p.Bg = lc.BgColor Bg: lc.Bg,
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) x := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
ps = append(ps, p) 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() { func (lc *LineChart) calcLabelX() {
@ -220,9 +224,9 @@ func (lc *LineChart) calcLayout() {
lc.maxY = lc.Data[0] lc.maxY = lc.Data[0]
// valid visible range // valid visible range
vrange := lc.innerWidth vrange := lc.innerArea.Dx()
if lc.Mode == "braille" { if lc.Mode == "braille" {
vrange = 2 * lc.innerWidth vrange = 2 * lc.innerArea.Dx()
} }
if vrange > len(lc.Data) { if vrange > len(lc.Data) {
vrange = len(lc.Data) vrange = len(lc.Data)
@ -247,40 +251,30 @@ func (lc *LineChart) calcLayout() {
lc.topValue = lc.maxY + 0.2*span lc.topValue = lc.maxY + 0.2*span
} }
lc.axisYHeight = lc.innerHeight - 2 lc.axisYHeight = lc.innerArea.Dy() - 2
lc.calcLabelY() lc.calcLabelY()
lc.axisXWidth = lc.innerWidth - 1 - lc.labelYSpace lc.axisXWidth = lc.innerArea.Dx() - 1 - lc.labelYSpace
lc.calcLabelX() lc.calcLabelX()
lc.drawingX = lc.innerX + 1 + lc.labelYSpace lc.drawingX = lc.innerArea.Min.X + 1 + lc.labelYSpace
lc.drawingY = lc.innerY lc.drawingY = lc.innerArea.Min.Y
} }
func (lc *LineChart) plotAxes() []Point { func (lc *LineChart) plotAxes() Buffer {
origY := lc.innerY + lc.innerHeight - 2 buf := NewBuffer()
origX := lc.innerX + lc.labelYSpace
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++ { for x := origX + 1; x < origX+lc.axisXWidth; x++ {
p := Point{} buf.Set(x, origY, Cell{Ch: HDASH, Fg: lc.AxesColor, Bg: lc.Bg})
p.X = x
p.Y = origY
p.Bg = lc.BgColor
p.Fg = lc.AxesColor
p.Ch = HDASH
ps = append(ps, p)
} }
for dy := 1; dy <= lc.axisYHeight; dy++ { for dy := 1; dy <= lc.axisYHeight; dy++ {
p := Point{} buf.Set(origX, origY-dy, Cell{Ch: VDASH, Fg: lc.AxesColor, Bg: lc.Bg})
p.X = origX
p.Y = origY - dy
p.Bg = lc.BgColor
p.Fg = lc.AxesColor
p.Ch = VDASH
ps = append(ps, p)
} }
// x label // x label
@ -290,13 +284,14 @@ func (lc *LineChart) plotAxes() []Point {
break break
} }
for j, r := range rs { for j, r := range rs {
p := Point{} c := Cell{
p.Ch = r Ch: r,
p.Fg = lc.AxesColor Fg: lc.AxesColor,
p.Bg = lc.BgColor Bg: lc.Bg,
p.X = origX + oft + j }
p.Y = lc.innerY + lc.innerHeight - 1 x := origX + oft + j
ps = append(ps, p) y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 1
buf.Set(x, y, c)
} }
oft += len(rs) + lc.axisXLebelGap oft += len(rs) + lc.axisXLebelGap
} }
@ -304,33 +299,31 @@ func (lc *LineChart) plotAxes() []Point {
// y labels // y labels
for i, rs := range lc.labelY { for i, rs := range lc.labelY {
for j, r := range rs { for j, r := range rs {
p := Point{} buf.Set(
p.Ch = r lc.innerArea.Min.X+j,
p.Fg = lc.AxesColor origY-i*(lc.axisYLebelGap+1),
p.Bg = lc.BgColor Cell{Ch: r, Fg: lc.AxesColor, Bg: lc.Bg})
p.X = lc.innerX + j
p.Y = origY - i*(lc.axisYLebelGap+1)
ps = append(ps, p)
} }
} }
return ps return buf
} }
// Buffer implements Bufferer interface. // Buffer implements Bufferer interface.
func (lc *LineChart) Buffer() []Point { func (lc *LineChart) Buffer() Buffer {
ps := lc.Block.Buffer() buf := lc.Block.Buffer()
if lc.Data == nil || len(lc.Data) == 0 { if lc.Data == nil || len(lc.Data) == 0 {
return ps return buf
} }
lc.calcLayout() lc.calcLayout()
ps = append(ps, lc.plotAxes()...) buf.Merge(lc.plotAxes())
if lc.Mode == "dot" { if lc.Mode == "dot" {
ps = append(ps, lc.renderDot()...) buf.Merge(lc.renderDot())
} else { } 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 { func NewList() *List {
l := &List{Block: *NewBlock()} l := &List{Block: *NewBlock()}
l.Overflow = "hidden" l.Overflow = "hidden"
l.ItemFgColor = theme.ListItemFg l.ItemFgColor = ThemeAttr("list.item.fg")
l.ItemBgColor = theme.ListItemBg l.ItemBgColor = ThemeAttr("list.item.bg")
return l return l
} }
// Buffer implements Bufferer interface. // Buffer implements Bufferer interface.
func (l *List) Buffer() []Point { func (l *List) Buffer() Buffer {
ps := l.Block.Buffer() buf := l.Block.Buffer()
switch l.Overflow { switch l.Overflow {
case "wrap": 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 i, j, k := 0, 0, 0
for i < l.innerHeight && k < len(rs) { for i < l.innerArea.Dy() && k < len(cs) {
w := charWidth(rs[k]) w := cs[k].Width()
if rs[k] == '\n' || j+w > l.innerWidth { if cs[k].Ch == '\n' || j+w > l.innerArea.Dx() {
i++ i++
j = 0 j = 0
if rs[k] == '\n' { if cs[k].Ch == '\n' {
k++ k++
} }
continue continue
} }
pi := Point{} buf.Set(l.innerArea.Min.X+j, l.innerArea.Min.Y+i, cs[k])
pi.X = l.innerX + j
pi.Y = l.innerY + i
pi.Ch = rs[k]
pi.Bg = l.ItemBgColor
pi.Fg = l.ItemFgColor
ps = append(ps, pi)
k++ k++
j++ j++
} }
case "hidden": case "hidden":
trimItems := l.Items trimItems := l.Items
if len(trimItems) > l.innerHeight { if len(trimItems) > l.innerArea.Dy() {
trimItems = trimItems[:l.innerHeight] trimItems = trimItems[:l.innerArea.Dy()]
} }
for i, v := range trimItems { for i, v := range trimItems {
rs := trimStr2Runes(v, l.innerWidth) cs := DTrimTxCls(DefaultTxBuilder.Build(v, l.ItemFgColor, l.ItemBgColor), l.innerArea.Dx())
j := 0 j := 0
for _, vv := range rs { for _, vv := range cs {
w := charWidth(vv) w := vv.Width()
p := Point{} buf.Set(l.innerArea.Min.X+j, l.innerArea.Min.Y+i, vv)
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)
j += w 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. // NewBarChart returns a new *BarChart with current theme.
func NewMBarChart() *MBarChart { func NewMBarChart() *MBarChart {
bc := &MBarChart{Block: *NewBlock()} bc := &MBarChart{Block: *NewBlock()}
bc.BarColor[0] = theme.MBarChartBar bc.BarColor[0] = ThemeAttr("mbarchart.bar.bg")
bc.NumColor[0] = theme.MBarChartNum bc.NumColor[0] = ThemeAttr("mbarchart.num.fg")
bc.TextColor = theme.MBarChartText bc.TextColor = ThemeAttr("mbarchart.text.fg")
bc.BarGap = 1 bc.BarGap = 1
bc.BarWidth = 3 bc.BarWidth = 3
return bc return bc
} }
func (bc *MBarChart) layout() { 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) bc.labels = make([][]rune, bc.numBar)
DataLen := 0 DataLen := 0
LabelLen := len(bc.DataLabels) LabelLen := len(bc.DataLabels)
@ -129,9 +129,9 @@ func (bc *MBarChart) layout() {
if bc.ShowScale { if bc.ShowScale {
s := fmt.Sprintf("%d", bc.max) s := fmt.Sprintf("%d", bc.max)
bc.maxScale = trimStr2Runes(s, len(s)) 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 { } 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. // Buffer implements Bufferer interface.
func (bc *MBarChart) Buffer() []Point { func (bc *MBarChart) Buffer() Buffer {
ps := bc.Block.Buffer() buf := bc.Block.Buffer()
bc.layout() bc.layout()
var oftX int var oftX int
@ -157,15 +157,17 @@ func (bc *MBarChart) Buffer() []Point {
// plot bars // plot bars
for j := 0; j < bc.BarWidth; j++ { for j := 0; j < bc.BarWidth; j++ {
for k := 0; k < h; k++ { for k := 0; k < h; k++ {
p := Point{} c := Cell{
p.Ch = ' ' Ch: ' ',
p.Bg = bc.BarColor[i1] Bg: bc.BarColor[i1],
if bc.BarColor[i1] == ColorDefault { // when color is default, space char treated as transparent!
p.Bg |= AttrReverse
} }
p.X = bc.innerX + i*(bc.BarWidth+bc.BarGap) + j if bc.BarColor[i1] == ColorDefault { // when color is default, space char treated as transparent!
p.Y = bc.innerY + bc.innerHeight - 2 - k - ph c.Bg |= AttrReverse
ps = append(ps, p) }
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 ph += h
@ -173,13 +175,14 @@ func (bc *MBarChart) Buffer() []Point {
// plot text // plot text
for j, k := 0, 0; j < len(bc.labels[i]); j++ { for j, k := 0, 0; j < len(bc.labels[i]); j++ {
w := charWidth(bc.labels[i][j]) w := charWidth(bc.labels[i][j])
p := Point{} c := Cell{
p.Ch = bc.labels[i][j] Ch: bc.labels[i][j],
p.Bg = bc.BgColor Bg: bc.Bg,
p.Fg = bc.TextColor Fg: bc.TextColor,
p.Y = bc.innerY + bc.innerHeight - 1 }
p.X = bc.innerX + oftX + ((bc.BarWidth - len(bc.labels[i])) / 2) + k y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 1
ps = append(ps, p) x := bc.innerArea.Max.X + oftX + ((bc.BarWidth - len(bc.labels[i])) / 2) + k
buf.Set(x, y, c)
k += w k += w
} }
// plot num // plot num
@ -187,19 +190,20 @@ func (bc *MBarChart) Buffer() []Point {
for i1 := 0; i1 < bc.numStack; i1++ { for i1 := 0; i1 < bc.numStack; i1++ {
h := int(float64(bc.Data[i1][i]) / bc.scale) h := int(float64(bc.Data[i1][i]) / bc.scale)
for j := 0; j < len(bc.dataNum[i1][i]) && h > 0; j++ { for j := 0; j < len(bc.dataNum[i1][i]) && h > 0; j++ {
p := Point{} c := Cell{
p.Ch = bc.dataNum[i1][i][j] Ch: bc.dataNum[i1][i][j],
p.Fg = bc.NumColor[i1] Fg: bc.NumColor[i1],
p.Bg = bc.BarColor[i1] Bg: bc.BarColor[i1],
}
if bc.BarColor[i1] == ColorDefault { // the same as above if bc.BarColor[i1] == ColorDefault { // the same as above
p.Bg |= AttrReverse c.Bg |= AttrReverse
} }
if h == 0 { 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 x := bc.innerArea.Min.X + oftX + (bc.BarWidth-len(bc.dataNum[i1][i]))/2 + j
p.Y = bc.innerY + bc.innerHeight - 2 - ph y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2 - ph
ps = append(ps, p) buf.Set(x, y, c)
} }
ph += h ph += h
} }
@ -208,26 +212,31 @@ func (bc *MBarChart) Buffer() []Point {
if bc.ShowScale { if bc.ShowScale {
//Currently bar graph only supprts data range from 0 to MAX //Currently bar graph only supprts data range from 0 to MAX
//Plot 0 //Plot 0
p := Point{} c := Cell{
p.Ch = '0' Ch: '0',
p.Bg = bc.BgColor Bg: bc.Bg,
p.Fg = bc.TextColor Fg: bc.TextColor,
p.Y = bc.innerY + bc.innerHeight - 2 }
p.X = bc.X
ps = append(ps, p) y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2
x := bc.X
buf.Set(x, y, c)
//Plot the maximum sacle value //Plot the maximum sacle value
for i := 0; i < len(bc.maxScale); i++ { for i := 0; i < len(bc.maxScale); i++ {
p := Point{} c := Cell{
p.Ch = bc.maxScale[i] Ch: bc.maxScale[i],
p.Bg = bc.BgColor Bg: bc.Bg,
p.Fg = bc.TextColor Fg: bc.TextColor,
p.Y = bc.innerY }
p.X = bc.X + i
ps = append(ps, p) 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) { func TestPar_NoBorderBackground(t *testing.T) {
par := NewPar("a") par := NewPar("a")
par.HasBorder = false par.Border = false
par.BgColor = ColorBlue par.Bg = ColorBlue
par.TextBgColor = ColorBlue par.TextBgColor = ColorBlue
par.Width = 2 par.Width = 2
par.Height = 2 par.Height = 2
pts := par.Buffer() pts := par.Buffer()
for _, p := range pts { for _, p := range pts.CellMap {
t.Log(p) t.Log(p)
if p.Bg != par.BgColor { if p.Bg != par.Bg {
t.Errorf("expected color to be %v but got %v", par.BgColor, p.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 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. // Bufferer should be implemented by all renderable components.
type Bufferer interface { type Bufferer interface {
Buffer() []Point Buffer() Buffer
} }
// Init initializes termui library. This function should be called before any others. // Init initializes termui library. This function should be called before any others.
// After initialization, the library must be finalized by 'Close' function. // After initialization, the library must be finalized by 'Close' function.
func Init() error { 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 = NewGrid()
Body.X = 0 Body.X = 0
Body.Y = 0 Body.Y = 0
Body.BgColor = theme.BodyBg Body.BgColor = ThemeAttr("bg")
defer func() { Body.Width = TermWidth()
w, _ := tm.Size()
Body.Width = w DefaultEvtStream.Init()
evtListen() DefaultEvtStream.Merge("termbox", NewSysEvtCh())
}() DefaultEvtStream.Merge("timer", NewTimerCh(time.Second))
return tm.Init() 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, // Close finalizes termui library,
@ -48,13 +76,24 @@ func TermHeight() int {
// Render renders all Bufferer in the given order from left to right, // Render renders all Bufferer in the given order from left to right,
// right could overlap on left ones. // right could overlap on left ones.
func Render(rs ...Bufferer) { func render(bs ...Bufferer) {
tm.Clear(tm.ColorDefault, toTmAttr(theme.BodyBg)) // set tm bg
for _, r := range rs { tm.Clear(tm.ColorDefault, toTmAttr(ThemeAttr("bg")))
buf := r.Buffer() for _, b := range bs {
for _, v := range buf { buf := b.Buffer()
tm.SetCell(v.X, v.Y, v.Ch, toTmAttr(v.Fg), toTmAttr(v.Bg)) // 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() 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 { func NewSparkline() Sparkline {
return Sparkline{ return Sparkline{
Height: 1, Height: 1,
TitleColor: theme.SparklineTitle, TitleColor: ThemeAttr("sparkline.title.fg"),
LineColor: theme.SparklineLine} LineColor: ThemeAttr("sparkline.line.fg")}
} }
// NewSparklines return a new *Spaklines with given Sparkline(s), you can always add a new Sparkline later. // 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.Lines[i].displayHeight = v.Height + 1
} }
} }
sl.displayWidth = sl.innerWidth sl.displayWidth = sl.innerArea.Dx()
// get how many lines gotta display // get how many lines gotta display
h := 0 h := 0
sl.displayLines = 0 sl.displayLines = 0
for _, v := range sl.Lines { for _, v := range sl.Lines {
if h+v.displayHeight <= sl.innerHeight { if h+v.displayHeight <= sl.innerArea.Dy() {
sl.displayLines++ sl.displayLines++
} else { } else {
break break
@ -96,8 +96,8 @@ func (sl *Sparklines) update() {
} }
// Buffer implements Bufferer interface. // Buffer implements Bufferer interface.
func (sl *Sparklines) Buffer() []Point { func (sl *Sparklines) Buffer() Buffer {
ps := sl.Block.Buffer() buf := sl.Block.Buffer()
sl.update() sl.update()
oftY := 0 oftY := 0
@ -105,22 +105,23 @@ func (sl *Sparklines) Buffer() []Point {
l := sl.Lines[i] l := sl.Lines[i]
data := l.Data data := l.Data
if len(data) > sl.innerWidth { if len(data) > sl.innerArea.Dx() {
data = data[len(data)-sl.innerWidth:] data = data[len(data)-sl.innerArea.Dx():]
} }
if l.Title != "" { if l.Title != "" {
rs := trimStr2Runes(l.Title, sl.innerWidth) rs := trimStr2Runes(l.Title, sl.innerArea.Dx())
oftX := 0 oftX := 0
for _, v := range rs { for _, v := range rs {
w := charWidth(v) w := charWidth(v)
p := Point{} c := Cell{
p.Ch = v Ch: v,
p.Fg = l.TitleColor Fg: l.TitleColor,
p.Bg = sl.BgColor Bg: sl.Bg,
p.X = sl.innerX + oftX }
p.Y = sl.innerY + oftY x := sl.innerArea.Min.X + oftX
ps = append(ps, p) y := sl.innerArea.Min.Y + oftY
buf.Set(x, y, c)
oftX += w oftX += w
} }
} }
@ -130,27 +131,30 @@ func (sl *Sparklines) Buffer() []Point {
barCnt := h / 8 barCnt := h / 8
barMod := h % 8 barMod := h % 8
for jj := 0; jj < barCnt; jj++ { for jj := 0; jj < barCnt; jj++ {
p := Point{} c := Cell{
p.X = sl.innerX + j Ch: ' ', // => sparks[7]
p.Y = sl.innerY + oftY + l.Height - jj Bg: l.LineColor,
p.Ch = ' ' // => sparks[7] }
p.Bg = l.LineColor x := sl.innerArea.Min.X + j
y := sl.innerArea.Min.Y + oftY + l.Height - jj
//p.Bg = sl.BgColor //p.Bg = sl.BgColor
ps = append(ps, p) buf.Set(x, y, c)
} }
if barMod != 0 { if barMod != 0 {
p := Point{} c := Cell{
p.X = sl.innerX + j Ch: sparks[barMod-1],
p.Y = sl.innerY + oftY + l.Height - barCnt Fg: l.LineColor,
p.Ch = sparks[barMod-1] Bg: sl.Bg,
p.Fg = l.LineColor }
p.Bg = sl.BgColor x := sl.innerArea.Min.X + j
ps = append(ps, p) y := sl.innerArea.Min.Y + oftY + l.Height - barCnt
buf.Set(x, y, c)
} }
} }
oftY += l.displayHeight 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 package termui
import "strings"
/*
// A ColorScheme represents the current look-and-feel of the dashboard. // A ColorScheme represents the current look-and-feel of the dashboard.
type ColorScheme struct { type ColorScheme struct {
BodyBg Attribute BodyBg Attribute
@ -84,3 +87,54 @@ func UseTheme(th string) {
theme = themeDefault 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)
}