mirror of
https://github.com/mum4k/termdash.git
synced 2025-04-25 13:48:50 +08:00
Merge branch '10-braille-canvas-copyto' into 20-braille-canvas
This commit is contained in:
commit
7c240faf4f
18
README.md
18
README.md
@ -81,7 +81,23 @@ Displays the progress of an operation. Run the
|
||||
Displays text content, supports trimming and scrolling of content. Run the
|
||||
[textdemo](widgets/text/demo/textdemo.go).
|
||||
|
||||
[<img src="./images/textdemo.gif" alt="gaugedemo" type="image/gif">](widgets/gauge/demo/gaugedemo.go)
|
||||
[<img src="./images/textdemo.gif" alt="textdemo" type="image/gif">](widgets/gauge/demo/gaugedemo.go)
|
||||
|
||||
### The SparkLine
|
||||
|
||||
Draws a graph showing a series of values as vertical bars. The bars can have
|
||||
sub-cell height. Run the
|
||||
[sparklinedemo](widgets/sparkline/sparklinedemo/sparklinedemo.go).
|
||||
|
||||
[<img src="./images/sparklinedemo.gif" alt="sparklinedemo" type="image/gif">](widgets/sparkline/sparklinedemo/sparklinedemo.go)
|
||||
|
||||
### The BarChart
|
||||
|
||||
Displays multiple bars showing relative ratios of values. Run the
|
||||
[barchartdemo](widgets/barchart/barchartdemo/barchartdemo.go).
|
||||
|
||||
[<img src="./images/barchartdemo.gif" alt="barchartdemo" type="image/gif">](widgets/barchart/barchartdemo/barchartdemo.go)
|
||||
|
||||
|
||||
## Disclaimer
|
||||
|
||||
|
BIN
images/barchartdemo.gif
Normal file
BIN
images/barchartdemo.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 160 KiB |
BIN
images/sparklinedemo.gif
Normal file
BIN
images/sparklinedemo.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 292 KiB |
@ -95,9 +95,12 @@ func MouseSubscriber(f func(*terminalapi.Mouse)) Option {
|
||||
// Blocks until the context expires.
|
||||
func Run(ctx context.Context, t terminalapi.Terminal, c *container.Container, opts ...Option) error {
|
||||
td := newTermdash(t, c, opts...)
|
||||
defer td.stop()
|
||||
|
||||
return td.start(ctx)
|
||||
err := td.start(ctx)
|
||||
// Only return the status (error or nil) after the termdash event
|
||||
// processing goroutine actually exits.
|
||||
td.stop()
|
||||
return err
|
||||
}
|
||||
|
||||
// Controller controls a termdash instance.
|
||||
|
76
widgets/sparkline/options.go
Normal file
76
widgets/sparkline/options.go
Normal file
@ -0,0 +1,76 @@
|
||||
// Copyright 2018 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sparkline
|
||||
|
||||
// options.go contains configurable options for SparkLine.
|
||||
|
||||
import "github.com/mum4k/termdash/cell"
|
||||
|
||||
// Option is used to provide options.
|
||||
type Option interface {
|
||||
// set sets the provided option.
|
||||
set(*options)
|
||||
}
|
||||
|
||||
// option implements Option.
|
||||
type option func(*options)
|
||||
|
||||
// set implements Option.set.
|
||||
func (o option) set(opts *options) {
|
||||
o(opts)
|
||||
}
|
||||
|
||||
// options holds the provided options.
|
||||
type options struct {
|
||||
label string
|
||||
labelCellOpts []cell.Option
|
||||
height int
|
||||
color cell.Color
|
||||
}
|
||||
|
||||
// newOptions returns options with the default values set.
|
||||
func newOptions() *options {
|
||||
return &options{
|
||||
color: DefaultColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Label adds a label above the SparkLine.
|
||||
func Label(text string, cOpts ...cell.Option) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.label = text
|
||||
opts.labelCellOpts = cOpts
|
||||
})
|
||||
}
|
||||
|
||||
// Height sets a fixed height for the SparkLine.
|
||||
// If not provided, the SparkLine takes all the available vertical space in the
|
||||
// container.
|
||||
func Height(h int) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.height = h
|
||||
})
|
||||
}
|
||||
|
||||
// DefaultColor is the default value for the Color option.
|
||||
const DefaultColor = cell.ColorGreen
|
||||
|
||||
// Color sets the color of the SparkLine.
|
||||
// Defaults to DefaultColor if not set.
|
||||
func Color(c cell.Color) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.color = c
|
||||
})
|
||||
}
|
223
widgets/sparkline/sparkline.go
Normal file
223
widgets/sparkline/sparkline.go
Normal file
@ -0,0 +1,223 @@
|
||||
// Copyright 2018 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package sparkline is a widget that draws a graph showing a series of values as vertical bars.
|
||||
package sparkline
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"sync"
|
||||
|
||||
"github.com/mum4k/termdash/canvas"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/draw"
|
||||
"github.com/mum4k/termdash/terminalapi"
|
||||
"github.com/mum4k/termdash/widgetapi"
|
||||
)
|
||||
|
||||
// SparkLine draws a graph showing a series of values as vertical bars.
|
||||
//
|
||||
// Bars can have sub-cell height. The graphs scale adjusts dynamically based on
|
||||
// the largest visible value.
|
||||
//
|
||||
// Implements widgetapi.Widget. This object is thread-safe.
|
||||
type SparkLine struct {
|
||||
// data are the data points the SparkLine displays.
|
||||
data []int
|
||||
|
||||
// mu protects the SparkLine.
|
||||
mu sync.Mutex
|
||||
|
||||
// opts are the provided options.
|
||||
opts *options
|
||||
}
|
||||
|
||||
// New returns a new SparkLine.
|
||||
func New(opts ...Option) *SparkLine {
|
||||
opt := newOptions()
|
||||
for _, o := range opts {
|
||||
o.set(opt)
|
||||
}
|
||||
return &SparkLine{
|
||||
opts: opt,
|
||||
}
|
||||
}
|
||||
|
||||
// Draw draws the SparkLine widget onto the canvas.
|
||||
// Implements widgetapi.Widget.Draw.
|
||||
func (sl *SparkLine) Draw(cvs *canvas.Canvas) error {
|
||||
sl.mu.Lock()
|
||||
defer sl.mu.Unlock()
|
||||
|
||||
ar := sl.area(cvs)
|
||||
visible, max := visibleMax(sl.data, ar.Dx())
|
||||
var curX int
|
||||
if len(visible) < ar.Dx() {
|
||||
curX = ar.Max.X - len(visible)
|
||||
} else {
|
||||
curX = ar.Min.X
|
||||
}
|
||||
|
||||
for _, v := range visible {
|
||||
blocks := toBlocks(v, max, ar.Dy())
|
||||
curY := ar.Max.Y - 1
|
||||
for i := 0; i < blocks.full; i++ {
|
||||
if _, err := cvs.SetCell(
|
||||
image.Point{curX, curY},
|
||||
sparks[len(sparks)-1], // Last spark represents full cell.
|
||||
cell.FgColor(sl.opts.color),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
curY--
|
||||
}
|
||||
|
||||
if blocks.partSpark != 0 {
|
||||
if _, err := cvs.SetCell(
|
||||
image.Point{curX, curY},
|
||||
blocks.partSpark,
|
||||
cell.FgColor(sl.opts.color),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
curX++
|
||||
}
|
||||
|
||||
if sl.opts.label != "" {
|
||||
// Label is placed immediately above the SparkLine.
|
||||
lStart := image.Point{ar.Min.X, ar.Min.Y - 1}
|
||||
if err := draw.Text(cvs, sl.opts.label, lStart,
|
||||
draw.TextCellOpts(sl.opts.labelCellOpts...),
|
||||
draw.TextOverrunMode(draw.OverrunModeThreeDot),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add adds data points to the SparkLine.
|
||||
// Each data point is represented by one bar on the SparkLine. Zero value data
|
||||
// points are valid and are represented by an empty space on the SparkLine
|
||||
// (i.e. a missing bar).
|
||||
//
|
||||
// At least one data point must be provided. All data points must be positive
|
||||
// integers.
|
||||
//
|
||||
// The last added data point will be the one displayed all the way on the right
|
||||
// of the SparkLine. If there are more data points than we can fit bars to the
|
||||
// width of the SparkLine, only the last n data points that fit will be
|
||||
// visible.
|
||||
//
|
||||
// Provided options override values set when New() was called.
|
||||
func (sl *SparkLine) Add(data []int, opts ...Option) error {
|
||||
sl.mu.Lock()
|
||||
defer sl.mu.Unlock()
|
||||
|
||||
for _, opt := range opts {
|
||||
opt.set(sl.opts)
|
||||
}
|
||||
|
||||
for i, d := range data {
|
||||
if d < 0 {
|
||||
return fmt.Errorf("data point[%d]: %v must be a positive integer", i, d)
|
||||
}
|
||||
}
|
||||
sl.data = append(sl.data, data...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clear removes all the data points in the SparkLine, effectively returning to
|
||||
// an empty graph.
|
||||
func (sl *SparkLine) Clear() {
|
||||
sl.mu.Lock()
|
||||
defer sl.mu.Unlock()
|
||||
|
||||
sl.data = nil
|
||||
}
|
||||
|
||||
// Keyboard input isn't supported on the SparkLine widget.
|
||||
func (*SparkLine) Keyboard(k *terminalapi.Keyboard) error {
|
||||
return errors.New("the SparkLine widget doesn't support keyboard events")
|
||||
}
|
||||
|
||||
// Mouse input isn't supported on the SparkLine widget.
|
||||
func (*SparkLine) Mouse(m *terminalapi.Mouse) error {
|
||||
return errors.New("the SparkLine widget doesn't support mouse events")
|
||||
}
|
||||
|
||||
// area returns the area of the canvas available to the SparkLine.
|
||||
func (sl *SparkLine) area(cvs *canvas.Canvas) image.Rectangle {
|
||||
cvsAr := cvs.Area()
|
||||
maxY := cvsAr.Max.Y
|
||||
|
||||
// Height is determined based on options (fixed height / label).
|
||||
var minY int
|
||||
if sl.opts.height > 0 {
|
||||
minY = maxY - sl.opts.height
|
||||
} else {
|
||||
minY = cvsAr.Min.Y
|
||||
|
||||
if sl.opts.label != "" {
|
||||
minY++ // Reserve one line for the label.
|
||||
}
|
||||
}
|
||||
return image.Rect(
|
||||
cvsAr.Min.X,
|
||||
minY,
|
||||
cvsAr.Max.X,
|
||||
maxY,
|
||||
)
|
||||
}
|
||||
|
||||
// minSize returns the minimum canvas size for the SparkLine based on the options.
|
||||
func (sl *SparkLine) minSize() image.Point {
|
||||
const minWidth = 1 // At least one data point.
|
||||
|
||||
var minHeight int
|
||||
if sl.opts.height > 0 {
|
||||
minHeight = sl.opts.height
|
||||
} else {
|
||||
minHeight = 1 // At least one line of characters.
|
||||
}
|
||||
|
||||
if sl.opts.label != "" {
|
||||
minHeight++ // One line for the text label.
|
||||
}
|
||||
return image.Point{minWidth, minHeight}
|
||||
}
|
||||
|
||||
// Options implements widgetapi.Widget.Options.
|
||||
func (sl *SparkLine) Options() widgetapi.Options {
|
||||
sl.mu.Lock()
|
||||
defer sl.mu.Unlock()
|
||||
|
||||
min := sl.minSize()
|
||||
var max image.Point
|
||||
if sl.opts.height > 0 {
|
||||
max = min // Fix the height to the one specified.
|
||||
}
|
||||
|
||||
return widgetapi.Options{
|
||||
MinimumSize: min,
|
||||
MaximumSize: max,
|
||||
WantKeyboard: false,
|
||||
WantMouse: false,
|
||||
}
|
||||
}
|
493
widgets/sparkline/sparkline_test.go
Normal file
493
widgets/sparkline/sparkline_test.go
Normal file
@ -0,0 +1,493 @@
|
||||
// Copyright 2018 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sparkline
|
||||
|
||||
import (
|
||||
"image"
|
||||
"testing"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
"github.com/mum4k/termdash/canvas"
|
||||
"github.com/mum4k/termdash/canvas/testcanvas"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/draw"
|
||||
"github.com/mum4k/termdash/draw/testdraw"
|
||||
"github.com/mum4k/termdash/terminal/faketerm"
|
||||
"github.com/mum4k/termdash/widgetapi"
|
||||
)
|
||||
|
||||
func TestSparkLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
sparkLine *SparkLine
|
||||
update func(*SparkLine) error // update gets called before drawing of the widget.
|
||||
canvas image.Rectangle
|
||||
want func(size image.Point) *faketerm.Terminal
|
||||
wantUpdateErr bool // whether to expect an error on a call to the update function
|
||||
wantDrawErr bool
|
||||
}{
|
||||
{
|
||||
desc: "draws empty for no data points",
|
||||
sparkLine: New(),
|
||||
update: func(sl *SparkLine) error {
|
||||
return nil
|
||||
},
|
||||
canvas: image.Rect(0, 0, 1, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
return faketerm.MustNew(size)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "fails on negative data points",
|
||||
sparkLine: New(),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add([]int{0, 3, -1, 2})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 1, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
return faketerm.MustNew(size)
|
||||
},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
{
|
||||
desc: "single height sparkline",
|
||||
sparkLine: New(),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add([]int{0, 1, 2, 3, 4, 5, 6, 7, 8})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 9, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustText(c, "▁▂▃▄▅▆▇█", image.Point{1, 0}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "sparkline can be cleared",
|
||||
sparkLine: New(),
|
||||
update: func(sl *SparkLine) error {
|
||||
if err := sl.Add([]int{0, 1, 2, 3, 4, 5, 6, 7, 8}); err != nil {
|
||||
return err
|
||||
}
|
||||
sl.Clear()
|
||||
return nil
|
||||
},
|
||||
canvas: image.Rect(0, 0, 9, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
return faketerm.MustNew(size)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "sets sparkline color",
|
||||
sparkLine: New(
|
||||
Color(cell.ColorMagenta),
|
||||
),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add([]int{0, 1, 2, 3, 4, 5, 6, 7, 8})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 9, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustText(c, "▁▂▃▄▅▆▇█", image.Point{1, 0}, draw.TextCellOpts(
|
||||
cell.FgColor(cell.ColorMagenta),
|
||||
))
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "sets sparkline color on a call to Add",
|
||||
sparkLine: New(),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add([]int{0, 1, 2, 3, 4, 5, 6, 7, 8}, Color(cell.ColorMagenta))
|
||||
},
|
||||
canvas: image.Rect(0, 0, 9, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustText(c, "▁▂▃▄▅▆▇█", image.Point{1, 0}, draw.TextCellOpts(
|
||||
cell.FgColor(cell.ColorMagenta),
|
||||
))
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
desc: "draws data points from the right",
|
||||
sparkLine: New(),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add([]int{7, 8})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 9, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustText(c, "▇█", image.Point{7, 0}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "single height sparkline with label",
|
||||
sparkLine: New(
|
||||
Label("Hello"),
|
||||
),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add([]int{0, 1, 2, 3, 8, 3, 2, 1, 1})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 9, 2),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustText(c, "Hello", image.Point{0, 0})
|
||||
testdraw.MustText(c, "▁▂▃█▃▂▁▁", image.Point{1, 1}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "too long label is trimmed",
|
||||
sparkLine: New(
|
||||
Label("Hello world"),
|
||||
),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add([]int{8})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 9, 2),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustText(c, "Hello wo…", image.Point{0, 0})
|
||||
testdraw.MustText(c, "█", image.Point{8, 1}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "stretches up to the height of the container",
|
||||
sparkLine: New(),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add([]int{0, 100, 50, 85})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 4, 4),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustText(c, "█", image.Point{1, 0}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
testdraw.MustText(c, "▃", image.Point{3, 0}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
testdraw.MustText(c, "█", image.Point{1, 1}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
testdraw.MustText(c, "█", image.Point{3, 1}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
testdraw.MustText(c, "███", image.Point{1, 2}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
testdraw.MustText(c, "███", image.Point{1, 3}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "stretches up to the height of the container with label",
|
||||
sparkLine: New(
|
||||
Label("zoo"),
|
||||
),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add([]int{0, 90, 30, 85})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 4, 4),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustText(c, "zoo", image.Point{0, 0})
|
||||
testdraw.MustText(c, "█", image.Point{1, 1}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
testdraw.MustText(c, "▇", image.Point{3, 1}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
testdraw.MustText(c, "█", image.Point{1, 2}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
testdraw.MustText(c, "█", image.Point{3, 2}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
testdraw.MustText(c, "███", image.Point{1, 3}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "respects fixed height",
|
||||
sparkLine: New(
|
||||
Height(2),
|
||||
),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add([]int{0, 100, 50, 85})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 4, 4),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustText(c, "█", image.Point{1, 2}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
testdraw.MustText(c, "▆", image.Point{3, 2}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
testdraw.MustText(c, "███", image.Point{1, 3}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "respects fixed height with label",
|
||||
sparkLine: New(
|
||||
Label("zoo"),
|
||||
Height(2),
|
||||
),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add([]int{0, 100, 50, 0})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 4, 4),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustText(c, "zoo", image.Point{0, 1}, draw.TextCellOpts(
|
||||
cell.FgColor(cell.ColorDefault),
|
||||
))
|
||||
testdraw.MustText(c, "█", image.Point{1, 2}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
testdraw.MustText(c, "██", image.Point{1, 3}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "sets label color",
|
||||
sparkLine: New(
|
||||
Label(
|
||||
"Hello",
|
||||
cell.FgColor(cell.ColorBlue),
|
||||
cell.BgColor(cell.ColorYellow),
|
||||
),
|
||||
),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add([]int{0, 1})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 9, 2),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustText(c, "Hello", image.Point{0, 0}, draw.TextCellOpts(
|
||||
cell.FgColor(cell.ColorBlue),
|
||||
cell.BgColor(cell.ColorYellow),
|
||||
))
|
||||
testdraw.MustText(c, "█", image.Point{8, 1}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "displays only data points that fit the width",
|
||||
sparkLine: New(),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add([]int{0, 1, 2, 3, 4, 5, 6, 7, 8})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 3, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustText(c, "▆▇█", image.Point{0, 0}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "data points not visible don't affect the determined max data point",
|
||||
sparkLine: New(),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add([]int{10, 4, 8})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 2, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustText(c, "▄█", image.Point{0, 0}, draw.TextCellOpts(
|
||||
cell.FgColor(DefaultColor),
|
||||
))
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
c, err := canvas.New(tc.canvas)
|
||||
if err != nil {
|
||||
t.Fatalf("canvas.New => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
err = tc.update(tc.sparkLine)
|
||||
if (err != nil) != tc.wantUpdateErr {
|
||||
t.Errorf("update => unexpected error: %v, wantUpdateErr: %v", err, tc.wantUpdateErr)
|
||||
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = tc.sparkLine.Draw(c)
|
||||
if (err != nil) != tc.wantDrawErr {
|
||||
t.Errorf("Draw => unexpected error: %v, wantDrawErr: %v", err, tc.wantDrawErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
got, err := faketerm.New(c.Size())
|
||||
if err != nil {
|
||||
t.Fatalf("faketerm.New => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if err := c.Apply(got); err != nil {
|
||||
t.Fatalf("Apply => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if diff := faketerm.Diff(tc.want(c.Size()), got); diff != "" {
|
||||
t.Errorf("Draw => %v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
sparkLine *SparkLine
|
||||
want widgetapi.Options
|
||||
}{
|
||||
{
|
||||
desc: "no label and no fixed height",
|
||||
sparkLine: New(),
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{1, 1},
|
||||
WantKeyboard: false,
|
||||
WantMouse: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "label and no fixed height",
|
||||
sparkLine: New(
|
||||
Label("foo"),
|
||||
),
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{1, 2},
|
||||
WantKeyboard: false,
|
||||
WantMouse: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "no label and fixed height",
|
||||
sparkLine: New(
|
||||
Height(3),
|
||||
),
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{1, 3},
|
||||
MaximumSize: image.Point{1, 3},
|
||||
WantKeyboard: false,
|
||||
WantMouse: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "label and fixed height",
|
||||
sparkLine: New(
|
||||
Label("foo"),
|
||||
Height(3),
|
||||
),
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{1, 4},
|
||||
MaximumSize: image.Point{1, 4},
|
||||
WantKeyboard: false,
|
||||
WantMouse: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := tc.sparkLine.Options()
|
||||
if diff := pretty.Compare(tc.want, got); diff != "" {
|
||||
t.Errorf("Options => unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
116
widgets/sparkline/sparklinedemo/sparklinedemo.go
Normal file
116
widgets/sparkline/sparklinedemo/sparklinedemo.go
Normal file
@ -0,0 +1,116 @@
|
||||
// Copyright 2018 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Binary sparklinedemo displays a couple of SparkLine widgets.
|
||||
// Exist when 'q' is pressed.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/mum4k/termdash"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/container"
|
||||
"github.com/mum4k/termdash/draw"
|
||||
"github.com/mum4k/termdash/terminal/termbox"
|
||||
"github.com/mum4k/termdash/terminalapi"
|
||||
"github.com/mum4k/termdash/widgets/sparkline"
|
||||
)
|
||||
|
||||
// playSparkLine continuously adds values to the SparkLine, once every delay.
|
||||
// Exits when the context expires.
|
||||
func playSparkLine(ctx context.Context, sl *sparkline.SparkLine, delay time.Duration) {
|
||||
const max = 100
|
||||
|
||||
ticker := time.NewTicker(delay)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
v := int(rand.Int31n(max + 1))
|
||||
if err := sl.Add([]int{v}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
t, err := termbox.New()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer t.Close()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
green := sparkline.New(
|
||||
sparkline.Label("Green SparkLine", cell.FgColor(cell.ColorBlue)),
|
||||
sparkline.Color(cell.ColorGreen),
|
||||
)
|
||||
go playSparkLine(ctx, green, 250*time.Millisecond)
|
||||
red := sparkline.New(
|
||||
sparkline.Label("Red SparkLine", cell.FgColor(cell.ColorBlue)),
|
||||
sparkline.Color(cell.ColorRed),
|
||||
)
|
||||
go playSparkLine(ctx, red, 500*time.Millisecond)
|
||||
yellow := sparkline.New(
|
||||
sparkline.Label("Yellow SparkLine", cell.FgColor(cell.ColorGreen)),
|
||||
sparkline.Color(cell.ColorYellow),
|
||||
)
|
||||
go playSparkLine(ctx, yellow, 1*time.Second)
|
||||
|
||||
c := container.New(
|
||||
t,
|
||||
container.Border(draw.LineStyleLight),
|
||||
container.BorderTitle("PRESS Q TO QUIT"),
|
||||
container.SplitVertical(
|
||||
container.Left(
|
||||
container.SplitHorizontal(
|
||||
container.Top(),
|
||||
container.Bottom(
|
||||
container.Border(draw.LineStyleLight),
|
||||
container.BorderTitle("SparkLine group"),
|
||||
container.SplitHorizontal(
|
||||
container.Top(
|
||||
container.PlaceWidget(green),
|
||||
),
|
||||
container.Bottom(
|
||||
container.PlaceWidget(red),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
container.Right(
|
||||
container.Border(draw.LineStyleLight),
|
||||
container.PlaceWidget(yellow),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
quitter := func(k *terminalapi.Keyboard) {
|
||||
if k.Key == 'q' || k.Key == 'Q' {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
if err := termdash.Run(ctx, t, c, termdash.KeyboardSubscriber(quitter)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
111
widgets/sparkline/sparks.go
Normal file
111
widgets/sparkline/sparks.go
Normal file
@ -0,0 +1,111 @@
|
||||
// Copyright 2018 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sparkline
|
||||
|
||||
// sparks.go contains code that determines which characters should be used to
|
||||
// represent a value on the SparkLine.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
runewidth "github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
// sparks are the characters used to draw the SparkLine.
|
||||
var sparks = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
||||
|
||||
// visibleMax determines the maximum visible data point given the canvas width.
|
||||
// Returns a slice that contains only visible data points and the maximum value
|
||||
// among them.
|
||||
func visibleMax(data []int, width int) ([]int, int) {
|
||||
if width <= 0 || len(data) == 0 {
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
if width < len(data) {
|
||||
data = data[len(data)-width:]
|
||||
}
|
||||
|
||||
var max int
|
||||
for _, v := range data {
|
||||
if v > max {
|
||||
max = v
|
||||
}
|
||||
}
|
||||
return data, max
|
||||
}
|
||||
|
||||
// blocks represents the building blocks that display one value on a SparkLine.
|
||||
// I.e. one vertical bar.
|
||||
type blocks struct {
|
||||
// full is the number of fully populated blocks.
|
||||
full int
|
||||
|
||||
// partSpark is the spark character from sparks that should be used in the
|
||||
// topmost block. Equals to zero if no partial block should be displayed.
|
||||
partSpark rune
|
||||
}
|
||||
|
||||
// toBlocks determines the number of full and partial vertical blocks required
|
||||
// to represent the provided value given the specified max visible value and
|
||||
// number of vertical cells available to the SparkLine.
|
||||
func toBlocks(value, max, vertCells int) blocks {
|
||||
if value <= 0 || max <= 0 || vertCells <= 0 {
|
||||
return blocks{}
|
||||
}
|
||||
|
||||
// How many of the smallest spark elements fit into a cell.
|
||||
cellSparks := len(sparks)
|
||||
|
||||
// Scale is how much of the max does one smallest spark element represent,
|
||||
// given the vertical cells that will be used to represent the value.
|
||||
scale := float64(cellSparks) * float64(vertCells) / float64(max)
|
||||
|
||||
// How many smallest spark elements are needed to represent the value.
|
||||
elements := int(round(float64(value) * scale))
|
||||
|
||||
b := blocks{
|
||||
full: elements / cellSparks,
|
||||
}
|
||||
|
||||
part := elements % cellSparks
|
||||
if part > 0 {
|
||||
b.partSpark = sparks[part-1]
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// round returns the nearest integer, rounding half away from zero.
|
||||
// Copied from the math package of Go 1.10 for backwards compatibility with Go
|
||||
// 1.8 where the math.Round function doesn't exist yet.
|
||||
func round(x float64) float64 {
|
||||
t := math.Trunc(x)
|
||||
if math.Abs(x-t) >= 0.5 {
|
||||
return t + math.Copysign(1, x)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// init ensures that all spark characters are half-width runes.
|
||||
// The SparkLine widget assumes that each value can be represented in a column
|
||||
// that has a width of one cell.
|
||||
func init() {
|
||||
for i, s := range sparks {
|
||||
if got := runewidth.RuneWidth(s); got > 1 {
|
||||
panic(fmt.Sprintf("all sparks must be half-width runes (width of one), spark[%d] has width %d", i, got))
|
||||
}
|
||||
}
|
||||
}
|
270
widgets/sparkline/sparks_test.go
Normal file
270
widgets/sparkline/sparks_test.go
Normal file
@ -0,0 +1,270 @@
|
||||
// Copyright 2018 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sparkline
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
)
|
||||
|
||||
func TestVisibleMax(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
data []int
|
||||
width int
|
||||
wantData []int
|
||||
wantMax int
|
||||
}{
|
||||
{
|
||||
desc: "zero for no data",
|
||||
width: 3,
|
||||
wantData: nil,
|
||||
wantMax: 0,
|
||||
},
|
||||
{
|
||||
desc: "zero for zero width",
|
||||
data: []int{0, 1},
|
||||
width: 0,
|
||||
wantData: nil,
|
||||
wantMax: 0,
|
||||
},
|
||||
{
|
||||
desc: "zero for negative width",
|
||||
data: []int{0, 1},
|
||||
width: -1,
|
||||
wantData: nil,
|
||||
wantMax: 0,
|
||||
},
|
||||
{
|
||||
desc: "all values are zero",
|
||||
data: []int{0, 0, 0},
|
||||
width: 3,
|
||||
wantData: []int{0, 0, 0},
|
||||
wantMax: 0,
|
||||
},
|
||||
{
|
||||
desc: "all values are visible",
|
||||
data: []int{8, 0, 1},
|
||||
width: 3,
|
||||
wantData: []int{8, 0, 1},
|
||||
wantMax: 8,
|
||||
},
|
||||
{
|
||||
desc: "width greater than number of values",
|
||||
data: []int{8, 0, 1},
|
||||
width: 10,
|
||||
wantData: []int{8, 0, 1},
|
||||
wantMax: 8,
|
||||
},
|
||||
{
|
||||
desc: "only some values are visible",
|
||||
data: []int{8, 2, 1},
|
||||
width: 2,
|
||||
wantData: []int{2, 1},
|
||||
wantMax: 2,
|
||||
},
|
||||
{
|
||||
desc: "only one value is visible",
|
||||
data: []int{8, 2, 1},
|
||||
width: 1,
|
||||
wantData: []int{1},
|
||||
wantMax: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
gotData, gotMax := visibleMax(tc.data, tc.width)
|
||||
if diff := pretty.Compare(tc.wantData, gotData); diff != "" {
|
||||
t.Errorf("visibleMax => unexpected visible data, diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
if gotMax != tc.wantMax {
|
||||
t.Errorf("visibleMax => gotMax %v, wantMax %v", gotMax, tc.wantMax)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToBlocks(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
value int
|
||||
max int
|
||||
vertCells int
|
||||
want blocks
|
||||
}{
|
||||
{
|
||||
desc: "zero value has no blocks",
|
||||
value: 0,
|
||||
max: 10,
|
||||
vertCells: 2,
|
||||
want: blocks{},
|
||||
},
|
||||
{
|
||||
desc: "negative value has no blocks",
|
||||
value: -1,
|
||||
max: 10,
|
||||
vertCells: 2,
|
||||
want: blocks{},
|
||||
},
|
||||
{
|
||||
desc: "zero max has no blocks",
|
||||
value: 10,
|
||||
max: 0,
|
||||
vertCells: 2,
|
||||
want: blocks{},
|
||||
},
|
||||
{
|
||||
desc: "negative max has no blocks",
|
||||
value: 10,
|
||||
max: -1,
|
||||
vertCells: 2,
|
||||
want: blocks{},
|
||||
},
|
||||
{
|
||||
desc: "zero vertCells has no blocks",
|
||||
value: 10,
|
||||
max: 10,
|
||||
vertCells: 0,
|
||||
want: blocks{},
|
||||
},
|
||||
{
|
||||
desc: "negative vertCells has no blocks",
|
||||
value: 10,
|
||||
max: 10,
|
||||
vertCells: -1,
|
||||
want: blocks{},
|
||||
},
|
||||
{
|
||||
desc: "single line, zero value",
|
||||
value: 0,
|
||||
max: 8,
|
||||
vertCells: 1,
|
||||
want: blocks{},
|
||||
},
|
||||
{
|
||||
desc: "single line, value is 1/8",
|
||||
value: 1,
|
||||
max: 8,
|
||||
vertCells: 1,
|
||||
want: blocks{full: 0, partSpark: sparks[0]},
|
||||
},
|
||||
{
|
||||
desc: "single line, value is 2/8",
|
||||
value: 2,
|
||||
max: 8,
|
||||
vertCells: 1,
|
||||
want: blocks{full: 0, partSpark: sparks[1]},
|
||||
},
|
||||
{
|
||||
desc: "single line, value is 3/8",
|
||||
value: 3,
|
||||
max: 8,
|
||||
vertCells: 1,
|
||||
want: blocks{full: 0, partSpark: sparks[2]},
|
||||
},
|
||||
{
|
||||
desc: "single line, value is 4/8",
|
||||
value: 4,
|
||||
max: 8,
|
||||
vertCells: 1,
|
||||
want: blocks{full: 0, partSpark: sparks[3]},
|
||||
},
|
||||
{
|
||||
desc: "single line, value is 5/8",
|
||||
value: 5,
|
||||
max: 8,
|
||||
vertCells: 1,
|
||||
want: blocks{full: 0, partSpark: sparks[4]},
|
||||
},
|
||||
{
|
||||
desc: "single line, value is 6/8",
|
||||
value: 6,
|
||||
max: 8,
|
||||
vertCells: 1,
|
||||
want: blocks{full: 0, partSpark: sparks[5]},
|
||||
},
|
||||
{
|
||||
desc: "single line, value is 7/8",
|
||||
value: 7,
|
||||
max: 8,
|
||||
vertCells: 1,
|
||||
want: blocks{full: 0, partSpark: sparks[6]},
|
||||
},
|
||||
{
|
||||
desc: "single line, value is 8/8",
|
||||
value: 8,
|
||||
max: 8,
|
||||
vertCells: 1,
|
||||
want: blocks{full: 1, partSpark: 0},
|
||||
},
|
||||
{
|
||||
desc: "multi line, zero value",
|
||||
value: 0,
|
||||
max: 24,
|
||||
vertCells: 3,
|
||||
want: blocks{},
|
||||
},
|
||||
{
|
||||
desc: "multi line, lowest block is partial",
|
||||
value: 2,
|
||||
max: 24,
|
||||
vertCells: 3,
|
||||
want: blocks{full: 0, partSpark: sparks[1]},
|
||||
},
|
||||
{
|
||||
desc: "multi line, two full blocks, no partial block",
|
||||
value: 16,
|
||||
max: 24,
|
||||
vertCells: 3,
|
||||
want: blocks{full: 2, partSpark: 0},
|
||||
},
|
||||
{
|
||||
desc: "multi line, topmost block is partial",
|
||||
value: 20,
|
||||
max: 24,
|
||||
vertCells: 3,
|
||||
want: blocks{full: 2, partSpark: sparks[3]},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := toBlocks(tc.value, tc.max, tc.vertCells)
|
||||
if diff := pretty.Compare(tc.want, got); diff != "" {
|
||||
t.Errorf("toBlocks => unexpected diff (-want, +got):\n%s", diff)
|
||||
if got.full != tc.want.full {
|
||||
t.Errorf("toBlocks => unexpected diff, blocks.full got %d, want %d", got.full, tc.want.full)
|
||||
}
|
||||
if got.partSpark != tc.want.partSpark {
|
||||
t.Errorf("toBlocks => unexpected diff, blocks.partSpark got '%c' (sparks[%d])), want '%c' (sparks[%d])",
|
||||
got.partSpark, findRune(got.partSpark, sparks), tc.want.partSpark, findRune(tc.want.partSpark, sparks))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// findRune finds the rune in the slice and returns its index.
|
||||
// Returns -1 if the rune isn't in the slice.
|
||||
func findRune(target rune, runes []rune) int {
|
||||
for i, r := range runes {
|
||||
if r == target {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user