mirror of https://github.com/mum4k/termdash.git
Implementing cell, buffer, container options and fake terminal.
Push after a partial commit to prevent data loss. This isn't complete and doesn't have complete test coverage.
This commit is contained in:
parent
53fe40fcec
commit
59e1bd6472
|
@ -2,27 +2,73 @@
|
|||
package canvas
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/terminalapi"
|
||||
)
|
||||
|
||||
// Canvas is where a widget draws its output for display on the terminal.
|
||||
type Canvas struct{}
|
||||
type Canvas struct {
|
||||
// area is the area the buffer was created for.
|
||||
area image.Rectangle
|
||||
|
||||
// Size returns the size of the 2-D canvas given to the widget.
|
||||
// buffer is where the drawing happens.
|
||||
buffer cell.Buffer
|
||||
}
|
||||
|
||||
// New returns a new Canvas with a buffer for the provided area.
|
||||
func New(area image.Rectangle) (*Canvas, error) {
|
||||
if area.Min.X < 0 || area.Min.Y < 0 || area.Max.X < 0 || area.Max.Y < 0 {
|
||||
return nil, fmt.Errorf("area cannot start or end on the negative axis, got: %+v", area)
|
||||
}
|
||||
size := image.Point{
|
||||
area.Dx() + 1,
|
||||
area.Dy() + 1,
|
||||
}
|
||||
b, err := cell.NewBuffer(size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Canvas{
|
||||
area: area,
|
||||
buffer: b,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Size returns the size of the 2-D canvas.
|
||||
func (c *Canvas) Size() image.Point {
|
||||
return image.Point{0, 0}
|
||||
return c.buffer.Size()
|
||||
}
|
||||
|
||||
// Clear clears all the content on the canvas.
|
||||
func (c *Canvas) Clear() {}
|
||||
|
||||
// FlushDesired provides a hint to the infrastructure that the canvas was
|
||||
// changed and should be flushed to the terminal.
|
||||
func (c *Canvas) FlushDesired() {}
|
||||
func (c *Canvas) Clear() error {
|
||||
b, err := cell.NewBuffer(c.Size())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.buffer = b
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetCell sets the value of the specified cell on the canvas.
|
||||
// Use the options to specify which attributes to modify, if an attribute
|
||||
// option isn't specified, the attribute retains its previous value.
|
||||
func (c *Canvas) SetCell(p image.Point, r rune, opts ...cell.Option) {}
|
||||
func (c *Canvas) SetCell(p image.Point, r rune, opts ...cell.Option) error {
|
||||
if area := c.buffer.Area(); !p.In(area) {
|
||||
return fmt.Errorf("cell at point %+v falls out of the canvas area %+v", p, area)
|
||||
}
|
||||
|
||||
cell := c.buffer[p.X][p.Y]
|
||||
cell.Rune = r
|
||||
cell.Apply(opts...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyTo copies the content of the canvas onto the provided terminal.
|
||||
// Guarantees to stay within limits of the area the canvas was created with.
|
||||
func (c *Canvas) Apply(t terminalapi.Terminal) error {
|
||||
return errors.New("unimplemented")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
package canvas
|
||||
|
||||
import (
|
||||
"image"
|
||||
"testing"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
area image.Rectangle
|
||||
wantSize image.Point
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "area min has negative X",
|
||||
area: image.Rect(-1, 0, 0, 0),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "area min has negative Y",
|
||||
area: image.Rect(0, -1, 0, 0),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "area max has negative X",
|
||||
area: image.Rect(0, 0, -1, 0),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "area max has negative Y",
|
||||
area: image.Rect(0, 0, 0, -1),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "smallest valid size",
|
||||
area: image.Rect(0, 0, 0, 0),
|
||||
wantSize: image.Point{1, 1},
|
||||
},
|
||||
{
|
||||
desc: "rectangular canvas 3 by 4",
|
||||
area: image.Rect(0, 0, 2, 3),
|
||||
wantSize: image.Point{3, 4},
|
||||
},
|
||||
{
|
||||
desc: "non-zero based area",
|
||||
area: image.Rect(1, 1, 2, 3),
|
||||
wantSize: image.Point{2, 3},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
c, err := New(tc.area)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("New => unexpected error: %v, wantErr: %v", err, tc.wantErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
got := c.Size()
|
||||
if diff := pretty.Compare(tc.wantSize, got); diff != "" {
|
||||
t.Errorf("Size => unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
66
cell/cell.go
66
cell/cell.go
|
@ -5,6 +5,11 @@ A cell is the smallest point on the terminal.
|
|||
*/
|
||||
package cell
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
)
|
||||
|
||||
// Option is used to provide options for cells on a 2-D terminal.
|
||||
type Option interface {
|
||||
// set sets the provided option.
|
||||
|
@ -26,6 +31,67 @@ func NewOptions(opts ...Option) *Options {
|
|||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
type Buffer [][]*Cell
|
||||
|
||||
// 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]),
|
||||
}
|
||||
}
|
||||
|
||||
// Area returns the area that is covered by this buffer.
|
||||
func (b Buffer) Area() image.Rectangle {
|
||||
s := b.Size()
|
||||
return image.Rect(0, 0, s.X-1, s.Y-1)
|
||||
}
|
||||
|
||||
// option implements Option.
|
||||
type option func(*Options)
|
||||
|
||||
|
|
|
@ -0,0 +1,277 @@
|
|||
package cell
|
||||
|
||||
import (
|
||||
"image"
|
||||
"testing"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
)
|
||||
|
||||
func TestNewOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
opts []Option
|
||||
want *Options
|
||||
}{
|
||||
{
|
||||
desc: "no provided options",
|
||||
want: &Options{},
|
||||
},
|
||||
{
|
||||
desc: "setting foreground color",
|
||||
opts: []Option{
|
||||
FgColor(ColorBlack),
|
||||
},
|
||||
want: &Options{
|
||||
FgColor: ColorBlack,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "setting background color",
|
||||
opts: []Option{
|
||||
BgColor(ColorRed),
|
||||
},
|
||||
want: &Options{
|
||||
BgColor: ColorRed,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "setting multiple options",
|
||||
opts: []Option{
|
||||
FgColor(ColorCyan),
|
||||
BgColor(ColorMagenta),
|
||||
},
|
||||
want: &Options{
|
||||
FgColor: ColorCyan,
|
||||
BgColor: ColorMagenta,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := NewOptions(tc.opts...)
|
||||
if diff := pretty.Compare(tc.want, got); diff != "" {
|
||||
t.Errorf("NewOptions => unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBufferArea(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
size image.Point
|
||||
want image.Rectangle
|
||||
}{
|
||||
{
|
||||
desc: "single cell buffer",
|
||||
size: image.Point{1, 1},
|
||||
want: image.Rectangle{
|
||||
Min: image.Point{0, 0},
|
||||
Max: image.Point{0, 0},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "rectangular buffer",
|
||||
size: image.Point{3, 4},
|
||||
want: image.Rectangle{
|
||||
Min: image.Point{0, 0},
|
||||
Max: image.Point{2, 3},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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 := b.Area()
|
||||
if diff := pretty.Compare(tc.want, got); diff != "" {
|
||||
t.Errorf("Area => unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -8,8 +8,10 @@ canvases assigned to the placed widgets.
|
|||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
|
||||
"github.com/mum4k/termdash/terminalapi"
|
||||
"github.com/mum4k/termdash/widget"
|
||||
)
|
||||
|
||||
// Container wraps either sub containers or widgets and positions them on the
|
||||
|
@ -22,32 +24,31 @@ type Container struct {
|
|||
second *Container
|
||||
|
||||
// term is the terminal this container is placed on.
|
||||
// All containers in the tree share the same terminal.
|
||||
term terminalapi.Terminal
|
||||
|
||||
// split identifies how is this container split.
|
||||
split splitType
|
||||
// area is the area of the terminal this container has access to.
|
||||
area image.Rectangle
|
||||
|
||||
// widget is the widget in the container.
|
||||
// A container can have either two sub containers (left and right) or a
|
||||
// widget. But not both.
|
||||
widget widget.Widget
|
||||
|
||||
// Alignment of the widget if present.
|
||||
hAlign hAlignType
|
||||
vAlign vAlignType
|
||||
// opts are the options provided to the container.
|
||||
opts *options
|
||||
}
|
||||
|
||||
// New returns a new root container that will use the provided terminal and
|
||||
// applies the provided options.
|
||||
func New(t terminalapi.Terminal, opts ...Option) *Container {
|
||||
c := &Container{
|
||||
term: t,
|
||||
o := &options{}
|
||||
for _, opt := range opts {
|
||||
opt.set(o)
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt.set(c)
|
||||
size := t.Size()
|
||||
return &Container{
|
||||
term: t,
|
||||
// The root container has access to the entire terminal.
|
||||
area: image.Rect(0, 0, size.X, size.Y),
|
||||
opts: o,
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Returns the parent container of this container.
|
||||
|
@ -59,7 +60,7 @@ func (c *Container) Parent(opts ...Option) *Container {
|
|||
|
||||
p := c.parent
|
||||
for _, opt := range opts {
|
||||
opt.set(p)
|
||||
opt.set(p.opts)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
@ -72,13 +73,13 @@ func (c *Container) Parent(opts ...Option) *Container {
|
|||
// Returns nil if this container contains a widget, containers with widgets
|
||||
// cannot have sub containers.
|
||||
func (c *Container) First(opts ...Option) *Container {
|
||||
if c == nil || c.widget != nil {
|
||||
if c == nil || c.opts.widget != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if child := c.first; child != nil {
|
||||
for _, opt := range opts {
|
||||
opt.set(child)
|
||||
opt.set(child.opts)
|
||||
}
|
||||
return child
|
||||
}
|
||||
|
@ -96,13 +97,13 @@ func (c *Container) First(opts ...Option) *Container {
|
|||
// Returns nil if this container contains a widget, containers with widgets
|
||||
// cannot have sub containers.
|
||||
func (c *Container) Second(opts ...Option) *Container {
|
||||
if c == nil || c.widget != nil {
|
||||
if c == nil || c.opts.widget != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if child := c.second; child != nil {
|
||||
for _, opt := range opts {
|
||||
opt.set(child)
|
||||
opt.set(child.opts)
|
||||
}
|
||||
return child
|
||||
}
|
||||
|
@ -111,3 +112,9 @@ func (c *Container) Second(opts ...Option) *Container {
|
|||
c.second.parent = c
|
||||
return c.second
|
||||
}
|
||||
|
||||
// Draw requests all widgets in this and all sub containers to draw on their
|
||||
// respective canvases.
|
||||
func (c *Container) Draw() error {
|
||||
return errors.New("unimplemented")
|
||||
}
|
||||
|
|
|
@ -7,31 +7,46 @@ import "github.com/mum4k/termdash/widget"
|
|||
// Option is used to provide options.
|
||||
type Option interface {
|
||||
// set sets the provided option.
|
||||
set(*Container)
|
||||
set(*options)
|
||||
}
|
||||
|
||||
// options stores the provided options.
|
||||
type options struct{}
|
||||
// options stores the options provided to the container.
|
||||
type options struct {
|
||||
// split identifies how is this container split.
|
||||
split splitType
|
||||
|
||||
// widget is the widget in the container.
|
||||
// A container can have either two sub containers (left and right) or a
|
||||
// widget. But not both.
|
||||
widget widget.Widget
|
||||
|
||||
// Alignment of the widget if present.
|
||||
hAlign hAlignType
|
||||
vAlign vAlignType
|
||||
|
||||
// border is the border around the container.
|
||||
border borderType
|
||||
}
|
||||
|
||||
// option implements Option.
|
||||
type option func(*Container)
|
||||
type option func(*options)
|
||||
|
||||
// set implements Option.set.
|
||||
func (o option) set(c *Container) {
|
||||
o(c)
|
||||
func (o option) set(opts *options) {
|
||||
o(opts)
|
||||
}
|
||||
|
||||
// PlaceWidget places the provided widget into the container.
|
||||
func PlaceWidget(w widget.Widget) Option {
|
||||
return option(func(c *Container) {
|
||||
c.widget = w
|
||||
return option(func(opts *options) {
|
||||
opts.widget = w
|
||||
})
|
||||
}
|
||||
|
||||
// SplitHorizontal configures the container for a horizontal split.
|
||||
func SplitHorizontal() Option {
|
||||
return option(func(c *Container) {
|
||||
c.split = splitTypeHorizontal
|
||||
return option(func(opts *options) {
|
||||
opts.split = splitTypeHorizontal
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -39,8 +54,8 @@ func SplitHorizontal() Option {
|
|||
// This is the default split type if neither if SplitHorizontal() or
|
||||
// SplitVertical() is specified.
|
||||
func SplitVertical() Option {
|
||||
return option(func(c *Container) {
|
||||
c.split = splitTypeVertical
|
||||
return option(func(opts *options) {
|
||||
opts.split = splitTypeVertical
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -48,8 +63,8 @@ func SplitVertical() Option {
|
|||
// container along the horizontal axis. Has no effect if the container contains
|
||||
// no widget. This is the default horizontal alignment if no other is specified.
|
||||
func HorizontalAlignLeft() Option {
|
||||
return option(func(c *Container) {
|
||||
c.hAlign = hAlignTypeLeft
|
||||
return option(func(opts *options) {
|
||||
opts.hAlign = hAlignTypeLeft
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -57,8 +72,8 @@ func HorizontalAlignLeft() Option {
|
|||
// container along the horizontal axis. Has no effect if the container contains
|
||||
// no widget.
|
||||
func HorizontalAlignCenter() Option {
|
||||
return option(func(c *Container) {
|
||||
c.hAlign = hAlignTypeCenter
|
||||
return option(func(opts *options) {
|
||||
opts.hAlign = hAlignTypeCenter
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -66,8 +81,8 @@ func HorizontalAlignCenter() Option {
|
|||
// container along the horizontal axis. Has no effect if the container contains
|
||||
// no widget.
|
||||
func HorizontalAlignRight() Option {
|
||||
return option(func(c *Container) {
|
||||
c.hAlign = hAlignTypeRight
|
||||
return option(func(opts *options) {
|
||||
opts.hAlign = hAlignTypeRight
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -75,8 +90,8 @@ func HorizontalAlignRight() Option {
|
|||
// container along the vertical axis. Has no effect if the container contains
|
||||
// no widget. This is the default vertical alignment if no other is specified.
|
||||
func VerticalAlignTop() Option {
|
||||
return option(func(c *Container) {
|
||||
c.vAlign = vAlignTypeTop
|
||||
return option(func(opts *options) {
|
||||
opts.vAlign = vAlignTypeTop
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -84,8 +99,8 @@ func VerticalAlignTop() Option {
|
|||
// container along the vertical axis. Has no effect if the container contains
|
||||
// no widget.
|
||||
func VerticalAlignMiddle() Option {
|
||||
return option(func(c *Container) {
|
||||
c.vAlign = vAlignTypeMiddle
|
||||
return option(func(opts *options) {
|
||||
opts.vAlign = vAlignTypeMiddle
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -93,8 +108,24 @@ func VerticalAlignMiddle() Option {
|
|||
// container along the vertical axis. Has no effect if the container contains
|
||||
// no widget.
|
||||
func VerticalAlignBottom() Option {
|
||||
return option(func(c *Container) {
|
||||
c.vAlign = vAlignTypeBottom
|
||||
return option(func(opts *options) {
|
||||
opts.vAlign = vAlignTypeBottom
|
||||
})
|
||||
}
|
||||
|
||||
// BorderNone configures the container to have no border.
|
||||
// This is the default if none of the Border options is specified.
|
||||
func BorderNone() Option {
|
||||
return option(func(opts *options) {
|
||||
opts.border = borderTypeNone
|
||||
})
|
||||
}
|
||||
|
||||
// BorderSolid configures the container to have a border made with a solid
|
||||
// line.
|
||||
func BorderSolid() Option {
|
||||
return option(func(opts *options) {
|
||||
opts.border = borderTypeSolid
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -167,3 +198,25 @@ const (
|
|||
vAlignTypeMiddle
|
||||
vAlignTypeBottom
|
||||
)
|
||||
|
||||
// borderType represents
|
||||
type borderType int
|
||||
|
||||
// String implements fmt.Stringer()
|
||||
func (bt borderType) String() string {
|
||||
if n, ok := borderTypeNames[bt]; ok {
|
||||
return n
|
||||
}
|
||||
return "borderTypeUnknown"
|
||||
}
|
||||
|
||||
// borderTypeNames maps borderType values to human readable names.
|
||||
var borderTypeNames = map[borderType]string{
|
||||
borderTypeNone: "borderTypeNone",
|
||||
borderTypeSolid: "borderTypeSolid",
|
||||
}
|
||||
|
||||
const (
|
||||
borderTypeNone borderType = iota
|
||||
borderTypeSolid
|
||||
)
|
||||
|
|
|
@ -108,9 +108,6 @@ Containers can be styled with borders and other options.
|
|||
All widgets indirectly write to the back buffer of the terminal implementation. The changes
|
||||
to the back buffer only become visible when the infrastructure flushes its content.
|
||||
|
||||
Widgets cannot force a flush, but they can indicate that a flush is desired.
|
||||
The infrastructure throttles the amount of times this happens.
|
||||
|
||||
#### Terminal resizing
|
||||
|
||||
The terminal resize events are processed by the infrastructure. Each widget
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
// Package faketerm is a fake implementation of the terminal for the use in tests.
|
||||
package faketerm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"log"
|
||||
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/terminalapi"
|
||||
)
|
||||
|
||||
// Option is used to provide options.
|
||||
type Option interface {
|
||||
// set sets the provided option.
|
||||
set(*Terminal)
|
||||
}
|
||||
|
||||
// option implements Option.
|
||||
type option func(*Terminal)
|
||||
|
||||
// set implements Option.set.
|
||||
func (o option) set(t *Terminal) {
|
||||
o(t)
|
||||
}
|
||||
|
||||
// Terminal is a fake terminal.
|
||||
// This implementation is thread-safe.
|
||||
type Terminal struct {
|
||||
// buffer holds the terminal cells.
|
||||
buffer cell.Buffer
|
||||
}
|
||||
|
||||
// New returns a new fake Terminal.
|
||||
func New(size image.Point, opts ...Option) (*Terminal, error) {
|
||||
b, err := cell.NewBuffer(size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := &Terminal{
|
||||
buffer: b,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.set(t)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Implements terminalapi.Terminal.Size.
|
||||
func (t *Terminal) Size() image.Point {
|
||||
return t.buffer.Size()
|
||||
}
|
||||
|
||||
// Implements terminalapi.Terminal.Clear.
|
||||
func (t *Terminal) Clear(opts ...cell.Option) error {
|
||||
b, err := cell.NewBuffer(t.buffer.Size())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.buffer = b
|
||||
return nil
|
||||
}
|
||||
|
||||
// Implements terminalapi.Terminal.Flush.
|
||||
func (t *Terminal) Flush() error {
|
||||
return errors.New("unimplemented")
|
||||
}
|
||||
|
||||
// Implements terminalapi.Terminal.SetCursor.
|
||||
func (t *Terminal) SetCursor(p image.Point) {
|
||||
log.Fatal("unimplemented")
|
||||
}
|
||||
|
||||
// Implements terminalapi.Terminal.HideCursor.
|
||||
func (t *Terminal) HideCursor() {
|
||||
log.Fatal("unimplemented")
|
||||
}
|
||||
|
||||
// Implements terminalapi.Terminal.SetCell.
|
||||
func (t *Terminal) SetCell(p image.Point, r rune, opts ...cell.Option) error {
|
||||
if area := t.buffer.Area(); !p.In(area) {
|
||||
return fmt.Errorf("cell at point %+v falls out of the terminal area %+v", p, area)
|
||||
}
|
||||
|
||||
cell := t.buffer[p.X][p.Y]
|
||||
cell.Rune = r
|
||||
cell.Apply(opts...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Implements terminalapi.Terminal.Event.
|
||||
func (t *Terminal) Event(ctx context.Context) terminalapi.Event {
|
||||
log.Fatal("unimplemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Closes the terminal. This is a no-op on the fake terminal.
|
||||
func (t *Terminal) Close() {}
|
|
@ -13,18 +13,17 @@ type Options struct {
|
|||
}
|
||||
|
||||
// Widget is a single widget on the dashboard.
|
||||
// Implementations must be thread safe.
|
||||
type Widget interface {
|
||||
// Draw executes the widget, when called the widget should draw on the
|
||||
// canvas. The widget can assume that the canvas content wasn't modified
|
||||
// since the last call, i.e. if the widget doesn't need to change anything in
|
||||
// the output, this can be a no-op.
|
||||
// When the infrastructure calls Draw(), the widget must block on the call
|
||||
// until it finishes drawing onto the provided canvas. When given the
|
||||
// canvas, the widget must first determine its size by calling
|
||||
// Canvas.Size(), then limit all its drawing to this area.
|
||||
//
|
||||
// The widget must not assume that the size of the canvas or its content
|
||||
// remains the same between calls.
|
||||
Draw(canvas *canvas.Canvas) error
|
||||
|
||||
// Redraw is called when the widget must redraw all of its content because
|
||||
// the previous canvas was invalidated. The widget must not assume that
|
||||
// anything on the canvas remained the same, including its size.
|
||||
Redraw(canvas *canvas.Canvas) error
|
||||
|
||||
// Keyboard is called when the widget is focused on the dashboard and a key
|
||||
// shortcut the widget registered for was pressed. Only called if the widget
|
||||
// registered for keyboard events.
|
||||
|
|
Loading…
Reference in New Issue