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

Defining the APIs.

This commit is contained in:
Jakub Sobon 2018-03-27 19:01:35 +01:00
parent 0617fd5ecf
commit bc42865277
No known key found for this signature in database
GPG Key ID: F2451A77FB05D3B7
13 changed files with 709 additions and 130 deletions

View File

@ -46,7 +46,7 @@ See the [design document](doc/design.md).
# Project status
- [x] High-Level Design.
- [ ] Submit the APIs.
- [x] Submit the APIs.
- [ ] Implement the terminal layer.
- [ ] Implement the container.
- [ ] Implement the input event pre-processing.

28
canvas/canvas.go Normal file
View File

@ -0,0 +1,28 @@
// Package canvas defines the canvas that the widgets draw on.
package canvas
import (
"image"
"github.com/mum4k/termdash/cell"
)
// Canvas is where a widget draws its output for display on the terminal.
type Canvas struct{}
// Size returns the size of the 2-D canvas given to the widget.
func (c *Canvas) Size() image.Point {
return image.Point{0, 0}
}
// 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() {}
// 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) {}

40
cell/cell.go Normal file
View File

@ -0,0 +1,40 @@
/*
Package cell implements cell options and attributes.
A cell is the smallest point on the terminal.
*/
package cell
// Option is used to provide options for cells on a 2-D terminal.
type Option interface {
// set sets the provided option.
set(*Options)
}
// Options stores the provided options.
type Options struct {
FgColor Color
BgColor Color
}
// option implements Option.
type option func(*Options)
// set implements Option.set.
func (co option) set(opts *Options) {
co(opts)
}
// FgColor sets the foreground color of the cell.
func FgColor(color Color) Option {
return option(func(co *Options) {
co.FgColor = color
})
}
// BgColor sets the background color of the cell.
func BgColor(color Color) Option {
return option(func(co *Options) {
co.BgColor = color
})
}

39
cell/color.go Normal file
View File

@ -0,0 +1,39 @@
package cell
// color.go defines constants for cell colors.
// Color is the color of a cell.
type Color int
// String implements fmt.Stringer()
func (cc Color) String() string {
if n, ok := colorNames[cc]; ok {
return n
}
return "ColorUnknown"
}
// colorNames maps Color values to human readable names.
var colorNames = map[Color]string{
ColorDefault: "ColorDefault",
ColorBlack: "ColorBlack",
ColorRed: "ColorRed",
ColorGreen: "ColorGreen",
ColorYellow: "ColorYellow",
ColorBlue: "ColorBlue",
ColorMagenta: "ColorMagenta",
ColorCyan: "ColorCyan",
ColorWhite: "ColorWhite",
}
const (
ColorDefault Color = iota
ColorBlack
ColorRed
ColorGreen
ColorYellow
ColorBlue
ColorMagenta
ColorCyan
ColorWhite
)

113
container/container.go Normal file
View File

@ -0,0 +1,113 @@
/*
Package container defines a type that wraps other containers or widgets.
The container supports splitting container into sub containers, defining
container styles and placing widgets. The container also creates and manages
canvases assigned to the placed widgets.
*/
package container
import (
"github.com/mum4k/termdash/terminalapi"
"github.com/mum4k/termdash/widget"
)
// Container wraps either sub containers or widgets and positions them on the
// terminal.
type Container struct {
// parent is the parent container, nil if this is the root container.
parent *Container
// The sub containers, if these aren't nil, the widget must be.
first *Container
second *Container
// term is the terminal this container is placed on.
term terminalapi.Terminal
// 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
}
// 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,
}
for _, opt := range opts {
opt.set(c)
}
return c
}
// Returns the parent container of this container.
// Returns nil if this container is the root of the container tree.
func (c *Container) Parent(opts ...Option) *Container {
if c == nil || c.parent == nil {
return nil
}
p := c.parent
for _, opt := range opts {
opt.set(p)
}
return p
}
// First returns the first sub container of this container.
// This is the left sub container when using SplitVertical() or the top sub
// container when using SplitHorizontal().
// If this container doesn't have the first sub container yet, it will be
// created. Applies the provided options to the first sub 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 {
return nil
}
if child := c.first; child != nil {
for _, opt := range opts {
opt.set(child)
}
return child
}
c.first = New(c.term, opts...)
c.first.parent = c
return c.first
}
// Second returns the second sub container of this container.
// This is the left sub container when using SplitVertical() or the top sub
// container when using SplitHorizontal().
// If this container doesn't have the second sub container yet, it will be
// created. Applies the provided options to the second sub 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 {
return nil
}
if child := c.second; child != nil {
for _, opt := range opts {
opt.set(child)
}
return child
}
c.second = New(c.term, opts...)
c.second.parent = c
return c.second
}

View File

@ -0,0 +1,19 @@
package container
// Example demonstrates how to use the Container API.
func Example() {
New( // Create the root container.
/* terminal = */ nil,
SplitHorizontal(),
).First( // This is the top half part of the terminal.
SplitVertical(),
).First( // Left side on the top.
VerticalAlignTop(),
PlaceWidget( /* widget = */ nil),
).Parent().Second( // Right side on the top.
HorizontalAlignRight(),
PlaceWidget( /* widget = */ nil),
).Parent().Parent().Second( // Bottom half of the terminal.
PlaceWidget( /* widget = */ nil),
)
}

169
container/options.go Normal file
View File

@ -0,0 +1,169 @@
package container
// options.go defines container options.
import "github.com/mum4k/termdash/widget"
// Option is used to provide options.
type Option interface {
// set sets the provided option.
set(*Container)
}
// options stores the provided options.
type options struct{}
// option implements Option.
type option func(*Container)
// set implements Option.set.
func (o option) set(c *Container) {
o(c)
}
// PlaceWidget places the provided widget into the container.
func PlaceWidget(w widget.Widget) Option {
return option(func(c *Container) {
c.widget = w
})
}
// SplitHorizontal configures the container for a horizontal split.
func SplitHorizontal() Option {
return option(func(c *Container) {
c.split = splitTypeHorizontal
})
}
// SplitVertical configures the container for a vertical split.
// 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
})
}
// HorizontalAlignLeft aligns the placed widget on the left of the
// 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
})
}
// HorizontalAlignCenter aligns the placed widget in the center of the
// 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
})
}
// HorizontalAlignRight aligns the placed widget on the right of the
// 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
})
}
// VerticalAlignTop aligns the placed widget on the top of the
// 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
})
}
// VerticalAlignMiddle aligns the placed widget in the middle of the
// 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
})
}
// VerticalAlignBottom aligns the placed widget at the bottom of the
// 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
})
}
// splitType identifies how a container is split.
type splitType int
// String implements fmt.Stringer()
func (st splitType) String() string {
if n, ok := splitTypeNames[st]; ok {
return n
}
return "splitTypeUnknown"
}
// splitTypeNames maps splitType values to human readable names.
var splitTypeNames = map[splitType]string{
splitTypeVertical: "splitTypeVertical",
splitTypeHorizontal: "splitTypeHorizontal",
}
const (
splitTypeVertical splitType = iota
splitTypeHorizontal
)
// hAlignType indicates the horizontal alignment of the widget in the container.
type hAlignType int
// String implements fmt.Stringer()
func (hat hAlignType) String() string {
if n, ok := hAlignTypeNames[hat]; ok {
return n
}
return "hAlignTypeUnknown"
}
// hAlignTypeNames maps hAlignType values to human readable names.
var hAlignTypeNames = map[hAlignType]string{
hAlignTypeLeft: "hAlignTypeLeft",
hAlignTypeCenter: "hAlignTypeCenter",
hAlignTypeRight: "hAlignTypeRight",
}
const (
hAlignTypeLeft hAlignType = iota
hAlignTypeCenter
hAlignTypeRight
)
// vAlignType represents
type vAlignType int
// String implements fmt.Stringer()
func (vat vAlignType) String() string {
if n, ok := vAlignTypeNames[vat]; ok {
return n
}
return "vAlignTypeUnknown"
}
// vAlignTypeNames maps vAlignType values to human readable names.
var vAlignTypeNames = map[vAlignType]string{
vAlignTypeTop: "vAlignTypeTop",
vAlignTypeMiddle: "vAlignTypeMiddle",
vAlignTypeBottom: "vAlignTypeBottom",
}
const (
vAlignTypeTop vAlignType = iota
vAlignTypeMiddle
vAlignTypeBottom
)

View File

@ -146,152 +146,45 @@ The Terminal API is an interface private to the terminal dashboard library. Its
primary purpose is to act as a shim layer over different terminal
implementations.
The following outlines the terminal API:
```go
// Terminal abstracts an implementation of a 2-D terminal.
// A terminal consists of a number of cells.
type Terminal interface {
// Size returns the terminal width and height in cells.
Size() image.Point
// Clear clears the content of the internal back buffer, resetting all cells
// to their default content and attributes.
Clear() error
// Flush flushes the internal back buffer to the terminal.
Flush() error
// SetCursor sets the position of the cursor.
SetCursor(p image.Point)
// HideCursos hides the cursor.
HideCursor()
// SetCell sets the value of the specified cell to the provided rune.
// Use the options to specify which attributes to modify, if an attribute
// option isn't specified, the attribute retains its previous value.
SetCell(p image.Point, r rune, opts ...CellOption)
// Event waits for the next event and returns it.
// This call blocks until the next event or cancellation of the context.
Event(ctx context.Context) Event
}
```
The Terminal API is defined in the
[terminalapi](http://github.com/mum4k/termdash/terminalapi/terminalapi.go)
package.
The **Event()** method returns the next input event. Different input event
types are defined as follows.
```go
// Event represents an input event.
type Event interface {
isEvent()
}
// Keyboard is the event used when a key is pressed.
// Implements Event.
type Keyboard struct {
// Key identifies the pressed key.
Key rune
}
func (*Keyboard) isEvent() {}
// Resize is the event used when the terminal was resized.
// Implements Event.
type Resize struct {
// Size is the new size of the terminal.
Size image.Point
}
func (*Resize) isEvent() {}
// Mouse is the event used when the mouse is moved or a mouse button is
// pressed.
// Implements Event.
type Mouse struct {
// Position of the mouse on the terminal.
Position() image.Point
// Button identifies the pressed button if any.
Button MouseButton
}
func (*Mouse) isEvent() {}
```
types are defined in the
[event.go](http://github.com/mum4k/termdash/terminalapi/event.go)
file.
### Container API
The container API is used to split the terminal and place the widgets. Each
The Container API is used to split the terminal and place the widgets. Each
container can be split to two sub containers or have a widget placed into it.
A container can be split either horizontally or vertically.
The containers further accept styling options and alignment options. The
following indicates how the container API will be used.
following indicates how the Container API will be used.
```go
func main() {
w := mywidget.New(widgetOptions)
t := terminal.New(terminalOptions)
The Container API is defined in the
[container](http://github.com/mum4k/termdash/container/container.go)
package.
t.VerticalSplit()
.Left(AlignCenter(), WithWidget(w))
.Right(SolidBorder())
}
```
A demonstration how this is used from the client perspective is in the
[container_test.go](http://github.com/mum4k/termdash/container/container_test.go)
file.
### Widget API
Each widget must implement the following API. All widget implementations must
Each widget must implement the Widget API. All widget implementations must
be thread-safe since the calls that update the displayed values come in
concurrently with requests and events from the infrastructure.
```go
// Canvas is where a widget draws its output for display on the terminal.
type Canvas struct {}
The Widget API is defined in the
[widget](http://github.com/mum4k/termdash/widget/widget.go)
package.
// Size returns the size of the 2-D canvas given to the widget.
func (c *Canvas) Size() image.Point {}
// 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() {}
// 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 ...CellOption) {}
// Widget is a single widget on the dashboard.
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.
Draw(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) 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.
Keyboard(s Shortcut) error
// Mouse is called when the widget is focused on the dashboard and a mouse
// event happens on its canvas. Only called if the widget registered for mouse
// events.
Mouse(m *Mouse) error
// Options returns registration options for the widget.
// This is how the widget indicates to the infrastructure whether it is
// interested in keyboard or mouse shortcuts, what is its minimum canvas
// size, etc.
Options() *WidgetOptions
}
```
Each widget gets a Canvas to draw on. The Canvas API is defined in the
[canvas](http://github.com/mum4k/termdash/canvas/canvas.go)
package.
## Testing plan

118
keyboard/keyboard.go Normal file
View File

@ -0,0 +1,118 @@
// Package keyboard defines well known keyboard keys and shortcuts.
package keyboard
// Button represents a single button on the keyboard.
type Button rune
// String implements fmt.Stringer()
func (b Button) String() string {
if n, ok := buttonNames[b]; ok {
return n
}
return "ButtonUnknown"
}
// buttonNames maps Button values to human readable names.
var buttonNames = map[Button]string{
ButtonArrowDown: "ButtonArrowDown",
ButtonArrowLeft: "ButtonArrowLeft",
ButtonArrowRight: "ButtonArrowRight",
ButtonArrowUp: "ButtonArrowUp",
ButtonBackspace: "ButtonBackspace",
ButtonDelete: "ButtonDelete",
ButtonEnd: "ButtonEnd",
ButtonEnter: "ButtonEnter",
ButtonEsc: "ButtonEsc",
ButtonF1: "ButtonF1",
ButtonF10: "ButtonF10",
ButtonF11: "ButtonF11",
ButtonF12: "ButtonF12",
ButtonF2: "ButtonF2",
ButtonF3: "ButtonF3",
ButtonF4: "ButtonF4",
ButtonF5: "ButtonF5",
ButtonF6: "ButtonF6",
ButtonF7: "ButtonF7",
ButtonF8: "ButtonF8",
ButtonF9: "ButtonF9",
ButtonHome: "ButtonHome",
ButtonInsert: "ButtonInsert",
ButtonPgdn: "ButtonPgdn",
ButtonPgup: "ButtonPgup",
ButtonSpace: "ButtonSpace",
ButtonTab: "ButtonTab",
ButtonTilde: "ButtonTilde",
}
const (
ButtonArrowDown Button = -(iota + 1)
ButtonArrowLeft
ButtonArrowRight
ButtonArrowUp
ButtonBackspace
ButtonDelete
ButtonEnd
ButtonEnter
ButtonEsc
ButtonF1
ButtonF10
ButtonF11
ButtonF12
ButtonF2
ButtonF3
ButtonF4
ButtonF5
ButtonF6
ButtonF7
ButtonF8
ButtonF9
ButtonHome
ButtonInsert
ButtonPgdn
ButtonPgup
ButtonSpace
ButtonTab
ButtonTilde
)
// Modifier represents a modified key on the keyboard, i.e. a keys that
// together with buttons can form shortcuts.
type Modifier int
// String implements fmt.Stringer()
func (m Modifier) String() string {
if n, ok := modifierNames[m]; ok {
return n
}
return "ModifierUnknown"
}
// modifierNames maps Modifier values to human readable names.
var modifierNames = map[Modifier]string{
ModifierShift: "ModifierShift",
ModifierCtrl: "ModifierCtrl",
ModifierAlt: "ModifierAlt",
ModifierMeta: "ModifierMeta",
}
const (
modifierUnknown Modifier = iota
ModifierShift
ModifierCtrl
ModifierAlt
// ModifierMeta is the platform specific key, i.e. Windows key on windows
// or Apple (command) key on MacOS keyboard.
ModifierMeta
)
// Shortcut is a key combination pressed on the keyboard.
type Shortcut struct {
// Modifiers contains zero or more unique modifier keys.
Modifiers []Modifier
// Key is the key pressed on the keyboard.
// Either equals to one of the defined Button values or contains the raw
// Unicode byte sequence.
Key Button
}

33
mouse/mouse.go Normal file
View File

@ -0,0 +1,33 @@
// Package mouse defines known mouse buttons.
package mouse
// Button represents
type Button int
// String implements fmt.Stringer()
func (b Button) String() string {
if n, ok := buttonNames[b]; ok {
return n
}
return "ButtonUnknown"
}
// buttonNames maps Button values to human readable names.
var buttonNames = map[Button]string{
ButtonLeft: "ButtonLeft",
ButtonRight: "ButtonRight",
ButtonMiddle: "ButtonMiddle",
ButtonRelease: "ButtonRelease",
ButtonWheelUp: "ButtonWheelUp",
ButtonWheelDown: "ButtonWheelDown",
}
const (
buttonUnknown Button = iota
ButtonLeft
ButtonRight
ButtonMiddle
ButtonRelease
ButtonWheelUp
ButtonWheelDown
)

48
terminalapi/event.go Normal file
View File

@ -0,0 +1,48 @@
package terminalapi
import (
"image"
"github.com/mum4k/termdash/keyboard"
"github.com/mum4k/termdash/mouse"
)
// event.go defines events that can be received through the terminal API.
// Event represents an input event.
type Event interface {
isEvent()
}
// Keyboard is the event used when a key is pressed.
// Implements terminalapi.Event.
type Keyboard struct {
// Key identifies the pressed key.
// The rune either has a negative int32 value equal to one of the
// keyboard.Button values or a positive int32 value for all other Unicode
// byte sequences.
Key keyboard.Button
}
func (*Keyboard) isEvent() {}
// Resize is the event used when the terminal was resized.
// Implements terminalapi.Event.
type Resize struct {
// Size is the new size of the terminal.
Size image.Point
}
func (*Resize) isEvent() {}
// Mouse is the event used when the mouse is moved or a mouse button is
// pressed.
// Implements terminalapi.Event.
type Mouse struct {
// Position of the mouse on the terminal.
Position image.Point
// Button identifies the pressed button if any.
Button mouse.Button
}
func (*Mouse) isEvent() {}

View File

@ -0,0 +1,36 @@
// Package terminalapi defines the API of all terminal implementations.
package terminalapi
import (
"context"
"image"
"github.com/mum4k/termdash/cell"
)
// Terminal abstracts an implementation of a 2-D terminal.
// A terminal consists of a number of cells.
type Terminal interface {
// Size returns the terminal width and height in cells.
Size() image.Point
// Clear clears the content of the internal back buffer, resetting all cells
// to their default content and attributes.
Clear() error
// Flush flushes the internal back buffer to the terminal.
Flush() error
// SetCursor sets the position of the cursor.
SetCursor(p image.Point)
// HideCursos hides the cursor.
HideCursor()
// SetCell sets the value of the specified cell to the provided rune.
// Use the options to specify which attributes to modify, if an attribute
// option isn't specified, the attribute retains its previous value.
SetCell(p image.Point, r rune, opts ...cell.Option) error
// Event waits for the next event and returns it.
// This call blocks until the next event or cancellation of the context.
Event(ctx context.Context) Event
}

43
widget/widget.go Normal file
View File

@ -0,0 +1,43 @@
// Package widget defines the API of a widget on the dashboard.
package widget
import (
"github.com/mum4k/termdash/canvas"
"github.com/mum4k/termdash/keyboard"
"github.com/mum4k/termdash/mouse"
)
// Options contains registration options for a widget.
// This is how the widget indicates its needs to the infrastructure.
type Options struct {
}
// Widget is a single widget on the dashboard.
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.
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.
Keyboard(s *keyboard.Shortcut) error
// Mouse is called when the widget is focused on the dashboard and a mouse
// event happens on its canvas. Only called if the widget registered for mouse
// events.
Mouse(m *mouse.Button) error
// Options returns registration options for the widget.
// This is how the widget indicates to the infrastructure whether it is
// interested in keyboard or mouse shortcuts, what is its minimum canvas
// size, etc.
Options() *Options
}