1
0
mirror of https://github.com/mum4k/termdash.git synced 2025-04-25 13:48:50 +08:00
Jakub Sobon 94d07aea18
Format files with gofmt from Golang 1.20.
Signed-off-by: Jakub Sobon <jakub.sobon@elohim.sk>
2023-02-08 13:15:27 -05:00

441 lines
10 KiB
Go

// 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 sixteen simulates a 16-segment display drawn on a canvas.
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 ASCII characters.
The following outlines segments in the display and their names.
A1 A2
------- -------
| \ | / |
| \ | / |
F | H J K | B
| \ | / |
| \ | / |
-G1---- ----G2-
| / | \ |
| / | \ |
E | N M L | C
| / | \ |
| / | \ |
------- -------
D1 D2
*/
package sixteen
import (
"fmt"
"strings"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/private/canvas"
"github.com/mum4k/termdash/private/segdisp"
"github.com/mum4k/termdash/private/segdisp/segment"
)
// 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{
A1: "A1",
A2: "A2",
B: "B",
C: "C",
D1: "D1",
D2: "D2",
E: "E",
F: "F",
G1: "G1",
G2: "G2",
H: "H",
J: "J",
K: "K",
L: "L",
M: "M",
N: "N",
}
const (
segmentUnknown Segment = iota
// A1 is a segment, see the diagram above.
A1
// A2 is a segment, see the diagram above.
A2
// B is a segment, see the diagram above.
B
// C is a segment, see the diagram above.
C
// D1 is a segment, see the diagram above.
D1
// D2 is a segment, see the diagram above.
D2
// E is a segment, see the diagram above.
E
// F is a segment, see the diagram above.
F
// G1 is a segment, see the diagram above.
G1
// G2 is a segment, see the diagram above.
G2
// H is a segment, see the diagram above.
H
// J is a segment, see the diagram above.
J
// K is a segment, see the diagram above.
K
// L is a segment, see the diagram above.
L
// M is a segment, see the diagram above.
M
// N is a segment, see the diagram above.
N
segmentMax // Used for validation.
)
// characterSegments maps characters that can be displayed on their segments.
// See doc/16-Segment-ASCII-All.jpg and:
// https://www.partsnotincluded.com/electronics/segmented-led-display-ascii-library
var characterSegments = map[rune][]Segment{
' ': nil,
'!': {B, C},
'"': {J, B},
'#': {J, B, G1, G2, M, C, D1, D2},
'$': {A1, A2, F, J, G1, G2, M, C, D1, D2},
'%': {A1, F, J, K, G1, G2, N, M, C, D2},
'&': {A1, H, J, G1, E, L, D1, D2},
'\'': {J},
'(': {K, L},
')': {H, N},
'*': {H, J, K, G1, G2, N, M, L},
'+': {J, G1, G2, M},
',': {N},
'-': {G1, G2},
'.': {D1},
'/': {N, K},
'0': {A1, A2, F, K, B, E, N, C, D1, D2},
'1': {K, B, C},
'2': {A1, A2, B, G1, G2, E, D1, D2},
'3': {A1, A2, B, G2, C, D1, D2},
'4': {F, B, G1, G2, C},
'5': {A1, A2, F, G1, L, D1, D2},
'6': {A1, A2, F, G1, G2, E, C, D1, D2},
'7': {A1, A2, B, C},
'8': {A1, A2, F, B, G1, G2, E, C, D1, D2},
'9': {A1, A2, F, B, G1, G2, C, D1, D2},
':': {J, M},
';': {J, N},
'<': {K, G1, L},
'=': {G1, G2, D1, D2},
'>': {H, G2, N},
'?': {A1, A2, B, G2, M},
'@': {A1, A2, F, J, B, G2, E, D1, D2},
'A': {A1, A2, F, B, G1, G2, E, C},
'B': {A1, A2, J, B, G2, M, C, D1, D2},
'C': {A1, A2, F, E, D1, D2},
'D': {A1, A2, J, B, M, C, D1, D2},
'E': {A1, A2, F, G1, E, D1, D2},
'F': {A1, A2, F, G1, E},
'G': {A1, A2, F, G2, E, C, D1, D2},
'H': {F, B, G1, G2, E, C},
'I': {A1, A2, J, M, D1, D2},
'J': {B, E, C, D1, D2},
'K': {F, K, G1, E, L},
'L': {F, E, D1, D2},
'M': {F, H, K, B, E, C},
'N': {F, H, B, E, L, C},
'O': {A1, A2, F, B, E, C, D1, D2},
'P': {A1, A2, F, B, G1, G2, E},
'Q': {A1, A2, F, B, E, L, C, D1, D2},
'R': {A1, A2, F, B, G1, G2, E, L},
'S': {A1, A2, F, G1, G2, C, D1, D2},
'T': {A1, A2, J, M},
'U': {F, B, E, C, D1, D2},
'V': {F, K, E, N},
'W': {F, E, N, L, C, B},
'X': {H, K, N, L},
'Y': {F, B, G1, G2, C, D1, D2},
'Z': {A1, A2, K, N, D1, D2},
'[': {A2, J, M, D2},
'\\': {H, L},
']': {A1, J, M, D1},
'^': {N, L},
'_': {D1, D2},
'`': {H},
'a': {G1, E, M, D1, D2},
'b': {F, G1, E, M, D1},
'c': {G1, E, D1},
'd': {B, G2, M, C, D2},
'e': {G1, E, N, D1},
'f': {A2, J, G1, G2, M},
'g': {A1, F, J, G1, M, D1},
'h': {F, G1, E, M},
'i': {M},
'j': {J, E, M, D1},
'k': {J, K, M, L},
'l': {F, E},
'm': {G1, G2, E, M, C},
'n': {G1, E, M},
'o': {G1, E, M, D1},
'p': {A1, F, J, G1, E},
'q': {A1, F, J, G1, M},
'r': {G1, E},
's': {A1, F, G1, M, D1},
't': {F, G1, E, D1},
'u': {E, M, D1},
'v': {E, N},
'w': {E, N, L, C},
'x': {H, K, N, L},
'y': {J, B, G2, C, D2},
'z': {G1, N, D1},
'{': {A2, J, G1, M, D2},
'|': {J, M},
'}': {A1, J, G2, M, D1},
'~': {K, G1, G2, N},
}
// SupportsChars asserts whether the display supports all runes in the
// provided string.
// The display only supports a subset of ASCII characters.
// Returns any unsupported runes found in the string in an unspecified order.
func SupportsChars(s string) (bool, []rune) {
unsupp := map[rune]bool{}
for _, r := range s {
if _, ok := characterSegments[r]; !ok {
unsupp[r] = true
}
}
var res []rune
for r := range unsupp {
res = append(res, r)
}
return len(res) == 0, res
}
// Sanitize returns a copy of the string, replacing all unsupported characters
// with a space character.
func Sanitize(s string) string {
var b strings.Builder
for _, r := range s {
if _, ok := characterSegments[r]; !ok {
b.WriteRune(' ')
continue
}
b.WriteRune(r)
}
return b.String()
}
// AllSegments returns all 16 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 a subset of ASCII characters, use SupportsChars()
// or Sanitize() to ensure the provided character is supported.
// 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, bcAr, err := segdisp.ToBraille(cvs)
if err != nil {
return err
}
attr := NewAttributes(bcAr)
var sOpts []segment.Option
if len(d.cellOpts) > 0 {
sOpts = append(sOpts, segment.CellOpts(d.cellOpts...))
}
for _, segArg := range []struct {
s Segment
opts []segment.Option
}{
{A1, nil},
{A2, nil},
{F, nil},
{J, []segment.Option{segment.SkipSlopesLTE(2)}},
{B, []segment.Option{segment.ReverseSlopes()}},
{G1, []segment.Option{segment.SkipSlopesLTE(2)}},
{G2, []segment.Option{segment.SkipSlopesLTE(2)}},
{E, nil},
{M, []segment.Option{segment.SkipSlopesLTE(2)}},
{C, []segment.Option{segment.ReverseSlopes()}},
{D1, []segment.Option{segment.ReverseSlopes()}},
{D2, []segment.Option{segment.ReverseSlopes()}},
} {
if !d.segments[segArg.s] {
continue
}
sOpts := append(sOpts, segArg.opts...)
ar := attr.hvSegArea(segArg.s)
if err := segment.HV(bc, ar, hvSegType[segArg.s], sOpts...); err != nil {
return fmt.Errorf("failed to draw segment %v, segment.HV => %v", segArg.s, err)
}
}
var dsOpts []segment.DiagonalOption
if len(d.cellOpts) > 0 {
dsOpts = append(dsOpts, segment.DiagonalCellOpts(d.cellOpts...))
}
for _, seg := range []Segment{H, K, N, L} {
if !d.segments[seg] {
continue
}
ar := attr.diaSegArea(seg)
if err := segment.Diagonal(bc, ar, attr.segSize, diaSegType[seg], dsOpts...); err != nil {
return fmt.Errorf("failed to draw segment %v, segment.Diagonal => %v", seg, err)
}
}
return bc.CopyTo(cvs)
}