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! list +#### 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) +}