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:
parent
0938ae91d8
commit
984f37245d
172
cell/cell.go
172
cell/cell.go
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
169
internal/canvas/buffer/buffer.go
Normal file
169
internal/canvas/buffer/buffer.go
Normal 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]),
|
||||
}
|
||||
}
|
580
internal/canvas/buffer/buffer_test.go
Normal file
580
internal/canvas/buffer/buffer_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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),
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user