mirror of
https://github.com/mum4k/termdash.git
synced 2025-04-28 13:48:51 +08:00
Defining API for the Table widget.
This commit is contained in:
parent
e6ef3f7ed1
commit
98cd828606
201
widgets/table/content.go
Normal file
201
widgets/table/content.go
Normal file
@ -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")
|
||||
}
|
125
widgets/table/content_cell.go
Normal file
125
widgets/table/content_cell.go
Normal file
@ -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
|
||||
}
|
55
widgets/table/content_data.go
Normal file
55
widgets/table/content_data.go
Normal file
@ -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,
|
||||
}
|
||||
}
|
154
widgets/table/content_row.go
Normal file
154
widgets/table/content_row.go
Normal file
@ -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
|
||||
}
|
53
widgets/table/content_test.go
Normal file
53
widgets/table/content_test.go
Normal file
@ -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() {
|
||||
|
||||
}
|
126
widgets/table/options.go
Normal file
126
widgets/table/options.go
Normal file
@ -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
|
||||
})
|
||||
}
|
109
widgets/table/table.go
Normal file
109
widgets/table/table.go
Normal file
@ -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,
|
||||
}
|
||||
}
|
131
widgets/table/table_test.go
Normal file
131
widgets/table/table_test.go
Normal file
@ -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)
|
||||
}
|
||||
|
||||
}
|
62
widgets/table/tabledemo/tabledemo.go
Normal file
62
widgets/table/tabledemo/tabledemo.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user