mirror of
https://github.com/mum4k/termdash.git
synced 2025-04-28 13:48:51 +08:00
POC code that determines column widths.
This commit is contained in:
parent
2fc4ffc9e3
commit
e36e1d7ba7
@ -33,6 +33,16 @@ func NewCells(text string, opts ...cell.Option) []*Cell {
|
||||
return res
|
||||
}
|
||||
|
||||
// CellsWidth returns the width the cells will use on a terminal if printed out.
|
||||
// This takes into account if some of the runes are full-width runes.
|
||||
func CellsWidth(cells []*Cell) int {
|
||||
width := 0
|
||||
for _, c := range cells {
|
||||
width += runewidth.RuneWidth(c.Rune)
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
// Cell represents a single cell on the terminal.
|
||||
type Cell struct {
|
||||
// Rune is the rune stored in the cell.
|
||||
|
@ -625,3 +625,35 @@ func TestRemWidth(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCellsWidth(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
cells []*Cell
|
||||
want int
|
||||
}{
|
||||
{
|
||||
desc: "ascii characters",
|
||||
cells: NewCells("hello"),
|
||||
want: 5,
|
||||
},
|
||||
{
|
||||
desc: "string from mattn/runewidth/runewidth_test",
|
||||
cells: NewCells("■㈱の世界①"),
|
||||
want: 10,
|
||||
},
|
||||
{
|
||||
desc: "string using termdash characters",
|
||||
cells: NewCells("⇄…⇧⇩"),
|
||||
want: 4,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if got := CellsWidth(tc.cells); got != tc.want {
|
||||
t.Errorf("CellsWidth => %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ package table
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/mum4k/termdash/align"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
@ -143,6 +144,8 @@ func ContentCellOpts(cellOpts ...cell.Option) ContentOption {
|
||||
|
||||
// ContentRowHeight sets the height of rows to the provided number of cells.
|
||||
// The number must be a non-zero positive integer.
|
||||
// Rows still use larger than provided height if wrapping is enabled and the
|
||||
// content doesn't fit.
|
||||
// Defaults to row height automatically adjusted to the content.
|
||||
// This is a hierarchical option and can be overridden when provided at Row
|
||||
// level.
|
||||
@ -229,8 +232,14 @@ type Content struct {
|
||||
// rows are the rows in the table.
|
||||
rows []*Row
|
||||
|
||||
// layout describes the layout of this table on a canvas.
|
||||
layout *contentLayout
|
||||
|
||||
// opts are the options provided to NewContent.
|
||||
opts *contentOptions
|
||||
|
||||
// mu protects the Content
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewContent returns a new Content instance.
|
||||
@ -240,8 +249,9 @@ type Content struct {
|
||||
// allowing for the CellColSpan option.
|
||||
func NewContent(cols Columns, rows []*Row, opts ...ContentOption) (*Content, error) {
|
||||
c := &Content{
|
||||
cols: cols,
|
||||
opts: newContentOptions(opts...),
|
||||
cols: cols,
|
||||
layout: &contentLayout{},
|
||||
opts: newContentOptions(opts...),
|
||||
}
|
||||
for _, r := range rows {
|
||||
if err := c.addRow(r); err != nil {
|
||||
@ -296,6 +306,9 @@ func (c *Content) addRow(row *Row) error {
|
||||
|
||||
// AddRowWithOpts adds a row to the content and applies the options.
|
||||
func (c *Content) AddRowWithOpts(cells []*Cell, opts ...RowOption) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if err := c.addRow(NewRowWithOpts(cells, opts...)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/mum4k/termdash/align"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/internal/canvas/buffer"
|
||||
"github.com/mum4k/termdash/internal/wrap"
|
||||
)
|
||||
|
||||
@ -69,6 +70,8 @@ func CellOpts(cellOpts ...cell.Option) CellOption {
|
||||
|
||||
// CellHeight sets the height of cells to the provided number of cells.
|
||||
// The number must be a non-zero positive integer.
|
||||
// Rows still use larger than provided height if wrapping is enabled and the
|
||||
// content doesn't fit.
|
||||
// Defaults to cell height automatically adjusted to the content.
|
||||
// This is a hierarchical option, it overrides the one provided at Content or
|
||||
// Row level.
|
||||
@ -156,6 +159,16 @@ func (c *Cell) String() string {
|
||||
return fmt.Sprintf("| %v ", b.String())
|
||||
}
|
||||
|
||||
// width returns the width of all the runes in this cell when they are printed
|
||||
// on the terminal.
|
||||
func (c *Cell) width() int {
|
||||
res := 0
|
||||
for _, d := range c.data {
|
||||
res += buffer.CellsWidth(d.cells)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// NewCell returns a new Cell with the provided text.
|
||||
// If you need to apply options at the Cell or Data level use NewCellWithOpts.
|
||||
// The text contain cannot control characters (unicode.IsControl) or space
|
||||
|
156
widgets/table/content_layout.go
Normal file
156
widgets/table/content_layout.go
Normal file
@ -0,0 +1,156 @@
|
||||
// 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
|
||||
|
||||
// content_layout.go stores layout calculated for a canvas size.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"log"
|
||||
"math"
|
||||
|
||||
"github.com/mum4k/termdash/internal/wrap"
|
||||
)
|
||||
|
||||
// columnWidth is the width of a column in cells.
|
||||
// This excludes any border, padding or spacing, i.e. this is the data portion
|
||||
// only.
|
||||
type columnWidth int
|
||||
|
||||
// contentLayout determines how the content gets placed onto the canvas.
|
||||
type contentLayout struct {
|
||||
// lastCvsAr is the are of the last canvas the content was drawn on.
|
||||
// This is image.ZR if the content hasn't been drawn yet.
|
||||
lastCvsAr image.Rectangle
|
||||
|
||||
// columnWidths are the widths of individual columns in the table.
|
||||
columnWidths []columnWidth
|
||||
|
||||
// Details about HV lines that are the borders.
|
||||
}
|
||||
|
||||
// newContentLayout calculates new layout for the content when drawn on a
|
||||
// canvas represented with the provided area.
|
||||
func newContentLayout(content *Content, cvsAr image.Rectangle) (*contentLayout, error) {
|
||||
return nil, errors.New("unimplemented")
|
||||
}
|
||||
|
||||
// columnWidths given the content and the available canvas width returns the
|
||||
// widths of individual columns.
|
||||
// The argument cvsWidth is assumed to exclude space required for any border,
|
||||
// padding or spacing.
|
||||
func columnWidths(content *Content, cvsWidth int) []columnWidth {
|
||||
// This is similar to the rod-cutting problem, except instead of maximizing
|
||||
// the price, we're minimizing the number of rows that would have their
|
||||
// content trimmed.
|
||||
|
||||
idxColumnCosts := columnCosts(content, cvsWidth)
|
||||
log.Printf("idxColumnCosts: %v", idxColumnCosts)
|
||||
minCost, minCuts := cutCanvas(idxColumnCosts, cvsWidth, cvsWidth, int(content.cols), 0, nil)
|
||||
log.Printf("minCost: %v", minCost)
|
||||
log.Printf("minCuts: %v", minCuts)
|
||||
|
||||
var res []columnWidth
|
||||
last := 0
|
||||
for _, cut := range minCuts {
|
||||
res = append(res, columnWidth(cut-last))
|
||||
last = cut
|
||||
}
|
||||
res = append(res, columnWidth(cvsWidth-last))
|
||||
return res
|
||||
}
|
||||
|
||||
func cutCanvas(idxColumnCosts map[int]widthCost, cvsWidth, remWidth, columns, colIdx int, cuts []int) (int, []int) {
|
||||
log.Printf("cutCanvas remWidth:%d, columns:%d, colIdx:%d, cuts:%v", remWidth, columns, colIdx, cuts)
|
||||
if remWidth <= 0 {
|
||||
log.Printf(" -> 0")
|
||||
return 0, cuts
|
||||
}
|
||||
|
||||
minCost := math.MaxInt32
|
||||
var minCuts []int
|
||||
|
||||
widthCosts := idxColumnCosts[colIdx]
|
||||
nextColIdx := colIdx + 1
|
||||
if nextColIdx > columns-1 {
|
||||
log.Printf(" -> no more cuts remWidth:%d cost:%d", remWidth, widthCosts[remWidth])
|
||||
return widthCosts[remWidth], cuts
|
||||
}
|
||||
|
||||
for colWidth := 1; colWidth < remWidth; colWidth++ {
|
||||
diff := cvsWidth - remWidth
|
||||
idxThisCut := diff + colWidth
|
||||
costThisCut := widthCosts[colWidth]
|
||||
nextCost, nextCuts := cutCanvas(
|
||||
idxColumnCosts,
|
||||
cvsWidth,
|
||||
remWidth-colWidth,
|
||||
columns,
|
||||
nextColIdx,
|
||||
//append(cuts, colWidth+colIdx),
|
||||
append(cuts, idxThisCut),
|
||||
)
|
||||
|
||||
if newMinCost := costThisCut + nextCost; newMinCost < minCost {
|
||||
log.Printf("at cuts %v, costThisCut from widthCosts:%v, at width %d:%d, nextCost:%d, minCost:%d", cuts, widthCosts, colWidth, costThisCut, nextCost, minCost)
|
||||
minCost = newMinCost
|
||||
minCuts = nextCuts
|
||||
log.Printf("new minCost:%d minCuts:%v", minCost, minCuts)
|
||||
}
|
||||
}
|
||||
log.Printf("cutCanvas remWidth:%d, columns:%d, colIdx:%d, cuts:%v -> minCost:%d, minCuts:%d", remWidth, columns, colIdx, cuts, minCost, minCuts)
|
||||
return minCost, minCuts
|
||||
}
|
||||
|
||||
// widthCost maps column widths to the number of rows that would be trimmed on
|
||||
// that width because data in the column are longer than the width.
|
||||
type widthCost map[int]int
|
||||
|
||||
// columnCosts calculates the costs of cutting the columns to various widths.
|
||||
// Returns a map of column indexes to their cut costs.
|
||||
func columnCosts(content *Content, cvsWidth int) map[int]widthCost {
|
||||
idxColumnCosts := map[int]widthCost{}
|
||||
maxColWidth := cvsWidth - (int(content.cols) - 1)
|
||||
for colIdx := 0; colIdx < int(content.cols); colIdx++ {
|
||||
for colWidth := 1; colWidth <= maxColWidth; colWidth++ {
|
||||
wc, ok := idxColumnCosts[colIdx]
|
||||
if !ok {
|
||||
wc = widthCost{}
|
||||
idxColumnCosts[colIdx] = wc
|
||||
}
|
||||
wc[colWidth] = trimmedRows(content, colIdx, colWidth)
|
||||
}
|
||||
}
|
||||
return idxColumnCosts
|
||||
}
|
||||
|
||||
// trimmedRows returns the number of rows that will have data cells with
|
||||
// trimmed content in column of the specified index if the assigned width of
|
||||
// the column is colWidth.
|
||||
func trimmedRows(content *Content, colIdx int, colWidth int) int {
|
||||
trimmed := 0
|
||||
for _, row := range content.rows {
|
||||
tgtCell := row.cells[colIdx]
|
||||
if tgtCell.hierarchical.getWrapMode() != wrap.Never {
|
||||
// Cells that have wrapping enabled are never trimmed.
|
||||
continue
|
||||
}
|
||||
if tgtCell.width() > colWidth {
|
||||
trimmed++
|
||||
}
|
||||
}
|
||||
return trimmed
|
||||
}
|
181
widgets/table/content_layout_test.go
Normal file
181
widgets/table/content_layout_test.go
Normal file
@ -0,0 +1,181 @@
|
||||
// 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 (
|
||||
"testing"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
)
|
||||
|
||||
func TestColumnWidths(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
columns Columns
|
||||
rows []*Row
|
||||
opts []ContentOption
|
||||
cvsWidth int
|
||||
want []columnWidth
|
||||
}{
|
||||
{
|
||||
desc: "single column, wide enough",
|
||||
columns: Columns(1),
|
||||
rows: []*Row{
|
||||
NewRow(
|
||||
NewCell("ab"),
|
||||
),
|
||||
NewRow(
|
||||
NewCell(""),
|
||||
),
|
||||
},
|
||||
cvsWidth: 2,
|
||||
want: []columnWidth{2},
|
||||
},
|
||||
{
|
||||
desc: "single column, not wide enough",
|
||||
columns: Columns(1),
|
||||
rows: []*Row{
|
||||
NewRow(
|
||||
NewCell("ab"),
|
||||
),
|
||||
NewRow(
|
||||
NewCell(""),
|
||||
),
|
||||
},
|
||||
cvsWidth: 1,
|
||||
want: []columnWidth{1},
|
||||
},
|
||||
{
|
||||
desc: "two columns, canvas wide enough, no trimming",
|
||||
columns: Columns(2),
|
||||
rows: []*Row{
|
||||
NewRow(
|
||||
NewCell("ab"),
|
||||
NewCell("cde"),
|
||||
),
|
||||
NewRow(
|
||||
NewCell("a"),
|
||||
NewCell("cde"),
|
||||
),
|
||||
NewRow(
|
||||
NewCell(""),
|
||||
NewCell("cde"),
|
||||
),
|
||||
},
|
||||
cvsWidth: 5,
|
||||
want: []columnWidth{2, 3},
|
||||
},
|
||||
{
|
||||
desc: "two columns, canvas not wide enough, optimal to trim first",
|
||||
columns: Columns(2),
|
||||
rows: []*Row{
|
||||
NewRow(
|
||||
NewCell("ab"),
|
||||
NewCell("cde"),
|
||||
),
|
||||
NewRow(
|
||||
NewCell("a"),
|
||||
NewCell("cde"),
|
||||
),
|
||||
NewRow(
|
||||
NewCell(""),
|
||||
NewCell("cde"),
|
||||
),
|
||||
},
|
||||
cvsWidth: 4,
|
||||
want: []columnWidth{1, 3},
|
||||
},
|
||||
{
|
||||
desc: "two columns, canvas not wide enough, optimal to trim second",
|
||||
columns: Columns(2),
|
||||
rows: []*Row{
|
||||
NewRow(
|
||||
NewCell("ab"),
|
||||
NewCell("c"),
|
||||
),
|
||||
NewRow(
|
||||
NewCell("ab"),
|
||||
NewCell("c"),
|
||||
),
|
||||
NewRow(
|
||||
NewCell(""),
|
||||
NewCell("cde"),
|
||||
),
|
||||
},
|
||||
cvsWidth: 4,
|
||||
want: []columnWidth{2, 2},
|
||||
},
|
||||
{
|
||||
desc: "three columns, canvas wide enough, no trimming",
|
||||
columns: Columns(3),
|
||||
rows: []*Row{
|
||||
NewRow(
|
||||
NewCell("ab"),
|
||||
NewCell("cde"),
|
||||
NewCell("fg"),
|
||||
),
|
||||
NewRow(
|
||||
NewCell("a"),
|
||||
NewCell("c"),
|
||||
NewCell("f"),
|
||||
),
|
||||
NewRow(
|
||||
NewCell("ab"),
|
||||
NewCell("cde"),
|
||||
NewCell("fg"),
|
||||
),
|
||||
},
|
||||
cvsWidth: 7,
|
||||
want: []columnWidth{2, 3, 2},
|
||||
},
|
||||
{
|
||||
desc: "three columns, canvas not wide enough, one very long cell",
|
||||
columns: Columns(3),
|
||||
rows: []*Row{
|
||||
NewRow(
|
||||
NewCell("00"),
|
||||
NewCell("11111111"),
|
||||
NewCell("22"),
|
||||
),
|
||||
NewRow(
|
||||
NewCell("00"),
|
||||
NewCell("11"),
|
||||
NewCell("22"),
|
||||
),
|
||||
NewRow(
|
||||
NewCell("00"),
|
||||
NewCell("111"),
|
||||
NewCell("22"),
|
||||
),
|
||||
},
|
||||
cvsWidth: 6,
|
||||
want: []columnWidth{2, 2, 2},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
content, err := NewContent(tc.columns, tc.rows, tc.opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("NewContent => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
got := columnWidths(content, tc.cvsWidth)
|
||||
if diff := pretty.Compare(tc.want, got); diff != "" {
|
||||
t.Errorf("columnWidths => unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -81,6 +81,8 @@ func RowCellOpts(cellOpts ...cell.Option) RowOption {
|
||||
|
||||
// RowHeight sets the height of rows to the provided number of cells.
|
||||
// The number must be a non-zero positive integer.
|
||||
// Rows still use larger than provided height if wrapping is enabled and the
|
||||
// content doesn't fit.
|
||||
// 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.
|
||||
|
@ -61,6 +61,9 @@ func validateContent(content *Content) error {
|
||||
return err
|
||||
}
|
||||
for _, d := range c.data {
|
||||
if len(d.cells) == 0 {
|
||||
continue
|
||||
}
|
||||
if err := wrap.ValidCells(d.cells); err != nil {
|
||||
return fmt.Errorf("invalid data: %v", err)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user