1
0
mirror of https://github.com/mum4k/termdash.git synced 2025-04-25 13:48:50 +08:00

Factoring private type buffer out of cell.

This commit is contained in:
Jakub Sobon 2019-02-24 16:29:44 -05:00
parent 0938ae91d8
commit 984f37245d
No known key found for this signature in database
GPG Key ID: F2451A77FB05D3B7
9 changed files with 876 additions and 783 deletions

View File

@ -12,25 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Package cell implements cell options and attributes.
A cell is the smallest point on the terminal.
*/
// Package cell implements cell options and attributes.
package cell
import (
"fmt"
"image"
"github.com/mum4k/termdash/internal/area"
"github.com/mum4k/termdash/internal/runewidth"
)
// Option is used to provide options for cells on a 2-D terminal.
type Option interface {
// set sets the provided option.
set(*Options)
// Set sets the provided option.
Set(*Options)
}
// Options stores the provided options.
@ -39,8 +27,8 @@ type Options struct {
BgColor Color
}
// set allows existing options to be passed as an option.
func (o *Options) set(other *Options) {
// Set allows existing options to be passed as an option.
func (o *Options) Set(other *Options) {
*other = *o
}
@ -48,160 +36,16 @@ func (o *Options) set(other *Options) {
func NewOptions(opts ...Option) *Options {
o := &Options{}
for _, opt := range opts {
opt.set(o)
opt.Set(o)
}
return o
}
// Cell represents a single cell on the terminal.
type Cell struct {
// Rune is the rune stored in the cell.
Rune rune
// Opts are the cell options.
Opts *Options
}
// Copy returns a copy the cell.
func (c *Cell) Copy() *Cell {
return &Cell{
Rune: c.Rune,
Opts: NewOptions(c.Opts),
}
}
// New returns a new cell.
func New(r rune, opts ...Option) *Cell {
return &Cell{
Rune: r,
Opts: NewOptions(opts...),
}
}
// Apply applies the provided options to the cell.
func (c *Cell) Apply(opts ...Option) {
for _, opt := range opts {
opt.set(c.Opts)
}
}
// Buffer is a 2-D buffer of cells.
// The axes increase right and down.
// Uninitialized buffer is invalid, use NewBuffer to create an instance.
// Don't set cells directly, use the SetCell method instead which safely
// handles limits and wide unicode characters.
type Buffer [][]*Cell
// SetCell sets the rune of the specified cell in the buffer. Returns the
// number of cells the rune occupies, wide runes can occupy multiple cells when
// printed on the terminal. See http://www.unicode.org/reports/tr11/.
// Use the options to specify which attributes to modify, if an attribute
// option isn't specified, the attribute retains its previous value.
func (b Buffer) SetCell(p image.Point, r rune, opts ...Option) (int, error) {
partial, err := b.IsPartial(p)
if err != nil {
return -1, err
}
if partial {
return -1, fmt.Errorf("cannot set rune %q at point %v, it is a partial cell occupied by a wide rune in the previous cell", r, p)
}
remW, err := b.RemWidth(p)
if err != nil {
return -1, err
}
rw := runewidth.RuneWidth(r)
if rw > remW {
return -1, fmt.Errorf("cannot set rune %q of width %d at point %v, only have %d remaining cells at this line", r, rw, p, remW)
}
cell := b[p.X][p.Y]
cell.Rune = r
cell.Apply(opts...)
return rw, nil
}
// IsPartial returns true if the cell at the specified point holds a part of a
// full width rune from a previous cell. See
// http://www.unicode.org/reports/tr11/.
func (b Buffer) IsPartial(p image.Point) (bool, error) {
size := b.Size()
ar, err := area.FromSize(size)
if err != nil {
return false, err
}
if !p.In(ar) {
return false, fmt.Errorf("point %v falls outside of the area %v occupied by the buffer", p, ar)
}
if p.X == 0 && p.Y == 0 {
return false, nil
}
prevP := image.Point{p.X - 1, p.Y}
if prevP.X < 0 {
prevP = image.Point{size.X - 1, p.Y - 1}
}
prevR := b[prevP.X][prevP.Y].Rune
switch rw := runewidth.RuneWidth(prevR); rw {
case 0, 1:
return false, nil
case 2:
return true, nil
default:
return false, fmt.Errorf("buffer cell %v contains rune %q which has an unsupported rune with %d", prevP, prevR, rw)
}
}
// RemWidth returns the remaining width (horizontal row of cells) available
// from and inclusive of the specified point.
func (b Buffer) RemWidth(p image.Point) (int, error) {
size := b.Size()
ar, err := area.FromSize(size)
if err != nil {
return -1, err
}
if !p.In(ar) {
return -1, fmt.Errorf("point %v falls outside of the area %v occupied by the buffer", p, ar)
}
return size.X - p.X, nil
}
// NewBuffer returns a new Buffer of the provided size.
func NewBuffer(size image.Point) (Buffer, error) {
if size.X <= 0 {
return nil, fmt.Errorf("invalid buffer width (size.X): %d, must be a positive number", size.X)
}
if size.Y <= 0 {
return nil, fmt.Errorf("invalid buffer height (size.Y): %d, must be a positive number", size.Y)
}
b := make([][]*Cell, size.X)
for col := range b {
b[col] = make([]*Cell, size.Y)
for row := range b[col] {
b[col][row] = New(0)
}
}
return b, nil
}
// Size returns the size of the buffer.
func (b Buffer) Size() image.Point {
return image.Point{
len(b),
len(b[0]),
}
}
// option implements Option.
type option func(*Options)
// set implements Option.set.
func (co option) set(opts *Options) {
// Set implements Option.set.
func (co option) Set(opts *Options) {
co(opts)
}

View File

@ -15,7 +15,6 @@
package cell
import (
"image"
"testing"
"github.com/kylelemons/godebug/pretty"
@ -60,6 +59,19 @@ func TestNewOptions(t *testing.T) {
BgColor: ColorMagenta,
},
},
{
desc: "setting options by passing the options struct",
opts: []Option{
&Options{
FgColor: ColorCyan,
BgColor: ColorMagenta,
},
},
want: &Options{
FgColor: ColorCyan,
BgColor: ColorMagenta,
},
},
}
for _, tc := range tests {
@ -71,523 +83,3 @@ func TestNewOptions(t *testing.T) {
})
}
}
func TestNew(t *testing.T) {
tests := []struct {
desc string
r rune
opts []Option
want Cell
}{
{
desc: "creates empty cell with default options",
want: Cell{
Opts: &Options{},
},
},
{
desc: "cell with the specified rune",
r: 'X',
want: Cell{
Rune: 'X',
Opts: &Options{},
},
},
{
desc: "cell with options",
r: 'X',
opts: []Option{
FgColor(ColorCyan),
BgColor(ColorMagenta),
},
want: Cell{
Rune: 'X',
Opts: &Options{
FgColor: ColorCyan,
BgColor: ColorMagenta,
},
},
},
{
desc: "passing full Options overwrites existing",
r: 'X',
opts: []Option{
&Options{
FgColor: ColorBlack,
BgColor: ColorBlue,
},
},
want: Cell{
Rune: 'X',
Opts: &Options{
FgColor: ColorBlack,
BgColor: ColorBlue,
},
},
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
got := New(tc.r, tc.opts...)
if diff := pretty.Compare(tc.want, got); diff != "" {
t.Errorf("New => unexpected diff (-want, +got):\n%s", diff)
}
})
}
}
func TestCellApply(t *testing.T) {
tests := []struct {
desc string
cell *Cell
opts []Option
want *Cell
}{
{
desc: "no options provided",
cell: New(0),
want: New(0),
},
{
desc: "no change in options",
cell: New(0, FgColor(ColorCyan)),
opts: []Option{
FgColor(ColorCyan),
},
want: New(0, FgColor(ColorCyan)),
},
{
desc: "retains previous values",
cell: New(0, FgColor(ColorCyan)),
opts: []Option{
BgColor(ColorBlack),
},
want: New(
0,
FgColor(ColorCyan),
BgColor(ColorBlack),
),
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
got := tc.cell
got.Apply(tc.opts...)
if diff := pretty.Compare(tc.want, got); diff != "" {
t.Errorf("Apply => unexpected diff (-want, +got):\n%s", diff)
}
})
}
}
func TestNewBuffer(t *testing.T) {
tests := []struct {
desc string
size image.Point
want Buffer
wantErr bool
}{
{
desc: "zero buffer is invalid",
wantErr: true,
},
{
desc: "width cannot be negative",
size: image.Point{-1, 1},
wantErr: true,
},
{
desc: "height cannot be negative",
size: image.Point{1, -1},
wantErr: true,
},
{
desc: "creates single cell buffer",
size: image.Point{1, 1},
want: Buffer{
{
New(0),
},
},
},
{
desc: "creates the buffer",
size: image.Point{2, 3},
want: Buffer{
{
New(0),
New(0),
New(0),
},
{
New(0),
New(0),
New(0),
},
},
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
got, err := NewBuffer(tc.size)
if (err != nil) != tc.wantErr {
t.Errorf("NewBuffer => unexpected error: %v, wantErr: %v", err, tc.wantErr)
}
if err != nil {
return
}
if diff := pretty.Compare(tc.want, got); diff != "" {
t.Errorf("NewBuffer => unexpected diff (-want, +got):\n%s", diff)
}
})
}
}
func TestBufferSize(t *testing.T) {
sizes := []image.Point{
{1, 1},
{2, 3},
}
for _, size := range sizes {
t.Run("", func(t *testing.T) {
b, err := NewBuffer(size)
if err != nil {
t.Fatalf("NewBuffer => unexpected error: %v", err)
}
got := b.Size()
if diff := pretty.Compare(size, got); diff != "" {
t.Errorf("Size => unexpected diff (-want, +got):\n%s", diff)
}
})
}
}
// mustNewBuffer returns a new Buffer or panics.
func mustNewBuffer(size image.Point) Buffer {
b, err := NewBuffer(size)
if err != nil {
panic(err)
}
return b
}
func TestSetCell(t *testing.T) {
size := image.Point{3, 3}
tests := []struct {
desc string
buffer Buffer
point image.Point
r rune
opts []Option
wantCells int
want Buffer
wantErr bool
}{
{
desc: "point falls before the buffer",
buffer: mustNewBuffer(size),
point: image.Point{-1, -1},
r: 'A',
wantErr: true,
},
{
desc: "point falls after the buffer",
buffer: mustNewBuffer(size),
point: image.Point{3, 3},
r: 'A',
wantErr: true,
},
{
desc: "point falls on cell with partial rune",
buffer: func() Buffer {
b := mustNewBuffer(size)
b[0][0].Rune = '世'
return b
}(),
point: image.Point{1, 0},
r: 'A',
wantErr: true,
},
{
desc: "point falls on cell with full-width rune and overwrites with half-width rune",
buffer: func() Buffer {
b := mustNewBuffer(size)
b[0][0].Rune = '世'
return b
}(),
point: image.Point{0, 0},
r: 'A',
wantCells: 1,
want: func() Buffer {
b := mustNewBuffer(size)
b[0][0].Rune = 'A'
return b
}(),
},
{
desc: "point falls on cell with full-width rune and overwrites with full-width rune",
buffer: func() Buffer {
b := mustNewBuffer(size)
b[0][0].Rune = '世'
return b
}(),
point: image.Point{0, 0},
r: '界',
wantCells: 2,
want: func() Buffer {
b := mustNewBuffer(size)
b[0][0].Rune = '界'
return b
}(),
},
{
desc: "not enough space for a wide rune on the line",
buffer: mustNewBuffer(image.Point{3, 3}),
point: image.Point{2, 0},
r: '界',
wantErr: true,
},
{
desc: "sets half-width rune in a cell",
buffer: mustNewBuffer(image.Point{3, 3}),
point: image.Point{1, 1},
r: 'A',
wantCells: 1,
want: func() Buffer {
b := mustNewBuffer(size)
b[1][1].Rune = 'A'
return b
}(),
},
{
desc: "sets full-width rune in a cell",
buffer: mustNewBuffer(image.Point{3, 3}),
point: image.Point{1, 2},
r: '界',
wantCells: 2,
want: func() Buffer {
b := mustNewBuffer(size)
b[1][2].Rune = '界'
return b
}(),
},
{
desc: "sets cell options",
buffer: mustNewBuffer(image.Point{3, 3}),
point: image.Point{1, 2},
r: 'A',
opts: []Option{
FgColor(ColorRed),
BgColor(ColorBlue),
},
wantCells: 1,
want: func() Buffer {
b := mustNewBuffer(size)
cell := b[1][2]
cell.Rune = 'A'
cell.Opts = NewOptions(FgColor(ColorRed), BgColor(ColorBlue))
return b
}(),
},
{
desc: "overwrites only provided options",
buffer: func() Buffer {
b := mustNewBuffer(size)
cell := b[1][2]
cell.Opts = NewOptions(BgColor(ColorBlue))
return b
}(),
point: image.Point{1, 2},
r: 'A',
opts: []Option{
FgColor(ColorRed),
},
wantCells: 1,
want: func() Buffer {
b := mustNewBuffer(size)
cell := b[1][2]
cell.Rune = 'A'
cell.Opts = NewOptions(FgColor(ColorRed), BgColor(ColorBlue))
return b
}(),
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
gotCells, err := tc.buffer.SetCell(tc.point, tc.r, tc.opts...)
if (err != nil) != tc.wantErr {
t.Errorf("SetCell => unexpected error: %v, wantErr: %v", err, tc.wantErr)
}
if err != nil {
return
}
if gotCells != tc.wantCells {
t.Errorf("SetCell => unexpected cell count, got %d, want %d", gotCells, tc.wantCells)
}
got := tc.buffer
if diff := pretty.Compare(tc.want, got); diff != "" {
t.Errorf("SetCell=> unexpected buffer, diff (-want, +got):\n%s", diff)
}
})
}
}
func TestIsPartial(t *testing.T) {
tests := []struct {
desc string
buffer Buffer
point image.Point
want bool
wantErr bool
}{
{
desc: "point falls before the buffer",
buffer: mustNewBuffer(image.Point{1, 1}),
point: image.Point{-1, -1},
wantErr: true,
},
{
desc: "point falls after the buffer",
buffer: mustNewBuffer(image.Point{1, 1}),
point: image.Point{1, 1},
wantErr: true,
},
{
desc: "the first cell cannot be partial",
buffer: mustNewBuffer(image.Point{1, 1}),
point: image.Point{0, 0},
want: false,
},
{
desc: "previous cell on the same line contains no rune",
buffer: mustNewBuffer(image.Point{3, 3}),
point: image.Point{1, 0},
want: false,
},
{
desc: "previous cell on the same line contains half-width rune",
buffer: func() Buffer {
b := mustNewBuffer(image.Point{3, 3})
b[0][0].Rune = 'A'
return b
}(),
point: image.Point{1, 0},
want: false,
},
{
desc: "previous cell on the same line contains full-width rune",
buffer: func() Buffer {
b := mustNewBuffer(image.Point{3, 3})
b[0][0].Rune = '世'
return b
}(),
point: image.Point{1, 0},
want: true,
},
{
desc: "previous cell on previous line contains no rune",
buffer: mustNewBuffer(image.Point{3, 3}),
point: image.Point{0, 1},
want: false,
},
{
desc: "previous cell on previous line contains half-width rune",
buffer: func() Buffer {
b := mustNewBuffer(image.Point{3, 3})
b[2][0].Rune = 'A'
return b
}(),
point: image.Point{0, 1},
want: false,
},
{
desc: "previous cell on previous line contains full-width rune",
buffer: func() Buffer {
b := mustNewBuffer(image.Point{3, 3})
b[2][0].Rune = '世'
return b
}(),
point: image.Point{0, 1},
want: true,
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
got, err := tc.buffer.IsPartial(tc.point)
if (err != nil) != tc.wantErr {
t.Errorf("IsPartial => unexpected error: %v, wantErr: %v", err, tc.wantErr)
}
if err != nil {
return
}
if got != tc.want {
t.Errorf("IsPartial => got %v, want %v", got, tc.want)
}
})
}
}
func TestRemWidth(t *testing.T) {
tests := []struct {
desc string
size image.Point
point image.Point
want int
wantErr bool
}{
{
desc: "point falls before the buffer",
size: image.Point{1, 1},
point: image.Point{-1, -1},
wantErr: true,
},
{
desc: "point falls after the buffer",
size: image.Point{1, 1},
point: image.Point{1, 1},
wantErr: true,
},
{
desc: "remaining width from the first cell on the line",
size: image.Point{3, 3},
point: image.Point{0, 1},
want: 3,
},
{
desc: "remaining width from the last cell on the line",
size: image.Point{3, 3},
point: image.Point{2, 2},
want: 1,
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
b, err := NewBuffer(tc.size)
if err != nil {
t.Fatalf("NewBuffer => unexpected error: %v", err)
}
got, err := b.RemWidth(tc.point)
if (err != nil) != tc.wantErr {
t.Errorf("RemWidth => unexpected error: %v, wantErr: %v", err, tc.wantErr)
}
if err != nil {
return
}
if got != tc.want {
t.Errorf("RemWidth => got %d, want %d", got, tc.want)
}
})
}
}

View File

@ -14,7 +14,10 @@
package cell
import "testing"
import (
"fmt"
"testing"
)
func TestColorNumber(t *testing.T) {
tests := []struct {
@ -46,6 +49,7 @@ func TestColorNumber(t *testing.T) {
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
t.Logf(fmt.Sprintf("color: %v", tc.want))
got := ColorNumber(tc.number)
if got != tc.want {
t.Errorf("ColorNumber(%v) => %v, want %v", tc.number, got, tc.want)

View File

@ -0,0 +1,169 @@
// 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 buffer implements a 2-D buffer of cells.
package buffer
import (
"fmt"
"image"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/internal/area"
"github.com/mum4k/termdash/internal/runewidth"
)
// Cell represents a single cell on the terminal.
type Cell struct {
// Rune is the rune stored in the cell.
Rune rune
// Opts are the cell options.
Opts *cell.Options
}
// NewCell returns a new cell.
func NewCell(r rune, opts ...cell.Option) *Cell {
return &Cell{
Rune: r,
Opts: cell.NewOptions(opts...),
}
}
// Copy returns a copy the cell.
func (c *Cell) Copy() *Cell {
return &Cell{
Rune: c.Rune,
Opts: cell.NewOptions(c.Opts),
}
}
// Apply applies the provided options to the cell.
func (c *Cell) Apply(opts ...cell.Option) {
for _, opt := range opts {
opt.Set(c.Opts)
}
}
// Buffer is a 2-D buffer of cells.
// The axes increase right and down.
// Uninitialized buffer is invalid, use New to create an instance.
// Don't set cells directly, use the SetCell method instead which safely
// handles limits and wide unicode characters.
type Buffer [][]*Cell
// New returns a new Buffer of the provided size.
func New(size image.Point) (Buffer, error) {
if size.X <= 0 {
return nil, fmt.Errorf("invalid buffer width (size.X): %d, must be a positive number", size.X)
}
if size.Y <= 0 {
return nil, fmt.Errorf("invalid buffer height (size.Y): %d, must be a positive number", size.Y)
}
b := make([][]*Cell, size.X)
for col := range b {
b[col] = make([]*Cell, size.Y)
for row := range b[col] {
b[col][row] = NewCell(0)
}
}
return b, nil
}
// SetCell sets the rune of the specified cell in the buffer. Returns the
// number of cells the rune occupies, wide runes can occupy multiple cells when
// printed on the terminal. See http://www.unicode.org/reports/tr11/.
// Use the options to specify which attributes to modify, if an attribute
// option isn't specified, the attribute retains its previous value.
func (b Buffer) SetCell(p image.Point, r rune, opts ...cell.Option) (int, error) {
partial, err := b.IsPartial(p)
if err != nil {
return -1, err
}
if partial {
return -1, fmt.Errorf("cannot set rune %q at point %v, it is a partial cell occupied by a wide rune in the previous cell", r, p)
}
remW, err := b.RemWidth(p)
if err != nil {
return -1, err
}
rw := runewidth.RuneWidth(r)
if rw > remW {
return -1, fmt.Errorf("cannot set rune %q of width %d at point %v, only have %d remaining cells at this line", r, rw, p, remW)
}
c := b[p.X][p.Y]
c.Rune = r
c.Apply(opts...)
return rw, nil
}
// IsPartial returns true if the cell at the specified point holds a part of a
// full width rune from a previous cell. See
// http://www.unicode.org/reports/tr11/.
func (b Buffer) IsPartial(p image.Point) (bool, error) {
size := b.Size()
ar, err := area.FromSize(size)
if err != nil {
return false, err
}
if !p.In(ar) {
return false, fmt.Errorf("point %v falls outside of the area %v occupied by the buffer", p, ar)
}
if p.X == 0 && p.Y == 0 {
return false, nil
}
prevP := image.Point{p.X - 1, p.Y}
if prevP.X < 0 {
prevP = image.Point{size.X - 1, p.Y - 1}
}
prevR := b[prevP.X][prevP.Y].Rune
switch rw := runewidth.RuneWidth(prevR); rw {
case 0, 1:
return false, nil
case 2:
return true, nil
default:
return false, fmt.Errorf("buffer cell %v contains rune %q which has an unsupported rune with %d", prevP, prevR, rw)
}
}
// RemWidth returns the remaining width (horizontal row of cells) available
// from and inclusive of the specified point.
func (b Buffer) RemWidth(p image.Point) (int, error) {
size := b.Size()
ar, err := area.FromSize(size)
if err != nil {
return -1, err
}
if !p.In(ar) {
return -1, fmt.Errorf("point %v falls outside of the area %v occupied by the buffer", p, ar)
}
return size.X - p.X, nil
}
// Size returns the size of the buffer.
func (b Buffer) Size() image.Point {
return image.Point{
len(b),
len(b[0]),
}
}

View File

@ -0,0 +1,580 @@
// 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 buffer
import (
"image"
"testing"
"github.com/kylelemons/godebug/pretty"
"github.com/mum4k/termdash/cell"
)
func TestNewCell(t *testing.T) {
tests := []struct {
desc string
r rune
opts []cell.Option
want Cell
}{
{
desc: "creates empty cell with default options",
want: Cell{
Opts: &cell.Options{},
},
},
{
desc: "cell with the specified rune",
r: 'X',
want: Cell{
Rune: 'X',
Opts: &cell.Options{},
},
},
{
desc: "cell with options",
r: 'X',
opts: []cell.Option{
cell.FgColor(cell.ColorCyan),
cell.BgColor(cell.ColorMagenta),
},
want: Cell{
Rune: 'X',
Opts: &cell.Options{
FgColor: cell.ColorCyan,
BgColor: cell.ColorMagenta,
},
},
},
{
desc: "passing full cell.Options overwrites existing",
r: 'X',
opts: []cell.Option{
&cell.Options{
FgColor: cell.ColorBlack,
BgColor: cell.ColorBlue,
},
},
want: Cell{
Rune: 'X',
Opts: &cell.Options{
FgColor: cell.ColorBlack,
BgColor: cell.ColorBlue,
},
},
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
got := NewCell(tc.r, tc.opts...)
if diff := pretty.Compare(tc.want, got); diff != "" {
t.Errorf("New => unexpected diff (-want, +got):\n%s", diff)
}
})
}
}
func TestCellCopy(t *testing.T) {
tests := []struct {
desc string
cell *Cell
want *Cell
}{
{
desc: "copies empty cell",
cell: NewCell(0),
want: NewCell(0),
},
{
desc: "copies cell with a rune",
cell: NewCell(33),
want: NewCell(33),
},
{
desc: "copies cell with rune and options",
cell: NewCell(42, cell.FgColor(cell.ColorCyan), cell.BgColor(cell.ColorBlack)),
want: NewCell(
42,
cell.FgColor(cell.ColorCyan),
cell.BgColor(cell.ColorBlack),
),
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
got := tc.cell.Copy()
if diff := pretty.Compare(tc.want, got); diff != "" {
t.Errorf("Copy => unexpected diff (-want, +got):\n%s", diff)
}
})
}
}
func TestCellApply(t *testing.T) {
tests := []struct {
desc string
cell *Cell
opts []cell.Option
want *Cell
}{
{
desc: "no options provided",
cell: NewCell(0),
want: NewCell(0),
},
{
desc: "no change in options",
cell: NewCell(0, cell.FgColor(cell.ColorCyan)),
opts: []cell.Option{
cell.FgColor(cell.ColorCyan),
},
want: NewCell(0, cell.FgColor(cell.ColorCyan)),
},
{
desc: "retains previous values",
cell: NewCell(0, cell.FgColor(cell.ColorCyan)),
opts: []cell.Option{
cell.BgColor(cell.ColorBlack),
},
want: NewCell(
0,
cell.FgColor(cell.ColorCyan),
cell.BgColor(cell.ColorBlack),
),
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
got := tc.cell
got.Apply(tc.opts...)
if diff := pretty.Compare(tc.want, got); diff != "" {
t.Errorf("Apply => unexpected diff (-want, +got):\n%s", diff)
}
})
}
}
func TestNew(t *testing.T) {
tests := []struct {
desc string
size image.Point
want Buffer
wantErr bool
}{
{
desc: "zero buffer is invalid",
wantErr: true,
},
{
desc: "width cannot be negative",
size: image.Point{-1, 1},
wantErr: true,
},
{
desc: "height cannot be negative",
size: image.Point{1, -1},
wantErr: true,
},
{
desc: "creates single cell buffer",
size: image.Point{1, 1},
want: Buffer{
{
NewCell(0),
},
},
},
{
desc: "creates the buffer",
size: image.Point{2, 3},
want: Buffer{
{
NewCell(0),
NewCell(0),
NewCell(0),
},
{
NewCell(0),
NewCell(0),
NewCell(0),
},
},
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
got, err := New(tc.size)
if (err != nil) != tc.wantErr {
t.Errorf("New => unexpected error: %v, wantErr: %v", err, tc.wantErr)
}
if err != nil {
return
}
if diff := pretty.Compare(tc.want, got); diff != "" {
t.Errorf("New => unexpected diff (-want, +got):\n%s", diff)
}
})
}
}
func TestBufferSize(t *testing.T) {
sizes := []image.Point{
{1, 1},
{2, 3},
}
for _, size := range sizes {
t.Run("", func(t *testing.T) {
b, err := New(size)
if err != nil {
t.Fatalf("New => unexpected error: %v", err)
}
got := b.Size()
if diff := pretty.Compare(size, got); diff != "" {
t.Errorf("Size => unexpected diff (-want, +got):\n%s", diff)
}
})
}
}
// mustNew returns a new Buffer or panics.
func mustNew(size image.Point) Buffer {
b, err := New(size)
if err != nil {
panic(err)
}
return b
}
func TestSetCell(t *testing.T) {
size := image.Point{3, 3}
tests := []struct {
desc string
buffer Buffer
point image.Point
r rune
opts []cell.Option
wantCells int
want Buffer
wantErr bool
}{
{
desc: "point falls before the buffer",
buffer: mustNew(size),
point: image.Point{-1, -1},
r: 'A',
wantErr: true,
},
{
desc: "point falls after the buffer",
buffer: mustNew(size),
point: image.Point{3, 3},
r: 'A',
wantErr: true,
},
{
desc: "point falls on cell with partial rune",
buffer: func() Buffer {
b := mustNew(size)
b[0][0].Rune = '世'
return b
}(),
point: image.Point{1, 0},
r: 'A',
wantErr: true,
},
{
desc: "point falls on cell with full-width rune and overwrites with half-width rune",
buffer: func() Buffer {
b := mustNew(size)
b[0][0].Rune = '世'
return b
}(),
point: image.Point{0, 0},
r: 'A',
wantCells: 1,
want: func() Buffer {
b := mustNew(size)
b[0][0].Rune = 'A'
return b
}(),
},
{
desc: "point falls on cell with full-width rune and overwrites with full-width rune",
buffer: func() Buffer {
b := mustNew(size)
b[0][0].Rune = '世'
return b
}(),
point: image.Point{0, 0},
r: '界',
wantCells: 2,
want: func() Buffer {
b := mustNew(size)
b[0][0].Rune = '界'
return b
}(),
},
{
desc: "not enough space for a wide rune on the line",
buffer: mustNew(image.Point{3, 3}),
point: image.Point{2, 0},
r: '界',
wantErr: true,
},
{
desc: "sets half-width rune in a cell",
buffer: mustNew(image.Point{3, 3}),
point: image.Point{1, 1},
r: 'A',
wantCells: 1,
want: func() Buffer {
b := mustNew(size)
b[1][1].Rune = 'A'
return b
}(),
},
{
desc: "sets full-width rune in a cell",
buffer: mustNew(image.Point{3, 3}),
point: image.Point{1, 2},
r: '界',
wantCells: 2,
want: func() Buffer {
b := mustNew(size)
b[1][2].Rune = '界'
return b
}(),
},
{
desc: "sets cell options",
buffer: mustNew(image.Point{3, 3}),
point: image.Point{1, 2},
r: 'A',
opts: []cell.Option{
cell.FgColor(cell.ColorRed),
cell.BgColor(cell.ColorBlue),
},
wantCells: 1,
want: func() Buffer {
b := mustNew(size)
c := b[1][2]
c.Rune = 'A'
c.Opts = cell.NewOptions(cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
return b
}(),
},
{
desc: "overwrites only provided options",
buffer: func() Buffer {
b := mustNew(size)
c := b[1][2]
c.Opts = cell.NewOptions(cell.BgColor(cell.ColorBlue))
return b
}(),
point: image.Point{1, 2},
r: 'A',
opts: []cell.Option{
cell.FgColor(cell.ColorRed),
},
wantCells: 1,
want: func() Buffer {
b := mustNew(size)
c := b[1][2]
c.Rune = 'A'
c.Opts = cell.NewOptions(cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
return b
}(),
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
gotCells, err := tc.buffer.SetCell(tc.point, tc.r, tc.opts...)
if (err != nil) != tc.wantErr {
t.Errorf("SetCell => unexpected error: %v, wantErr: %v", err, tc.wantErr)
}
if err != nil {
return
}
if gotCells != tc.wantCells {
t.Errorf("SetCell => unexpected cell count, got %d, want %d", gotCells, tc.wantCells)
}
got := tc.buffer
if diff := pretty.Compare(tc.want, got); diff != "" {
t.Errorf("SetCell=> unexpected buffer, diff (-want, +got):\n%s", diff)
}
})
}
}
func TestIsPartial(t *testing.T) {
tests := []struct {
desc string
buffer Buffer
point image.Point
want bool
wantErr bool
}{
{
desc: "point falls before the buffer",
buffer: mustNew(image.Point{1, 1}),
point: image.Point{-1, -1},
wantErr: true,
},
{
desc: "point falls after the buffer",
buffer: mustNew(image.Point{1, 1}),
point: image.Point{1, 1},
wantErr: true,
},
{
desc: "the first cell cannot be partial",
buffer: mustNew(image.Point{1, 1}),
point: image.Point{0, 0},
want: false,
},
{
desc: "previous cell on the same line contains no rune",
buffer: mustNew(image.Point{3, 3}),
point: image.Point{1, 0},
want: false,
},
{
desc: "previous cell on the same line contains half-width rune",
buffer: func() Buffer {
b := mustNew(image.Point{3, 3})
b[0][0].Rune = 'A'
return b
}(),
point: image.Point{1, 0},
want: false,
},
{
desc: "previous cell on the same line contains full-width rune",
buffer: func() Buffer {
b := mustNew(image.Point{3, 3})
b[0][0].Rune = '世'
return b
}(),
point: image.Point{1, 0},
want: true,
},
{
desc: "previous cell on previous line contains no rune",
buffer: mustNew(image.Point{3, 3}),
point: image.Point{0, 1},
want: false,
},
{
desc: "previous cell on previous line contains half-width rune",
buffer: func() Buffer {
b := mustNew(image.Point{3, 3})
b[2][0].Rune = 'A'
return b
}(),
point: image.Point{0, 1},
want: false,
},
{
desc: "previous cell on previous line contains full-width rune",
buffer: func() Buffer {
b := mustNew(image.Point{3, 3})
b[2][0].Rune = '世'
return b
}(),
point: image.Point{0, 1},
want: true,
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
got, err := tc.buffer.IsPartial(tc.point)
if (err != nil) != tc.wantErr {
t.Errorf("IsPartial => unexpected error: %v, wantErr: %v", err, tc.wantErr)
}
if err != nil {
return
}
if got != tc.want {
t.Errorf("IsPartial => got %v, want %v", got, tc.want)
}
})
}
}
func TestRemWidth(t *testing.T) {
tests := []struct {
desc string
size image.Point
point image.Point
want int
wantErr bool
}{
{
desc: "point falls before the buffer",
size: image.Point{1, 1},
point: image.Point{-1, -1},
wantErr: true,
},
{
desc: "point falls after the buffer",
size: image.Point{1, 1},
point: image.Point{1, 1},
wantErr: true,
},
{
desc: "remaining width from the first cell on the line",
size: image.Point{3, 3},
point: image.Point{0, 1},
want: 3,
},
{
desc: "remaining width from the last cell on the line",
size: image.Point{3, 3},
point: image.Point{2, 2},
want: 1,
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
b, err := New(tc.size)
if err != nil {
t.Fatalf("New => unexpected error: %v", err)
}
got, err := b.RemWidth(tc.point)
if (err != nil) != tc.wantErr {
t.Errorf("RemWidth => unexpected error: %v, wantErr: %v", err, tc.wantErr)
}
if err != nil {
return
}
if got != tc.want {
t.Errorf("RemWidth => got %d, want %d", got, tc.want)
}
})
}
}

View File

@ -21,6 +21,7 @@ import (
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/internal/area"
"github.com/mum4k/termdash/internal/canvas/buffer"
"github.com/mum4k/termdash/internal/runewidth"
"github.com/mum4k/termdash/terminal/terminalapi"
)
@ -33,7 +34,7 @@ type Canvas struct {
area image.Rectangle
// buffer is where the drawing happens.
buffer cell.Buffer
buffer buffer.Buffer
}
// New returns a new Canvas with a buffer for the provided area.
@ -42,7 +43,7 @@ func New(ar image.Rectangle) (*Canvas, error) {
return nil, fmt.Errorf("area cannot start or end on the negative axis, got: %+v", ar)
}
b, err := cell.NewBuffer(area.Size(ar))
b, err := buffer.New(area.Size(ar))
if err != nil {
return nil, err
}
@ -65,7 +66,7 @@ func (c *Canvas) Area() image.Rectangle {
// Clear clears all the content on the canvas.
func (c *Canvas) Clear() error {
b, err := cell.NewBuffer(c.Size())
b, err := buffer.New(c.Size())
if err != nil {
return err
}
@ -83,7 +84,7 @@ func (c *Canvas) SetCell(p image.Point, r rune, opts ...cell.Option) (int, error
}
// Cell returns a copy of the specified cell.
func (c *Canvas) Cell(p image.Point) (*cell.Cell, error) {
func (c *Canvas) Cell(p image.Point) (*buffer.Cell, error) {
ar, err := area.FromSize(c.Size())
if err != nil {
return nil, err

View File

@ -21,6 +21,7 @@ import (
"github.com/kylelemons/godebug/pretty"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/internal/area"
"github.com/mum4k/termdash/internal/canvas/buffer"
"github.com/mum4k/termdash/internal/faketerm"
)
@ -547,7 +548,7 @@ func TestSetCellAndApply(t *testing.T) {
point image.Point
r rune
opts []cell.Option
want cell.Buffer // Expected back buffer in the fake terminal.
want buffer.Buffer // Expected back buffer in the fake terminal.
wantCells int
wantSetCellErr bool
wantApplyErr bool
@ -566,21 +567,21 @@ func TestSetCellAndApply(t *testing.T) {
point: image.Point{0, 0},
r: 'X',
wantCells: 1,
want: cell.Buffer{
want: buffer.Buffer{
{
cell.New(0),
cell.New(0),
cell.New(0),
buffer.NewCell(0),
buffer.NewCell(0),
buffer.NewCell(0),
},
{
cell.New(0),
cell.New('X'),
cell.New(0),
buffer.NewCell(0),
buffer.NewCell('X'),
buffer.NewCell(0),
},
{
cell.New(0),
cell.New(0),
cell.New(0),
buffer.NewCell(0),
buffer.NewCell(0),
buffer.NewCell(0),
},
},
},
@ -591,21 +592,21 @@ func TestSetCellAndApply(t *testing.T) {
point: image.Point{0, 0},
r: '界',
wantCells: 2,
want: cell.Buffer{
want: buffer.Buffer{
{
cell.New(0),
cell.New(0),
cell.New(0),
buffer.NewCell(0),
buffer.NewCell(0),
buffer.NewCell(0),
},
{
cell.New(0),
cell.New('界'),
cell.New(0),
buffer.NewCell(0),
buffer.NewCell('界'),
buffer.NewCell(0),
},
{
cell.New(0),
cell.New(0),
cell.New(0),
buffer.NewCell(0),
buffer.NewCell(0),
buffer.NewCell(0),
},
},
},
@ -624,21 +625,21 @@ func TestSetCellAndApply(t *testing.T) {
point: image.Point{1, 0},
r: 'X',
wantCells: 1,
want: cell.Buffer{
want: buffer.Buffer{
{
cell.New(0),
cell.New(0),
cell.New(0),
buffer.NewCell(0),
buffer.NewCell(0),
buffer.NewCell(0),
},
{
cell.New(0),
cell.New(0),
cell.New(0),
buffer.NewCell(0),
buffer.NewCell(0),
buffer.NewCell(0),
},
{
cell.New(0),
cell.New('X'),
cell.New(0),
buffer.NewCell(0),
buffer.NewCell('X'),
buffer.NewCell(0),
},
},
},
@ -649,21 +650,21 @@ func TestSetCellAndApply(t *testing.T) {
point: image.Point{0, 1},
r: 'X',
wantCells: 1,
want: cell.Buffer{
want: buffer.Buffer{
{
cell.New(0),
cell.New(0),
cell.New(0),
buffer.NewCell(0),
buffer.NewCell(0),
buffer.NewCell(0),
},
{
cell.New(0),
cell.New(0),
cell.New('X'),
buffer.NewCell(0),
buffer.NewCell(0),
buffer.NewCell('X'),
},
{
cell.New(0),
cell.New(0),
cell.New(0),
buffer.NewCell(0),
buffer.NewCell(0),
buffer.NewCell(0),
},
},
},
@ -674,21 +675,21 @@ func TestSetCellAndApply(t *testing.T) {
point: image.Point{1, 1},
r: 'Z',
wantCells: 1,
want: cell.Buffer{
want: buffer.Buffer{
{
cell.New(0),
cell.New(0),
cell.New(0),
buffer.NewCell(0),
buffer.NewCell(0),
buffer.NewCell(0),
},
{
cell.New(0),
cell.New(0),
cell.New(0),
buffer.NewCell(0),
buffer.NewCell(0),
buffer.NewCell(0),
},
{
cell.New(0),
cell.New(0),
cell.New('Z'),
buffer.NewCell(0),
buffer.NewCell(0),
buffer.NewCell('Z'),
},
},
},
@ -702,21 +703,21 @@ func TestSetCellAndApply(t *testing.T) {
cell.BgColor(cell.ColorRed),
},
wantCells: 1,
want: cell.Buffer{
want: buffer.Buffer{
{
cell.New(0),
cell.New(0),
cell.New(0),
buffer.NewCell(0),
buffer.NewCell(0),
buffer.NewCell(0),
},
{
cell.New(0),
cell.New(0),
cell.New(0),
buffer.NewCell(0),
buffer.NewCell(0),
buffer.NewCell(0),
},
{
cell.New(0),
cell.New(0),
cell.New('A', cell.BgColor(cell.ColorRed)),
buffer.NewCell(0),
buffer.NewCell(0),
buffer.NewCell('A', cell.BgColor(cell.ColorRed)),
},
},
},
@ -727,9 +728,9 @@ func TestSetCellAndApply(t *testing.T) {
point: image.Point{0, 0},
r: 'A',
wantCells: 1,
want: cell.Buffer{
want: buffer.Buffer{
{
cell.New('A'),
buffer.NewCell('A'),
},
},
},
@ -806,21 +807,21 @@ func TestClear(t *testing.T) {
t.Fatalf("Apply => unexpected error: %v", err)
}
want := cell.Buffer{
want := buffer.Buffer{
{
cell.New('A'),
cell.New(0),
cell.New(0),
buffer.NewCell('A'),
buffer.NewCell(0),
buffer.NewCell(0),
},
{
cell.New(0),
cell.New('X'),
cell.New(0),
buffer.NewCell(0),
buffer.NewCell('X'),
buffer.NewCell(0),
},
{
cell.New(0),
cell.New(0),
cell.New(0),
buffer.NewCell(0),
buffer.NewCell(0),
buffer.NewCell(0),
},
}
got := ft.BackBuffer()
@ -837,21 +838,21 @@ func TestClear(t *testing.T) {
t.Fatalf("Apply => unexpected error: %v", err)
}
want = cell.Buffer{
want = buffer.Buffer{
{
cell.New('A'),
cell.New(0),
cell.New(0),
buffer.NewCell('A'),
buffer.NewCell(0),
buffer.NewCell(0),
},
{
cell.New(0),
cell.New(0),
cell.New(0),
buffer.NewCell(0),
buffer.NewCell(0),
buffer.NewCell(0),
},
{
cell.New(0),
cell.New(0),
cell.New(0),
buffer.NewCell(0),
buffer.NewCell(0),
buffer.NewCell(0),
},
}
@ -889,9 +890,9 @@ func TestApplyFullWidthRunes(t *testing.T) {
t.Fatalf("Apply => unexpected error: %v", err)
}
want, err := cell.NewBuffer(area.Size(ar))
want, err := buffer.New(area.Size(ar))
if err != nil {
t.Fatalf("NewBuffer => unexpected error: %v", err)
t.Fatalf("buffer.New => unexpected error: %v", err)
}
want[fullP.X][fullP.Y].Rune = '界'
want[partP.X][partP.Y].Rune = 'A'
@ -907,7 +908,7 @@ func TestCell(t *testing.T) {
desc string
cvs func() (*Canvas, error)
point image.Point
want *cell.Cell
want *buffer.Cell
wantErr bool
}{
{
@ -939,7 +940,7 @@ func TestCell(t *testing.T) {
return cvs, nil
},
point: image.Point{1, 1},
want: &cell.Cell{
want: &buffer.Cell{
Rune: 'A',
Opts: cell.NewOptions(
cell.FgColor(cell.ColorRed),

View File

@ -21,6 +21,7 @@ import (
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/internal/canvas"
"github.com/mum4k/termdash/internal/canvas/buffer"
"github.com/mum4k/termdash/internal/faketerm"
)
@ -59,7 +60,7 @@ func MustSetAreaCells(c *canvas.Canvas, cellArea image.Rectangle, r rune, opts .
}
// MustCell returns the cell or panics.
func MustCell(c *canvas.Canvas, p image.Point) *cell.Cell {
func MustCell(c *canvas.Canvas, p image.Point) *buffer.Cell {
cell, err := c.Cell(p)
if err != nil {
panic(fmt.Sprintf("canvas.Cell => unexpected error: %v", err))

View File

@ -24,6 +24,7 @@ import (
"sync"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/internal/canvas/buffer"
"github.com/mum4k/termdash/internal/event/eventqueue"
"github.com/mum4k/termdash/terminal/terminalapi"
)
@ -55,7 +56,7 @@ func WithEventQueue(eq *eventqueue.Unbound) Option {
// This implementation is thread-safe.
type Terminal struct {
// buffer holds the terminal cells.
buffer cell.Buffer
buffer buffer.Buffer
// events is a queue of input events.
events *eventqueue.Unbound
@ -66,7 +67,7 @@ type Terminal struct {
// New returns a new fake Terminal.
func New(size image.Point, opts ...Option) (*Terminal, error) {
b, err := cell.NewBuffer(size)
b, err := buffer.New(size)
if err != nil {
return nil, err
}
@ -95,7 +96,7 @@ func (t *Terminal) Resize(size image.Point) error {
t.mu.Lock()
defer t.mu.Unlock()
b, err := cell.NewBuffer(size)
b, err := buffer.New(size)
if err != nil {
return err
}
@ -105,7 +106,7 @@ func (t *Terminal) Resize(size image.Point) error {
}
// BackBuffer returns the back buffer of the fake terminal.
func (t *Terminal) BackBuffer() cell.Buffer {
func (t *Terminal) BackBuffer() buffer.Buffer {
t.mu.Lock()
defer t.mu.Unlock()
@ -155,7 +156,7 @@ func (t *Terminal) Clear(opts ...cell.Option) error {
t.mu.Lock()
defer t.mu.Unlock()
b, err := cell.NewBuffer(t.buffer.Size())
b, err := buffer.New(t.buffer.Size())
if err != nil {
return err
}