1
0
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:
Jakub Sobon 2019-04-28 23:34:02 -04:00
parent 27c830cebf
commit f81cb554b0
No known key found for this signature in database
GPG Key ID: F2451A77FB05D3B7
3 changed files with 556 additions and 0 deletions

View 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)
}

View 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)
}
}

View 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))
}
}