mirror of
https://github.com/mum4k/termdash.git
synced 2025-04-28 13:48:51 +08:00
Skeleton of the dotseg package.
This commit is contained in:
parent
27c830cebf
commit
f81cb554b0
223
internal/segdisp/dotseg/dotseg.go
Normal file
223
internal/segdisp/dotseg/dotseg.go
Normal file
@ -0,0 +1,223 @@
|
||||
// Copyright 2019 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 dotseg simulates a segment display that can draw dots.
|
||||
|
||||
Given a canvas, determines the placement and size of the individual
|
||||
segments and exposes API that can turn individual segments on and off or
|
||||
display dot characters.
|
||||
|
||||
The following outlines segments in the display and their names.
|
||||
|
||||
---------------
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| o D1 |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| o D2 |
|
||||
| |
|
||||
| |
|
||||
| o D3 |
|
||||
---------------
|
||||
*/
|
||||
package dotseg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/internal/canvas"
|
||||
"github.com/mum4k/termdash/internal/segdisp"
|
||||
)
|
||||
|
||||
// Segment represents a single segment in the display.
|
||||
type Segment int
|
||||
|
||||
// String implements fmt.Stringer()
|
||||
func (s Segment) String() string {
|
||||
if n, ok := segmentNames[s]; ok {
|
||||
return n
|
||||
}
|
||||
return "SegmentUnknown"
|
||||
}
|
||||
|
||||
// segmentNames maps Segment values to human readable names.
|
||||
var segmentNames = map[Segment]string{
|
||||
D1: "D1",
|
||||
D2: "D2",
|
||||
D3: "D3",
|
||||
}
|
||||
|
||||
const (
|
||||
segmentUnknown Segment = iota
|
||||
|
||||
// D1 is a segment, see the diagram above.
|
||||
D1
|
||||
// D2 is a segment, see the diagram above.
|
||||
D2
|
||||
// D3 is a segment, see the diagram above.
|
||||
D3
|
||||
|
||||
segmentMax // Used for validation.
|
||||
)
|
||||
|
||||
// characterSegments maps characters that can be displayed on their segments.
|
||||
var characterSegments = map[rune][]Segment{
|
||||
':': {D1, D2},
|
||||
'.': {D3},
|
||||
}
|
||||
|
||||
// SupportedChars returns all characters this display supports.
|
||||
func SupportedChars() string {
|
||||
var b strings.Builder
|
||||
for r := range characterSegments {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// AllSegments returns all segments in an undefined order.
|
||||
func AllSegments() []Segment {
|
||||
var res []Segment
|
||||
for s := range segmentNames {
|
||||
res = append(res, s)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Option is used to provide options.
|
||||
type Option interface {
|
||||
// set sets the provided option.
|
||||
set(*Display)
|
||||
}
|
||||
|
||||
// option implements Option.
|
||||
type option func(*Display)
|
||||
|
||||
// set implements Option.set.
|
||||
func (o option) set(d *Display) {
|
||||
o(d)
|
||||
}
|
||||
|
||||
// CellOpts sets the cell options on the cells that contain the segment display.
|
||||
func CellOpts(cOpts ...cell.Option) Option {
|
||||
return option(func(d *Display) {
|
||||
d.cellOpts = cOpts
|
||||
})
|
||||
}
|
||||
|
||||
// Display represents the segment display.
|
||||
// This object is not thread-safe.
|
||||
type Display struct {
|
||||
// segments maps segments to their current status.
|
||||
segments map[Segment]bool
|
||||
|
||||
cellOpts []cell.Option
|
||||
}
|
||||
|
||||
// New creates a new segment display.
|
||||
// Initially all the segments are off.
|
||||
func New(opts ...Option) *Display {
|
||||
d := &Display{
|
||||
segments: map[Segment]bool{},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt.set(d)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// Clear clears the entire display, turning all segments off.
|
||||
func (d *Display) Clear(opts ...Option) {
|
||||
for _, opt := range opts {
|
||||
opt.set(d)
|
||||
}
|
||||
|
||||
d.segments = map[Segment]bool{}
|
||||
}
|
||||
|
||||
// SetSegment sets the specified segment on.
|
||||
// This method is idempotent.
|
||||
func (d *Display) SetSegment(s Segment) error {
|
||||
if s <= segmentUnknown || s >= segmentMax {
|
||||
return fmt.Errorf("unknown segment %v(%d)", s, s)
|
||||
}
|
||||
d.segments[s] = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearSegment sets the specified segment off.
|
||||
// This method is idempotent.
|
||||
func (d *Display) ClearSegment(s Segment) error {
|
||||
if s <= segmentUnknown || s >= segmentMax {
|
||||
return fmt.Errorf("unknown segment %v(%d)", s, s)
|
||||
}
|
||||
d.segments[s] = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToggleSegment toggles the state of the specified segment, i.e it either sets
|
||||
// or clears it depending on its current state.
|
||||
func (d *Display) ToggleSegment(s Segment) error {
|
||||
if s <= segmentUnknown || s >= segmentMax {
|
||||
return fmt.Errorf("unknown segment %v(%d)", s, s)
|
||||
}
|
||||
if d.segments[s] {
|
||||
d.segments[s] = false
|
||||
} else {
|
||||
d.segments[s] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetCharacter sets all the segments that are needed to display the provided
|
||||
// character.
|
||||
// The display only supports characters returned by SupportedsChars().
|
||||
// Doesn't clear the display of segments set previously.
|
||||
func (d *Display) SetCharacter(c rune) error {
|
||||
seg, ok := characterSegments[c]
|
||||
if !ok {
|
||||
return fmt.Errorf("display doesn't support character %q rune(%v)", c, c)
|
||||
}
|
||||
|
||||
for _, s := range seg {
|
||||
if err := d.SetSegment(s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Draw draws the current state of the segment display onto the canvas.
|
||||
// The canvas must be at least MinCols x MinRows cells, or an error will be
|
||||
// returned.
|
||||
// Any options provided to draw overwrite the values provided to New.
|
||||
func (d *Display) Draw(cvs *canvas.Canvas, opts ...Option) error {
|
||||
for _, o := range opts {
|
||||
o.set(d)
|
||||
}
|
||||
|
||||
bc, _, err := segdisp.ToBraille(cvs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bc.CopyTo(cvs)
|
||||
}
|
296
internal/segdisp/dotseg/dotseg_test.go
Normal file
296
internal/segdisp/dotseg/dotseg_test.go
Normal file
@ -0,0 +1,296 @@
|
||||
// Copyright 2019 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 dotseg
|
||||
|
||||
import (
|
||||
"image"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
"github.com/mum4k/termdash/internal/area"
|
||||
"github.com/mum4k/termdash/internal/canvas"
|
||||
"github.com/mum4k/termdash/internal/canvas/testcanvas"
|
||||
"github.com/mum4k/termdash/internal/faketerm"
|
||||
"github.com/mum4k/termdash/internal/segdisp"
|
||||
)
|
||||
|
||||
func TestDraw(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
opts []Option
|
||||
drawOpts []Option
|
||||
cellCanvas image.Rectangle
|
||||
// If not nil, it is called before Draw is called and can set, clear or
|
||||
// toggle segments or characters.
|
||||
update func(*Display) error
|
||||
want func(size image.Point) *faketerm.Terminal
|
||||
wantErr bool
|
||||
wantUpdateErr bool
|
||||
}{
|
||||
{
|
||||
desc: "fails for area not wide enough",
|
||||
cellCanvas: image.Rect(0, 0, segdisp.MinCols-1, segdisp.MinRows),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails for area not tall enough",
|
||||
cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows-1),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails to set invalid segment (too small)",
|
||||
cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
|
||||
update: func(d *Display) error {
|
||||
return d.SetSegment(Segment(-1))
|
||||
},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails to set invalid segment (too large)",
|
||||
cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
|
||||
update: func(d *Display) error {
|
||||
return d.SetSegment(Segment(segmentMax))
|
||||
},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails to clear invalid segment (too small)",
|
||||
cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
|
||||
update: func(d *Display) error {
|
||||
return d.ClearSegment(Segment(-1))
|
||||
},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails to clear invalid segment (too large)",
|
||||
cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
|
||||
update: func(d *Display) error {
|
||||
return d.ClearSegment(Segment(segmentMax))
|
||||
},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails to toggle invalid segment (too small)",
|
||||
cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
|
||||
update: func(d *Display) error {
|
||||
return d.ToggleSegment(Segment(-1))
|
||||
},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails to toggle invalid segment (too large)",
|
||||
cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
|
||||
update: func(d *Display) error {
|
||||
return d.ToggleSegment(Segment(segmentMax))
|
||||
},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
{
|
||||
desc: "empty when no segments set",
|
||||
cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
d := New(tc.opts...)
|
||||
if tc.update != nil {
|
||||
err := tc.update(d)
|
||||
if (err != nil) != tc.wantUpdateErr {
|
||||
t.Errorf("tc.update => unexpected error: %v, wantUpdateErr: %v", err, tc.wantUpdateErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cvs, err := canvas.New(tc.cellCanvas)
|
||||
if err != nil {
|
||||
t.Fatalf("canvas.New => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
{
|
||||
err := d.Draw(cvs, tc.drawOpts...)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("Draw => unexpected error: %v, wantErr: %v", err, tc.wantErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
size := area.Size(tc.cellCanvas)
|
||||
want := faketerm.MustNew(size)
|
||||
if tc.want != nil {
|
||||
want = tc.want(size)
|
||||
}
|
||||
|
||||
got, err := faketerm.New(size)
|
||||
if err != nil {
|
||||
t.Fatalf("faketerm.New => unexpected error: %v", err)
|
||||
}
|
||||
if err := cvs.Apply(got); err != nil {
|
||||
t.Fatalf("bc.Apply => unexpected error: %v", err)
|
||||
}
|
||||
if diff := faketerm.Diff(want, got); diff != "" {
|
||||
t.Fatalf("Draw => %v", diff)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// mustDrawSegments returns a fake terminal of the specified size with the
|
||||
// segments drawn on it or panics.
|
||||
func mustDrawSegments(size image.Point, seg ...Segment) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
d := New()
|
||||
for _, s := range seg {
|
||||
if err := d.SetSegment(s); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := d.Draw(cvs); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
}
|
||||
|
||||
func TestSetCharacter(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
char rune
|
||||
// If not nil, it is called before Draw is called and can set, clear or
|
||||
// toggle segments or characters.
|
||||
update func(*Display) error
|
||||
want func(size image.Point) *faketerm.Terminal
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "fails on unsupported character",
|
||||
char: 'A',
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "doesn't clear the display",
|
||||
update: func(d *Display) error {
|
||||
return d.SetSegment(D3)
|
||||
},
|
||||
char: ':',
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
return mustDrawSegments(size, D1, D2, D3)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "displays '.'",
|
||||
char: '.',
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
return mustDrawSegments(size, D3)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "displays ':'",
|
||||
char: ':',
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
return mustDrawSegments(size, D1, D2)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
d := New()
|
||||
if tc.update != nil {
|
||||
err := tc.update(d)
|
||||
if err != nil {
|
||||
t.Fatalf("tc.update => unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
err := d.SetCharacter(tc.char)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("SetCharacter => unexpected error: %v, wantErr: %v", err, tc.wantErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ar := image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows)
|
||||
cvs, err := canvas.New(ar)
|
||||
if err != nil {
|
||||
t.Fatalf("canvas.New => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if err := d.Draw(cvs); err != nil {
|
||||
t.Fatalf("Draw => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
size := area.Size(ar)
|
||||
want := faketerm.MustNew(size)
|
||||
if tc.want != nil {
|
||||
want = tc.want(size)
|
||||
}
|
||||
|
||||
got, err := faketerm.New(size)
|
||||
if err != nil {
|
||||
t.Fatalf("faketerm.New => unexpected error: %v", err)
|
||||
}
|
||||
if err := cvs.Apply(got); err != nil {
|
||||
t.Fatalf("bc.Apply => unexpected error: %v", err)
|
||||
}
|
||||
if diff := faketerm.Diff(want, got); diff != "" {
|
||||
t.Fatalf("SetCharacter => %v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllSegments(t *testing.T) {
|
||||
want := []Segment{D1, D2, D3}
|
||||
got := AllSegments()
|
||||
sort.Slice(got, func(i, j int) bool {
|
||||
return int(got[i]) < int(got[j])
|
||||
})
|
||||
if diff := pretty.Compare(want, got); diff != "" {
|
||||
t.Errorf("AllSegments => unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupportedsChars(t *testing.T) {
|
||||
want := []rune{'.', ':'}
|
||||
|
||||
gotStr := SupportedChars()
|
||||
var got []rune
|
||||
for _, r := range gotStr {
|
||||
got = append(got, r)
|
||||
}
|
||||
sort.Slice(got, func(i, j int) bool {
|
||||
return int(got[i]) < int(got[j])
|
||||
})
|
||||
sort.Slice(want, func(i, j int) bool {
|
||||
return int(want[i]) < int(want[j])
|
||||
})
|
||||
if diff := pretty.Compare(want, got); diff != "" {
|
||||
t.Errorf("SupportedChars => unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
37
internal/segdisp/dotseg/testdotseg/testdotseg.go
Normal file
37
internal/segdisp/dotseg/testdotseg/testdotseg.go
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright 2019 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 testdotseg provides helpers for tests that use the dotseg package.
|
||||
package testdotseg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mum4k/termdash/internal/canvas"
|
||||
"github.com/mum4k/termdash/internal/segdisp/dotseg"
|
||||
)
|
||||
|
||||
// MustSetCharacter sets the character on the display or panics.
|
||||
func MustSetCharacter(d *dotseg.Display, c rune) {
|
||||
if err := d.SetCharacter(c); err != nil {
|
||||
panic(fmt.Errorf("dotseg.Display.SetCharacter => unexpected error: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// MustDraw draws the display onto the canvas or panics.
|
||||
func MustDraw(d *dotseg.Display, cvs *canvas.Canvas, opts ...dotseg.Option) {
|
||||
if err := d.Draw(cvs, opts...); err != nil {
|
||||
panic(fmt.Errorf("dotseg.Display.Draw => unexpected error: %v", err))
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user