Repository: jedib0t/go-pretty (go)

Issue:
[Feature Request] - Table full width based on terminal size / Header / Row content auto positioning
**Is your feature request related to a problem? Please describe.**
Tables with and column spacing seem to be dependent on row content

Header / Row content alignment, I found in using the `text.Hyperlink(text, link)` it table formatting

**Describe the solution you'd like**
A  config option allow the table to calculate no only the full table width, but also split the header / column locations based on the terminal size 

Header / Row content alignment, I found in using the `text.Hyperlink(text, link)` seems to break the content alignment

**Describe alternatives you've considered**
Tried adjusting the `.SetColumnConfigs( WidthMin, WidthMax)`

**Additional context**

Examples:

#### using regular non hyperlink - works as expected, wish I could force it to be the full size of the terminal with everything aligned perfectly
![image](https://github.com/user-attachments/assets/97882f2f-b3ac-4655-8460-95590e35823e)
![image](https://github.com/user-attachments/assets/9fd2f58a-a052-42cc-8c98-1b567adae19e)

#### using hyperlink - throws alignment off just a bit
![image](https://github.com/user-attachments/assets/c76f161a-06cb-4e3f-b371-5910d6feff79)
![image](https://github.com/user-attachments/assets/bc59f4e4-a9fa-4b5c-8091-75e397d8e498)

####I was able to find fix the alignment issue by doing the following but it's very hacky and I haven't tested it enough, would be better if this was automagically calculated 

![image](https://github.com/user-attachments/assets/c2b8d666-85d5-4ae4-9165-fdd00a53f6d5)
![image](https://github.com/user-attachments/assets/047c28e6-ed06-4e30-94c9-b7de614d7c53)

```go
// DirectoryResults struct for the results
type DirectoryResult struct {
	Directory string
	Count     int
}

// EntryResults struct for more in depth entry info
type EntryResults struct {
	Directory string
	FileName  string
	FileSize  string
}

// The `FormatPath` function converts file paths to either Windows or Unix style based on the operating.
// system specified.
func FormatPath(path, goos string) string {
	switch goos {
	case "windows":
		// Convert to Windows style paths (with backslashes)
		return filepath.FromSlash(path)
	case "linux", "darwin":
		// Convert to Unix style paths (with forward slashes)
		return filepath.ToSlash(path)
	default:
		// Default to Unix style paths
		return path
	}
}

// DetailedResults example
detailedResults := []EntryResults {
    {Directory: "S:\\testdata\\zips", FileName: "tamara.zip", FileSize: "32.37 MB"},
    {Directory: "S:\\testdata\\zips", FileName: "pop.7z", FileSize: "121.22 MB"},
    {Directory: "S:\\testdata\\One\\Videos\\One", FileName: "videofile.mkv", FileSize: "0 B"},
}

// Non Detailed example
nonDetailedResuls := []DirectoryResult {
  {Directory: "s:\\testdata\\One\\Videos\\One", Count: 1},
  {Directory: "s:\\testdata\\zips" , Count: 2},
}

// shared vars
totalCount := 3
totalFileSize := 161061273

// renderResults to table takes and interface and renders the table accordingly.
```go
func renderResultsToTable(results interface{}, totalCount int, totalFileSize int64, ff types.FileFinder) {
	t := table.Table{}

	// Determine header and footer based on the type of results
	var header table.Row
	var footer table.Row
	switch results.(type) {
	case []types.DirectoryResult:
		header = table.Row{"Directory", "\t\tCount"}
		footer = table.Row{"Total", pterm.Sprintf("\t\t  %v", totalCount)}
	case []types.EntryResult:
		header = table.Row{"Directory", "\t\tFileName", "\t\tFileSize"}
		footer = table.Row{"Total", pterm.Sprintf("\t\t  %v", totalCount), pterm.Sprintf("\t\t%v", commonFormatters.FormatSize(totalFileSize))}
	default:
		return // Exit if results type is not supported
	}

	t.AppendHeader(header)

	// Append rows based on the display mode
	switch results := results.(type) {
	case []types.DirectoryResult:
		for _, result := range results {
			t.AppendRow(table.Row{
				text.Hyperlink(commonFormatters.FormatPath(result.Directory, runtime.GOOS), result.Directory),
				pterm.Sprintf("\t   %v", result.Count),
			})
		}
	case []types.EntryResult:
		if ff.DisplayDetailedResults {
			for _, result := range results {
				t.AppendRow(table.Row{
					text.Hyperlink(commonFormatters.FormatPath(result.Directory, runtime.GOOS), result.Directory),
					"\t " + result.FileName,
					"\t " + result.FileSize,
				})
			}
		}
	}

	t.AppendFooter(footer)

	t.SetStyle(table.StyleColoredDark)
	t.SetOutputMirror(os.Stdout)
	t.Render()
}
```

#### another example using colors with hyperlink illustration how alignment is broken - illustration how my temp fix is garbage

```go
func formatResultHyperLink(link, txt string) string {
	text.EnableColors()

	link = commonFormatters.FormatPath(link, runtime.GOOS)
	txt = text.FgGreen.Sprint(txt)
	
	return text.Hyperlink(link, txt)
}


func renderResultsToTable(results interface{}, totalCount int, totalFileSize int64, ff types.FileFinder) {
	t := table.Table{}

	// Determine header and footer based on the type of results
	var header table.Row
	var footer table.Row
	switch results.(type) {
	case []types.DirectoryResult:
		header = table.Row{"Directory", "Count"}
		footer = table.Row{"Total", pterm.Sprintf("%v", totalCount)}
	case []types.EntryResult:
		header = table.Row{"Directory", "FileName", "FileSize"}
		footer = table.Row{"Total", pterm.Sprintf("%v", totalCount), pterm.Sprintf("%v", commonFormatters.FormatSize(totalFileSize))}
	default:
		return // Exit if results type is not supported
	}

	t.AppendHeader(header)

	// Append rows based on the display mode
	switch results := results.(type) {
	case []types.DirectoryResult:
		for _, result := range results {
			t.AppendRow(table.Row{
				formatResultHyperLink(result.Directory, result.Directory),
				pterm.Sprintf("%v", result.Count),
			})
		}
	case []types.EntryResult:
		if ff.DisplayDetailedResults {
			for _, result := range results {
				newLink := pterm.Sprintf("%s/%s", result.Directory, result.FileName)
				t.AppendRow(table.Row{
					formatResultHyperLink(result.Directory, result.Directory),
					formatResultHyperLink(newLink, result.FileName),
					result.FileSize,
				})
			}
		}
	}

	t.AppendFooter(footer)

	t.SetStyle(table.StyleColoredDark)
	t.SetOutputMirror(os.Stdout)
	t.Render()
}
```
![image](https://github.com/user-attachments/assets/db7a7c5e-b02d-42b1-8f5e-b19ae39ab3d2)
![image](https://github.com/user-attachments/assets/be9f8ddd-d686-42a9-9ab5-31c8ba9f0ce5)



Pull request:
table: auto expand columns with Style().Size.WidthMin
## Proposed Changes
  - Introduce Style().Size with options to control min/max width for the table

![image](https://github.com/user-attachments/assets/72e730a5-5977-4f4d-be16-b385cb80d50d)

Fixes #328.


Patch:
diff --git a/table/README.md b/table/README.md
index e7d3e1b..3375bd3 100644
--- a/table/README.md
+++ b/table/README.md
@@ -13,6 +13,7 @@ Pretty-print tables into ASCII/Unicode strings.
   - Limit the length of
     - Rows (`SetAllowedRowLength`)
     - Columns (`ColumnConfig.Width*`)
+  - Auto-size Rows (`Style().Size.WidthMin` and `Style().Size.WidthMax`)
   - Page results by a specified number of Lines (`SetPageSize`)
   - Alignment - Horizontal & Vertical
     - Auto (horizontal) Align (numeric columns aligned Right)
diff --git a/table/render.go b/table/render.go
index bb883e9..7641da3 100644
--- a/table/render.go
+++ b/table/render.go
@@ -185,7 +185,7 @@ func (t *Table) renderLine(out *strings.Builder, row rowStr, hint renderHint) {
 
 	// use a brand-new strings.Builder if a row length limit has been set
 	var outLine *strings.Builder
-	if t.allowedRowLength > 0 {
+	if t.style.Size.WidthMax > 0 {
 		outLine = &strings.Builder{}
 	} else {
 		outLine = out
@@ -227,8 +227,8 @@ func (t *Table) renderLine(out *strings.Builder, row rowStr, hint renderHint) {
 
 func (t *Table) renderLineMergeOutputs(out *strings.Builder, outLine *strings.Builder) {
 	outLineStr := outLine.String()
-	if text.RuneWidthWithoutEscSequences(outLineStr) > t.allowedRowLength {
-		trimLength := t.allowedRowLength - utf8.RuneCountInString(t.style.Box.UnfinishedRow)
+	if text.RuneWidthWithoutEscSequences(outLineStr) > t.style.Size.WidthMax {
+		trimLength := t.style.Size.WidthMax - utf8.RuneCountInString(t.style.Box.UnfinishedRow)
 		if trimLength > 0 {
 			out.WriteString(text.Trim(outLineStr, trimLength))
 			out.WriteString(t.style.Box.UnfinishedRow)
@@ -385,8 +385,11 @@ func (t *Table) renderTitle(out *strings.Builder) {
 		colors := t.style.Title.Colors
 		colorsBorder := t.getBorderColors(renderHint{isTitleRow: true})
 		rowLength := t.maxRowLength
-		if t.allowedRowLength != 0 && t.allowedRowLength < rowLength {
-			rowLength = t.allowedRowLength
+		if wm := t.style.Size.WidthMax; wm > 0 && wm < rowLength {
+			rowLength = wm
+		}
+		if wm := t.style.Size.WidthMin; wm > 0 && wm > rowLength {
+			rowLength = wm
 		}
 		if t.style.Options.DrawBorder {
 			lenBorder := rowLength - text.RuneWidthWithoutEscSequences(t.style.Box.TopLeft+t.style.Box.TopRight)
diff --git a/table/render_init.go b/table/render_init.go
index e9b574a..2627e01 100644
--- a/table/render_init.go
+++ b/table/render_init.go
@@ -105,6 +105,8 @@ func (t *Table) initForRender() {
 
 	// find the longest continuous line in each column
 	t.initForRenderColumnLengths()
+	t.initForRenderMaxRowLength()
+	t.initForRenderPaddedColumns()
 
 	// generate a separator row and calculate maximum row length
 	t.initForRenderRowSeparator()
@@ -172,6 +174,50 @@ func (t *Table) initForRenderHideColumns() {
 	t.columnConfigMap = columnConfigMap
 }
 
+func (t *Table) initForRenderMaxRowLength() {
+	t.maxRowLength = 0
+	if t.autoIndex {
+		t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingLeft)
+		t.maxRowLength += len(fmt.Sprint(len(t.rows)))
+		t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingRight)
+		if t.style.Options.SeparateColumns {
+			t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.MiddleSeparator)
+		}
+	}
+	if t.style.Options.SeparateColumns {
+		t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.MiddleSeparator) * (t.numColumns - 1)
+	}
+	for _, maxColumnLength := range t.maxColumnLengths {
+		maxColumnLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingLeft + t.style.Box.PaddingRight)
+		t.maxRowLength += maxColumnLength
+	}
+	if t.style.Options.DrawBorder {
+		t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.Left + t.style.Box.Right)
+	}
+}
+
+func (t *Table) initForRenderPaddedColumns() {
+	paddingSize := t.style.Size.WidthMin - t.maxRowLength
+	for paddingSize > 0 {
+		// distribute padding equally among all columns
+		numColumnsPadded := 0
+		for colIdx := 0; paddingSize > 0 && colIdx < t.numColumns; colIdx++ {
+			colWidthMax := t.getColumnWidthMax(colIdx)
+			if colWidthMax == 0 || t.maxColumnLengths[colIdx] < colWidthMax {
+				t.maxColumnLengths[colIdx]++
+				numColumnsPadded++
+				paddingSize--
+			}
+		}
+
+		// avoid endless looping because all columns are at max size and cannot
+		// be expanded any further
+		if numColumnsPadded == 0 {
+			break
+		}
+	}
+}
+
 func (t *Table) initForRenderRows() {
 	// auto-index: calc the index column's max length
 	t.autoIndexVIndexMaxLength = len(fmt.Sprint(len(t.rowsRaw)))
@@ -206,27 +252,11 @@ func (t *Table) initForRenderRowsStringify(rows []Row, hint renderHint) []rowStr
 }
 
 func (t *Table) initForRenderRowSeparator() {
-	t.maxRowLength = 0
-	if t.autoIndex {
-		t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingLeft)
-		t.maxRowLength += len(fmt.Sprint(len(t.rows)))
-		t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingRight)
-		if t.style.Options.SeparateColumns {
-			t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.MiddleSeparator)
-		}
-	}
-	if t.style.Options.SeparateColumns {
-		t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.MiddleSeparator) * (t.numColumns - 1)
-	}
 	t.rowSeparator = make(rowStr, t.numColumns)
 	for colIdx, maxColumnLength := range t.maxColumnLengths {
 		maxColumnLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingLeft + t.style.Box.PaddingRight)
-		t.maxRowLength += maxColumnLength
 		t.rowSeparator[colIdx] = text.RepeatAndTrim(t.style.Box.MiddleHorizontal, maxColumnLength)
 	}
-	if t.style.Options.DrawBorder {
-		t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.Left + t.style.Box.Right)
-	}
 }
 
 func (t *Table) initForRenderSortRows() {
diff --git a/table/style.go b/table/style.go
index cb850e8..b4fe092 100644
--- a/table/style.go
+++ b/table/style.go
@@ -13,6 +13,7 @@ type Style struct {
 	Format  FormatOptions // formatting options for the rows and columns
 	HTML    HTMLOptions   // rendering options for HTML mode
 	Options Options       // misc. options for the table
+	Size    SizeOptions   // size (width) options for the table
 	Title   TitleOptions  // formation options for the title text
 }
 
@@ -34,6 +35,7 @@ var (
 		Format:  FormatOptionsDefault,
 		HTML:    DefaultHTMLOptions,
 		Options: OptionsDefault,
+		Size:    SizeOptionsDefault,
 		Title:   TitleOptionsDefault,
 	}
 
@@ -54,6 +56,7 @@ var (
 		Format:  FormatOptionsDefault,
 		HTML:    DefaultHTMLOptions,
 		Options: OptionsDefault,
+		Size:    SizeOptionsDefault,
 		Title:   TitleOptionsDefault,
 	}
 
@@ -67,6 +70,7 @@ var (
 		Format:  FormatOptionsDefault,
 		HTML:    DefaultHTMLOptions,
 		Options: OptionsNoBordersAndSeparators,
+		Size:    SizeOptionsDefault,
 		Title:   TitleOptionsDark,
 	}
 
@@ -80,6 +84,7 @@ var (
 		Format:  FormatOptionsDefault,
 		HTML:    DefaultHTMLOptions,
 		Options: OptionsNoBordersAndSeparators,
+		Size:    SizeOptionsDefault,
 		Title:   TitleOptionsBright,
 	}
 
@@ -93,6 +98,7 @@ var (
 		Format:  FormatOptionsDefault,
 		HTML:    DefaultHTMLOptions,
 		Options: OptionsNoBordersAndSeparators,
+		Size:    SizeOptionsDefault,
 		Title:   TitleOptionsBlueOnBlack,
 	}
 
@@ -106,6 +112,7 @@ var (
 		Format:  FormatOptionsDefault,
 		HTML:    DefaultHTMLOptions,
 		Options: OptionsNoBordersAndSeparators,
+		Size:    SizeOptionsDefault,
 		Title:   TitleOptionsCyanOnBlack,
 	}
 
@@ -119,6 +126,7 @@ var (
 		Format:  FormatOptionsDefault,
 		HTML:    DefaultHTMLOptions,
 		Options: OptionsNoBordersAndSeparators,
+		Size:    SizeOptionsDefault,
 		Title:   TitleOptionsGreenOnBlack,
 	}
 
@@ -132,6 +140,7 @@ var (
 		Format:  FormatOptionsDefault,
 		HTML:    DefaultHTMLOptions,
 		Options: OptionsNoBordersAndSeparators,
+		Size:    SizeOptionsDefault,
 		Title:   TitleOptionsMagentaOnBlack,
 	}
 
@@ -145,6 +154,7 @@ var (
 		Format:  FormatOptionsDefault,
 		HTML:    DefaultHTMLOptions,
 		Options: OptionsNoBordersAndSeparators,
+		Size:    SizeOptionsDefault,
 		Title:   TitleOptionsYellowOnBlack,
 	}
 
@@ -158,6 +168,7 @@ var (
 		Format:  FormatOptionsDefault,
 		HTML:    DefaultHTMLOptions,
 		Options: OptionsNoBordersAndSeparators,
+		Size:    SizeOptionsDefault,
 		Title:   TitleOptionsRedOnBlack,
 	}
 
@@ -171,6 +182,7 @@ var (
 		Format:  FormatOptionsDefault,
 		HTML:    DefaultHTMLOptions,
 		Options: OptionsNoBordersAndSeparators,
+		Size:    SizeOptionsDefault,
 		Title:   TitleOptionsBlackOnBlue,
 	}
 
@@ -184,6 +196,7 @@ var (
 		Format:  FormatOptionsDefault,
 		HTML:    DefaultHTMLOptions,
 		Options: OptionsNoBordersAndSeparators,
+		Size:    SizeOptionsDefault,
 		Title:   TitleOptionsBlackOnCyan,
 	}
 
@@ -197,6 +210,7 @@ var (
 		Format:  FormatOptionsDefault,
 		HTML:    DefaultHTMLOptions,
 		Options: OptionsNoBordersAndSeparators,
+		Size:    SizeOptionsDefault,
 		Title:   TitleOptionsBlackOnGreen,
 	}
 
@@ -210,6 +224,7 @@ var (
 		Format:  FormatOptionsDefault,
 		HTML:    DefaultHTMLOptions,
 		Options: OptionsNoBordersAndSeparators,
+		Size:    SizeOptionsDefault,
 		Title:   TitleOptionsBlackOnMagenta,
 	}
 
@@ -223,6 +238,7 @@ var (
 		Format:  FormatOptionsDefault,
 		HTML:    DefaultHTMLOptions,
 		Options: OptionsNoBordersAndSeparators,
+		Size:    SizeOptionsDefault,
 		Title:   TitleOptionsBlackOnRed,
 	}
 
@@ -236,6 +252,7 @@ var (
 		Format:  FormatOptionsDefault,
 		HTML:    DefaultHTMLOptions,
 		Options: OptionsNoBordersAndSeparators,
+		Size:    SizeOptionsDefault,
 		Title:   TitleOptionsBlackOnYellow,
 	}
 
@@ -256,6 +273,7 @@ var (
 		Format:  FormatOptionsDefault,
 		HTML:    DefaultHTMLOptions,
 		Options: OptionsDefault,
+		Size:    SizeOptionsDefault,
 		Title:   TitleOptionsDefault,
 	}
 
@@ -276,6 +294,7 @@ var (
 		Format:  FormatOptionsDefault,
 		HTML:    DefaultHTMLOptions,
 		Options: OptionsDefault,
+		Size:    SizeOptionsDefault,
 		Title:   TitleOptionsDefault,
 	}
 
@@ -296,6 +315,7 @@ var (
 		Format:  FormatOptionsDefault,
 		HTML:    DefaultHTMLOptions,
 		Options: OptionsDefault,
+		Size:    SizeOptionsDefault,
 		Title:   TitleOptionsDefault,
 	}
 
@@ -316,6 +336,7 @@ var (
 		Format:  FormatOptionsDefault,
 		HTML:    DefaultHTMLOptions,
 		Options: OptionsDefault,
+		Size:    SizeOptionsDefault,
 		Title:   TitleOptionsDefault,
 	}
 )
@@ -798,6 +819,26 @@ var (
 	}
 )
 
+// SizeOptions defines the way to control the width of the table output.
+type SizeOptions struct {
+	// WidthMax is the maximum allotted width for the full row;
+	// any content beyond this will be truncated using the text
+	// in Style.Box.UnfinishedRow
+	WidthMax int
+	// WidthMin is the minimum allotted width for the full row;
+	// columns will be auto-expanded until the overall width
+	// is met
+	WidthMin int
+}
+
+var (
+	// SizeOptionsDefault defines sensible size options - basically NONE.
+	SizeOptionsDefault = SizeOptions{
+		WidthMax: 0,
+		WidthMin: 0,
+	}
+)
+
 // TitleOptions defines the way the title text is to be rendered.
 type TitleOptions struct {
 	Align  text.Align
diff --git a/table/table.go b/table/table.go
index 8081dfb..34e5a34 100644
--- a/table/table.go
+++ b/table/table.go
@@ -236,6 +236,8 @@ func (t *Table) ResetRows() {
 // SetAllowedRowLength sets the maximum allowed length or a row (or line of
 // output) when rendered as a table. Rows that are longer than this limit will
 // be "snipped" to the length. Length has to be a positive value to take effect.
+//
+// Deprecated: in favor if Style().Size.WidthMax
 func (t *Table) SetAllowedRowLength(length int) {
 	t.allowedRowLength = length
 }
@@ -319,6 +321,11 @@ func (t *Table) Style() *Style {
 		tempStyle := StyleDefault
 		t.style = &tempStyle
 	}
+	// override WidthMax with allowedRowLength until allowedRowLength is
+	// removed from code
+	if t.allowedRowLength > 0 {
+		t.style.Size.WidthMax = t.allowedRowLength
+	}
 	return t.style
 }
 
diff --git a/table/writer.go b/table/writer.go
index f5642aa..ec7a53a 100644
--- a/table/writer.go
+++ b/table/writer.go
@@ -21,7 +21,6 @@ type Writer interface {
 	ResetFooters()
 	ResetHeaders()
 	ResetRows()
-	SetAllowedRowLength(length int)
 	SetAutoIndex(autoIndex bool)
 	SetCaption(format string, a ...interface{})
 	SetColumnConfigs(configs []ColumnConfig)
@@ -35,6 +34,8 @@ type Writer interface {
 	SuppressEmptyColumns()
 	SuppressTrailingSpaces()
 
+	// deprecated; in favor if Style().Size.WidthMax
+	SetAllowedRowLength(length int)
 	// deprecated; in favor of Style().HTML.CSSClass
 	SetHTMLCSSClass(cssClass string)
 	// deprecated; in favor of Pager()


Test patch:
diff --git a/table/render_test.go b/table/render_test.go
index 5a44dac..c7134c6 100644
--- a/table/render_test.go
+++ b/table/render_test.go
@@ -1352,3 +1352,186 @@ func TestTable_Render_SuppressTrailingSpaces(t *testing.T) {
  R123  Some big name here and it's pretty big  2021-04-19 13:37  Abcdefghijklmnopqrstuvwxyz
  R123  Small name                              2021-04-19 13:37  Abcdefghijklmnopqrstuvwxyz`)
 }
+
+func TestTable_Render_AutoWidth(t *testing.T) {
+	tw := NewWriter()
+	tw.AppendHeader(testHeader)
+	tw.AppendRows(testRows)
+	tw.AppendFooter(testFooter)
+	tw.SetStyle(StyleLight)
+	compareOutput(t, tw.Render(), `
+┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
+│   # │ FIRST NAME │ LAST NAME │ SALARY │                             │
+├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
+│   1 │ Arya       │ Stark     │   3000 │                             │
+│  20 │ Jon        │ Snow      │   2000 │ You know nothing, Jon Snow! │
+│ 300 │ Tyrion     │ Lannister │   5000 │                             │
+├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
+│     │            │ TOTAL     │  10000 │                             │
+└─────┴────────────┴───────────┴────────┴─────────────────────────────┘`)
+
+	tw.SetTitle("Game of Thrones")
+	tw.Style().Size = SizeOptions{
+		WidthMax: 0,
+		WidthMin: 100,
+	}
+	compareOutput(t, tw.Render(), `
+┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
+│ Game of Thrones                                                                                  │
+├───────────┬──────────────────┬─────────────────┬──────────────┬──────────────────────────────────┤
+│         # │ FIRST NAME       │ LAST NAME       │       SALARY │                                  │
+├───────────┼──────────────────┼─────────────────┼──────────────┼──────────────────────────────────┤
+│         1 │ Arya             │ Stark           │         3000 │                                  │
+│        20 │ Jon              │ Snow            │         2000 │ You know nothing, Jon Snow!      │
+│       300 │ Tyrion           │ Lannister       │         5000 │                                  │
+├───────────┼──────────────────┼─────────────────┼──────────────┼──────────────────────────────────┤
+│           │                  │ TOTAL           │        10000 │                                  │
+└───────────┴──────────────────┴─────────────────┴──────────────┴──────────────────────────────────┘`)
+
+	tw.SetTitle("")
+	tw.Style().Size = SizeOptions{
+		WidthMax: 0,
+		WidthMin: 120,
+	}
+	compareOutput(t, tw.Render(), `
+┌───────────────┬──────────────────────┬─────────────────────┬──────────────────┬──────────────────────────────────────┐
+│             # │ FIRST NAME           │ LAST NAME           │           SALARY │                                      │
+├───────────────┼──────────────────────┼─────────────────────┼──────────────────┼──────────────────────────────────────┤
+│             1 │ Arya                 │ Stark               │             3000 │                                      │
+│            20 │ Jon                  │ Snow                │             2000 │ You know nothing, Jon Snow!          │
+│           300 │ Tyrion               │ Lannister           │             5000 │                                      │
+├───────────────┼──────────────────────┼─────────────────────┼──────────────────┼──────────────────────────────────────┤
+│               │                      │ TOTAL               │            10000 │                                      │
+└───────────────┴──────────────────────┴─────────────────────┴──────────────────┴──────────────────────────────────────┘`)
+
+	tw.SetColumnConfigs([]ColumnConfig{
+		{Number: 1, WidthMax: 4},
+	})
+	compareOutput(t, tw.Render(), `
+┌──────┬────────────────────────┬───────────────────────┬────────────────────┬─────────────────────────────────────────┐
+│    # │ FIRST NAME             │ LAST NAME             │             SALARY │                                         │
+├──────┼────────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────────────────┤
+│    1 │ Arya                   │ Stark                 │               3000 │                                         │
+│   20 │ Jon                    │ Snow                  │               2000 │ You know nothing, Jon Snow!             │
+│  300 │ Tyrion                 │ Lannister             │               5000 │                                         │
+├──────┼────────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────────────────┤
+│      │                        │ TOTAL                 │              10000 │                                         │
+└──────┴────────────────────────┴───────────────────────┴────────────────────┴─────────────────────────────────────────┘`)
+
+	tw.SetColumnConfigs([]ColumnConfig{
+		{Number: 1, WidthMax: 4},
+		{Number: 2, WidthMax: 10},
+	})
+	compareOutput(t, tw.Render(), `
+┌──────┬────────────┬───────────────────────────┬────────────────────────┬─────────────────────────────────────────────┐
+│    # │ FIRST NAME │ LAST NAME                 │                 SALARY │                                             │
+├──────┼────────────┼───────────────────────────┼────────────────────────┼─────────────────────────────────────────────┤
+│    1 │ Arya       │ Stark                     │                   3000 │                                             │
+│   20 │ Jon        │ Snow                      │                   2000 │ You know nothing, Jon Snow!                 │
+│  300 │ Tyrion     │ Lannister                 │                   5000 │                                             │
+├──────┼────────────┼───────────────────────────┼────────────────────────┼─────────────────────────────────────────────┤
+│      │            │ TOTAL                     │                  10000 │                                             │
+└──────┴────────────┴───────────────────────────┴────────────────────────┴─────────────────────────────────────────────┘`)
+
+	tw.SetColumnConfigs([]ColumnConfig{
+		{Number: 1, WidthMax: 4},
+		{Number: 2, WidthMax: 10},
+		{Number: 3, WidthMax: 10},
+	})
+	compareOutput(t, tw.Render(), `
+┌──────┬────────────┬────────────┬────────────────────────────────┬────────────────────────────────────────────────────┐
+│    # │ FIRST NAME │ LAST NAME  │                         SALARY │                                                    │
+├──────┼────────────┼────────────┼────────────────────────────────┼────────────────────────────────────────────────────┤
+│    1 │ Arya       │ Stark      │                           3000 │                                                    │
+│   20 │ Jon        │ Snow       │                           2000 │ You know nothing, Jon Snow!                        │
+│  300 │ Tyrion     │ Lannister  │                           5000 │                                                    │
+├──────┼────────────┼────────────┼────────────────────────────────┼────────────────────────────────────────────────────┤
+│      │            │ TOTAL      │                          10000 │                                                    │
+└──────┴────────────┴────────────┴────────────────────────────────┴────────────────────────────────────────────────────┘`)
+
+	tw.SetColumnConfigs([]ColumnConfig{
+		{Number: 1, WidthMax: 4},
+		{Number: 2, WidthMax: 10},
+		{Number: 3, WidthMax: 10},
+		{Number: 4, WidthMax: 6},
+	})
+	compareOutput(t, tw.Render(), `
+┌──────┬────────────┬────────────┬────────┬────────────────────────────────────────────────────────────────────────────┐
+│    # │ FIRST NAME │ LAST NAME  │ SALARY │                                                                            │
+├──────┼────────────┼────────────┼────────┼────────────────────────────────────────────────────────────────────────────┤
+│    1 │ Arya       │ Stark      │   3000 │                                                                            │
+│   20 │ Jon        │ Snow       │   2000 │ You know nothing, Jon Snow!                                                │
+│  300 │ Tyrion     │ Lannister  │   5000 │                                                                            │
+├──────┼────────────┼────────────┼────────┼────────────────────────────────────────────────────────────────────────────┤
+│      │            │ TOTAL      │  10000 │                                                                            │
+└──────┴────────────┴────────────┴────────┴────────────────────────────────────────────────────────────────────────────┘`)
+
+	tw.SetColumnConfigs([]ColumnConfig{
+		{Number: 1, WidthMax: 4},
+		{Number: 2, WidthMax: 10},
+		{Number: 3, WidthMax: 10},
+		{Number: 4, WidthMax: 6},
+		{Number: 5, WidthMax: 27},
+	})
+	compareOutput(t, tw.Render(), `
+┌──────┬────────────┬────────────┬────────┬─────────────────────────────┐
+│    # │ FIRST NAME │ LAST NAME  │ SALARY │                             │
+├──────┼────────────┼────────────┼────────┼─────────────────────────────┤
+│    1 │ Arya       │ Stark      │   3000 │                             │
+│   20 │ Jon        │ Snow       │   2000 │ You know nothing, Jon Snow! │
+│  300 │ Tyrion     │ Lannister  │   5000 │                             │
+├──────┼────────────┼────────────┼────────┼─────────────────────────────┤
+│      │            │ TOTAL      │  10000 │                             │
+└──────┴────────────┴────────────┴────────┴─────────────────────────────┘`)
+
+	tw.SetColumnConfigs([]ColumnConfig{
+		{Number: 2, WidthMax: 10},
+		{Number: 3, WidthMax: 10},
+		{Number: 4, WidthMax: 6},
+		{Number: 5, WidthMax: 27},
+	})
+	compareOutput(t, tw.Render(), `
+┌─────────────────────────────────────────────────────┬────────────┬────────────┬────────┬─────────────────────────────┐
+│                                                   # │ FIRST NAME │ LAST NAME  │ SALARY │                             │
+├─────────────────────────────────────────────────────┼────────────┼────────────┼────────┼─────────────────────────────┤
+│                                                   1 │ Arya       │ Stark      │   3000 │                             │
+│                                                  20 │ Jon        │ Snow       │   2000 │ You know nothing, Jon Snow! │
+│                                                 300 │ Tyrion     │ Lannister  │   5000 │                             │
+├─────────────────────────────────────────────────────┼────────────┼────────────┼────────┼─────────────────────────────┤
+│                                                     │            │ TOTAL      │  10000 │                             │
+└─────────────────────────────────────────────────────┴────────────┴────────────┴────────┴─────────────────────────────┘`)
+
+	tw.SetColumnConfigs(nil)
+	tw.Style().Size = SizeOptions{
+		WidthMax: 60,
+		WidthMin: 0,
+	}
+	compareOutput(t, tw.Render(), `
+┌─────┬────────────┬───────────┬────────┬───────────────── ≈
+│   # │ FIRST NAME │ LAST NAME │ SALARY │                  ≈
+├─────┼────────────┼───────────┼────────┼───────────────── ≈
+│   1 │ Arya       │ Stark     │   3000 │                  ≈
+│  20 │ Jon        │ Snow      │   2000 │ You know nothing ≈
+│ 300 │ Tyrion     │ Lannister │   5000 │                  ≈
+├─────┼────────────┼───────────┼────────┼───────────────── ≈
+│     │            │ TOTAL     │  10000 │                  ≈
+└─────┴────────────┴───────────┴────────┴───────────────── ≈`)
+
+	// expanded columns, but truncated row - not a valid usage scenario;
+	// no enforcement on min < max at this point
+	tw.SetColumnConfigs(nil)
+	tw.Style().Size = SizeOptions{
+		WidthMax: 60,
+		WidthMin: 80,
+	}
+	compareOutput(t, tw.Render(), `
+┌───────┬──────────────┬─────────────┬──────────┬───────── ≈
+│     # │ FIRST NAME   │ LAST NAME   │   SALARY │          ≈
+├───────┼──────────────┼─────────────┼──────────┼───────── ≈
+│     1 │ Arya         │ Stark       │     3000 │          ≈
+│    20 │ Jon          │ Snow        │     2000 │ You know ≈
+│   300 │ Tyrion       │ Lannister   │     5000 │          ≈
+├───────┼──────────────┼─────────────┼──────────┼───────── ≈
+│       │              │ TOTAL       │    10000 │          ≈
+└───────┴──────────────┴─────────────┴──────────┴───────── ≈`)
+}