diff --git a/widgets/table/content.go b/widgets/table/content.go new file mode 100644 index 0000000..1e4a216 --- /dev/null +++ b/widgets/table/content.go @@ -0,0 +1,201 @@ +package table + +// content.go defines a type that allow callers to populate the table with +// content. + +import ( + "errors" + + "github.com/mum4k/termdash/align" + "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/linestyle" +) + +// ContentOption is used to provide options to NewContent. +type ContentOption interface { + // set sets the provided option. + set(*contentOptions) +} + +// contentOptions stores options that apply to the content level. +type contentOptions struct { + border linestyle.LineStyle + borderCellOpts []cell.Option + columnWidthsPercent []int + + horizontalCellSpacing int + verticalCellSpacing int + + hierarchical *hierarchicalOptions +} + +// hierarchicalOptions stores options that can be applied at multiple levels or +// hierarchy, i.e. the Content (top level), the Row or the Cell. +type hierarchicalOptions struct { + cellOpts []cell.Option + horizontalCellPadding *int + verticalCellPadding *int + alignHorizontal *align.Horizontal + alignVertical *align.Vertical + height int +} + +// contentOption implements ContentOption. +type contentOption func(*contentOptions) + +// set implements Option.set. +func (co contentOption) set(cOpts *contentOptions) { + co(cOpts) +} + +// Border configures the table to have a border of the specified line style. +// Defaults to linestyle.None which means no border. +func Border(ls linestyle.LineStyle) ContentOption { + return contentOption(func(cOpts *contentOptions) { + cOpts.border = ls + }) +} + +// BorderCellOpts sets cell options for the cells that contain the border. +func BorderCellOpts(cellOpts ...cell.Option) ContentOption { + return contentOption(func(cOpts *contentOptions) { + cOpts.borderCellOpts = cellOpts + }) +} + +// ColumnWidthsPercent sets the widths of columns to the provided percentage. +// The number of values must match the number of Columns specified on the call +// to NewContent. All the values must be in the range 0 < v <= 100 and the sum +// of the values must be 100. +// Defaults to column width automatically adjusted to the content. +func ColumnWidthsPercent(widths ...int) ContentOption { + return contentOption(func(cOpts *contentOptions) { + cOpts.columnWidthsPercent = widths + }) +} + +// HorizontalCellSpacing sets the horizontal space between cells as the number +// of cells on the terminal that are left empty. +// The value must be a non-zero positive integer. +// Defaults to zero cells. +func HorizontalCellSpacing(cells int) ContentOption { + return contentOption(func(cOpts *contentOptions) { + cOpts.horizontalCellSpacing = cells + }) +} + +// VerticalCellSpacing sets the vertical space between cells as the number +// of cells on the terminal that are left empty. +// The value must be a non-zero positive integer. +// Defaults to zero cells. +func VerticalCellSpacing(cells int) ContentOption { + return contentOption(func(cOpts *contentOptions) { + cOpts.verticalCellSpacing = cells + }) +} + +// ContentCellOpts sets cell options for the cells that contain the table rows +// and cells. +// This is a hierarchical option and can be overridden when provided at Row, +// Cell or Data level. +func ContentCellOpts(cellOpts ...cell.Option) ContentOption { + return contentOption(func(cOpts *contentOptions) { + cOpts.hierarchical.cellOpts = cellOpts + }) +} + +// ContentRowHeight sets the height of rows to the provided number of cells. +// The number must be a non-zero positive integer. +// Defaults to row height automatically adjusted to the content. +// This is a hierarchical option and can be overridden when provided at Row +// level. +func ContentRowHeight(height int) ContentOption { + return contentOption(func(cOpts *contentOptions) { + cOpts.hierarchical.height = height + }) +} + +// HorizontalCellPadding sets the horizontal space between cell wall and its +// content as the number of cells on the terminal that are left empty. +// The value must be a non-zero positive integer. +// Defaults to zero cells. +// This is a hierarchical option and can be overridden when provided at Row +// or Cell level. +func HorizontalCellPadding(cells int) ContentOption { + return contentOption(func(cOpts *contentOptions) { + cOpts.hierarchical.horizontalCellPadding = &cells + }) +} + +// VerticalCellPadding sets the vertical space between cell wall and its +// content as the number of cells on the terminal that are left empty. +// The value must be a non-zero positive integer. +// Defaults to zero cells. +// This is a hierarchical option and can be overridden when provided at Row +// or Cell level. +func VerticalCellPadding(cells int) ContentOption { + return contentOption(func(cOpts *contentOptions) { + cOpts.hierarchical.verticalCellPadding = &cells + }) +} + +// AlignHorizontal sets the horizontal alignment for the content. +// Defaults for left horizontal alignment. +// This is a hierarchical option and can be overridden when provided at Row +// or Cell level. +func AlignHorizontal(h align.Horizontal) ContentOption { + return contentOption(func(cOpts *contentOptions) { + cOpts.hierarchical.alignHorizontal = &h + }) +} + +// AlignVertical sets the vertical alignment for the content. +// Defaults for top vertical alignment. +// This is a hierarchical option and can be overridden when provided at Row +// or Cell level. +func AlignVertical(v align.Vertical) ContentOption { + return contentOption(func(cOpts *contentOptions) { + cOpts.hierarchical.alignVertical = &v + }) +} + +// Columns specifies the number of columns in the table. +type Columns int + +// Content is the content displayed in the table. +// +// Content is organized into rows of cells. Each cell can zero, one or multiple +// instances of text data with their own cell options. +// +// Certain options are applied hierarchically, the values provided at the +// Content level apply to all child rows and cells. Specifying a different +// value at a lower level overrides the values provided above. +// +// This object is thread-safe. +type Content struct { + colNum Columns + header *Row + rows []*Row + + opts *contentOptions +} + +// NewContent returns a new Content instance. +// +// The number of columns must be a non-zero positive integer. +// All rows must contain the same number of columns (the same number of cells) +// allowing for the CellColSpan option. +func NewContent(cols Columns, rows []*Row, opts ...ContentOption) (*Content, error) { + return nil, errors.New("unimplemented") +} + +// AddRow adds a row to the content. +// If you need to apply options at the Row level, use AddRowWithOpts. +func (c *Content) AddRow(cells ...*Cell) error { + return errors.New("unimplemented") +} + +// AddRowWithOpts adds a row to the content and applies the options. +func (c *Content) AddRowWithOpts(cells []*Cell, opts ...RowOption) error { + return errors.New("unimplemented") +} diff --git a/widgets/table/content_cell.go b/widgets/table/content_cell.go new file mode 100644 index 0000000..6ca7119 --- /dev/null +++ b/widgets/table/content_cell.go @@ -0,0 +1,125 @@ +package table + +// content_cell.go defines a type that represents a single cell in the table. + +import ( + "github.com/mum4k/termdash/align" + "github.com/mum4k/termdash/cell" +) + +// CellOption is used to provide options to NewCellWithOpts. +type CellOption interface { + // set sets the provided option. + set(*Cell) +} + +// cellOption implements CellOption. +type cellOption func(*Cell) + +// set implements Option.set. +func (co cellOption) set(c *Cell) { + co(c) +} + +// CellColSpan configures the number of columns this cell spans. +// The number must be a non-zero positive integer. +// Defaults to a cell spanning just one column. +func CellColSpan(cols int) CellOption { + return cellOption(func(c *Cell) { + c.colSpan = cols + }) +} + +// CellRowSpan configures the number of rows this cell spans. +// The number must be a non-zero positive integer. +// Defaults to a cell spanning just one row. +func CellRowSpan(rows int) CellOption { + return cellOption(func(c *Cell) { + c.rowSpan = rows + }) +} + +// CellOpts sets cell options for the cells that contain the table cells and +// cells. +// This is a hierarchical option, it overrides the one provided at Content or +// Row level and can be overridden at the Data level. +func CellOpts(cellOpts ...cell.Option) CellOption { + return cellOption(func(c *Cell) { + c.hierarchical.cellOpts = cellOpts + }) +} + +// CellHeight sets the height of cells to the provided number of cells. +// The number must be a non-zero positive integer. +// Defaults to cell height automatically adjusted to the content. +// This is a hierarchical option, it overrides the one provided at Content or +// Row level. +func CellHeight(height int) CellOption { + return cellOption(func(c *Cell) { + c.hierarchical.height = height + }) +} + +// CellHorizontalCellPadding sets the horizontal space between cell wall and its +// content as the number of cells on the terminal that are left empty. +// The value must be a non-zero positive integer. +// Defaults to zero cells. +// This is a hierarchical option, it overrides the one provided at Content or +// Row level. +func CellHorizontalCellPadding(cells int) CellOption { + return cellOption(func(c *Cell) { + c.hierarchical.horizontalCellPadding = &cells + }) +} + +// CellVerticalCellPadding sets the vertical space between cell wall and its +// content as the number of cells on the terminal that are left empty. +// The value must be a non-zero positive integer. +// Defaults to zero cells. +// This is a hierarchical option, it overrides the one provided at Content or +// Row level. +func CellVerticalCellPadding(cells int) CellOption { + return cellOption(func(c *Cell) { + c.hierarchical.verticalCellPadding = &cells + }) +} + +// CellAlignHorizontal sets the horizontal alignment for the content. +// Defaults for left horizontal alignment. +// This is a hierarchical option, it overrides the one provided at Content or +// Row level. +func CellAlignHorizontal(h align.Horizontal) CellOption { + return cellOption(func(c *Cell) { + c.hierarchical.alignHorizontal = &h + }) +} + +// CellAlignVertical sets the vertical alignment for the content. +// Defaults for top vertical alignment. +// This is a hierarchical option, it overrides the one provided at Content or +// Row level. +func CellAlignVertical(v align.Vertical) CellOption { + return cellOption(func(c *Cell) { + c.hierarchical.alignVertical = &v + }) +} + +// Cell is one cell in a Row. +type Cell struct { + data []*Data + + colSpan int + rowSpan int + hierarchical *hierarchicalOptions +} + +// NewCell returns a new Cell with the provided text. +// If you need to apply options at the Cell or Data level use NewCellWithOpts. +func NewCell(text string) *Cell { + return nil +} + +// NewCellWithOpts returns a new Cell with the provided data and options. +func NewCellWithOpts(data []*Data, opts ...CellOption) *Cell { + return nil +} diff --git a/widgets/table/content_data.go b/widgets/table/content_data.go new file mode 100644 index 0000000..90e51cc --- /dev/null +++ b/widgets/table/content_data.go @@ -0,0 +1,55 @@ +package table + +// content_data.go defines a type that represents data within a table cell. + +import ( + "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/internal/canvas/buffer" +) + +// DataOption is used to provide options to NewDataWithOpts. +type DataOption interface { + // set sets the provided option. + set(*dataOptions) +} + +// dataOptions stores the provided data options. +type dataOptions struct { + cellOpts []cell.Option +} + +// dataOption implements DataOption. +type dataOption func(*dataOptions) + +// set implements Option.set. +func (do dataOption) set(dOpts *dataOptions) { + do(dOpts) +} + +// DataCellOpts sets options on the cells that contain the data. +func DataCellOpts(cellOpts ...cell.Option) DataOption { + return dataOption(func(dOpts *dataOptions) { + dOpts.cellOpts = cellOpts + }) +} + +// Data is part of (or the full) the data that is displayed inside one Cell. +type Data struct { + cells []*buffer.Cell +} + +// NewData creates new Data with the provided text and applies the options. +func NewData(text string, opts ...DataOption) *Data { + dOpts := &dataOptions{} + for _, opt := range opts { + opt.set(dOpts) + } + + var cells []*buffer.Cell + for _, r := range text { + cells = append(cells, buffer.NewCell(r, dOpts.cellOpts...)) + } + return &Data{ + cells: cells, + } +} diff --git a/widgets/table/content_row.go b/widgets/table/content_row.go new file mode 100644 index 0000000..96b6f53 --- /dev/null +++ b/widgets/table/content_row.go @@ -0,0 +1,154 @@ +package table + +// content_row.go defines a type that represents a single row in the table. + +import ( + "github.com/mum4k/termdash/align" + "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/terminal/terminalapi" +) + +// RowCallbackFn is a function called when the user activates a Row. +// +// Only Rows that have the RowCallback option can be activated. A Row can be +// activated by a mouse click or by pressing any non-navigation key (see the +// NavigationKeys option) while the row is highlighted. +// +// The called function receives the event that was used to activate the Row. +// The event is either *terminalapi.Keyboard or *terminalapi.Mouse. +// +// The callback function must be light-weight, ideally just storing a value and +// returning, since more button presses might occur. +// +// The callback function must be thread-safe as the mouse or keyboard events +// that activate the row are processed in a separate goroutine. +// +// If the function returns an error, the widget will forward it back to the +// termdash infrastructure which causes a panic, unless the user provided a +// termdash.ErrorHandler. +type RowCallbackFn func(event terminalapi.Event) error + +// RowOption is used to provide options to NewRowWithOpts. +type RowOption interface { + // set sets the provided option. + set(*Row) +} + +// rowOption implements RowOption. +type rowOption func(*Row) + +// set implements Option.set. +func (ro rowOption) set(r *Row) { + ro(r) +} + +// RowHighlighted sets the row as highlighted, the user can then change which +// row is highlighted using keyboard or mouse input. +func RowHighlighted() RowOption { + return rowOption(func(r *Row) { + r.highlighted = true + }) +} + +// RowCallback allows this row to be activated and provides a function that +// should be called upon each row activation by the user. +func RowCallback(fn RowCallbackFn) RowOption { + return rowOption(func(r *Row) { + r.rowCallback = fn + }) +} + +// RowCellOpts sets cell options for the cells that contain the table rows and +// cells. +// This is a hierarchical option, it overrides the one provided at Content +// level and can be overridden when provided at the Cell or Data level. +func RowCellOpts(cellOpts ...cell.Option) RowOption { + return rowOption(func(r *Row) { + r.hierarchical.cellOpts = cellOpts + }) +} + +// RowHeight sets the height of rows to the provided number of cells. +// The number must be a non-zero positive integer. +// Defaults to row height automatically adjusted to the content. +// This is a hierarchical option, it overrides the one provided at Content +// level and can be overridden when provided at the Cell level. +func RowHeight(height int) RowOption { + return rowOption(func(r *Row) { + r.hierarchical.height = height + }) +} + +// RowHorizontalCellPadding sets the horizontal space between cell wall and its +// content as the number of cells on the terminal that are left empty. +// The value must be a non-zero positive integer. +// Defaults to zero cells. +// This is a hierarchical option, it overrides the one provided at Content +// level and can be overridden when provided at the Cell level. +func RowHorizontalCellPadding(cells int) RowOption { + return rowOption(func(r *Row) { + r.hierarchical.horizontalCellPadding = &cells + }) +} + +// RowVerticalCellPadding sets the vertical space between cell wall and its +// content as the number of cells on the terminal that are left empty. +// The value must be a non-zero positive integer. +// Defaults to zero cells. +// This is a hierarchical option, it overrides the one provided at Content +// level and can be overridden when provided at the Cell level. +func RowVerticalCellPadding(cells int) RowOption { + return rowOption(func(r *Row) { + r.hierarchical.verticalCellPadding = &cells + }) +} + +// RowAlignHorizontal sets the horizontal alignment for the content. +// Defaults for left horizontal alignment. +// This is a hierarchical option, it overrides the one provided at Content +// level and can be overridden when provided at the Cell level. +func RowAlignHorizontal(h align.Horizontal) RowOption { + return rowOption(func(r *Row) { + r.hierarchical.alignHorizontal = &h + }) +} + +// RowAlignVertical sets the vertical alignment for the content. +// Defaults for top vertical alignment. +// This is a hierarchical option, it overrides the one provided at Content +// level and can be overridden when provided at the Cell level. +func RowAlignVertical(v align.Vertical) RowOption { + return rowOption(func(r *Row) { + r.hierarchical.alignVertical = &v + }) +} + +// Row is one row in the table. +type Row struct { + cells []*Cell + + rowCallback RowCallbackFn + highlighted bool + hierarchical *hierarchicalOptions +} + +// NewHeader returns a new Row that will be the header of the table. +// The header remains visible while scrolling and allows for sorting of content +// based on its values. Header row cannot be highlighted. +// Content can only have one header Row. +func NewHeader(cells []*Cell, opts ...RowOption) *Row { + return nil +} + +// NewRow returns a new Row instance with the provided cells. +// If you need to apply options at the Row level, use NewRowWithOpts. +// If you need to add a table header Row, use NewHeader. +func NewRow(cells ...*Cell) *Row { + return nil +} + +// NewRowWithOpts returns a new Row instance with the provided cells and applies +// the row options. +func NewRowWithOpts(cells []*Cell, opts ...RowOption) *Row { + return nil +} diff --git a/widgets/table/content_test.go b/widgets/table/content_test.go new file mode 100644 index 0000000..7287d88 --- /dev/null +++ b/widgets/table/content_test.go @@ -0,0 +1,53 @@ +package table + +func Example_DisplayDataOnly() { + rows := []*Row{ + NewRow( + NewCell("hello"), + NewCell("world"), + ), + NewRow( + NewCell("hello"), + NewCell("world"), + ), + } + + _, err := NewContent(Columns(3), rows) + if err != nil { + panic(err) + } + + data := [][]string{} + myRows := []*Row{} + for _, dataRow := range data { + cells := []*Cell{} + for _, dataCol := range dataRow { + cells = append(cells, NewCell(dataCol)) + } + myRows = append(myRows, NewRow(cells...)) + } +} + +func Example_ColorInheritance() { + +} + +func Example_ColAndRowSpan() { + +} + +func Example_TextTrimmingAndWrapping() { + +} + +func Example_AddRow() { + +} + +func Example_DeleteRow() { + +} + +func Example_Callback() { + +} diff --git a/widgets/table/options.go b/widgets/table/options.go new file mode 100644 index 0000000..d2269ff --- /dev/null +++ b/widgets/table/options.go @@ -0,0 +1,126 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package table + +import ( + "time" + + "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/keyboard" +) + +// options.go contains configurable options for Table. + +// Option is used to provide options. +type Option interface { + // set sets the provided option. + set(*options) +} + +// option implements Option. +type option func(*options) + +// set implements Option.set. +func (o option) set(opts *options) { + o(opts) +} + +// options holds the provided options. +type options struct { + keyUp keyboard.Key + keyDown keyboard.Key + keyPgUp keyboard.Key + keyPgDown keyboard.Key + disableHighlight bool + disableSorting bool + highlightDelay time.Duration + highlightCellOpts []cell.Option +} + +// validate validates the provided options. +func (o *options) validate() error { + return nil +} + +// newOptions returns options with the default values set. +func newOptions() *options { + return &options{ + keyUp: DefaultKeyUp, + keyDown: DefaultKeyDown, + keyPgUp: DefaultKeyPageUp, + keyPgDown: DefaultKeyPageDown, + } +} + +// The default keys for navigating the content. +const ( + DefaultKeyUp = keyboard.KeyArrowUp + DefaultKeyDown = keyboard.KeyArrowDown + DefaultKeyPageUp = keyboard.KeyPgUp + DefaultKeyPageDown = keyboard.KeyPgDn +) + +// NavigationKeys configures the keyboard keys that allow the user to navigate +// the table content. +// The provided keys must be unique, e.g. the same key cannot be both up and +// down. +func NavigationKeys(up, down, pageUp, pageDown keyboard.Key) Option { + return option(func(opts *options) { + opts.keyUp = up + opts.keyDown = down + opts.keyPgUp = pageUp + opts.keyPgDown = pageDown + }) +} + +// DisableHighlight disables the ability to highlight a row using keyboard or +// mouse. +// Navigation and scrolling still works, but it no longer moves the highlighted +// row, it just moves up or down a row at a time. +// The highlight functionality is enabled by default. +func DisableHighlight() Option { + return option(func(opts *options) { + opts.disableHighlight = true + }) +} + +// DisableSorting disables the ability to sort the content by column values. +// The sorting functionality is enabled by default. +func DisableSorting() Option { + return option(func(opts *options) { + opts.disableSorting = true + }) +} + +// HighlightDelay configures the time after which a highlighted row loses the +// highlight. +// This only works if the manual termdash redraw or the periodic redraw +// interval are reasonably close to this delay. +// The duration cannot be negative. +// Defaults to zero which means a highlighted row remains highlighted forever. +func HighlightDelay(d time.Duration) Option { + return option(func(opts *options) { + opts.highlightDelay = d + }) +} + +// HighlightCellOpts sets the cell options on cells that are part of a +// highlighted row. +// Defaults to DefaultHighlightColor. +func HighlightColor(cellOpts ...cell.Option) Option { + return option(func(opts *options) { + opts.highlightCellOpts = cellOpts + }) +} diff --git a/widgets/table/table.go b/widgets/table/table.go new file mode 100644 index 0000000..2537181 --- /dev/null +++ b/widgets/table/table.go @@ -0,0 +1,109 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package table is a widget that displays text a table consisting of rows and +// columns. +package table + +import ( + "errors" + "image" + "sync" + + "github.com/mum4k/termdash/internal/canvas" + "github.com/mum4k/termdash/terminal/terminalapi" + "github.com/mum4k/termdash/widgetapi" +) + +// Table displays text content aligned to rows and columns. +// +// Sizes of columns and rows are automatically sized based on the content or +// specified by the caller. Each table cell supports text alignment, trimming +// or wrapping. Table data can be sorted according to values in individual +// columns. +// +// The entire table can be scrolled if it has more content that fits into the +// container. The table content can be interacted with using keyboard or mouse. +// +// Implements widgetapi.Widget. This object is thread-safe. +type Table struct { + // mu protects the widget. + mu sync.Mutex + + // opts are the provided options. + opts *options +} + +// New returns a new Table. +func New(opts ...Option) (*Table, error) { + opt := newOptions() + for _, o := range opts { + o.set(opt) + } + if err := opt.validate(); err != nil { + return nil, err + } + return &Table{}, nil +} + +// Reset resets the table to an empty content. +func (t *Table) Reset() { + t.mu.Lock() + defer t.mu.Unlock() +} + +// Write writes the provided content and replaces any content written +// previously. +// The caller is allowed to modify the content after Write, changes to content +// will become visible. +func (t *Table) Write(c *Content) error { + t.mu.Lock() + defer t.mu.Unlock() + return errors.New("unimplemented") +} + +// Draw draws the Table widget onto the canvas. +// Implements widgetapi.Widget.Draw. +func (t *Table) Draw(cvs *canvas.Canvas) error { + t.mu.Lock() + defer t.mu.Unlock() + return nil +} + +// Keyboard processes keyboard events. +// The Table widget uses keyboard events to highlight selected rows or columns. +// Implements widgetapi.Widget.Keyboard. +func (t *Table) Keyboard(k *terminalapi.Keyboard) error { + t.mu.Lock() + defer t.mu.Unlock() + return errors.New("unimplemented") +} + +// Mouse processes mouse events. +// Mouse events are used to scroll the content and highlight rows or columns. +// Implements widgetapi.Widget.Mouse. +func (t *Table) Mouse(m *terminalapi.Mouse) error { + t.mu.Lock() + defer t.mu.Unlock() + return errors.New("unimplemented") +} + +// Options implements widgetapi.Widget.Options. +func (t *Table) Options() widgetapi.Options { + return widgetapi.Options{ + MinimumSize: image.Point{1, 1}, + WantKeyboard: widgetapi.KeyScopeNone, + WantMouse: widgetapi.MouseScopeNone, + } +} diff --git a/widgets/table/table_test.go b/widgets/table/table_test.go new file mode 100644 index 0000000..78b82f5 --- /dev/null +++ b/widgets/table/table_test.go @@ -0,0 +1,131 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package table + +import ( + "image" + "testing" + + "github.com/kylelemons/godebug/pretty" + "github.com/mum4k/termdash/internal/canvas" + "github.com/mum4k/termdash/internal/faketerm" + "github.com/mum4k/termdash/terminal/terminalapi" + "github.com/mum4k/termdash/widgetapi" +) + +func TestTable(t *testing.T) { + tests := []struct { + desc string + opts []Option + update func(*Table) error // update gets called before drawing of the widget. + canvas image.Rectangle + want func(size image.Point) *faketerm.Terminal + wantNewErr bool + wantUpdateErr bool // whether to expect an error on a call to the update function + wantDrawErr bool + }{} + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + tbl, err := New(tc.opts...) + if (err != nil) != tc.wantNewErr { + t.Errorf("New => unexpected error: %v, wantNewErr: %v", err, tc.wantNewErr) + } + if err != nil { + return + } + + c, err := canvas.New(tc.canvas) + if err != nil { + t.Fatalf("canvas.New => unexpected error: %v", err) + } + + if tc.update != nil { + err = tc.update(tbl) + if (err != nil) != tc.wantUpdateErr { + t.Errorf("update => unexpected error: %v, wantUpdateErr: %v", err, tc.wantUpdateErr) + } + if err != nil { + return + } + } + + err = tbl.Draw(c) + if (err != nil) != tc.wantDrawErr { + t.Errorf("Draw => unexpected error: %v, wantDrawErr: %v", err, tc.wantDrawErr) + } + if err != nil { + return + } + + got, err := faketerm.New(c.Size()) + if err != nil { + t.Fatalf("faketerm.New => unexpected error: %v", err) + } + + if err := c.Apply(got); err != nil { + t.Fatalf("Apply => unexpected error: %v", err) + } + + var want *faketerm.Terminal + if tc.want != nil { + want = tc.want(c.Size()) + } else { + want = faketerm.MustNew(c.Size()) + } + + if diff := faketerm.Diff(want, got); diff != "" { + t.Errorf("Draw => %v", diff) + } + }) + } +} + +func TestKeyboard(t *testing.T) { + tbl, err := New() + if err != nil { + t.Fatalf("New => unexpected error: %v", err) + } + if err := tbl.Keyboard(&terminalapi.Keyboard{}); err == nil { + t.Errorf("Keyboard => got nil err, wanted one") + } +} + +func TestMouse(t *testing.T) { + tbl, err := New() + if err != nil { + t.Fatalf("New => unexpected error: %v", err) + } + if err := tbl.Mouse(&terminalapi.Mouse{}); err == nil { + t.Errorf("Mouse => got nil err, wanted one") + } +} + +func TestOptions(t *testing.T) { + tbl, err := New() + if err != nil { + t.Fatalf("New => unexpected error: %v", err) + } + got := tbl.Options() + want := widgetapi.Options{ + MinimumSize: image.Point{1, 1}, + WantKeyboard: widgetapi.KeyScopeNone, + WantMouse: widgetapi.MouseScopeNone, + } + if diff := pretty.Compare(want, got); diff != "" { + t.Errorf("Options => unexpected diff (-want, +got):\n%s", diff) + } + +} diff --git a/widgets/table/tabledemo/tabledemo.go b/widgets/table/tabledemo/tabledemo.go new file mode 100644 index 0000000..14060a0 --- /dev/null +++ b/widgets/table/tabledemo/tabledemo.go @@ -0,0 +1,62 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Binary tabledemo shows the functionality of the table widget. +package main + +import ( + "context" + "time" + + "github.com/mum4k/termdash" + "github.com/mum4k/termdash/container" + "github.com/mum4k/termdash/linestyle" + "github.com/mum4k/termdash/terminal/termbox" + "github.com/mum4k/termdash/terminal/terminalapi" + "github.com/mum4k/termdash/widgets/table" +) + +func main() { + t, err := termbox.New() + if err != nil { + panic(err) + } + defer t.Close() + + ctx, cancel := context.WithCancel(context.Background()) + tbl, err := table.New() + if err != nil { + panic(err) + } + + c, err := container.New( + t, + container.Border(linestyle.Light), + container.BorderTitle("PRESS Q TO QUIT"), + container.PlaceWidget(tbl), + ) + if err != nil { + panic(err) + } + + quitter := func(k *terminalapi.Keyboard) { + if k.Key == 'q' || k.Key == 'Q' { + cancel() + } + } + + if err := termdash.Run(ctx, t, c, termdash.KeyboardSubscriber(quitter), termdash.RedrawInterval(1*time.Second)); err != nil { + panic(err) + } +}