diff --git a/.gitignore b/.gitignore
index 10b6ae9..eb1369f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,4 +22,4 @@ _testmain.go
*.exe
*.test
*.prof
-/.DS_Store
+.DS_Store
diff --git a/README.md b/README.md
index 01562e9..42513e4 100644
--- a/README.md
+++ b/README.md
@@ -117,6 +117,11 @@ The `helloworld` color scheme drops in some colors!
+#### Colored List
+[demo code](https://github.com/gizak/termui/blob/master/example/coloredList.go)
+
+TODO: Image (let's wait until the implementation is finished).
+
#### Gauge
[demo code](https://github.com/gizak/termui/blob/master/example/gauge.go)
diff --git a/example/barchart.go b/_example/barchart.go
similarity index 83%
rename from example/barchart.go
rename to _example/barchart.go
index 83947f5..9f7784e 100644
--- a/example/barchart.go
+++ b/_example/barchart.go
@@ -9,18 +9,15 @@ package main
import "github.com/gizak/termui"
func main() {
- err := termui.Init()
- if err != nil {
+ if termui.Init() != nil {
panic(err)
}
defer termui.Close()
- termui.UseTheme("helloworld")
-
bc := termui.NewBarChart()
data := []int{3, 2, 5, 3, 9, 5, 3, 2, 5, 8, 3, 2, 4, 5, 3, 2, 5, 7, 5, 3, 2, 6, 7, 4, 6, 3, 6, 7, 8, 3, 6, 4, 5, 3, 2, 4, 6, 4, 8, 5, 9, 4, 3, 6, 5, 3, 6}
bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"}
- bc.Border.Label = "Bar Chart"
+ bc.BorderLabel = "Bar Chart"
bc.Data = data
bc.Width = 26
bc.Height = 10
@@ -31,5 +28,9 @@ func main() {
termui.Render(bc)
- <-termui.EventCh()
+ termui.Handle("/sys/kbd/q", func(termui.Event) {
+ termui.StopLoop()
+ })
+ termui.Loop()
+
}
diff --git a/example/barchart.png b/_example/barchart.png
similarity index 100%
rename from example/barchart.png
rename to _example/barchart.png
diff --git a/example/dashboard.gif b/_example/dashboard.gif
similarity index 100%
rename from example/dashboard.gif
rename to _example/dashboard.gif
diff --git a/example/dashboard.go b/_example/dashboard.go
similarity index 80%
rename from example/dashboard.go
rename to _example/dashboard.go
index c14bb44..ecf8921 100644
--- a/example/dashboard.go
+++ b/_example/dashboard.go
@@ -9,11 +9,8 @@ package main
import ui "github.com/gizak/termui"
import "math"
-import "time"
-
func main() {
- err := ui.Init()
- if err != nil {
+ if err := ui.Init(); err != nil {
panic(err)
}
defer ui.Close()
@@ -22,14 +19,22 @@ func main() {
p.Height = 3
p.Width = 50
p.TextFgColor = ui.ColorWhite
- p.Border.Label = "Text Box"
- p.Border.FgColor = ui.ColorCyan
+ p.BorderLabel = "Text Box"
+ p.BorderFg = ui.ColorCyan
+ p.Handle("/timer/1s", func(e ui.Event) {
+ cnt := e.Data.(ui.EvtTimer)
+ if cnt.Count%2 == 0 {
+ p.TextFgColor = ui.ColorRed
+ } else {
+ p.TextFgColor = ui.ColorWhite
+ }
+ })
strs := []string{"[0] gizak/termui", "[1] editbox.go", "[2] iterrupt.go", "[3] keyboard.go", "[4] output.go", "[5] random_out.go", "[6] dashboard.go", "[7] nsf/termbox-go"}
list := ui.NewList()
list.Items = strs
list.ItemFgColor = ui.ColorYellow
- list.Border.Label = "List"
+ list.BorderLabel = "List"
list.Height = 7
list.Width = 25
list.Y = 4
@@ -39,10 +44,10 @@ func main() {
g.Width = 50
g.Height = 3
g.Y = 11
- g.Border.Label = "Gauge"
+ g.BorderLabel = "Gauge"
g.BarColor = ui.ColorRed
- g.Border.FgColor = ui.ColorWhite
- g.Border.LabelFgColor = ui.ColorCyan
+ g.BorderFg = ui.ColorWhite
+ g.BorderLabelFg = ui.ColorCyan
spark := ui.Sparkline{}
spark.Height = 1
@@ -62,7 +67,7 @@ func main() {
sp := ui.NewSparklines(spark, spark1)
sp.Width = 25
sp.Height = 7
- sp.Border.Label = "Sparkline"
+ sp.BorderLabel = "Sparkline"
sp.Y = 4
sp.X = 25
@@ -76,7 +81,7 @@ func main() {
})()
lc := ui.NewLineChart()
- lc.Border.Label = "dot-mode Line Chart"
+ lc.BorderLabel = "dot-mode Line Chart"
lc.Data = sinps
lc.Width = 50
lc.Height = 11
@@ -89,7 +94,7 @@ func main() {
bc := ui.NewBarChart()
bcdata := []int{3, 2, 5, 3, 9, 5, 3, 2, 5, 8, 3, 2, 4, 5, 3, 2, 5, 7, 5, 3, 2, 6, 7, 4, 6, 3, 6, 7, 8, 3, 6, 4, 5, 3, 2, 4, 6, 4, 8, 5, 9, 4, 3, 6, 5, 3, 6}
bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"}
- bc.Border.Label = "Bar Chart"
+ bc.BorderLabel = "Bar Chart"
bc.Width = 26
bc.Height = 10
bc.X = 51
@@ -99,7 +104,7 @@ func main() {
bc.NumColor = ui.ColorBlack
lc1 := ui.NewLineChart()
- lc1.Border.Label = "braille-mode Line Chart"
+ lc1.BorderLabel = "braille-mode Line Chart"
lc1.Data = sinps
lc1.Width = 26
lc1.Height = 11
@@ -109,7 +114,7 @@ func main() {
lc1.LineColor = ui.ColorYellow | ui.AttrBold
p1 := ui.NewPar("Hey!\nI am a borderless block!")
- p1.HasBorder = false
+ p1.Border = false
p1.Width = 26
p1.Height = 2
p1.TextFgColor = ui.ColorMagenta
@@ -126,23 +131,12 @@ func main() {
bc.Data = bcdata[t/2%10:]
ui.Render(p, list, g, sp, lc, bc, lc1, p1)
}
-
- evt := ui.EventCh()
-
- i := 0
- for {
- select {
- case e := <-evt:
- if e.Type == ui.EventKey && e.Ch == 'q' {
- return
- }
- default:
- draw(i)
- i++
- if i == 102 {
- return
- }
- time.Sleep(time.Second / 2)
- }
- }
+ ui.Handle("/sys/kbd/q", func(ui.Event) {
+ ui.StopLoop()
+ })
+ ui.Handle("/timer/1s", func(e ui.Event) {
+ t := e.Data.(ui.EvtTimer)
+ draw(int(t.Count))
+ })
+ ui.Loop()
}
diff --git a/example/gauge.go b/_example/gauge.go
similarity index 69%
rename from example/gauge.go
rename to _example/gauge.go
index 91bc47b..2b3d3c4 100644
--- a/example/gauge.go
+++ b/_example/gauge.go
@@ -15,16 +15,23 @@ func main() {
}
defer termui.Close()
- termui.UseTheme("helloworld")
+ //termui.UseTheme("helloworld")
g0 := termui.NewGauge()
g0.Percent = 40
g0.Width = 50
g0.Height = 3
- g0.Border.Label = "Slim Gauge"
+ g0.BorderLabel = "Slim Gauge"
g0.BarColor = termui.ColorRed
- g0.Border.FgColor = termui.ColorWhite
- g0.Border.LabelFgColor = termui.ColorCyan
+ g0.BorderFg = termui.ColorWhite
+ g0.BorderLabelFg = termui.ColorCyan
+
+ gg := termui.NewBlock()
+ gg.Width = 50
+ gg.Height = 5
+ gg.Y = 12
+ gg.BorderLabel = "TEST"
+ gg.Align()
g2 := termui.NewGauge()
g2.Percent = 60
@@ -32,27 +39,27 @@ func main() {
g2.Height = 3
g2.PercentColor = termui.ColorBlue
g2.Y = 3
- g2.Border.Label = "Slim Gauge"
+ g2.BorderLabel = "Slim Gauge"
g2.BarColor = termui.ColorYellow
- g2.Border.FgColor = termui.ColorWhite
+ g2.BorderFg = termui.ColorWhite
g1 := termui.NewGauge()
g1.Percent = 30
g1.Width = 50
g1.Height = 5
g1.Y = 6
- g1.Border.Label = "Big Gauge"
+ g1.BorderLabel = "Big Gauge"
g1.PercentColor = termui.ColorYellow
g1.BarColor = termui.ColorGreen
- g1.Border.FgColor = termui.ColorWhite
- g1.Border.LabelFgColor = termui.ColorMagenta
+ g1.BorderFg = termui.ColorWhite
+ g1.BorderLabelFg = termui.ColorMagenta
g3 := termui.NewGauge()
g3.Percent = 50
g3.Width = 50
g3.Height = 3
g3.Y = 11
- g3.Border.Label = "Gauge with custom label"
+ g3.BorderLabel = "Gauge with custom label"
g3.Label = "{{percent}}% (100MBs free)"
g3.LabelAlign = termui.AlignRight
@@ -69,5 +76,9 @@ func main() {
termui.Render(g0, g1, g2, g3, g4)
- <-termui.EventCh()
+ termui.Handle("/sys/kbd/q", func(termui.Event) {
+ termui.StopLoop()
+ })
+
+ termui.Loop()
}
diff --git a/example/gauge.png b/_example/gauge.png
similarity index 100%
rename from example/gauge.png
rename to _example/gauge.png
diff --git a/example/grid.gif b/_example/grid.gif
similarity index 100%
rename from example/grid.gif
rename to _example/grid.gif
diff --git a/example/grid.go b/_example/grid.go
similarity index 66%
rename from example/grid.go
rename to _example/grid.go
index 4912141..0c97ab9 100644
--- a/example/grid.go
+++ b/_example/grid.go
@@ -7,12 +7,11 @@
package main
import ui "github.com/gizak/termui"
+
import "math"
-import "time"
func main() {
- err := ui.Init()
- if err != nil {
+ if err := ui.Init(); err != nil {
panic(err)
}
defer ui.Close()
@@ -33,8 +32,6 @@ func main() {
return ps
})()
- ui.UseTheme("helloworld")
-
spark := ui.Sparkline{}
spark.Height = 8
spdata := sinpsint
@@ -44,10 +41,10 @@ func main() {
sp := ui.NewSparklines(spark)
sp.Height = 11
- sp.Border.Label = "Sparkline"
+ sp.BorderLabel = "Sparkline"
lc := ui.NewLineChart()
- lc.Border.Label = "braille-mode Line Chart"
+ lc.BorderLabel = "braille-mode Line Chart"
lc.Data = sinps
lc.Height = 11
lc.AxesColor = ui.ColorWhite
@@ -56,15 +53,16 @@ func main() {
gs := make([]*ui.Gauge, 3)
for i := range gs {
gs[i] = ui.NewGauge()
+ //gs[i].LabelAlign = ui.AlignCenter
gs[i].Height = 2
- gs[i].HasBorder = false
+ gs[i].Border = false
gs[i].Percent = i * 10
gs[i].PaddingBottom = 1
gs[i].BarColor = ui.ColorRed
}
ls := ui.NewList()
- ls.HasBorder = false
+ ls.Border = false
ls.Items = []string{
"[1] Downloading File 1",
"", // == \newline
@@ -76,7 +74,7 @@ func main() {
par := ui.NewPar("<> This row has 3 columns\n<- Widgets can be stacked up like left side\n<- Stacked widgets are treated as a single widget")
par.Height = 5
- par.Border.Label = "Demonstration"
+ par.BorderLabel = "Demonstration"
// build layout
ui.Body.AddRows(
@@ -91,44 +89,33 @@ func main() {
// calculate layout
ui.Body.Align()
- done := make(chan bool)
- redraw := make(chan bool)
-
- update := func() {
- for i := 0; i < 103; i++ {
- for _, g := range gs {
- g.Percent = (g.Percent + 3) % 100
- }
-
- sp.Lines[0].Data = spdata[:100+i]
- lc.Data = sinps[2*i:]
-
- time.Sleep(time.Second / 2)
- redraw <- true
- }
- done <- true
- }
-
- evt := ui.EventCh()
-
ui.Render(ui.Body)
- go update()
- for {
- select {
- case e := <-evt:
- if e.Type == ui.EventKey && e.Ch == 'q' {
- return
- }
- if e.Type == ui.EventResize {
- ui.Body.Width = ui.TermWidth()
- ui.Body.Align()
- go func() { redraw <- true }()
- }
- case <-done:
+ ui.Handle("/sys/kbd/q", func(ui.Event) {
+ ui.StopLoop()
+ })
+ ui.Handle("/timer/1s", func(e ui.Event) {
+ t := e.Data.(ui.EvtTimer)
+ i := t.Count
+ if i > 103 {
+ ui.StopLoop()
return
- case <-redraw:
- ui.Render(ui.Body)
}
- }
+
+ for _, g := range gs {
+ g.Percent = (g.Percent + 3) % 100
+ }
+
+ sp.Lines[0].Data = spdata[:100+i]
+ lc.Data = sinps[2*i:]
+ ui.Render(ui.Body)
+ })
+
+ ui.Handle("/sys/wnd/resize", func(e ui.Event) {
+ ui.Body.Width = ui.TermWidth()
+ ui.Body.Align()
+ ui.Render(ui.Body)
+ })
+
+ ui.Loop()
}
diff --git a/example/linechart.go b/_example/linechart.go
similarity index 82%
rename from example/linechart.go
rename to _example/linechart.go
index 1db5434..1749e7b 100644
--- a/example/linechart.go
+++ b/_example/linechart.go
@@ -19,7 +19,7 @@ func main() {
}
defer termui.Close()
- termui.UseTheme("helloworld")
+ //termui.UseTheme("helloworld")
sinps := (func() []float64 {
n := 220
@@ -31,7 +31,7 @@ func main() {
})()
lc0 := termui.NewLineChart()
- lc0.Border.Label = "braille-mode Line Chart"
+ lc0.BorderLabel = "braille-mode Line Chart"
lc0.Data = sinps
lc0.Width = 50
lc0.Height = 12
@@ -41,7 +41,7 @@ func main() {
lc0.LineColor = termui.ColorGreen | termui.AttrBold
lc1 := termui.NewLineChart()
- lc1.Border.Label = "dot-mode Line Chart"
+ lc1.BorderLabel = "dot-mode Line Chart"
lc1.Mode = "dot"
lc1.Data = sinps
lc1.Width = 26
@@ -52,7 +52,7 @@ func main() {
lc1.LineColor = termui.ColorYellow | termui.AttrBold
lc2 := termui.NewLineChart()
- lc2.Border.Label = "dot-mode Line Chart"
+ lc2.BorderLabel = "dot-mode Line Chart"
lc2.Mode = "dot"
lc2.Data = sinps[4:]
lc2.Width = 77
@@ -63,6 +63,9 @@ func main() {
lc2.LineColor = termui.ColorCyan | termui.AttrBold
termui.Render(lc0, lc1, lc2)
+ termui.Handle("/sys/kbd/q", func(termui.Event) {
+ termui.StopLoop()
+ })
+ termui.Loop()
- <-termui.EventCh()
}
diff --git a/example/linechart.png b/_example/linechart.png
similarity index 100%
rename from example/linechart.png
rename to _example/linechart.png
diff --git a/example/list.go b/_example/list.go
similarity index 69%
rename from example/list.go
rename to _example/list.go
index d33a361..e1914c6 100644
--- a/example/list.go
+++ b/_example/list.go
@@ -15,13 +15,13 @@ func main() {
}
defer termui.Close()
- termui.UseTheme("helloworld")
+ //termui.UseTheme("helloworld")
strs := []string{
"[0] github.com/gizak/termui",
- "[1] 你好,世界",
- "[2] こんにちは世界",
- "[3] keyboard.go",
+ "[1] [你好,世界](fg-blue)",
+ "[2] [こんにちは世界](fg-red)",
+ "[3] [color output](fg-white,bg-green)",
"[4] output.go",
"[5] random_out.go",
"[6] dashboard.go",
@@ -30,12 +30,15 @@ func main() {
ls := termui.NewList()
ls.Items = strs
ls.ItemFgColor = termui.ColorYellow
- ls.Border.Label = "List"
+ ls.BorderLabel = "List"
ls.Height = 7
ls.Width = 25
ls.Y = 0
termui.Render(ls)
+ termui.Handle("/sys/kbd/q", func(termui.Event) {
+ termui.StopLoop()
+ })
+ termui.Loop()
- <-termui.EventCh()
}
diff --git a/example/list.png b/_example/list.png
similarity index 100%
rename from example/list.png
rename to _example/list.png
diff --git a/example/mbarchart.go b/_example/mbarchart.go
similarity index 83%
rename from example/mbarchart.go
rename to _example/mbarchart.go
index a32a28e..0fed643 100644
--- a/example/mbarchart.go
+++ b/_example/mbarchart.go
@@ -15,7 +15,7 @@ func main() {
}
defer termui.Close()
- termui.UseTheme("helloworld")
+ //termui.UseTheme("helloworld")
bc := termui.NewMBarChart()
math := []int{90, 85, 90, 80}
@@ -27,10 +27,10 @@ func main() {
bc.Data[2] = science
bc.Data[3] = compsci
studentsName := []string{"Ken", "Rob", "Dennis", "Linus"}
- bc.Border.Label = "Student's Marks X-Axis=Name Y-Axis=Marks[Math,English,Science,ComputerScience] in %"
+ bc.BorderLabel = "Student's Marks X-Axis=Name Y-Axis=Marks[Math,English,Science,ComputerScience] in %"
bc.Width = 100
- bc.Height = 50
- bc.Y = 10
+ bc.Height = 30
+ bc.Y = 0
bc.BarWidth = 10
bc.DataLabels = studentsName
bc.ShowScale = true //Show y_axis scale value (min and max)
@@ -46,5 +46,9 @@ func main() {
termui.Render(bc)
- <-termui.EventCh()
+ termui.Handle("/sys/kbd/q", func(termui.Event) {
+ termui.StopLoop()
+ })
+ termui.Loop()
+
}
diff --git a/example/mbarchart.png b/_example/mbarchart.png
similarity index 100%
rename from example/mbarchart.png
rename to _example/mbarchart.png
diff --git a/example/par.go b/_example/par.go
similarity index 63%
rename from example/par.go
rename to _example/par.go
index ffbc60a..f8539fe 100644
--- a/example/par.go
+++ b/_example/par.go
@@ -15,34 +15,38 @@ func main() {
}
defer termui.Close()
- termui.UseTheme("helloworld")
+ //termui.UseTheme("helloworld")
par0 := termui.NewPar("Borderless Text")
par0.Height = 1
par0.Width = 20
par0.Y = 1
- par0.HasBorder = false
+ par0.Border = false
par1 := termui.NewPar("你好,世界。")
par1.Height = 3
par1.Width = 17
par1.X = 20
- par1.Border.Label = "标签"
+ par1.BorderLabel = "标签"
- par2 := termui.NewPar("Simple text\nwith label. It can be multilined with \\n or break automatically")
+ par2 := termui.NewPar("Simple colored text\nwith label. It [can be](fg-red) multilined with \\n or [break automatically](fg-red,fg-bold)")
par2.Height = 5
par2.Width = 37
par2.Y = 4
- par2.Border.Label = "Multiline"
- par2.Border.FgColor = termui.ColorYellow
+ par2.BorderLabel = "Multiline"
+ par2.BorderFg = termui.ColorYellow
par3 := termui.NewPar("Long text with label and it is auto trimmed.")
par3.Height = 3
par3.Width = 37
par3.Y = 9
- par3.Border.Label = "Auto Trim"
+ par3.BorderLabel = "Auto Trim"
termui.Render(par0, par1, par2, par3)
- <-termui.EventCh()
+ termui.Handle("/sys/kbd/q", func(termui.Event) {
+ termui.StopLoop()
+ })
+ termui.Loop()
+
}
diff --git a/example/par.png b/_example/par.png
similarity index 100%
rename from example/par.png
rename to _example/par.png
diff --git a/example/sparklines.go b/_example/sparklines.go
similarity index 82%
rename from example/sparklines.go
rename to _example/sparklines.go
index f04baf5..4b3a5b6 100644
--- a/example/sparklines.go
+++ b/_example/sparklines.go
@@ -15,7 +15,7 @@ func main() {
}
defer termui.Close()
- termui.UseTheme("helloworld")
+ //termui.UseTheme("helloworld")
data := []int{4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6}
spl0 := termui.NewSparkline()
@@ -27,7 +27,7 @@ func main() {
spls0 := termui.NewSparklines(spl0)
spls0.Height = 2
spls0.Width = 20
- spls0.HasBorder = false
+ spls0.Border = false
spl1 := termui.NewSparkline()
spl1.Data = data
@@ -44,7 +44,7 @@ func main() {
spls1.Height = 8
spls1.Width = 20
spls1.Y = 3
- spls1.Border.Label = "Group Sparklines"
+ spls1.BorderLabel = "Group Sparklines"
spl3 := termui.NewSparkline()
spl3.Data = data
@@ -55,11 +55,15 @@ func main() {
spls2 := termui.NewSparklines(spl3)
spls2.Height = 11
spls2.Width = 30
- spls2.Border.FgColor = termui.ColorCyan
+ spls2.BorderFg = termui.ColorCyan
spls2.X = 21
- spls2.Border.Label = "Tweeked Sparkline"
+ spls2.BorderLabel = "Tweeked Sparkline"
termui.Render(spls0, spls1, spls2)
- <-termui.EventCh()
+ termui.Handle("/sys/kbd/q", func(termui.Event) {
+ termui.StopLoop()
+ })
+ termui.Loop()
+
}
diff --git a/example/sparklines.png b/_example/sparklines.png
similarity index 100%
rename from example/sparklines.png
rename to _example/sparklines.png
diff --git a/example/tabs.go b/_example/tabs.go
similarity index 100%
rename from example/tabs.go
rename to _example/tabs.go
diff --git a/example/theme.go b/_example/theme.go
similarity index 100%
rename from example/theme.go
rename to _example/theme.go
diff --git a/example/themedefault.png b/_example/themedefault.png
similarity index 100%
rename from example/themedefault.png
rename to _example/themedefault.png
diff --git a/example/themehelloworld.png b/_example/themehelloworld.png
similarity index 100%
rename from example/themehelloworld.png
rename to _example/themehelloworld.png
diff --git a/example/ttop.go b/_example/ttop.go
similarity index 100%
rename from example/ttop.go
rename to _example/ttop.go
diff --git a/bar.go b/barchart.go
similarity index 70%
rename from bar.go
rename to barchart.go
index 57bae0a..ed59184 100644
--- a/bar.go
+++ b/barchart.go
@@ -39,16 +39,16 @@ type BarChart struct {
// NewBarChart returns a new *BarChart with current theme.
func NewBarChart() *BarChart {
bc := &BarChart{Block: *NewBlock()}
- bc.BarColor = theme.BarChartBar
- bc.NumColor = theme.BarChartNum
- bc.TextColor = theme.BarChartText
+ bc.BarColor = ThemeAttr("barchart.bar.bg")
+ bc.NumColor = ThemeAttr("barchart.num.fg")
+ bc.TextColor = ThemeAttr("barchart.text.fg")
bc.BarGap = 1
bc.BarWidth = 3
return bc
}
func (bc *BarChart) layout() {
- bc.numBar = bc.innerWidth / (bc.BarGap + bc.BarWidth)
+ bc.numBar = bc.innerArea.Dx() / (bc.BarGap + bc.BarWidth)
bc.labels = make([][]rune, bc.numBar)
bc.dataNum = make([][]rune, len(bc.Data))
@@ -69,7 +69,7 @@ func (bc *BarChart) layout() {
bc.max = bc.Data[i]
}
}
- bc.scale = float64(bc.max) / float64(bc.innerHeight-1)
+ bc.scale = float64(bc.max) / float64(bc.innerArea.Dy()-1)
}
func (bc *BarChart) SetMax(max int) {
@@ -80,8 +80,8 @@ func (bc *BarChart) SetMax(max int) {
}
// Buffer implements Bufferer interface.
-func (bc *BarChart) Buffer() []Point {
- ps := bc.Block.Buffer()
+func (bc *BarChart) Buffer() Buffer {
+ buf := bc.Block.Buffer()
bc.layout()
for i := 0; i < bc.numBar && i < len(bc.Data) && i < len(bc.DataLabels); i++ {
@@ -90,46 +90,49 @@ func (bc *BarChart) Buffer() []Point {
// plot bar
for j := 0; j < bc.BarWidth; j++ {
for k := 0; k < h; k++ {
- p := Point{}
- p.Ch = ' '
- p.Bg = bc.BarColor
- if bc.BarColor == ColorDefault { // when color is default, space char treated as transparent!
- p.Bg |= AttrReverse
+ c := Cell{
+ Ch: ' ',
+ Bg: bc.BarColor,
}
- p.X = bc.innerX + i*(bc.BarWidth+bc.BarGap) + j
- p.Y = bc.innerY + bc.innerHeight - 2 - k
- ps = append(ps, p)
+ if bc.BarColor == ColorDefault { // when color is default, space char treated as transparent!
+ c.Bg |= AttrReverse
+ }
+ x := bc.innerArea.Min.X + i*(bc.BarWidth+bc.BarGap) + j
+ y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2 - k
+ buf.Set(x, y, c)
}
}
// plot text
for j, k := 0, 0; j < len(bc.labels[i]); j++ {
w := charWidth(bc.labels[i][j])
- p := Point{}
- p.Ch = bc.labels[i][j]
- p.Bg = bc.BgColor
- p.Fg = bc.TextColor
- p.Y = bc.innerY + bc.innerHeight - 1
- p.X = bc.innerX + oftX + k
- ps = append(ps, p)
+ c := Cell{
+ Ch: bc.labels[i][j],
+ Bg: bc.Bg,
+ Fg: bc.TextColor,
+ }
+ y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 1
+ x := bc.innerArea.Min.X + oftX + k
+ buf.Set(x, y, c)
k += w
}
// plot num
for j := 0; j < len(bc.dataNum[i]); j++ {
- p := Point{}
- p.Ch = bc.dataNum[i][j]
- p.Fg = bc.NumColor
- p.Bg = bc.BarColor
+ c := Cell{
+ Ch: bc.dataNum[i][j],
+ Fg: bc.NumColor,
+ Bg: bc.BarColor,
+ }
if bc.BarColor == ColorDefault { // the same as above
- p.Bg |= AttrReverse
+ c.Bg |= AttrReverse
}
if h == 0 {
- p.Bg = bc.BgColor
+ c.Bg = bc.Bg
}
- p.X = bc.innerX + oftX + (bc.BarWidth-len(bc.dataNum[i]))/2 + j
- p.Y = bc.innerY + bc.innerHeight - 2
- ps = append(ps, p)
+ x := bc.innerArea.Min.X + oftX + (bc.BarWidth-len(bc.dataNum[i]))/2 + j
+ y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2
+ buf.Set(x, y, c)
}
}
- return bc.Block.chopOverflow(ps)
+ return buf
}
diff --git a/block.go b/block.go
index 7fc0abf..fabd098 100644
--- a/block.go
+++ b/block.go
@@ -4,163 +4,237 @@
package termui
+import "image"
+
+// Hline is a horizontal line.
+type Hline struct {
+ X int
+ Y int
+ Len int
+ Fg Attribute
+ Bg Attribute
+}
+
+// Vline is a vertical line.
+type Vline struct {
+ X int
+ Y int
+ Len int
+ Fg Attribute
+ Bg Attribute
+}
+
+// Buffer draws a horizontal line.
+func (l Hline) Buffer() Buffer {
+ if l.Len <= 0 {
+ return NewBuffer()
+ }
+ return NewFilledBuffer(l.X, l.Y, l.X+l.Len, l.Y+1, HORIZONTAL_LINE, l.Fg, l.Bg)
+}
+
+// Buffer draws a vertical line.
+func (l Vline) Buffer() Buffer {
+ if l.Len <= 0 {
+ return NewBuffer()
+ }
+ return NewFilledBuffer(l.X, l.Y, l.X+1, l.Y+l.Len, VERTICAL_LINE, l.Fg, l.Bg)
+}
+
+// Buffer draws a box border.
+func (b Block) drawBorder(buf Buffer) {
+ if !b.Border {
+ return
+ }
+
+ min := b.area.Min
+ max := b.area.Max
+
+ x0 := min.X
+ y0 := min.Y
+ x1 := max.X - 1
+ y1 := max.Y - 1
+
+ // draw lines
+ if b.BorderTop {
+ buf.Merge(Hline{x0, y0, x1 - x0, b.BorderFg, b.BorderBg}.Buffer())
+ }
+ if b.BorderBottom {
+ buf.Merge(Hline{x0, y1, x1 - x0, b.BorderFg, b.BorderBg}.Buffer())
+ }
+ if b.BorderLeft {
+ buf.Merge(Vline{x0, y0, y1 - y0, b.BorderFg, b.BorderBg}.Buffer())
+ }
+ if b.BorderRight {
+ buf.Merge(Vline{x1, y0, y1 - y0, b.BorderFg, b.BorderBg}.Buffer())
+ }
+
+ // draw corners
+ if b.BorderTop && b.BorderLeft && b.area.Dx() > 0 && b.area.Dy() > 0 {
+ buf.Set(x0, y0, Cell{TOP_LEFT, b.BorderFg, b.BorderBg})
+ }
+ if b.BorderTop && b.BorderRight && b.area.Dx() > 1 && b.area.Dy() > 0 {
+ buf.Set(x1, y0, Cell{TOP_RIGHT, b.BorderFg, b.BorderBg})
+ }
+ if b.BorderBottom && b.BorderLeft && b.area.Dx() > 0 && b.area.Dy() > 1 {
+ buf.Set(x0, y1, Cell{BOTTOM_LEFT, b.BorderFg, b.BorderBg})
+ }
+ if b.BorderBottom && b.BorderRight && b.area.Dx() > 1 && b.area.Dy() > 1 {
+ buf.Set(x1, y1, Cell{BOTTOM_RIGHT, b.BorderFg, b.BorderBg})
+ }
+}
+
+func (b Block) drawBorderLabel(buf Buffer) {
+ maxTxtW := b.area.Dx() - 2
+ tx := DTrimTxCls(DefaultTxBuilder.Build(b.BorderLabel, b.BorderLabelFg, b.BorderLabelBg), maxTxtW)
+
+ for i, w := 0, 0; i < len(tx); i++ {
+ buf.Set(b.area.Min.X+1+w, b.area.Min.Y, tx[i])
+ w += tx[i].Width()
+ }
+}
+
// Block is a base struct for all other upper level widgets,
// consider it as css: display:block.
// Normally you do not need to create it manually.
type Block struct {
+ area image.Rectangle
+ innerArea image.Rectangle
X int
Y int
- Border labeledBorder
- IsDisplay bool
- HasBorder bool
- BgColor Attribute
+ Border bool
+ BorderFg Attribute
+ BorderBg Attribute
+ BorderLeft bool
+ BorderRight bool
+ BorderTop bool
+ BorderBottom bool
+ BorderLabel string
+ BorderLabelFg Attribute
+ BorderLabelBg Attribute
+ Display bool
+ Bg Attribute
Width int
Height int
- innerWidth int
- innerHeight int
- innerX int
- innerY int
PaddingTop int
PaddingBottom int
PaddingLeft int
PaddingRight int
+ id string
+ Float Align
}
// NewBlock returns a *Block which inherits styles from current theme.
func NewBlock() *Block {
- d := Block{}
- d.IsDisplay = true
- d.HasBorder = theme.HasBorder
- d.Border.BgColor = theme.BorderBg
- d.Border.FgColor = theme.BorderFg
- d.Border.LabelBgColor = theme.BorderLabelTextBg
- d.Border.LabelFgColor = theme.BorderLabelTextFg
- d.BgColor = theme.BlockBg
- d.Width = 2
- d.Height = 2
- return &d
+ b := Block{}
+ b.Display = true
+ b.Border = true
+ b.BorderLeft = true
+ b.BorderRight = true
+ b.BorderTop = true
+ b.BorderBottom = true
+ b.BorderBg = ThemeAttr("border.bg")
+ b.BorderFg = ThemeAttr("border.fg")
+ b.BorderLabelBg = ThemeAttr("label.bg")
+ b.BorderLabelFg = ThemeAttr("label.fg")
+ b.Bg = ThemeAttr("block.bg")
+ b.Width = 2
+ b.Height = 2
+ b.id = GenId()
+ b.Float = AlignNone
+ return &b
}
-// compute box model
-func (d *Block) align() {
- d.innerWidth = d.Width - d.PaddingLeft - d.PaddingRight
- d.innerHeight = d.Height - d.PaddingTop - d.PaddingBottom
- d.innerX = d.X + d.PaddingLeft
- d.innerY = d.Y + d.PaddingTop
+func (b Block) Id() string {
+ return b.id
+}
- if d.HasBorder {
- d.innerHeight -= 2
- d.innerWidth -= 2
- d.Border.X = d.X
- d.Border.Y = d.Y
- d.Border.Width = d.Width
- d.Border.Height = d.Height
- d.innerX++
- d.innerY++
- }
+// Align computes box model
+func (b *Block) Align() {
+ // outer
+ b.area.Min.X = 0
+ b.area.Min.Y = 0
+ b.area.Max.X = b.Width
+ b.area.Max.Y = b.Height
- if d.innerHeight < 0 {
- d.innerHeight = 0
- }
- if d.innerWidth < 0 {
- d.innerWidth = 0
- }
+ // float
+ b.area = AlignArea(TermRect(), b.area, b.Float)
+ b.area = MoveArea(b.area, b.X, b.Y)
+ // inner
+ b.innerArea.Min.X = b.area.Min.X + b.PaddingLeft
+ b.innerArea.Min.Y = b.area.Min.Y + b.PaddingTop
+ b.innerArea.Max.X = b.area.Max.X - b.PaddingRight
+ b.innerArea.Max.Y = b.area.Max.Y - b.PaddingBottom
+
+ if b.Border {
+ if b.BorderLeft {
+ b.innerArea.Min.X++
+ }
+ if b.BorderRight {
+ b.innerArea.Max.X--
+ }
+ if b.BorderTop {
+ b.innerArea.Min.Y++
+ }
+ if b.BorderBottom {
+ b.innerArea.Max.Y--
+ }
+ }
}
// InnerBounds returns the internal bounds of the block after aligning and
// calculating the padding and border, if any.
-func (d *Block) InnerBounds() (x, y, width, height int) {
- d.align()
- return d.innerX, d.innerY, d.innerWidth, d.innerHeight
+func (b *Block) InnerBounds() image.Rectangle {
+ b.Align()
+ return b.innerArea
}
// Buffer implements Bufferer interface.
// Draw background and border (if any).
-func (d *Block) Buffer() []Point {
- d.align()
+func (b *Block) Buffer() Buffer {
+ b.Align()
- ps := []Point{}
- if !d.IsDisplay {
- return ps
- }
+ buf := NewBuffer()
+ buf.SetArea(b.area)
+ buf.Fill(' ', ColorDefault, b.Bg)
- if d.HasBorder {
- ps = d.Border.Buffer()
- }
+ b.drawBorder(buf)
+ b.drawBorderLabel(buf)
- for i := 0; i < d.innerWidth; i++ {
- for j := 0; j < d.innerHeight; j++ {
- p := Point{}
- p.X = d.X + i
- p.Y = d.Y + j
- if d.HasBorder {
- p.X++
- p.Y++
- }
- p.Ch = ' '
- p.Bg = d.BgColor
- ps = append(ps, p)
- }
- }
- return ps
+ return buf
}
// GetHeight implements GridBufferer.
// It returns current height of the block.
-func (d Block) GetHeight() int {
- return d.Height
+func (b Block) GetHeight() int {
+ return b.Height
}
// SetX implements GridBufferer interface, which sets block's x position.
-func (d *Block) SetX(x int) {
- d.X = x
+func (b *Block) SetX(x int) {
+ b.X = x
}
// SetY implements GridBufferer interface, it sets y position for block.
-func (d *Block) SetY(y int) {
- d.Y = y
+func (b *Block) SetY(y int) {
+ b.Y = y
}
// SetWidth implements GridBuffer interface, it sets block's width.
-func (d *Block) SetWidth(w int) {
- d.Width = w
-}
-
-// chop the overflow parts
-func (d *Block) chopOverflow(ps []Point) []Point {
- nps := make([]Point, 0, len(ps))
- x := d.X
- y := d.Y
- w := d.Width
- h := d.Height
- for _, v := range ps {
- if v.X >= x &&
- v.X < x+w &&
- v.Y >= y &&
- v.Y < y+h {
- nps = append(nps, v)
- }
- }
- return nps
+func (b *Block) SetWidth(w int) {
+ b.Width = w
}
func (b Block) InnerWidth() int {
- return b.innerWidth
+ return b.innerArea.Dx()
}
func (b Block) InnerHeight() int {
- return b.innerHeight
+ return b.innerArea.Dy()
}
func (b Block) InnerX() int {
- return b.innerX
+ return b.innerArea.Min.X
}
-func (b Block) InnerY() int {
- return b.innerY
-}
-
-func (b *Block) Align() {
- b.align()
-}
+func (b Block) InnerY() int { return b.innerArea.Min.Y }
diff --git a/box_others.go b/block_common.go
similarity index 100%
rename from box_others.go
rename to block_common.go
diff --git a/block_test.go b/block_test.go
index 2de205b..9be8aa7 100644
--- a/block_test.go
+++ b/block_test.go
@@ -1,8 +1,25 @@
package termui
-import "testing"
+import (
+ "testing"
+)
+
+func TestBlockFloat(t *testing.T) {
+ Init()
+ defer Close()
+
+ b := NewBlock()
+ b.X = 10
+ b.Y = 20
+
+ b.Float = AlignCenter
+ b.Align()
+}
+
+func TestBlockInnerBounds(t *testing.T) {
+ Init()
+ defer Close()
-func TestBlock_InnerBounds(t *testing.T) {
b := NewBlock()
b.X = 10
b.Y = 11
@@ -11,12 +28,17 @@ func TestBlock_InnerBounds(t *testing.T) {
assert := func(name string, x, y, w, h int) {
t.Log(name)
- cx, cy, cw, ch := b.InnerBounds()
+ area := b.InnerBounds()
+ cx := area.Min.X
+ cy := area.Min.Y
+ cw := area.Dx()
+ ch := area.Dy()
+
if cx != x {
t.Errorf("expected x to be %d but got %d", x, cx)
}
if cy != y {
- t.Errorf("expected y to be %d but got %d", y, cy)
+ t.Errorf("expected y to be %d but got %d\n%+v", y, cy, area)
}
if cw != w {
t.Errorf("expected width to be %d but got %d", w, cw)
@@ -26,10 +48,10 @@ func TestBlock_InnerBounds(t *testing.T) {
}
}
- b.HasBorder = false
+ b.Border = false
assert("no border, no padding", 10, 11, 12, 13)
- b.HasBorder = true
+ b.Border = true
assert("border, no padding", 11, 12, 10, 11)
b.PaddingBottom = 2
diff --git a/box_windows.go b/block_windows.go
similarity index 100%
rename from box_windows.go
rename to block_windows.go
diff --git a/box.go b/box.go
deleted file mode 100644
index 1dcfd86..0000000
--- a/box.go
+++ /dev/null
@@ -1,117 +0,0 @@
-// Copyright 2015 Zack Guo . 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
-}
diff --git a/buffer.go b/buffer.go
new file mode 100644
index 0000000..6eabc02
--- /dev/null
+++ b/buffer.go
@@ -0,0 +1,106 @@
+// Copyright 2015 Zack Guo . 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
+}
diff --git a/buffer_test.go b/buffer_test.go
new file mode 100644
index 0000000..8fbf812
--- /dev/null
+++ b/buffer_test.go
@@ -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)
+ }
+}
diff --git a/canvas.go b/canvas.go
index 614635e..9422f5e 100644
--- a/canvas.go
+++ b/canvas.go
@@ -63,12 +63,10 @@ func (c Canvas) Unset(x, y int) {
}
// Buffer returns un-styled points
-func (c Canvas) Buffer() []Point {
- ps := make([]Point, len(c))
- i := 0
+func (c Canvas) Buffer() Buffer {
+ buf := NewBuffer()
for k, v := range c {
- ps[i] = newPoint(v+brailleBase, k[0], k[1])
- i++
+ buf.Set(k[0], k[1], Cell{Ch: v + brailleBase})
}
- return ps
+ return buf
}
diff --git a/canvas_test.go b/canvas_test.go
index 021949c..a955587 100644
--- a/canvas_test.go
+++ b/canvas_test.go
@@ -1,3 +1,5 @@
+// +build ignore
+
package termui
import (
@@ -47,9 +49,5 @@ func TestCanvasBuffer(t *testing.T) {
c.Set(8, 1)
c.Set(9, 0)
bufs := c.Buffer()
- rs := make([]rune, len(bufs))
- for i, v := range bufs {
- rs[i] = v.Ch
- }
- spew.Dump(string(rs))
+ spew.Dump(bufs)
}
diff --git a/debug/debuger.go b/debug/debuger.go
new file mode 100644
index 0000000..ac86226
--- /dev/null
+++ b/debug/debuger.go
@@ -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...)
+}
diff --git a/events.go b/events.go
index f310dac..22e7193 100644
--- a/events.go
+++ b/events.go
@@ -8,166 +8,313 @@
package termui
-import "github.com/nsf/termbox-go"
+import (
+ "path"
+ "strconv"
+ "sync"
+ "time"
-/***********************************termbox-go**************************************/
-
-type (
- EventType uint8
- Modifier uint8
- Key uint16
+ "github.com/nsf/termbox-go"
)
-// This type represents a termbox event. The 'Mod', 'Key' and 'Ch' fields are
-// valid if 'Type' is EventKey. The 'Width' and 'Height' fields are valid if
-// 'Type' is EventResize. The 'Err' field is valid if 'Type' is EventError.
type Event struct {
- Type EventType // one of Event* constants
- Mod Modifier // one of Mod* constants or 0
- Key Key // one of Key* constants, invalid if 'Ch' is not 0
- Ch rune // a unicode character
- Width int // width of the screen
- Height int // height of the screen
- Err error // error in case if input failed
- MouseX int // x coord of mouse
- MouseY int // y coord of mouse
- N int // number of bytes written when getting a raw event
+ Type string
+ Path string
+ From string
+ To string
+ Data interface{}
+ Time int64
}
-const (
- KeyF1 Key = 0xFFFF - iota
- KeyF2
- KeyF3
- KeyF4
- KeyF5
- KeyF6
- KeyF7
- KeyF8
- KeyF9
- KeyF10
- KeyF11
- KeyF12
- KeyInsert
- KeyDelete
- KeyHome
- KeyEnd
- KeyPgup
- KeyPgdn
- KeyArrowUp
- KeyArrowDown
- KeyArrowLeft
- KeyArrowRight
- key_min // see terminfo
- MouseLeft
- MouseMiddle
- MouseRight
-)
+var sysEvtChs []chan Event
-const (
- KeyCtrlTilde Key = 0x00
- KeyCtrl2 Key = 0x00
- KeyCtrlSpace Key = 0x00
- KeyCtrlA Key = 0x01
- KeyCtrlB Key = 0x02
- KeyCtrlC Key = 0x03
- KeyCtrlD Key = 0x04
- KeyCtrlE Key = 0x05
- KeyCtrlF Key = 0x06
- KeyCtrlG Key = 0x07
- KeyBackspace Key = 0x08
- KeyCtrlH Key = 0x08
- KeyTab Key = 0x09
- KeyCtrlI Key = 0x09
- KeyCtrlJ Key = 0x0A
- KeyCtrlK Key = 0x0B
- KeyCtrlL Key = 0x0C
- KeyEnter Key = 0x0D
- KeyCtrlM Key = 0x0D
- KeyCtrlN Key = 0x0E
- KeyCtrlO Key = 0x0F
- KeyCtrlP Key = 0x10
- KeyCtrlQ Key = 0x11
- KeyCtrlR Key = 0x12
- KeyCtrlS Key = 0x13
- KeyCtrlT Key = 0x14
- KeyCtrlU Key = 0x15
- KeyCtrlV Key = 0x16
- KeyCtrlW Key = 0x17
- KeyCtrlX Key = 0x18
- KeyCtrlY Key = 0x19
- KeyCtrlZ Key = 0x1A
- KeyEsc Key = 0x1B
- KeyCtrlLsqBracket Key = 0x1B
- KeyCtrl3 Key = 0x1B
- KeyCtrl4 Key = 0x1C
- KeyCtrlBackslash Key = 0x1C
- KeyCtrl5 Key = 0x1D
- KeyCtrlRsqBracket Key = 0x1D
- KeyCtrl6 Key = 0x1E
- KeyCtrl7 Key = 0x1F
- KeyCtrlSlash Key = 0x1F
- KeyCtrlUnderscore Key = 0x1F
- KeySpace Key = 0x20
- KeyBackspace2 Key = 0x7F
- KeyCtrl8 Key = 0x7F
-)
-
-// Alt modifier constant, see Event.Mod field and SetInputMode function.
-const (
- ModAlt Modifier = 0x01
-)
-
-// Event type. See Event.Type field.
-const (
- EventKey EventType = iota
- EventResize
- EventMouse
- EventError
- EventInterrupt
- EventRaw
- EventNone
-)
-
-/**************************************end**************************************/
-
-// convert termbox.Event to termui.Event
-func uiEvt(e termbox.Event) Event {
- event := Event{}
- event.Type = EventType(e.Type)
- event.Mod = Modifier(e.Mod)
- event.Key = Key(e.Key)
- event.Ch = e.Ch
- event.Width = e.Width
- event.Height = e.Height
- event.Err = e.Err
- event.MouseX = e.MouseX
- event.MouseY = e.MouseY
- event.N = e.N
-
- return event
+type EvtKbd struct {
+ KeyStr string
}
-var evtChs = make([]chan Event, 0)
+func evtKbd(e termbox.Event) EvtKbd {
+ ek := EvtKbd{}
-// EventCh returns an output-only event channel.
-// This function can be called many times (multiplexer).
-func EventCh() <-chan Event {
- out := make(chan Event)
- evtChs = append(evtChs, out)
- return out
-}
+ k := string(e.Ch)
+ pre := ""
+ mod := ""
-// turn on event listener
-func evtListen() {
- go func() {
- for {
- e := termbox.PollEvent()
- // dispatch
- for _, c := range evtChs {
- go func(ch chan Event) {
- ch <- uiEvt(e)
- }(c)
+ if e.Mod == termbox.ModAlt {
+ mod = "M-"
+ }
+ if e.Ch == 0 {
+ if e.Key > 0xFFFF-12 {
+ k = ""
+ } else if e.Key > 0xFFFF-25 {
+ ks := []string{"", "", "", "", "", "", "", "", "", ""}
+ k = ks[0xFFFF-int(e.Key)-12]
+ }
+
+ if e.Key <= 0x7F {
+ pre = "C-"
+ k = string('a' - 1 + int(e.Key))
+ kmap := map[termbox.Key][2]string{
+ termbox.KeyCtrlSpace: {"C-", ""},
+ termbox.KeyBackspace: {"", ""},
+ termbox.KeyTab: {"", ""},
+ termbox.KeyEnter: {"", ""},
+ termbox.KeyEsc: {"", ""},
+ termbox.KeyCtrlBackslash: {"C-", "\\"},
+ termbox.KeyCtrlSlash: {"C-", "/"},
+ termbox.KeySpace: {"", ""},
+ 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
+}
diff --git a/events_test.go b/events_test.go
index 1137b1d..c85634a 100644
--- a/events_test.go
+++ b/events_test.go
@@ -8,21 +8,34 @@
package termui
-import (
- "errors"
- "testing"
+import "testing"
- termbox "github.com/nsf/termbox-go"
- "github.com/stretchr/testify/assert"
-)
+var ps = []string{
+ "",
+ "/",
+ "/a",
+ "/b",
+ "/a/c",
+ "/a/b",
+ "/a/b/c",
+ "/a/b/c/d",
+ "/a/b/c/d/"}
-type boxEvent termbox.Event
+func TestMatchScore(t *testing.T) {
+ chk := func(a, b string, s bool) {
+ if c := isPathMatch(a, b); c != s {
+ t.Errorf("\na:%s\nb:%s\nshould:%t\nactual:%t", a, b, s, c)
+ }
+ }
+
+ chk(ps[1], ps[1], true)
+ chk(ps[1], ps[2], true)
+ chk(ps[2], ps[1], false)
+ chk(ps[4], ps[1], false)
+ chk(ps[6], ps[2], false)
+ chk(ps[4], ps[5], false)
+}
+
+func TestCrtEvt(t *testing.T) {
-func TestUiEvt(t *testing.T) {
- err := errors.New("This is a mock error")
- event := boxEvent{3, 5, 2, 'H', 200, 500, err, 50, 30, 2}
- expetced := Event{3, 5, 2, 'H', 200, 500, err, 50, 30, 2}
-
- // We need to do that ugly casting so that vet does not complain
- assert.Equal(t, uiEvt(termbox.Event(event)), expetced)
}
diff --git a/gauge.go b/gauge.go
index c27a65c..f2753fe 100644
--- a/gauge.go
+++ b/gauge.go
@@ -21,17 +21,7 @@ import (
g.PercentColor = termui.ColorBlue
*/
-// Align is the position of the gauge's label.
-type Align int
-
-// All supported positions.
-const (
- AlignLeft Align = iota
- AlignCenter
- AlignRight
-)
-
-const uint16max = ^uint16(0)
+const ColorUndef Attribute = Attribute(^uint16(0))
type Gauge struct {
Block
@@ -47,11 +37,11 @@ type Gauge struct {
func NewGauge() *Gauge {
g := &Gauge{
Block: *NewBlock(),
- PercentColor: theme.GaugePercent,
- BarColor: theme.GaugeBar,
+ PercentColor: ThemeAttr("gauge.percent.fg"),
+ BarColor: ThemeAttr("gauge.bar.bg"),
Label: "{{percent}}%",
LabelAlign: AlignCenter,
- PercentColorHighlighted: Attribute(uint16max),
+ PercentColorHighlighted: ColorUndef,
}
g.Width = 12
@@ -60,28 +50,26 @@ func NewGauge() *Gauge {
}
// Buffer implements Bufferer interface.
-func (g *Gauge) Buffer() []Point {
- ps := g.Block.Buffer()
+func (g *Gauge) Buffer() Buffer {
+ buf := g.Block.Buffer()
// plot bar
- w := g.Percent * g.innerWidth / 100
- for i := 0; i < g.innerHeight; i++ {
+ w := g.Percent * g.innerArea.Dx() / 100
+ for i := 0; i < g.innerArea.Dy(); i++ {
for j := 0; j < w; j++ {
- p := Point{}
- p.X = g.innerX + j
- p.Y = g.innerY + i
- p.Ch = ' '
- p.Bg = g.BarColor
- if p.Bg == ColorDefault {
- p.Bg |= AttrReverse
+ c := Cell{}
+ c.Ch = ' '
+ c.Bg = g.BarColor
+ if c.Bg == ColorDefault {
+ c.Bg |= AttrReverse
}
- ps = append(ps, p)
+ buf.Set(g.innerArea.Min.X+j, g.innerArea.Min.Y+i, c)
}
}
// plot percentage
s := strings.Replace(g.Label, "{{percent}}", strconv.Itoa(g.Percent), -1)
- pry := g.innerY + g.innerHeight/2
+ pry := g.innerArea.Min.Y + g.innerArea.Dy()/2
rs := str2runes(s)
var pos int
switch g.LabelAlign {
@@ -89,33 +77,33 @@ func (g *Gauge) Buffer() []Point {
pos = 0
case AlignCenter:
- pos = (g.innerWidth - strWidth(s)) / 2
+ pos = (g.innerArea.Dx() - strWidth(s)) / 2
case AlignRight:
- pos = g.innerWidth - strWidth(s)
+ pos = g.innerArea.Dx() - strWidth(s) - 1
}
- pos += g.innerX
+ pos += g.innerArea.Min.X
for i, v := range rs {
- p := Point{}
- p.X = 1 + pos + i
- p.Y = pry
- p.Ch = v
- p.Fg = g.PercentColor
- if w+g.innerX > pos+i {
- p.Bg = g.BarColor
- if p.Bg == ColorDefault {
- p.Bg |= AttrReverse
- }
-
- if g.PercentColorHighlighted != Attribute(uint16max) {
- p.Fg = g.PercentColorHighlighted
- }
- } else {
- p.Bg = g.Block.BgColor
+ c := Cell{
+ Ch: v,
+ Fg: g.PercentColor,
}
- ps = append(ps, p)
+ if w+g.innerArea.Min.X > pos+i {
+ c.Bg = g.BarColor
+ if c.Bg == ColorDefault {
+ c.Bg |= AttrReverse
+ }
+
+ if g.PercentColorHighlighted != ColorUndef {
+ c.Fg = g.PercentColorHighlighted
+ }
+ } else {
+ c.Bg = g.Block.Bg
+ }
+
+ buf.Set(1+pos+i, pry, c)
}
- return g.Block.chopOverflow(ps)
+ return buf
}
diff --git a/grid.go b/grid.go
index 5f6e85e..264b760 100644
--- a/grid.go
+++ b/grid.go
@@ -160,8 +160,8 @@ func (r *Row) SetWidth(w int) {
// Buffer implements Bufferer interface,
// recursively merge all widgets buffer
-func (r *Row) Buffer() []Point {
- merged := []Point{}
+func (r *Row) Buffer() Buffer {
+ merged := NewBuffer()
if r.isRenderableLeaf() {
return r.Widget.Buffer()
@@ -169,13 +169,13 @@ func (r *Row) Buffer() []Point {
// for those are not leaves but have a renderable widget
if r.Widget != nil {
- merged = append(merged, r.Widget.Buffer()...)
+ merged.Merge(r.Widget.Buffer())
}
// collect buffer from children
if !r.isLeaf() {
for _, c := range r.Cols {
- merged = append(merged, c.Buffer()...)
+ merged.Merge(c.Buffer())
}
}
@@ -267,13 +267,13 @@ func (g *Grid) Align() {
}
// Buffer implments Bufferer interface.
-func (g Grid) Buffer() []Point {
- ps := []Point{}
+func (g Grid) Buffer() Buffer {
+ buf := NewBuffer()
+
for _, r := range g.Rows {
- ps = append(ps, r.Buffer()...)
+ buf.Merge(r.Buffer())
}
- return ps
+ return buf
}
-// Body corresponds to the entire terminal display region.
var Body *Grid
diff --git a/grid_test.go b/grid_test.go
index cdafb20..9829586 100644
--- a/grid_test.go
+++ b/grid_test.go
@@ -13,13 +13,13 @@ import (
var r *Row
func TestRowWidth(t *testing.T) {
- p0 := NewPar("p0")
+ p0 := NewBlock()
p0.Height = 1
- p1 := NewPar("p1")
+ p1 := NewBlock()
p1.Height = 1
- p2 := NewPar("p2")
+ p2 := NewBlock()
p2.Height = 1
- p3 := NewPar("p3")
+ p3 := NewBlock()
p3.Height = 1
/* test against tree:
@@ -34,24 +34,6 @@ func TestRowWidth(t *testing.T) {
/
1100:w
*/
- /*
- r = &row{
- Span: 12,
- Cols: []*row{
- &row{Widget: p0, Span: 6},
- &row{
- Span: 6,
- Cols: []*row{
- &row{Widget: p1, Span: 6},
- &row{
- Span: 6,
- Cols: []*row{
- &row{
- Span: 12,
- Widget: p2,
- Cols: []*row{
- &row{Span: 12, Widget: p3}}}}}}}}}
- */
r = NewRow(
NewCol(6, 0, p0),
diff --git a/helper.go b/helper.go
index 80d8a02..840c9bb 100644
--- a/helper.go
+++ b/helper.go
@@ -4,7 +4,12 @@
package termui
-import tm "github.com/nsf/termbox-go"
+import (
+ "regexp"
+ "strings"
+
+ tm "github.com/nsf/termbox-go"
+)
import rw "github.com/mattn/go-runewidth"
/* ---------------Port from termbox-go --------------------- */
@@ -12,6 +17,7 @@ import rw "github.com/mattn/go-runewidth"
// Attribute is printable cell's color and style.
type Attribute uint16
+// 8 basic clolrs
const (
ColorDefault Attribute = iota
ColorBlack
@@ -24,7 +30,10 @@ const (
ColorWhite
)
-const NumberofColors = 8 //Have a constant that defines number of colors
+//Have a constant that defines number of colors
+const NumberofColors = 8
+
+// Text style
const (
AttrBold Attribute = 1 << (iota + 9)
AttrUnderline
@@ -46,15 +55,39 @@ func str2runes(s string) []rune {
return []rune(s)
}
+// Here for backwards-compatibility.
func trimStr2Runes(s string, w int) []rune {
+ return TrimStr2Runes(s, w)
+}
+
+// TrimStr2Runes trims string to w[-1 rune], appends …, and returns the runes
+// of that string if string is grather then n. If string is small then w,
+// return the runes.
+func TrimStr2Runes(s string, w int) []rune {
if w <= 0 {
return []rune{}
}
+
sw := rw.StringWidth(s)
if sw > w {
return []rune(rw.Truncate(s, w, dot))
}
- return str2runes(s) //[]rune(rw.Truncate(s, w, ""))
+ return str2runes(s)
+}
+
+// TrimStrIfAppropriate trim string to "s[:-1] + …"
+// if string > width otherwise return string
+func TrimStrIfAppropriate(s string, w int) string {
+ if w <= 0 {
+ return ""
+ }
+
+ sw := rw.StringWidth(s)
+ if sw > w {
+ return rw.Truncate(s, w, dot)
+ }
+
+ return s
}
func strWidth(s string) int {
@@ -64,3 +97,118 @@ func strWidth(s string) int {
func charWidth(ch rune) int {
return rw.RuneWidth(ch)
}
+
+var whiteSpaceRegex = regexp.MustCompile(`\s`)
+
+// StringToAttribute converts text to a termui attribute. You may specifiy more
+// then one attribute like that: "BLACK, BOLD, ...". All whitespaces
+// are ignored.
+func StringToAttribute(text string) Attribute {
+ text = whiteSpaceRegex.ReplaceAllString(strings.ToLower(text), "")
+ attributes := strings.Split(text, ",")
+ result := Attribute(0)
+
+ for _, theAttribute := range attributes {
+ var match Attribute
+ switch theAttribute {
+ case "reset", "default":
+ match = ColorDefault
+
+ case "black":
+ match = ColorBlack
+
+ case "red":
+ match = ColorRed
+
+ case "green":
+ match = ColorGreen
+
+ case "yellow":
+ match = ColorYellow
+
+ case "blue":
+ match = ColorBlue
+
+ case "magenta":
+ match = ColorMagenta
+
+ case "cyan":
+ match = ColorCyan
+
+ case "white":
+ match = ColorWhite
+
+ case "bold":
+ match = AttrBold
+
+ case "underline":
+ match = AttrUnderline
+
+ case "reverse":
+ match = AttrReverse
+ }
+
+ result |= match
+ }
+
+ return result
+}
+
+// TextCells returns a coloured text cells []Cell
+func TextCells(s string, fg, bg Attribute) []Cell {
+ cs := make([]Cell, 0, len(s))
+
+ // sequence := MarkdownTextRendererFactory{}.TextRenderer(s).Render(fg, bg)
+ // runes := []rune(sequence.NormalizedText)
+ runes := str2runes(s)
+
+ for n := range runes {
+ // point, _ := sequence.PointAt(n, 0, 0)
+ // cs = append(cs, Cell{point.Ch, point.Fg, point.Bg})
+ cs = append(cs, Cell{runes[n], fg, bg})
+ }
+ return cs
+}
+
+// Width returns the actual screen space the cell takes (usually 1 or 2).
+func (c Cell) Width() int {
+ return charWidth(c.Ch)
+}
+
+// Copy return a copy of c
+func (c Cell) Copy() Cell {
+ return c
+}
+
+// TrimTxCells trims the overflowed text cells sequence.
+func TrimTxCells(cs []Cell, w int) []Cell {
+ if len(cs) <= w {
+ return cs
+ }
+ return cs[:w]
+}
+
+// DTrimTxCls trims the overflowed text cells sequence and append dots at the end.
+func DTrimTxCls(cs []Cell, w int) []Cell {
+ l := len(cs)
+ if l <= 0 {
+ return []Cell{}
+ }
+
+ rt := make([]Cell, 0, w)
+ csw := 0
+ for i := 0; i < l && csw <= w; i++ {
+ c := cs[i]
+ cw := c.Width()
+
+ if cw+csw < w {
+ rt = append(rt, c)
+ csw += cw
+ } else {
+ rt = append(rt, Cell{'…', c.Fg, c.Bg})
+ break
+ }
+ }
+
+ return rt
+}
diff --git a/helper_test.go b/helper_test.go
index 6d1a561..5d277de 100644
--- a/helper_test.go
+++ b/helper_test.go
@@ -7,22 +7,20 @@ package termui
import (
"testing"
- "github.com/davecgh/go-spew/spew"
+ "github.com/stretchr/testify/assert"
)
func TestStr2Rune(t *testing.T) {
s := "你好,世界."
rs := str2runes(s)
if len(rs) != 6 {
- t.Error()
+ t.Error(t)
}
}
func TestWidth(t *testing.T) {
s0 := "つのだ☆HIRO"
s1 := "11111111111"
- spew.Dump(s0)
- spew.Dump(s1)
// above not align for setting East Asian Ambiguous to wide!!
if strWidth(s0) != strWidth(s1) {
@@ -56,3 +54,17 @@ func TestTrim(t *testing.T) {
t.Error("avoid trim failed")
}
}
+
+func TestTrimStrIfAppropriate_NoTrim(t *testing.T) {
+ assert.Equal(t, "hello", TrimStrIfAppropriate("hello", 5))
+}
+
+func TestTrimStrIfAppropriate(t *testing.T) {
+ assert.Equal(t, "hel…", TrimStrIfAppropriate("hello", 4))
+ assert.Equal(t, "h…", TrimStrIfAppropriate("hello", 2))
+}
+
+func TestStringToAttribute(t *testing.T) {
+ assert.Equal(t, ColorRed, StringToAttribute("ReD"))
+ assert.Equal(t, ColorRed|AttrBold, StringToAttribute("RED, bold"))
+}
diff --git a/chart.go b/linechart.go
similarity index 70%
rename from chart.go
rename to linechart.go
index d6fb8bc..0689487 100644
--- a/chart.go
+++ b/linechart.go
@@ -74,8 +74,8 @@ type LineChart struct {
// NewLineChart returns a new LineChart with current theme.
func NewLineChart() *LineChart {
lc := &LineChart{Block: *NewBlock()}
- lc.AxesColor = theme.LineChartAxes
- lc.LineColor = theme.LineChartLine
+ lc.AxesColor = ThemeAttr("linechart.axes.fg")
+ lc.LineColor = ThemeAttr("linechart.line.fg")
lc.Mode = "braille"
lc.DotStyle = '•'
lc.axisXLebelGap = 2
@@ -87,8 +87,8 @@ func NewLineChart() *LineChart {
// one cell contains two data points
// so the capicity is 2x as dot-mode
-func (lc *LineChart) renderBraille() []Point {
- ps := []Point{}
+func (lc *LineChart) renderBraille() Buffer {
+ buf := NewBuffer()
// return: b -> which cell should the point be in
// m -> in the cell, divided into 4 equal height levels, which subcell?
@@ -104,44 +104,48 @@ func (lc *LineChart) renderBraille() []Point {
b1, m1 := getPos(lc.Data[2*i+1])
if b0 == b1 {
- p := Point{}
- p.Ch = braillePatterns[[2]int{m0, m1}]
- p.Bg = lc.BgColor
- p.Fg = lc.LineColor
- p.Y = lc.innerY + lc.innerHeight - 3 - b0
- p.X = lc.innerX + lc.labelYSpace + 1 + i
- ps = append(ps, p)
+ c := Cell{
+ Ch: braillePatterns[[2]int{m0, m1}],
+ Bg: lc.Bg,
+ Fg: lc.LineColor,
+ }
+ y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
+ x := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
+ buf.Set(x, y, c)
} else {
- p0 := newPointWithAttrs(lSingleBraille[m0],
- lc.innerX+lc.labelYSpace+1+i,
- lc.innerY+lc.innerHeight-3-b0,
- lc.LineColor,
- lc.BgColor)
- p1 := newPointWithAttrs(rSingleBraille[m1],
- lc.innerX+lc.labelYSpace+1+i,
- lc.innerY+lc.innerHeight-3-b1,
- lc.LineColor,
- lc.BgColor)
- ps = append(ps, p0, p1)
+ c0 := Cell{Ch: lSingleBraille[m0],
+ Fg: lc.LineColor,
+ Bg: lc.Bg}
+ x0 := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
+ y0 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
+ buf.Set(x0, y0, c0)
+
+ c1 := Cell{Ch: rSingleBraille[m1],
+ Fg: lc.LineColor,
+ Bg: lc.Bg}
+ x1 := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
+ y1 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b1
+ buf.Set(x1, y1, c1)
}
}
- return ps
+ return buf
}
-func (lc *LineChart) renderDot() []Point {
- ps := []Point{}
+func (lc *LineChart) renderDot() Buffer {
+ buf := NewBuffer()
for i := 0; i < len(lc.Data) && i < lc.axisXWidth; i++ {
- p := Point{}
- p.Ch = lc.DotStyle
- p.Fg = lc.LineColor
- p.Bg = lc.BgColor
- p.X = lc.innerX + lc.labelYSpace + 1 + i
- p.Y = lc.innerY + lc.innerHeight - 3 - int((lc.Data[i]-lc.bottomValue)/lc.scale+0.5)
- ps = append(ps, p)
+ c := Cell{
+ Ch: lc.DotStyle,
+ Fg: lc.LineColor,
+ Bg: lc.Bg,
+ }
+ x := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
+ y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - int((lc.Data[i]-lc.bottomValue)/lc.scale+0.5)
+ buf.Set(x, y, c)
}
- return ps
+ return buf
}
func (lc *LineChart) calcLabelX() {
@@ -220,9 +224,9 @@ func (lc *LineChart) calcLayout() {
lc.maxY = lc.Data[0]
// valid visible range
- vrange := lc.innerWidth
+ vrange := lc.innerArea.Dx()
if lc.Mode == "braille" {
- vrange = 2 * lc.innerWidth
+ vrange = 2 * lc.innerArea.Dx()
}
if vrange > len(lc.Data) {
vrange = len(lc.Data)
@@ -247,40 +251,30 @@ func (lc *LineChart) calcLayout() {
lc.topValue = lc.maxY + 0.2*span
}
- lc.axisYHeight = lc.innerHeight - 2
+ lc.axisYHeight = lc.innerArea.Dy() - 2
lc.calcLabelY()
- lc.axisXWidth = lc.innerWidth - 1 - lc.labelYSpace
+ lc.axisXWidth = lc.innerArea.Dx() - 1 - lc.labelYSpace
lc.calcLabelX()
- lc.drawingX = lc.innerX + 1 + lc.labelYSpace
- lc.drawingY = lc.innerY
+ lc.drawingX = lc.innerArea.Min.X + 1 + lc.labelYSpace
+ lc.drawingY = lc.innerArea.Min.Y
}
-func (lc *LineChart) plotAxes() []Point {
- origY := lc.innerY + lc.innerHeight - 2
- origX := lc.innerX + lc.labelYSpace
+func (lc *LineChart) plotAxes() Buffer {
+ buf := NewBuffer()
- ps := []Point{newPointWithAttrs(ORIGIN, origX, origY, lc.AxesColor, lc.BgColor)}
+ origY := lc.innerArea.Min.Y + lc.innerArea.Dy() - 2
+ origX := lc.innerArea.Min.X + lc.labelYSpace
+
+ buf.Set(origX, origY, Cell{Ch: ORIGIN, Fg: lc.AxesColor, Bg: lc.Bg})
for x := origX + 1; x < origX+lc.axisXWidth; x++ {
- p := Point{}
- p.X = x
- p.Y = origY
- p.Bg = lc.BgColor
- p.Fg = lc.AxesColor
- p.Ch = HDASH
- ps = append(ps, p)
+ buf.Set(x, origY, Cell{Ch: HDASH, Fg: lc.AxesColor, Bg: lc.Bg})
}
for dy := 1; dy <= lc.axisYHeight; dy++ {
- p := Point{}
- p.X = origX
- p.Y = origY - dy
- p.Bg = lc.BgColor
- p.Fg = lc.AxesColor
- p.Ch = VDASH
- ps = append(ps, p)
+ buf.Set(origX, origY-dy, Cell{Ch: VDASH, Fg: lc.AxesColor, Bg: lc.Bg})
}
// x label
@@ -290,13 +284,14 @@ func (lc *LineChart) plotAxes() []Point {
break
}
for j, r := range rs {
- p := Point{}
- p.Ch = r
- p.Fg = lc.AxesColor
- p.Bg = lc.BgColor
- p.X = origX + oft + j
- p.Y = lc.innerY + lc.innerHeight - 1
- ps = append(ps, p)
+ c := Cell{
+ Ch: r,
+ Fg: lc.AxesColor,
+ Bg: lc.Bg,
+ }
+ x := origX + oft + j
+ y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 1
+ buf.Set(x, y, c)
}
oft += len(rs) + lc.axisXLebelGap
}
@@ -304,33 +299,31 @@ func (lc *LineChart) plotAxes() []Point {
// y labels
for i, rs := range lc.labelY {
for j, r := range rs {
- p := Point{}
- p.Ch = r
- p.Fg = lc.AxesColor
- p.Bg = lc.BgColor
- p.X = lc.innerX + j
- p.Y = origY - i*(lc.axisYLebelGap+1)
- ps = append(ps, p)
+ buf.Set(
+ lc.innerArea.Min.X+j,
+ origY-i*(lc.axisYLebelGap+1),
+ Cell{Ch: r, Fg: lc.AxesColor, Bg: lc.Bg})
}
}
- return ps
+ return buf
}
// Buffer implements Bufferer interface.
-func (lc *LineChart) Buffer() []Point {
- ps := lc.Block.Buffer()
+func (lc *LineChart) Buffer() Buffer {
+ buf := lc.Block.Buffer()
+
if lc.Data == nil || len(lc.Data) == 0 {
- return ps
+ return buf
}
lc.calcLayout()
- ps = append(ps, lc.plotAxes()...)
+ buf.Merge(lc.plotAxes())
if lc.Mode == "dot" {
- ps = append(ps, lc.renderDot()...)
+ buf.Merge(lc.renderDot())
} else {
- ps = append(ps, lc.renderBraille()...)
+ buf.Merge(lc.renderBraille())
}
- return lc.Block.chopOverflow(ps)
+ return buf
}
diff --git a/chart_others.go b/linechart_others.go
similarity index 100%
rename from chart_others.go
rename to linechart_others.go
diff --git a/chart_windows.go b/linechart_windows.go
similarity index 100%
rename from chart_windows.go
rename to linechart_windows.go
diff --git a/list.go b/list.go
index 0640932..50361f2 100644
--- a/list.go
+++ b/list.go
@@ -41,64 +41,49 @@ type List struct {
func NewList() *List {
l := &List{Block: *NewBlock()}
l.Overflow = "hidden"
- l.ItemFgColor = theme.ListItemFg
- l.ItemBgColor = theme.ListItemBg
+ l.ItemFgColor = ThemeAttr("list.item.fg")
+ l.ItemBgColor = ThemeAttr("list.item.bg")
return l
}
// Buffer implements Bufferer interface.
-func (l *List) Buffer() []Point {
- ps := l.Block.Buffer()
+func (l *List) Buffer() Buffer {
+ buf := l.Block.Buffer()
+
switch l.Overflow {
case "wrap":
- rs := str2runes(strings.Join(l.Items, "\n"))
+ cs := DefaultTxBuilder.Build(strings.Join(l.Items, "\n"), l.ItemFgColor, l.ItemBgColor)
i, j, k := 0, 0, 0
- for i < l.innerHeight && k < len(rs) {
- w := charWidth(rs[k])
- if rs[k] == '\n' || j+w > l.innerWidth {
+ for i < l.innerArea.Dy() && k < len(cs) {
+ w := cs[k].Width()
+ if cs[k].Ch == '\n' || j+w > l.innerArea.Dx() {
i++
j = 0
- if rs[k] == '\n' {
+ if cs[k].Ch == '\n' {
k++
}
continue
}
- pi := Point{}
- pi.X = l.innerX + j
- pi.Y = l.innerY + i
+ buf.Set(l.innerArea.Min.X+j, l.innerArea.Min.Y+i, cs[k])
- pi.Ch = rs[k]
- pi.Bg = l.ItemBgColor
- pi.Fg = l.ItemFgColor
-
- ps = append(ps, pi)
k++
j++
}
case "hidden":
trimItems := l.Items
- if len(trimItems) > l.innerHeight {
- trimItems = trimItems[:l.innerHeight]
+ if len(trimItems) > l.innerArea.Dy() {
+ trimItems = trimItems[:l.innerArea.Dy()]
}
for i, v := range trimItems {
- rs := trimStr2Runes(v, l.innerWidth)
-
+ cs := DTrimTxCls(DefaultTxBuilder.Build(v, l.ItemFgColor, l.ItemBgColor), l.innerArea.Dx())
j := 0
- for _, vv := range rs {
- w := charWidth(vv)
- p := Point{}
- p.X = l.innerX + j
- p.Y = l.innerY + i
-
- p.Ch = vv
- p.Bg = l.ItemBgColor
- p.Fg = l.ItemFgColor
-
- ps = append(ps, p)
+ for _, vv := range cs {
+ w := vv.Width()
+ buf.Set(l.innerArea.Min.X+j, l.innerArea.Min.Y+i, vv)
j += w
}
}
}
- return l.Block.chopOverflow(ps)
+ return buf
}
diff --git a/mbar.go b/mbarchart.go
similarity index 76%
rename from mbar.go
rename to mbarchart.go
index 9d18c2c..c9d0c44 100644
--- a/mbar.go
+++ b/mbarchart.go
@@ -48,16 +48,16 @@ type MBarChart struct {
// NewBarChart returns a new *BarChart with current theme.
func NewMBarChart() *MBarChart {
bc := &MBarChart{Block: *NewBlock()}
- bc.BarColor[0] = theme.MBarChartBar
- bc.NumColor[0] = theme.MBarChartNum
- bc.TextColor = theme.MBarChartText
+ bc.BarColor[0] = ThemeAttr("mbarchart.bar.bg")
+ bc.NumColor[0] = ThemeAttr("mbarchart.num.fg")
+ bc.TextColor = ThemeAttr("mbarchart.text.fg")
bc.BarGap = 1
bc.BarWidth = 3
return bc
}
func (bc *MBarChart) layout() {
- bc.numBar = bc.innerWidth / (bc.BarGap + bc.BarWidth)
+ bc.numBar = bc.innerArea.Dx() / (bc.BarGap + bc.BarWidth)
bc.labels = make([][]rune, bc.numBar)
DataLen := 0
LabelLen := len(bc.DataLabels)
@@ -129,9 +129,9 @@ func (bc *MBarChart) layout() {
if bc.ShowScale {
s := fmt.Sprintf("%d", bc.max)
bc.maxScale = trimStr2Runes(s, len(s))
- bc.scale = float64(bc.max) / float64(bc.innerHeight-2)
+ bc.scale = float64(bc.max) / float64(bc.innerArea.Dy()-2)
} else {
- bc.scale = float64(bc.max) / float64(bc.innerHeight-1)
+ bc.scale = float64(bc.max) / float64(bc.innerArea.Dy()-1)
}
}
@@ -144,8 +144,8 @@ func (bc *MBarChart) SetMax(max int) {
}
// Buffer implements Bufferer interface.
-func (bc *MBarChart) Buffer() []Point {
- ps := bc.Block.Buffer()
+func (bc *MBarChart) Buffer() Buffer {
+ buf := bc.Block.Buffer()
bc.layout()
var oftX int
@@ -157,15 +157,17 @@ func (bc *MBarChart) Buffer() []Point {
// plot bars
for j := 0; j < bc.BarWidth; j++ {
for k := 0; k < h; k++ {
- p := Point{}
- p.Ch = ' '
- p.Bg = bc.BarColor[i1]
- if bc.BarColor[i1] == ColorDefault { // when color is default, space char treated as transparent!
- p.Bg |= AttrReverse
+ c := Cell{
+ Ch: ' ',
+ Bg: bc.BarColor[i1],
}
- p.X = bc.innerX + i*(bc.BarWidth+bc.BarGap) + j
- p.Y = bc.innerY + bc.innerHeight - 2 - k - ph
- ps = append(ps, p)
+ if bc.BarColor[i1] == ColorDefault { // when color is default, space char treated as transparent!
+ c.Bg |= AttrReverse
+ }
+ x := bc.innerArea.Min.X + i*(bc.BarWidth+bc.BarGap) + j
+ y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2 - k - ph
+ buf.Set(x, y, c)
+
}
}
ph += h
@@ -173,13 +175,14 @@ func (bc *MBarChart) Buffer() []Point {
// plot text
for j, k := 0, 0; j < len(bc.labels[i]); j++ {
w := charWidth(bc.labels[i][j])
- p := Point{}
- p.Ch = bc.labels[i][j]
- p.Bg = bc.BgColor
- p.Fg = bc.TextColor
- p.Y = bc.innerY + bc.innerHeight - 1
- p.X = bc.innerX + oftX + ((bc.BarWidth - len(bc.labels[i])) / 2) + k
- ps = append(ps, p)
+ c := Cell{
+ Ch: bc.labels[i][j],
+ Bg: bc.Bg,
+ Fg: bc.TextColor,
+ }
+ y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 1
+ x := bc.innerArea.Max.X + oftX + ((bc.BarWidth - len(bc.labels[i])) / 2) + k
+ buf.Set(x, y, c)
k += w
}
// plot num
@@ -187,19 +190,20 @@ func (bc *MBarChart) Buffer() []Point {
for i1 := 0; i1 < bc.numStack; i1++ {
h := int(float64(bc.Data[i1][i]) / bc.scale)
for j := 0; j < len(bc.dataNum[i1][i]) && h > 0; j++ {
- p := Point{}
- p.Ch = bc.dataNum[i1][i][j]
- p.Fg = bc.NumColor[i1]
- p.Bg = bc.BarColor[i1]
+ c := Cell{
+ Ch: bc.dataNum[i1][i][j],
+ Fg: bc.NumColor[i1],
+ Bg: bc.BarColor[i1],
+ }
if bc.BarColor[i1] == ColorDefault { // the same as above
- p.Bg |= AttrReverse
+ c.Bg |= AttrReverse
}
if h == 0 {
- p.Bg = bc.BgColor
+ c.Bg = bc.Bg
}
- p.X = bc.innerX + oftX + (bc.BarWidth-len(bc.dataNum[i1][i]))/2 + j
- p.Y = bc.innerY + bc.innerHeight - 2 - ph
- ps = append(ps, p)
+ x := bc.innerArea.Min.X + oftX + (bc.BarWidth-len(bc.dataNum[i1][i]))/2 + j
+ y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2 - ph
+ buf.Set(x, y, c)
}
ph += h
}
@@ -208,26 +212,31 @@ func (bc *MBarChart) Buffer() []Point {
if bc.ShowScale {
//Currently bar graph only supprts data range from 0 to MAX
//Plot 0
- p := Point{}
- p.Ch = '0'
- p.Bg = bc.BgColor
- p.Fg = bc.TextColor
- p.Y = bc.innerY + bc.innerHeight - 2
- p.X = bc.X
- ps = append(ps, p)
+ c := Cell{
+ Ch: '0',
+ Bg: bc.Bg,
+ Fg: bc.TextColor,
+ }
+
+ y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2
+ x := bc.X
+ buf.Set(x, y, c)
//Plot the maximum sacle value
for i := 0; i < len(bc.maxScale); i++ {
- p := Point{}
- p.Ch = bc.maxScale[i]
- p.Bg = bc.BgColor
- p.Fg = bc.TextColor
- p.Y = bc.innerY
- p.X = bc.X + i
- ps = append(ps, p)
+ c := Cell{
+ Ch: bc.maxScale[i],
+ Bg: bc.Bg,
+ Fg: bc.TextColor,
+ }
+
+ y := bc.innerArea.Min.Y
+ x := bc.X + i
+
+ buf.Set(x, y, c)
}
}
- return bc.Block.chopOverflow(ps)
+ return buf
}
diff --git a/p.go b/p.go
deleted file mode 100644
index e327d74..0000000
--- a/p.go
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright 2015 Zack Guo . 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)
-}
diff --git a/par.go b/par.go
new file mode 100644
index 0000000..03a3c97
--- /dev/null
+++ b/par.go
@@ -0,0 +1,64 @@
+// Copyright 2015 Zack Guo . 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
+}
diff --git a/p_test.go b/par_test.go
similarity index 54%
rename from p_test.go
rename to par_test.go
index a7a9bb1..e689273 100644
--- a/p_test.go
+++ b/par_test.go
@@ -4,17 +4,17 @@ import "testing"
func TestPar_NoBorderBackground(t *testing.T) {
par := NewPar("a")
- par.HasBorder = false
- par.BgColor = ColorBlue
+ par.Border = false
+ par.Bg = ColorBlue
par.TextBgColor = ColorBlue
par.Width = 2
par.Height = 2
pts := par.Buffer()
- for _, p := range pts {
+ for _, p := range pts.CellMap {
t.Log(p)
- if p.Bg != par.BgColor {
- t.Errorf("expected color to be %v but got %v", par.BgColor, p.Bg)
+ if p.Bg != par.Bg {
+ t.Errorf("expected color to be %v but got %v", par.Bg, p.Bg)
}
}
}
diff --git a/point.go b/point.go
deleted file mode 100644
index c381af9..0000000
--- a/point.go
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright 2015 Zack Guo . 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
-}
diff --git a/pos.go b/pos.go
new file mode 100644
index 0000000..b26fd11
--- /dev/null
+++ b/pos.go
@@ -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())
+}
diff --git a/pos_test.go b/pos_test.go
new file mode 100644
index 0000000..0454345
--- /dev/null
+++ b/pos_test.go
@@ -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")
+ }
+}
diff --git a/render.go b/render.go
index d697d0a..0cf9b88 100644
--- a/render.go
+++ b/render.go
@@ -4,26 +4,54 @@
package termui
-import tm "github.com/nsf/termbox-go"
+import (
+ "time"
+
+ tm "github.com/nsf/termbox-go"
+)
// Bufferer should be implemented by all renderable components.
type Bufferer interface {
- Buffer() []Point
+ Buffer() Buffer
}
// Init initializes termui library. This function should be called before any others.
// After initialization, the library must be finalized by 'Close' function.
func Init() error {
+ if err := tm.Init(); err != nil {
+ return err
+ }
+
+ sysEvtChs = make([]chan Event, 0)
+ go hookTermboxEvt()
+
+ renderJobs = make(chan []Bufferer)
+ go func() {
+ for bs := range renderJobs {
+ render(bs...)
+ }
+ }()
+
Body = NewGrid()
Body.X = 0
Body.Y = 0
- Body.BgColor = theme.BodyBg
- defer func() {
- w, _ := tm.Size()
- Body.Width = w
- evtListen()
- }()
- return tm.Init()
+ Body.BgColor = ThemeAttr("bg")
+ Body.Width = TermWidth()
+
+ DefaultEvtStream.Init()
+ DefaultEvtStream.Merge("termbox", NewSysEvtCh())
+ DefaultEvtStream.Merge("timer", NewTimerCh(time.Second))
+ DefaultEvtStream.Merge("custom", usrEvtCh)
+
+ DefaultEvtStream.Handle("/", DefualtHandler)
+ DefaultEvtStream.Handle("/sys/wnd/resize", func(e Event) {
+ w := e.Data.(EvtWnd)
+ Body.Width = w.Width
+ })
+
+ DefaultWgtMgr = NewWgtMgr()
+ DefaultEvtStream.Hook(DefaultWgtMgr.WgtHandlersHook())
+ return nil
}
// Close finalizes termui library,
@@ -48,13 +76,24 @@ func TermHeight() int {
// Render renders all Bufferer in the given order from left to right,
// right could overlap on left ones.
-func Render(rs ...Bufferer) {
- tm.Clear(tm.ColorDefault, toTmAttr(theme.BodyBg))
- for _, r := range rs {
- buf := r.Buffer()
- for _, v := range buf {
- tm.SetCell(v.X, v.Y, v.Ch, toTmAttr(v.Fg), toTmAttr(v.Bg))
+func render(bs ...Bufferer) {
+ // set tm bg
+ tm.Clear(tm.ColorDefault, toTmAttr(ThemeAttr("bg")))
+ for _, b := range bs {
+ buf := b.Buffer()
+ // set cels in buf
+ for p, c := range buf.CellMap {
+ if p.In(buf.Area) {
+ tm.SetCell(p.X, p.Y, c.Ch, toTmAttr(c.Fg), toTmAttr(c.Bg))
+ }
}
}
+ // render
tm.Flush()
}
+
+var renderJobs chan []Bufferer
+
+func Render(bs ...Bufferer) {
+ go func() { renderJobs <- bs }()
+}
diff --git a/sparkline.go b/sparkline.go
index c63a585..02a1034 100644
--- a/sparkline.go
+++ b/sparkline.go
@@ -49,8 +49,8 @@ func (s *Sparklines) Add(sl Sparkline) {
func NewSparkline() Sparkline {
return Sparkline{
Height: 1,
- TitleColor: theme.SparklineTitle,
- LineColor: theme.SparklineLine}
+ TitleColor: ThemeAttr("sparkline.title.fg"),
+ LineColor: ThemeAttr("sparkline.line.fg")}
}
// NewSparklines return a new *Spaklines with given Sparkline(s), you can always add a new Sparkline later.
@@ -67,13 +67,13 @@ func (sl *Sparklines) update() {
sl.Lines[i].displayHeight = v.Height + 1
}
}
- sl.displayWidth = sl.innerWidth
+ sl.displayWidth = sl.innerArea.Dx()
// get how many lines gotta display
h := 0
sl.displayLines = 0
for _, v := range sl.Lines {
- if h+v.displayHeight <= sl.innerHeight {
+ if h+v.displayHeight <= sl.innerArea.Dy() {
sl.displayLines++
} else {
break
@@ -96,8 +96,8 @@ func (sl *Sparklines) update() {
}
// Buffer implements Bufferer interface.
-func (sl *Sparklines) Buffer() []Point {
- ps := sl.Block.Buffer()
+func (sl *Sparklines) Buffer() Buffer {
+ buf := sl.Block.Buffer()
sl.update()
oftY := 0
@@ -105,22 +105,23 @@ func (sl *Sparklines) Buffer() []Point {
l := sl.Lines[i]
data := l.Data
- if len(data) > sl.innerWidth {
- data = data[len(data)-sl.innerWidth:]
+ if len(data) > sl.innerArea.Dx() {
+ data = data[len(data)-sl.innerArea.Dx():]
}
if l.Title != "" {
- rs := trimStr2Runes(l.Title, sl.innerWidth)
+ rs := trimStr2Runes(l.Title, sl.innerArea.Dx())
oftX := 0
for _, v := range rs {
w := charWidth(v)
- p := Point{}
- p.Ch = v
- p.Fg = l.TitleColor
- p.Bg = sl.BgColor
- p.X = sl.innerX + oftX
- p.Y = sl.innerY + oftY
- ps = append(ps, p)
+ c := Cell{
+ Ch: v,
+ Fg: l.TitleColor,
+ Bg: sl.Bg,
+ }
+ x := sl.innerArea.Min.X + oftX
+ y := sl.innerArea.Min.Y + oftY
+ buf.Set(x, y, c)
oftX += w
}
}
@@ -130,27 +131,30 @@ func (sl *Sparklines) Buffer() []Point {
barCnt := h / 8
barMod := h % 8
for jj := 0; jj < barCnt; jj++ {
- p := Point{}
- p.X = sl.innerX + j
- p.Y = sl.innerY + oftY + l.Height - jj
- p.Ch = ' ' // => sparks[7]
- p.Bg = l.LineColor
+ c := Cell{
+ Ch: ' ', // => sparks[7]
+ Bg: l.LineColor,
+ }
+ x := sl.innerArea.Min.X + j
+ y := sl.innerArea.Min.Y + oftY + l.Height - jj
+
//p.Bg = sl.BgColor
- ps = append(ps, p)
+ buf.Set(x, y, c)
}
if barMod != 0 {
- p := Point{}
- p.X = sl.innerX + j
- p.Y = sl.innerY + oftY + l.Height - barCnt
- p.Ch = sparks[barMod-1]
- p.Fg = l.LineColor
- p.Bg = sl.BgColor
- ps = append(ps, p)
+ c := Cell{
+ Ch: sparks[barMod-1],
+ Fg: l.LineColor,
+ Bg: sl.Bg,
+ }
+ x := sl.innerArea.Min.X + j
+ y := sl.innerArea.Min.Y + oftY + l.Height - barCnt
+ buf.Set(x, y, c)
}
}
oftY += l.displayHeight
}
- return sl.Block.chopOverflow(ps)
+ return buf
}
diff --git a/test/runtest.go b/test/runtest.go
new file mode 100644
index 0000000..97caf83
--- /dev/null
+++ b/test/runtest.go
@@ -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()
+}
diff --git a/textbuilder.go b/textbuilder.go
new file mode 100644
index 0000000..79271fd
--- /dev/null
+++ b/textbuilder.go
@@ -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{}
+}
diff --git a/textbuilder_test.go b/textbuilder_test.go
new file mode 100644
index 0000000..93aa62e
--- /dev/null
+++ b/textbuilder_test.go
@@ -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")
+ }
+}
diff --git a/theme.go b/theme.go
index 0196231..7ee1fbb 100644
--- a/theme.go
+++ b/theme.go
@@ -4,6 +4,9 @@
package termui
+import "strings"
+
+/*
// A ColorScheme represents the current look-and-feel of the dashboard.
type ColorScheme struct {
BodyBg Attribute
@@ -84,3 +87,54 @@ func UseTheme(th string) {
theme = themeDefault
}
}
+*/
+
+var ColorMap = map[string]Attribute{
+ "fg": ColorWhite,
+ "bg": ColorDefault,
+ "border.fg": ColorWhite,
+ "label.fg": ColorGreen,
+ "par.fg": ColorYellow,
+ "par.label.bg": ColorWhite,
+}
+
+func ThemeAttr(name string) Attribute {
+ return lookUpAttr(ColorMap, name)
+}
+
+func lookUpAttr(clrmap map[string]Attribute, name string) Attribute {
+
+ a, ok := clrmap[name]
+ if ok {
+ return a
+ }
+
+ ns := strings.Split(name, ".")
+ for i := range ns {
+ nn := strings.Join(ns[i:len(ns)], ".")
+ a, ok = ColorMap[nn]
+ if ok {
+ break
+ }
+ }
+
+ return a
+}
+
+// 0<=r,g,b <= 5
+func ColorRGB(r, g, b int) Attribute {
+ within := func(n int) int {
+ if n < 0 {
+ return 0
+ }
+
+ if n > 5 {
+ return 5
+ }
+
+ return n
+ }
+
+ r, b, g = within(r), within(b), within(g)
+ return Attribute(0x0f + 36*r + 6*g + b)
+}
diff --git a/theme_test.go b/theme_test.go
new file mode 100644
index 0000000..b488a09
--- /dev/null
+++ b/theme_test.go
@@ -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)
+ }
+ }
+}
diff --git a/widget.go b/widget.go
new file mode 100644
index 0000000..df15b5d
--- /dev/null
+++ b/widget.go
@@ -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)
+}