build(deps): bump github.com/olekukonko/tablewriter from 1.0.6 to 1.0.7

Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 1.0.6 to 1.0.7.
- [Commits](https://github.com/olekukonko/tablewriter/compare/v1.0.6...v1.0.7)

---
updated-dependencies:
- dependency-name: github.com/olekukonko/tablewriter
  dependency-version: 1.0.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot]
2025-06-02 15:00:14 +00:00
committed by GitHub
parent 524e13ae89
commit 57572a1fcb
21 changed files with 5467 additions and 1055 deletions

4
go.mod
View File

@@ -58,7 +58,7 @@ require (
github.com/nats-io/nats-server/v2 v2.11.4
github.com/nats-io/nats.go v1.42.0
github.com/oklog/run v1.1.0
github.com/olekukonko/tablewriter v1.0.6
github.com/olekukonko/tablewriter v1.0.7
github.com/onsi/ginkgo v1.16.5
github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.37.0
@@ -266,7 +266,7 @@ require (
github.com/nats-io/nuid v1.0.1 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
github.com/olekukonko/ll v0.0.8-0.20250516010636-22ea57d81985 // indirect
github.com/olekukonko/ll v0.0.8 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/pablodz/inotifywaitgo v0.0.9 // indirect

8
go.sum
View File

@@ -844,11 +844,11 @@ github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DV
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo=
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.8-0.20250516010636-22ea57d81985 h1:V2wKiwjwAfRJRtUP6pC7wt4opeF14enO0du2dRV6Llo=
github.com/olekukonko/ll v0.0.8-0.20250516010636-22ea57d81985/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/olekukonko/tablewriter v1.0.6 h1:/T45mIHc5hcEvibgzBzvMy7ruT+RjgoQRvkHbnl6OWA=
github.com/olekukonko/tablewriter v1.0.6/go.mod h1:SJ0MV1aHb/89fLcsBMXMp30Xg3g5eGoOUu0RptEk4AU=
github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw=
github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=

View File

@@ -431,6 +431,15 @@ func Print(args ...any) {
defaultLogger.Print(args...)
}
// Println logs a message at Info level without format specifiers, minimizing allocations
// by concatenating arguments with spaces. It is thread-safe via the log method.
// Example:
//
// ll.Println("message", "value") // Output: [] INFO: message value [New Line]
func Println(args ...any) {
defaultLogger.Println(args...)
}
// Printf logs a message at Info level with a format string using the default logger.
// It formats the message and delegates to defaultLoggers Printf method. Thread-safe via
// the Loggers log method.
@@ -573,7 +582,7 @@ func Disable() *Logger {
// x := 42
// ll.Dbg(x) // Output: [file.go:123] x = 42
func Dbg(any ...interface{}) {
defaultLogger.Dbg(any...)
defaultLogger.dbg(2, any...)
}
// Dump displays a hex and ASCII representation of a values binary form using the default logger.
@@ -638,3 +647,13 @@ func Line(lines ...int) *Logger {
func Indent(depth int) *Logger {
return defaultLogger.Indent(depth)
}
// Mark logs the current file and line number where it's called, without any additional debug information.
// It's useful for tracing execution flow without the verbosity of Dbg.
// Example:
//
// logger.Mark() // *MARK*: [file.go:123]
func Mark(names ...string) {
defaultLogger.mark(2, names...)
}

View File

@@ -232,18 +232,6 @@ func (l *Logger) Debug(args ...any) {
l.log(lx.LevelDebug, lx.ClassText, concatSpaced(args...), nil, false)
}
func (l *Logger) Debug2(args ...any) {
// Skip logging if Debug level is not enabled
if !l.enabled {
return
}
l.log(lx.LevelDebug, lx.ClassText, concatSpaced(args...), nil, false)
}
func (l *Logger) Debug3(args ...any) {
l.log(lx.LevelDebug, lx.ClassText, concatSpaced(args...), nil, false)
}
// Debugf logs a formatted message at Debug level, delegating to Debug. It is thread-safe.
// Example:
//
@@ -736,6 +724,44 @@ func (l *Logger) Line(lines ...int) *Logger {
return l
}
// Mark logs the current file and line number where it's called, without any additional debug information.
// It's useful for tracing execution flow without the verbosity of Dbg.
// Example:
//
// logger.Mark() // *MARK*: [file.go:123]
func (l *Logger) Mark(name ...string) {
l.mark(2, name...)
}
func (l *Logger) mark(skip int, names ...string) {
// Skip logging if Info level is not enabled
if !l.shouldLog(lx.LevelInfo) {
return
}
// Get caller information (file, line)
_, file, line, ok := runtime.Caller(skip)
if !ok {
l.log(lx.LevelError, lx.ClassText, "Mark: Unable to parse runtime caller", nil, false)
return
}
// Extract just the filename (without full path)
shortFile := file
if idx := strings.LastIndex(file, "/"); idx >= 0 {
shortFile = file[idx+1:]
}
name := strings.Join(names, l.separator)
if name == "" {
name = "MARK"
}
// Format as [filename:line]
out := fmt.Sprintf("[*%s*]: [%s:%d]\n", name, shortFile, line)
l.log(lx.LevelInfo, lx.ClassRaw, out, nil, false)
}
// Measure benchmarks function execution, logging the duration at Info level with a
// "duration" field. It is thread-safe via Fields and log methods.
// Example:
@@ -920,6 +946,24 @@ func (l *Logger) Print(args ...any) {
return
}
// Skip logging if Info level is not enabled
if !l.shouldLog(lx.LevelInfo) {
return
}
l.log(lx.LevelNone, lx.ClassRaw, concatSpaced(args...), nil, false)
}
// Println logs a message at Info level without format specifiers, minimizing allocations
// by concatenating arguments with spaces. It is thread-safe via the log method.
// Example:
//
// logger := New("app").Enable()
// logger.Println("message", "value") // Output: [app] INFO: message value
func (l *Logger) Println(args ...any) {
if l.suspend {
return
}
// Skip logging if Info level is not enabled
if !l.shouldLog(lx.LevelInfo) {
return

3042
vendor/github.com/olekukonko/tablewriter/MIGRATION.md generated vendored Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -28,14 +28,14 @@ go get github.com/olekukonko/tablewriter@v0.0.5
#### Latest Version
The latest stable version
```bash
go get github.com/olekukonko/tablewriter@v1.0.6
go get github.com/olekukonko/tablewriter@v1.0.7
```
**Warning:** Version `v1.0.0` contains missing functionality and should not be used.
> **Version Guidance**
> - Production: Use `v0.0.5` (stable)
> - Legacy: Use `v0.0.5` (stable)
> - New Features: Use `@latest` (includes generics, super fast streaming APIs)
> - Legacy Docs: See [README_LEGACY.md](README_LEGACY.md)
@@ -62,7 +62,7 @@ func main() {
data := [][]string{
{"Package", "Version", "Status"},
{"tablewriter", "v0.0.5", "legacy"},
{"tablewriter", "v1.0.6", "latest"},
{"tablewriter", "v1.0.7", "latest"},
}
table := tablewriter.NewWriter(os.Stdout)
@@ -77,7 +77,7 @@ func main() {
│ PACKAGE │ VERSION │ STATUS │
├─────────────┼─────────┼────────┤
│ tablewriter │ v0.0.5 │ legacy │
│ tablewriter │ v1.0.6 │ latest │
│ tablewriter │ v1.0.7 │ latest │
└─────────────┴─────────┴────────┘
```
@@ -297,7 +297,7 @@ func main() {
}
table.Configure(func(config *tablewriter.Config) {
config.Row.Formatting.Alignment = tw.AlignLeft
config.Row.Alignment.Global = tw.AlignLeft
})
table.Render()
}
@@ -368,14 +368,12 @@ func main() {
tablewriter.WithRenderer(renderer.NewColorized(colorCfg)),
tablewriter.WithConfig(tablewriter.Config{
Row: tw.CellConfig{
Formatting: tw.CellFormatting{
AutoWrap: tw.WrapNormal, // Wrap long content
Alignment: tw.AlignLeft, // Left-align rows
},
Formatting: tw.CellFormatting{AutoWrap: tw.WrapNormal}, // Wrap long content
Alignment: tw.CellAlignment{Global: tw.AlignLeft}, // Left-align rows
ColMaxWidths: tw.CellWidth{Global: 25},
},
Footer: tw.CellConfig{
Formatting: tw.CellFormatting{Alignment: tw.AlignRight},
Alignment: tw.CellAlignment{Global: tw.AlignRight},
},
}),
)
@@ -480,19 +478,13 @@ func main() {
table := tablewriter.NewTable(os.Stdout,
tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{
Settings: tw.Settings{
Separators: tw.Separators{BetweenRows: tw.On},
},
Settings: tw.Settings{Separators: tw.Separators{BetweenRows: tw.On}},
})),
tablewriter.WithConfig(tablewriter.Config{
Header: tw.CellConfig{
Formatting: tw.CellFormatting{Alignment: tw.AlignCenter},
},
Header: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}},
Row: tw.CellConfig{
Formatting: tw.CellFormatting{
MergeMode: tw.MergeHierarchical,
Alignment: tw.AlignLeft,
},
Formatting: tw.CellFormatting{MergeMode: tw.MergeHierarchical},
Alignment: tw.CellAlignment{Global: tw.AlignLeft},
},
}),
)
@@ -546,21 +538,20 @@ func main() {
table := tablewriter.NewTable(os.Stdout,
tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{
Settings: tw.Settings{
Separators: tw.Separators{BetweenRows: tw.On},
},
Settings: tw.Settings{Separators: tw.Separators{BetweenRows: tw.On}},
})),
tablewriter.WithConfig(tablewriter.Config{
Row: tw.CellConfig{
Formatting: tw.CellFormatting{MergeMode: tw.MergeBoth},
ColumnAligns: []tw.Align{tw.Skip, tw.Skip, tw.AlignRight, tw.AlignLeft},
Formatting: tw.CellFormatting{MergeMode: tw.MergeBoth},
Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.Skip, tw.Skip, tw.AlignRight, tw.AlignLeft}},
},
Footer: tw.CellConfig{
Padding: tw.CellPadding{
Global: tw.Padding{Left: "*", Right: "*"},
PerColumn: []tw.Padding{{}, {}, {Bottom: "^"}, {Bottom: "^"}},
},
ColumnAligns: []tw.Align{tw.Skip, tw.Skip, tw.AlignRight, tw.AlignLeft},
Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.Skip, tw.Skip, tw.AlignRight, tw.AlignLeft}},
},
}),
)
@@ -617,9 +608,7 @@ func main() {
})),
tablewriter.WithConfig(tablewriter.Config{
MaxWidth: 10,
Row: tw.CellConfig{
Formatting: tw.CellFormatting{Alignment: tw.AlignCenter},
},
Row: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}},
}),
)
table.Append([]string{s, s})
@@ -631,16 +620,12 @@ func main() {
// Main table
table := tablewriter.NewTable(os.Stdout,
tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{
Borders: tw.BorderNone,
Settings: tw.Settings{
Separators: tw.Separators{BetweenColumns: tw.On},
},
Borders: tw.BorderNone,
Settings: tw.Settings{Separators: tw.Separators{BetweenColumns: tw.On}},
})),
tablewriter.WithConfig(tablewriter.Config{
MaxWidth: 30,
Row: tw.CellConfig{
Formatting: tw.CellFormatting{Alignment: tw.AlignCenter},
},
Row: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}},
}),
)
table.Append([]string{createSubTable("A"), createSubTable("B")})
@@ -711,14 +696,11 @@ func main() {
tablewriter.WithStringer(employeeStringer),
tablewriter.WithConfig(tablewriter.Config{
Header: tw.CellConfig{
Formatting: tw.CellFormatting{Alignment: tw.AlignCenter, AutoFormat: tw.On},
},
Row: tw.CellConfig{
Formatting: tw.CellFormatting{Alignment: tw.AlignLeft},
},
Footer: tw.CellConfig{
Formatting: tw.CellFormatting{Alignment: tw.AlignRight},
Formatting: tw.CellFormatting{AutoFormat: tw.On},
Alignment: tw.CellAlignment{Global: tw.AlignCenter},
},
Row: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignLeft}},
Footer: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignRight}},
}),
)
table.Header([]string{"ID", "Name", "Age", "Department", "Salary"})
@@ -784,23 +766,17 @@ func main() {
}
table := tablewriter.NewTable(os.Stdout,
tablewriter.WithRenderer(renderer.NewHTML(os.Stdout, false, htmlCfg)),
tablewriter.WithRenderer(renderer.NewHTML(htmlCfg)),
tablewriter.WithConfig(tablewriter.Config{
Header: tw.CellConfig{
Formatting: tw.CellFormatting{
Alignment: tw.AlignCenter,
MergeMode: tw.MergeHorizontal, // Merge identical header cells
},
Formatting: tw.CellFormatting{MergeMode: tw.MergeHorizontal}, // Merge identical header cells
Alignment: tw.CellAlignment{Global: tw.AlignCenter},
},
Row: tw.CellConfig{
Formatting: tw.CellFormatting{
MergeMode: tw.MergeHorizontal, // Merge identical row cells
Alignment: tw.AlignLeft,
},
},
Footer: tw.CellConfig{
Formatting: tw.CellFormatting{Alignment: tw.AlignRight},
Formatting: tw.CellFormatting{MergeMode: tw.MergeHorizontal}, // Merge identical row cells
Alignment: tw.CellAlignment{Global: tw.AlignLeft},
},
Footer: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignRight}},
}),
)

View File

File diff suppressed because it is too large Load Diff

View File

@@ -2,9 +2,27 @@ package tablewriter
import "github.com/olekukonko/tablewriter/tw"
// Deprecated: WithBorders is no longer used.
// Border control has been moved to the renderer, which now manages its own borders.
// This Option has no effect on the Table and may be removed in future versions.
// WithBorders configures the table's border settings by updating the renderer's border configuration.
// This function is deprecated and will be removed in a future version.
//
// Deprecated: Use [WithRendition] to configure border settings for renderers that support
// [tw.Renditioning], or update the renderer's [tw.RenderConfig] directly via its Config() method.
// This function has no effect if no renderer is set on the table.
//
// Example migration:
//
// // Old (deprecated)
// table.Options(WithBorders(tw.Border{Top: true, Bottom: true}))
// // New (recommended)
// table.Options(WithRendition(tw.Rendition{Borders: tw.Border{Top: true, Bottom: true}}))
//
// Parameters:
// - borders: The [tw.Border] configuration to apply to the renderer's borders.
//
// Returns:
//
// An [Option] that updates the renderer's border settings if a renderer is set.
// Logs a debug message if debugging is enabled and a renderer is present.
func WithBorders(borders tw.Border) Option {
return func(target *Table) {
if target.renderer != nil {
@@ -17,16 +35,55 @@ func WithBorders(borders tw.Border) Option {
}
}
// Deprecated: WithBorders is no longer supported.
// Use [tw.Behavior] directly to configure border settings.
// Behavior is an alias for [tw.Behavior] to configure table behavior settings.
// This type is deprecated and will be removed in a future version.
//
// Deprecated: Use [tw.Behavior] directly to configure settings such as auto-hiding empty
// columns, trimming spaces, or controlling header/footer visibility.
//
// Example migration:
//
// // Old (deprecated)
// var b tablewriter.Behavior = tablewriter.Behavior{AutoHide: tw.On}
// // New (recommended)
// var b tw.Behavior = tw.Behavior{AutoHide: tw.On}
type Behavior tw.Behavior
// Deprecated: WithRendererSettings i sno longer supported.
// Settings is an alias for [tw.Settings] to configure renderer settings.
// This type is deprecated and will be removed in a future version.
//
// Deprecated: Use [tw.Settings] directly to configure renderer settings, such as
// separators and line styles.
//
// Example migration:
//
// // Old (deprecated)
// var s tablewriter.Settings = tablewriter.Settings{Separator: "|"}
// // New (recommended)
// var s tw.Settings = tw.Settings{Separator: "|"}
type Settings tw.Settings
// WithRendererSettings updates the renderer's settings (e.g., separators, lines).
// Render setting has move to renders directly
// you can also use WithRendition for renders that have rendition support
// WithRendererSettings updates the renderer's settings, such as separators and line styles.
// This function is deprecated and will be removed in a future version.
//
// Deprecated: Use [WithRendition] to update renderer settings for renderers that implement
// [tw.Renditioning], or configure the renderer's [tw.Settings] directly via its
// [tw.Renderer.Config] method. This function has no effect if no renderer is set.
//
// Example migration:
//
// // Old (deprecated)
// table.Options(WithRendererSettings(tw.Settings{Separator: "|"}))
// // New (recommended)
// table.Options(WithRendition(tw.Rendition{Settings: tw.Settings{Separator: "|"}}))
//
// Parameters:
// - settings: The [tw.Settings] configuration to apply to the renderer.
//
// Returns:
//
// An [Option] that updates the renderer's settings if a renderer is set.
// Logs a debug message if debugging is enabled and a renderer is present.
func WithRendererSettings(settings tw.Settings) Option {
return func(target *Table) {
if target.renderer != nil {
@@ -38,3 +95,124 @@ func WithRendererSettings(settings tw.Settings) Option {
}
}
}
// WithAlignment sets the text alignment for footer cells within the formatting configuration.
// This method is deprecated and will be removed in the next version.
//
// Deprecated: Use [FooterConfigBuilder.Alignment] with [AlignmentConfigBuilder.WithGlobal]
// or [AlignmentConfigBuilder.WithPerColumn] to configure footer alignments.
// Alternatively, apply a complete [tw.CellAlignment] configuration using
// [WithFooterAlignmentConfig].
//
// Example migration:
//
// // Old (deprecated)
// builder.Footer().Formatting().WithAlignment(tw.AlignRight)
// // New (recommended)
// builder.Footer().Alignment().WithGlobal(tw.AlignRight)
// // Or
// table.Options(WithFooterAlignmentConfig(tw.CellAlignment{Global: tw.AlignRight}))
//
// Parameters:
// - align: The [tw.Align] value to set for footer cells. Valid values are
// [tw.AlignLeft], [tw.AlignRight], [tw.AlignCenter], and [tw.AlignNone].
// Invalid alignments are ignored.
//
// Returns:
//
// The [FooterFormattingBuilder] instance for method chaining.
func (ff *FooterFormattingBuilder) WithAlignment(align tw.Align) *FooterFormattingBuilder {
if align != tw.AlignLeft && align != tw.AlignRight && align != tw.AlignCenter && align != tw.AlignNone {
return ff
}
ff.config.Alignment = align
return ff
}
// WithAlignment sets the text alignment for header cells within the formatting configuration.
// This method is deprecated and will be removed in the next version.
//
// Deprecated: Use [HeaderConfigBuilder.Alignment] with [AlignmentConfigBuilder.WithGlobal]
// or [AlignmentConfigBuilder.WithPerColumn] to configure header alignments.
// Alternatively, apply a complete [tw.CellAlignment] configuration using
// [WithHeaderAlignmentConfig].
//
// Example migration:
//
// // Old (deprecated)
// builder.Header().Formatting().WithAlignment(tw.AlignCenter)
// // New (recommended)
// builder.Header().Alignment().WithGlobal(tw.AlignCenter)
// // Or
// table.Options(WithHeaderAlignmentConfig(tw.CellAlignment{Global: tw.AlignCenter}))
//
// Parameters:
// - align: The [tw.Align] value to set for header cells. Valid values are
// [tw.AlignLeft], [tw.AlignRight], [tw.AlignCenter], and [tw.AlignNone].
// Invalid alignments are ignored.
//
// Returns:
//
// The [HeaderFormattingBuilder] instance for method chaining.
func (hf *HeaderFormattingBuilder) WithAlignment(align tw.Align) *HeaderFormattingBuilder {
if align != tw.AlignLeft && align != tw.AlignRight && align != tw.AlignCenter && align != tw.AlignNone {
return hf
}
hf.config.Alignment = align
return hf
}
// WithAlignment sets the text alignment for row cells within the formatting configuration.
// This method is deprecated and will be removed in the next version.
//
// Deprecated: Use [RowConfigBuilder.Alignment] with [AlignmentConfigBuilder.WithGlobal]
// or [AlignmentConfigBuilder.WithPerColumn] to configure row alignments.
// Alternatively, apply a complete [tw.CellAlignment] configuration using
// [WithRowAlignmentConfig].
//
// Example migration:
//
// // Old (deprecated)
// builder.Row().Formatting().WithAlignment(tw.AlignLeft)
// // New (recommended)
// builder.Row().Alignment().WithGlobal(tw.AlignLeft)
// // Or
// table.Options(WithRowAlignmentConfig(tw.CellAlignment{Global: tw.AlignLeft}))
//
// Parameters:
// - align: The [tw.Align] value to set for row cells. Valid values are
// [tw.AlignLeft], [tw.AlignRight], [tw.AlignCenter], and [tw.AlignNone].
// Invalid alignments are ignored.
//
// Returns:
//
// The [RowFormattingBuilder] instance for method chaining.
func (rf *RowFormattingBuilder) WithAlignment(align tw.Align) *RowFormattingBuilder {
if align != tw.AlignLeft && align != tw.AlignRight && align != tw.AlignCenter && align != tw.AlignNone {
return rf
}
rf.config.Alignment = align
return rf
}
// WithTableMax sets the maximum width of the entire table in characters.
// Negative values are ignored, and the change is logged if debugging is enabled.
// The width constrains the table's rendering, potentially causing text wrapping or truncation
// based on the configuration's wrapping settings (e.g., tw.WrapTruncate).
// If debug logging is enabled via WithDebug(true), the applied width is logged.
//
// Deprecated: Use WithMaxWidth instead, which provides the same functionality with a clearer name
// and consistent naming across the package. For example:
//
// tablewriter.NewTable(os.Stdout, tablewriter.WithMaxWidth(80))
func WithTableMax(width int) Option {
return func(target *Table) {
if width < 0 {
return
}
target.config.MaxWidth = width
if target.logger != nil {
target.logger.Debugf("Option: WithTableMax applied to Table: %v", width)
}
}
}

859
vendor/github.com/olekukonko/tablewriter/option.go generated vendored Normal file
View File

@@ -0,0 +1,859 @@
package tablewriter
import (
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/tw"
"reflect"
)
// Option defines a function type for configuring a Table instance.
type Option func(target *Table)
// WithAutoHide enables or disables automatic hiding of columns with empty data rows.
// Logs the change if debugging is enabled.
func WithAutoHide(state tw.State) Option {
return func(target *Table) {
target.config.Behavior.AutoHide = state
if target.logger != nil {
target.logger.Debugf("Option: WithAutoHide applied to Table: %v", state)
}
}
}
// WithColumnMax sets a global maximum column width for the table in streaming mode.
// Negative values are ignored, and the change is logged if debugging is enabled.
func WithColumnMax(width int) Option {
return func(target *Table) {
if width < 0 {
return
}
target.config.Widths.Global = width
if target.logger != nil {
target.logger.Debugf("Option: WithColumnMax applied to Table: %v", width)
}
}
}
// WithMaxWidth sets a global maximum table width for the table.
// Negative values are ignored, and the change is logged if debugging is enabled.
func WithMaxWidth(width int) Option {
return func(target *Table) {
if width < 0 {
return
}
target.config.MaxWidth = width
if target.logger != nil {
target.logger.Debugf("Option: WithTableMax applied to Table: %v", width)
}
}
}
// WithWidths sets per-column widths for the table.
// Negative widths are removed, and the change is logged if debugging is enabled.
func WithWidths(width tw.CellWidth) Option {
return func(target *Table) {
target.config.Widths = width
if target.logger != nil {
target.logger.Debugf("Option: WithColumnWidths applied to Table: %v", width)
}
}
}
// WithColumnWidths sets per-column widths for the table.
// Negative widths are removed, and the change is logged if debugging is enabled.
func WithColumnWidths(widths tw.Mapper[int, int]) Option {
return func(target *Table) {
for k, v := range widths {
if v < 0 {
delete(widths, k)
}
}
target.config.Widths.PerColumn = widths
if target.logger != nil {
target.logger.Debugf("Option: WithColumnWidths applied to Table: %v", widths)
}
}
}
// WithConfig applies a custom configuration to the table by merging it with the default configuration.
func WithConfig(cfg Config) Option {
return func(target *Table) {
target.config = mergeConfig(defaultConfig(), cfg)
}
}
// WithDebug enables or disables debug logging and adjusts the logger level accordingly.
// Logs the change if debugging is enabled.
func WithDebug(debug bool) Option {
return func(target *Table) {
target.config.Debug = debug
}
}
// WithFooter sets the table footers by calling the Footer method.
func WithFooter(footers []string) Option {
return func(target *Table) {
target.Footer(footers)
}
}
// WithFooterConfig applies a full footer configuration to the table.
// Logs the change if debugging is enabled.
func WithFooterConfig(config tw.CellConfig) Option {
return func(target *Table) {
target.config.Footer = config
if target.logger != nil {
target.logger.Debug("Option: WithFooterConfig applied to Table.")
}
}
}
// WithFooterAlignmentConfig applies a footer alignment configuration to the table.
// Logs the change if debugging is enabled.
func WithFooterAlignmentConfig(alignment tw.CellAlignment) Option {
return func(target *Table) {
target.config.Footer.Alignment = alignment
if target.logger != nil {
target.logger.Debugf("Option: WithFooterAlignmentConfig applied to Table: %+v", alignment)
}
}
}
// WithFooterMergeMode sets the merge mode for footer cells.
// Invalid merge modes are ignored, and the change is logged if debugging is enabled.
func WithFooterMergeMode(mergeMode int) Option {
return func(target *Table) {
if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical {
return
}
target.config.Footer.Formatting.MergeMode = mergeMode
if target.logger != nil {
target.logger.Debugf("Option: WithFooterMergeMode applied to Table: %v", mergeMode)
}
}
}
// WithFooterAutoWrap sets the wrapping behavior for footer cells.
// Invalid wrap modes are ignored, and the change is logged if debugging is enabled.
func WithFooterAutoWrap(wrap int) Option {
return func(target *Table) {
if wrap < tw.WrapNone || wrap > tw.WrapBreak {
return
}
target.config.Footer.Formatting.AutoWrap = wrap
if target.logger != nil {
target.logger.Debugf("Option: WithFooterAutoWrap applied to Table: %v", wrap)
}
}
}
// WithFooterFilter sets the filter configuration for footer cells.
// Logs the change if debugging is enabled.
func WithFooterFilter(filter tw.CellFilter) Option {
return func(target *Table) {
target.config.Footer.Filter = filter
if target.logger != nil {
target.logger.Debug("Option: WithFooterFilter applied to Table.")
}
}
}
// WithFooterCallbacks sets the callback configuration for footer cells.
// Logs the change if debugging is enabled.
func WithFooterCallbacks(callbacks tw.CellCallbacks) Option {
return func(target *Table) {
target.config.Footer.Callbacks = callbacks
if target.logger != nil {
target.logger.Debug("Option: WithFooterCallbacks applied to Table.")
}
}
}
// WithFooterPaddingPerColumn sets per-column padding for footer cells.
// Logs the change if debugging is enabled.
func WithFooterPaddingPerColumn(padding []tw.Padding) Option {
return func(target *Table) {
target.config.Footer.Padding.PerColumn = padding
if target.logger != nil {
target.logger.Debugf("Option: WithFooterPaddingPerColumn applied to Table: %+v", padding)
}
}
}
// WithFooterMaxWidth sets the maximum content width for footer cells.
// Negative values are ignored, and the change is logged if debugging is enabled.
func WithFooterMaxWidth(maxWidth int) Option {
return func(target *Table) {
if maxWidth < 0 {
return
}
target.config.Footer.ColMaxWidths.Global = maxWidth
if target.logger != nil {
target.logger.Debugf("Option: WithFooterMaxWidth applied to Table: %v", maxWidth)
}
}
}
// WithHeader sets the table headers by calling the Header method.
func WithHeader(headers []string) Option {
return func(target *Table) {
target.Header(headers)
}
}
// WithHeaderAlignment sets the text alignment for header cells.
// Invalid alignments are ignored, and the change is logged if debugging is enabled.
func WithHeaderAlignment(align tw.Align) Option {
return func(target *Table) {
if align != tw.AlignLeft && align != tw.AlignRight && align != tw.AlignCenter && align != tw.AlignNone {
return
}
target.config.Header.Alignment.Global = align
if target.logger != nil {
target.logger.Debugf("Option: WithHeaderAlignment applied to Table: %v", align)
}
}
}
// WithHeaderAutoWrap sets the wrapping behavior for header cells.
// Invalid wrap modes are ignored, and the change is logged if debugging is enabled.
func WithHeaderAutoWrap(wrap int) Option {
return func(target *Table) {
if wrap < tw.WrapNone || wrap > tw.WrapBreak {
return
}
target.config.Header.Formatting.AutoWrap = wrap
if target.logger != nil {
target.logger.Debugf("Option: WithHeaderAutoWrap applied to Table: %v", wrap)
}
}
}
// WithHeaderMergeMode sets the merge mode for header cells.
// Invalid merge modes are ignored, and the change is logged if debugging is enabled.
func WithHeaderMergeMode(mergeMode int) Option {
return func(target *Table) {
if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical {
return
}
target.config.Header.Formatting.MergeMode = mergeMode
if target.logger != nil {
target.logger.Debugf("Option: WithHeaderMergeMode applied to Table: %v", mergeMode)
}
}
}
// WithHeaderFilter sets the filter configuration for header cells.
// Logs the change if debugging is enabled.
func WithHeaderFilter(filter tw.CellFilter) Option {
return func(target *Table) {
target.config.Header.Filter = filter
if target.logger != nil {
target.logger.Debug("Option: WithHeaderFilter applied to Table.")
}
}
}
// WithHeaderCallbacks sets the callback configuration for header cells.
// Logs the change if debugging is enabled.
func WithHeaderCallbacks(callbacks tw.CellCallbacks) Option {
return func(target *Table) {
target.config.Header.Callbacks = callbacks
if target.logger != nil {
target.logger.Debug("Option: WithHeaderCallbacks applied to Table.")
}
}
}
// WithHeaderPaddingPerColumn sets per-column padding for header cells.
// Logs the change if debugging is enabled.
func WithHeaderPaddingPerColumn(padding []tw.Padding) Option {
return func(target *Table) {
target.config.Header.Padding.PerColumn = padding
if target.logger != nil {
target.logger.Debugf("Option: WithHeaderPaddingPerColumn applied to Table: %+v", padding)
}
}
}
// WithHeaderMaxWidth sets the maximum content width for header cells.
// Negative values are ignored, and the change is logged if debugging is enabled.
func WithHeaderMaxWidth(maxWidth int) Option {
return func(target *Table) {
if maxWidth < 0 {
return
}
target.config.Header.ColMaxWidths.Global = maxWidth
if target.logger != nil {
target.logger.Debugf("Option: WithHeaderMaxWidth applied to Table: %v", maxWidth)
}
}
}
// WithRowAlignment sets the text alignment for row cells.
// Invalid alignments are ignored, and the change is logged if debugging is enabled.
func WithRowAlignment(align tw.Align) Option {
return func(target *Table) {
if err := align.Validate(); err != nil {
return
}
target.config.Row.Alignment.Global = align
if target.logger != nil {
target.logger.Debugf("Option: WithRowAlignment applied to Table: %v", align)
}
}
}
// WithRowAutoWrap sets the wrapping behavior for row cells.
// Invalid wrap modes are ignored, and the change is logged if debugging is enabled.
func WithRowAutoWrap(wrap int) Option {
return func(target *Table) {
if wrap < tw.WrapNone || wrap > tw.WrapBreak {
return
}
target.config.Row.Formatting.AutoWrap = wrap
if target.logger != nil {
target.logger.Debugf("Option: WithRowAutoWrap applied to Table: %v", wrap)
}
}
}
// WithRowMergeMode sets the merge mode for row cells.
// Invalid merge modes are ignored, and the change is logged if debugging is enabled.
func WithRowMergeMode(mergeMode int) Option {
return func(target *Table) {
if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical {
return
}
target.config.Row.Formatting.MergeMode = mergeMode
if target.logger != nil {
target.logger.Debugf("Option: WithRowMergeMode applied to Table: %v", mergeMode)
}
}
}
// WithRowFilter sets the filter configuration for row cells.
// Logs the change if debugging is enabled.
func WithRowFilter(filter tw.CellFilter) Option {
return func(target *Table) {
target.config.Row.Filter = filter
if target.logger != nil {
target.logger.Debug("Option: WithRowFilter applied to Table.")
}
}
}
// WithRowCallbacks sets the callback configuration for row cells.
// Logs the change if debugging is enabled.
func WithRowCallbacks(callbacks tw.CellCallbacks) Option {
return func(target *Table) {
target.config.Row.Callbacks = callbacks
if target.logger != nil {
target.logger.Debug("Option: WithRowCallbacks applied to Table.")
}
}
}
// WithRowPaddingPerColumn sets per-column padding for row cells.
// Logs the change if debugging is enabled.
func WithRowPaddingPerColumn(padding []tw.Padding) Option {
return func(target *Table) {
target.config.Row.Padding.PerColumn = padding
if target.logger != nil {
target.logger.Debugf("Option: WithRowPaddingPerColumn applied to Table: %+v", padding)
}
}
}
// WithHeaderAlignmentConfig applies a header alignment configuration to the table.
// Logs the change if debugging is enabled.
func WithHeaderAlignmentConfig(alignment tw.CellAlignment) Option {
return func(target *Table) {
target.config.Header.Alignment = alignment
if target.logger != nil {
target.logger.Debugf("Option: WithHeaderAlignmentConfig applied to Table: %+v", alignment)
}
}
}
// WithHeaderConfig applies a full header configuration to the table.
// Logs the change if debugging is enabled.
func WithHeaderConfig(config tw.CellConfig) Option {
return func(target *Table) {
target.config.Header = config
if target.logger != nil {
target.logger.Debug("Option: WithHeaderConfig applied to Table.")
}
}
}
// WithLogger sets a custom logger for the table and updates the renderer if present.
// Logs the change if debugging is enabled.
func WithLogger(logger *ll.Logger) Option {
return func(target *Table) {
target.logger = logger
if target.logger != nil {
target.logger.Debug("Option: WithLogger applied to Table.")
if target.renderer != nil {
target.renderer.Logger(target.logger)
}
}
}
}
// WithRenderer sets a custom renderer for the table and attaches the logger if present.
// Logs the change if debugging is enabled.
func WithRenderer(f tw.Renderer) Option {
return func(target *Table) {
target.renderer = f
if target.logger != nil {
target.logger.Debugf("Option: WithRenderer applied to Table: %T", f)
f.Logger(target.logger)
}
}
}
// WithRowConfig applies a full row configuration to the table.
// Logs the change if debugging is enabled.
func WithRowConfig(config tw.CellConfig) Option {
return func(target *Table) {
target.config.Row = config
if target.logger != nil {
target.logger.Debug("Option: WithRowConfig applied to Table.")
}
}
}
// WithRowAlignmentConfig applies a row alignment configuration to the table.
// Logs the change if debugging is enabled.
func WithRowAlignmentConfig(alignment tw.CellAlignment) Option {
return func(target *Table) {
target.config.Row.Alignment = alignment
if target.logger != nil {
target.logger.Debugf("Option: WithRowAlignmentConfig applied to Table: %+v", alignment)
}
}
}
// WithRowMaxWidth sets the maximum content width for row cells.
// Negative values are ignored, and the change is logged if debugging is enabled.
func WithRowMaxWidth(maxWidth int) Option {
return func(target *Table) {
if maxWidth < 0 {
return
}
target.config.Row.ColMaxWidths.Global = maxWidth
if target.logger != nil {
target.logger.Debugf("Option: WithRowMaxWidth applied to Table: %v", maxWidth)
}
}
}
// WithStreaming applies a streaming configuration to the table by merging it with the existing configuration.
// Logs the change if debugging is enabled.
func WithStreaming(c tw.StreamConfig) Option {
return func(target *Table) {
target.config.Stream = mergeStreamConfig(target.config.Stream, c)
if target.logger != nil {
target.logger.Debug("Option: WithStreaming applied to Table.")
}
}
}
// WithStringer sets a custom stringer function for converting row data and clears the stringer cache.
// Logs the change if debugging is enabled.
func WithStringer(stringer interface{}) Option {
return func(t *Table) {
t.stringer = stringer
t.stringerCacheMu.Lock()
t.stringerCache = make(map[reflect.Type]reflect.Value)
t.stringerCacheMu.Unlock()
if t.logger != nil {
t.logger.Debug("Stringer updated, cache cleared")
}
}
}
// WithStringerCache enables caching for the stringer function.
// Logs the change if debugging is enabled.
func WithStringerCache() Option {
return func(t *Table) {
t.stringerCacheEnabled = true
if t.logger != nil {
t.logger.Debug("Option: WithStringerCache enabled")
}
}
}
// WithTrimSpace sets whether leading and trailing spaces are automatically trimmed.
// Logs the change if debugging is enabled.
func WithTrimSpace(state tw.State) Option {
return func(target *Table) {
target.config.Behavior.TrimSpace = state
if target.logger != nil {
target.logger.Debugf("Option: WithTrimSpace applied to Table: %v", state)
}
}
}
// WithHeaderAutoFormat enables or disables automatic formatting for header cells.
// Logs the change if debugging is enabled.
func WithHeaderAutoFormat(state tw.State) Option {
return func(target *Table) {
target.config.Header.Formatting.AutoFormat = state
if target.logger != nil {
target.logger.Debugf("Option: WithHeaderAutoFormat applied to Table: %v", state)
}
}
}
// WithFooterAutoFormat enables or disables automatic formatting for footer cells.
// Logs the change if debugging is enabled.
func WithFooterAutoFormat(state tw.State) Option {
return func(target *Table) {
target.config.Footer.Formatting.AutoFormat = state
if target.logger != nil {
target.logger.Debugf("Option: WithFooterAutoFormat applied to Table: %v", state)
}
}
}
// WithRowAutoFormat enables or disables automatic formatting for row cells.
// Logs the change if debugging is enabled.
func WithRowAutoFormat(state tw.State) Option {
return func(target *Table) {
target.config.Row.Formatting.AutoFormat = state
if target.logger != nil {
target.logger.Debugf("Option: WithRowAutoFormat applied to Table: %v", state)
}
}
}
// WithHeaderControl sets the control behavior for the table header.
// Logs the change if debugging is enabled.
func WithHeaderControl(control tw.Control) Option {
return func(target *Table) {
target.config.Behavior.Header = control
if target.logger != nil {
target.logger.Debugf("Option: WithHeaderControl applied to Table: %v", control)
}
}
}
// WithFooterControl sets the control behavior for the table footer.
// Logs the change if debugging is enabled.
func WithFooterControl(control tw.Control) Option {
return func(target *Table) {
target.config.Behavior.Footer = control
if target.logger != nil {
target.logger.Debugf("Option: WithFooterControl applied to Table: %v", control)
}
}
}
// WithAlignment sets the default column alignment for the header, rows, and footer.
// Logs the change if debugging is enabled.
func WithAlignment(alignment tw.Alignment) Option {
return func(target *Table) {
target.config.Header.Alignment.PerColumn = alignment
target.config.Row.Alignment.PerColumn = alignment
target.config.Footer.Alignment.PerColumn = alignment
if target.logger != nil {
target.logger.Debugf("Option: WithAlignment applied to Table: %+v", alignment)
}
}
}
// WithBehavior applies a behavior configuration to the table.
// Logs the change if debugging is enabled.
func WithBehavior(behavior tw.Behavior) Option {
return func(target *Table) {
target.config.Behavior = behavior
if target.logger != nil {
target.logger.Debugf("Option: WithBehavior applied to Table: %+v", behavior)
}
}
}
// WithPadding sets the global padding for the header, rows, and footer.
// Logs the change if debugging is enabled.
func WithPadding(padding tw.Padding) Option {
return func(target *Table) {
target.config.Header.Padding.Global = padding
target.config.Row.Padding.Global = padding
target.config.Footer.Padding.Global = padding
if target.logger != nil {
target.logger.Debugf("Option: WithPadding applied to Table: %+v", padding)
}
}
}
// WithRendition allows updating the active renderer's rendition configuration
// by merging the provided rendition.
// If the renderer does not implement tw.Renditioning, a warning is logged.
// Logs the change if debugging is enabled.
func WithRendition(rendition tw.Rendition) Option {
return func(target *Table) {
if target.renderer == nil {
if target.logger != nil {
target.logger.Warn("Option: WithRendition: No renderer set on table.")
}
return
}
if ru, ok := target.renderer.(tw.Renditioning); ok {
ru.Rendition(rendition)
if target.logger != nil {
target.logger.Debugf("Option: WithRendition: Applied to renderer via Renditioning.SetRendition(): %+v", rendition)
}
} else {
if target.logger != nil {
target.logger.Warnf("Option: WithRendition: Current renderer type %T does not implement tw.Renditioning. Rendition may not be applied as expected.", target.renderer)
}
}
}
}
// WithSymbols sets the symbols used for drawing table borders and separators.
// The symbols are applied to the table's renderer configuration, if a renderer is set.
// If no renderer is set (target.renderer is nil), this option has no effect. .
func WithSymbols(symbols tw.Symbols) Option {
return func(target *Table) {
if target.renderer != nil {
cfg := target.renderer.Config()
cfg.Symbols = symbols
if ru, ok := target.renderer.(tw.Renditioning); ok {
ru.Rendition(cfg)
if target.logger != nil {
target.logger.Debugf("Option: WithRendition: Applied to renderer via Renditioning.SetRendition(): %+v", cfg)
}
} else {
if target.logger != nil {
target.logger.Warnf("Option: WithRendition: Current renderer type %T does not implement tw.Renditioning. Rendition may not be applied as expected.", target.renderer)
}
}
}
}
}
// defaultConfig returns a default Config with sensible settings for headers, rows, footers, and behavior.
func defaultConfig() Config {
return Config{
MaxWidth: 0,
Header: tw.CellConfig{
Formatting: tw.CellFormatting{
AutoWrap: tw.WrapTruncate,
AutoFormat: tw.On,
MergeMode: tw.MergeNone,
},
Padding: tw.CellPadding{
Global: tw.PaddingDefault,
},
Alignment: tw.CellAlignment{
Global: tw.AlignCenter,
PerColumn: []tw.Align{},
},
},
Row: tw.CellConfig{
Formatting: tw.CellFormatting{
AutoWrap: tw.WrapNormal,
AutoFormat: tw.Off,
MergeMode: tw.MergeNone,
},
Padding: tw.CellPadding{
Global: tw.PaddingDefault,
},
Alignment: tw.CellAlignment{
Global: tw.AlignLeft,
PerColumn: []tw.Align{},
},
},
Footer: tw.CellConfig{
Formatting: tw.CellFormatting{
AutoWrap: tw.WrapNormal,
AutoFormat: tw.Off,
MergeMode: tw.MergeNone,
},
Padding: tw.CellPadding{
Global: tw.PaddingDefault,
},
Alignment: tw.CellAlignment{
Global: tw.AlignRight,
PerColumn: []tw.Align{},
},
},
Stream: tw.StreamConfig{},
Debug: false,
Behavior: tw.Behavior{
AutoHide: tw.Off,
TrimSpace: tw.On,
},
}
}
// mergeCellConfig merges a source CellConfig into a destination CellConfig, prioritizing non-default source values.
// It handles deep merging for complex fields like padding and callbacks.
func mergeCellConfig(dst, src tw.CellConfig) tw.CellConfig {
if src.Formatting.Alignment != tw.Empty {
dst.Formatting.Alignment = src.Formatting.Alignment
}
if src.Formatting.AutoWrap != 0 {
dst.Formatting.AutoWrap = src.Formatting.AutoWrap
}
if src.ColMaxWidths.Global != 0 {
dst.ColMaxWidths.Global = src.ColMaxWidths.Global
}
if src.Formatting.MergeMode != 0 {
dst.Formatting.MergeMode = src.Formatting.MergeMode
}
dst.Formatting.AutoFormat = src.Formatting.AutoFormat
if src.Padding.Global.Paddable() {
dst.Padding.Global = src.Padding.Global
}
if len(src.Padding.PerColumn) > 0 {
if dst.Padding.PerColumn == nil {
dst.Padding.PerColumn = make([]tw.Padding, len(src.Padding.PerColumn))
} else if len(src.Padding.PerColumn) > len(dst.Padding.PerColumn) {
dst.Padding.PerColumn = append(dst.Padding.PerColumn, make([]tw.Padding, len(src.Padding.PerColumn)-len(dst.Padding.PerColumn))...)
}
for i, pad := range src.Padding.PerColumn {
if pad.Paddable() {
dst.Padding.PerColumn[i] = pad
}
}
}
if src.Callbacks.Global != nil {
dst.Callbacks.Global = src.Callbacks.Global
}
if len(src.Callbacks.PerColumn) > 0 {
if dst.Callbacks.PerColumn == nil {
dst.Callbacks.PerColumn = make([]func(), len(src.Callbacks.PerColumn))
} else if len(src.Callbacks.PerColumn) > len(dst.Callbacks.PerColumn) {
dst.Callbacks.PerColumn = append(dst.Callbacks.PerColumn, make([]func(), len(src.Callbacks.PerColumn)-len(dst.Callbacks.PerColumn))...)
}
for i, cb := range src.Callbacks.PerColumn {
if cb != nil {
dst.Callbacks.PerColumn[i] = cb
}
}
}
if src.Filter.Global != nil {
dst.Filter.Global = src.Filter.Global
}
if len(src.Filter.PerColumn) > 0 {
if dst.Filter.PerColumn == nil {
dst.Filter.PerColumn = make([]func(string) string, len(src.Filter.PerColumn))
} else if len(src.Filter.PerColumn) > len(dst.Filter.PerColumn) {
dst.Filter.PerColumn = append(dst.Filter.PerColumn, make([]func(string) string, len(src.Filter.PerColumn)-len(dst.Filter.PerColumn))...)
}
for i, filter := range src.Filter.PerColumn {
if filter != nil {
dst.Filter.PerColumn[i] = filter
}
}
}
// Merge Alignment
if src.Alignment.Global != tw.Empty {
dst.Alignment.Global = src.Alignment.Global
}
if len(src.Alignment.PerColumn) > 0 {
if dst.Alignment.PerColumn == nil {
dst.Alignment.PerColumn = make([]tw.Align, len(src.Alignment.PerColumn))
} else if len(src.Alignment.PerColumn) > len(dst.Alignment.PerColumn) {
dst.Alignment.PerColumn = append(dst.Alignment.PerColumn, make([]tw.Align, len(src.Alignment.PerColumn)-len(dst.Alignment.PerColumn))...)
}
for i, align := range src.Alignment.PerColumn {
if align != tw.Skip {
dst.Alignment.PerColumn[i] = align
}
}
}
if len(src.ColumnAligns) > 0 {
if dst.ColumnAligns == nil {
dst.ColumnAligns = make([]tw.Align, len(src.ColumnAligns))
} else if len(src.ColumnAligns) > len(dst.ColumnAligns) {
dst.ColumnAligns = append(dst.ColumnAligns, make([]tw.Align, len(src.ColumnAligns)-len(dst.ColumnAligns))...)
}
for i, align := range src.ColumnAligns {
if align != tw.Skip {
dst.ColumnAligns[i] = align
}
}
}
if len(src.ColMaxWidths.PerColumn) > 0 {
if dst.ColMaxWidths.PerColumn == nil {
dst.ColMaxWidths.PerColumn = make(map[int]int)
}
for k, v := range src.ColMaxWidths.PerColumn {
if v != 0 {
dst.ColMaxWidths.PerColumn[k] = v
}
}
}
return dst
}
// mergeConfig merges a source Config into a destination Config, prioritizing non-default source values.
// It performs deep merging for complex types like Header, Row, Footer, and Stream.
func mergeConfig(dst, src Config) Config {
if src.MaxWidth != 0 {
dst.MaxWidth = src.MaxWidth
}
dst.Debug = src.Debug || dst.Debug
dst.Behavior.AutoHide = src.Behavior.AutoHide
dst.Behavior.TrimSpace = src.Behavior.TrimSpace
dst.Behavior.Compact = src.Behavior.Compact
dst.Behavior.Header = src.Behavior.Header
dst.Behavior.Footer = src.Behavior.Footer
if src.Widths.Global != 0 {
dst.Widths.Global = src.Widths.Global
}
if len(src.Widths.PerColumn) > 0 {
if dst.Widths.PerColumn == nil {
dst.Widths.PerColumn = make(map[int]int)
}
for k, v := range src.Widths.PerColumn {
if v != 0 {
dst.Widths.PerColumn[k] = v
}
}
}
dst.Header = mergeCellConfig(dst.Header, src.Header)
dst.Row = mergeCellConfig(dst.Row, src.Row)
dst.Footer = mergeCellConfig(dst.Footer, src.Footer)
dst.Stream = mergeStreamConfig(dst.Stream, src.Stream)
return dst
}
// mergeStreamConfig merges a source StreamConfig into a destination StreamConfig, prioritizing non-default source values.
func mergeStreamConfig(dst, src tw.StreamConfig) tw.StreamConfig {
if src.Enable {
dst.Enable = true
}
return dst
}
// padLine pads a line to the specified column count by appending empty strings as needed.
func padLine(line []string, numCols int) []string {
if len(line) >= numCols {
return line
}
padded := make([]string, numCols)
copy(padded, line)
for i := len(line); i < numCols; i++ {
padded[i] = tw.Empty
}
return padded
}

View File

@@ -140,10 +140,10 @@ func (m *Markdown) resolveAlignment(ctx tw.Formatting) tw.Alignment {
// build default alignment
for i := 0; i < total; i++ {
m.alignment = append(m.alignment, tw.AlignLeft)
m.alignment = append(m.alignment, tw.AlignNone) // Default to AlignNone
}
// add per colum alignment if it exits
// add per column alignment if it exists
for i := 0; i < total; i++ {
m.alignment[i] = ctx.Row.Current[i].Align
}
@@ -255,9 +255,10 @@ func (m *Markdown) formatSeparator(width int, align tw.Align) string {
sb.WriteRune(':')
sb.WriteString(strings.Repeat("-", targetWidth-2))
sb.WriteRune(':')
case tw.AlignNone:
sb.WriteString(strings.Repeat("-", targetWidth))
default:
sb.WriteRune(':')
sb.WriteString(strings.Repeat("-", targetWidth-1))
sb.WriteString(strings.Repeat("-", targetWidth)) // Fallback
}
result := sb.String()
@@ -321,12 +322,11 @@ func (m *Markdown) renderMarkdownLine(line []string, ctx tw.Formatting, isHeader
defaultPadding := tw.Padding{Left: tw.Space, Right: tw.Space}
if !ok {
cellCtx = tw.CellContext{
Data: tw.Empty, Align: align, Padding: defaultPadding,
Width: ctx.Row.Widths.Get(colIndex), Merge: tw.MergeState{},
}
} else if cellCtx.Padding == (tw.Padding{}) {
} else if !cellCtx.Padding.Paddable() {
cellCtx.Padding = defaultPadding
}
@@ -339,18 +339,6 @@ func (m *Markdown) renderMarkdownLine(line []string, ctx tw.Formatting, isHeader
// Calculate width and span
span := 1
if align == tw.AlignNone || align == tw.Empty {
if ctx.Row.Position == tw.Header && !isHeaderSep {
align = tw.AlignCenter
} else if ctx.Row.Position == tw.Footer {
align = tw.AlignRight
} else {
align = tw.AlignLeft
}
m.logger.Debugf("renderMarkdownLine: Col %d using default align '%s'", colIndex, align)
}
visualWidth := 0
isHMergeStart := ok && cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start
if isHMergeStart {
@@ -383,10 +371,11 @@ func (m *Markdown) renderMarkdownLine(line []string, ctx tw.Formatting, isHeader
var formattedSegment string
if isHeaderSep {
// Use header's alignment from ctx.Row.Previous
headerAlign := tw.AlignCenter // Default for headers
headerAlign := align
if headerCellCtx, headerOK := ctx.Row.Previous[colIndex]; headerOK {
headerAlign = headerCellCtx.Align
if headerAlign == tw.AlignNone || headerAlign == tw.Empty {
// Preserve tw.AlignNone for separator
if headerAlign != tw.AlignNone && (headerAlign == tw.Empty || headerAlign == tw.Skip) {
headerAlign = tw.AlignCenter
}
}
@@ -403,6 +392,16 @@ func (m *Markdown) renderMarkdownLine(line []string, ctx tw.Formatting, isHeader
rowAlign = headerCellCtx.Align
}
}
if rowAlign == tw.AlignNone || rowAlign == tw.Empty {
if ctx.Row.Position == tw.Header {
rowAlign = tw.AlignCenter
} else if ctx.Row.Position == tw.Footer {
rowAlign = tw.AlignRight
} else {
rowAlign = tw.AlignLeft
}
m.logger.Debugf("renderMarkdownLine: Col %d using default align '%s'", colIndex, rowAlign)
}
formattedSegment = m.formatCell(content, visualWidth, rowAlign, cellCtx.Padding)
}
output.WriteString(formattedSegment)

View File

@@ -340,7 +340,7 @@ func (o *Ocean) renderContentLine(ctx tw.Formatting, lineData []string) {
if cellCtx.Align.Validate() == nil && cellCtx.Align != tw.AlignNone {
align = cellCtx.Align
}
if cellCtx.Padding != (tw.Padding{}) {
if cellCtx.Padding.Paddable() {
padding = cellCtx.Padding
}
} else if colIdx < len(lineData) {

View File

@@ -113,10 +113,10 @@ func (t *Table) Start() error {
// Calculate initial fixed widths if provided in StreamConfig.Widths
// These widths will be used for all subsequent rendering in streaming mode.
if t.config.Stream.Widths.PerColumn != nil && t.config.Stream.Widths.PerColumn.Len() > 0 {
if t.config.Widths.PerColumn != nil && t.config.Widths.PerColumn.Len() > 0 {
// Use per-column stream widths if set
t.logger.Debugf("Using per-column stream widths from StreamConfig: %v", t.config.Stream.Widths.PerColumn)
t.streamWidths = t.config.Stream.Widths.PerColumn.Clone()
t.logger.Debugf("Using per-column stream widths from StreamConfig: %v", t.config.Widths.PerColumn)
t.streamWidths = t.config.Widths.PerColumn.Clone()
// Determine numCols from the highest index in PerColumn map
maxColIdx := -1
t.streamWidths.Each(func(col int, width int) {
@@ -139,14 +139,14 @@ func (t *Table) Start() error {
t.logger.Debugf("PerColumn widths map is effectively empty or contains only negative values, streamNumCols = 0.")
}
} else if t.config.Stream.Widths.Global > 0 {
} else if t.config.Widths.Global > 0 {
// Global width is set, but we don't know the number of columns yet.
// Defer applying global width until the first data (Header or first Row) arrives.
// Store a placeholder or flag indicating global width should be used.
// The simple way for now: Keep streamWidths empty, signal the global width preference.
// The width calculation function called later will need to check StreamConfig.Widths.Global
// if streamWidths is empty.
t.logger.Debugf("Global stream width %d set in StreamConfig. Will derive numCols from first data.", t.config.Stream.Widths.Global)
t.logger.Debugf("Global stream width %d set in StreamConfig. Will derive numCols from first data.", t.config.Widths.Global)
t.streamWidths = tw.NewMapper[int, int]() // Initialize as empty, will be populated later
// Note: No need to store Global width value here, it's available in t.config.Stream.Widths.Global
@@ -451,26 +451,26 @@ func (t *Table) streamBuildCellContexts(
// The paddingConfig should be the CellPadding config relevant to the sample data (Header/Row/Footer).
// Returns the determined number of columns.
// This function should only be called when t.streamWidths is currently empty.
func (t *Table) streamCalculateWidths(sampleDataLines []string, sectionConfigForSampleData tw.CellConfig) int {
func (t *Table) streamCalculateWidths(sampling []string, config tw.CellConfig) int {
if t.streamWidths != nil && t.streamWidths.Len() > 0 {
t.logger.Debug("streamCalculateWidths: Called when streaming widths are already set (%d columns). Reusing existing.", t.streamNumCols)
return t.streamNumCols
}
t.logger.Debug("streamCalculateWidths: Calculating streaming widths. Sample data cells: %d. Using section config: %+v", len(sampleDataLines), sectionConfigForSampleData.Formatting)
t.logger.Debug("streamCalculateWidths: Calculating streaming widths. Sample data cells: %d. Using section config: %+v", len(sampling), config.Formatting)
determinedNumCols := 0
if t.config.Stream.Widths.PerColumn != nil && t.config.Stream.Widths.PerColumn.Len() > 0 {
if t.config.Widths.PerColumn != nil && t.config.Widths.PerColumn.Len() > 0 {
maxColIdx := -1
t.config.Stream.Widths.PerColumn.Each(func(col int, width int) {
t.config.Widths.PerColumn.Each(func(col int, width int) {
if col > maxColIdx {
maxColIdx = col
}
})
determinedNumCols = maxColIdx + 1
t.logger.Debug("streamCalculateWidths: Determined numCols (%d) from StreamConfig.Widths.PerColumn", determinedNumCols)
} else if len(sampleDataLines) > 0 {
determinedNumCols = len(sampleDataLines)
} else if len(sampling) > 0 {
determinedNumCols = len(sampling)
t.logger.Debug("streamCalculateWidths: Determined numCols (%d) from sample data length", determinedNumCols)
} else {
t.logger.Debug("streamCalculateWidths: Cannot determine numCols (no PerColumn config, no sample data)")
@@ -482,14 +482,14 @@ func (t *Table) streamCalculateWidths(sampleDataLines []string, sectionConfigFor
t.streamNumCols = determinedNumCols
t.streamWidths = tw.NewMapper[int, int]()
// Use padding and autowrap from the provided sectionConfigForSampleData
paddingForWidthCalc := sectionConfigForSampleData.Padding
autoWrapForWidthCalc := sectionConfigForSampleData.Formatting.AutoWrap
// Use padding and autowrap from the provided config
paddingForWidthCalc := config.Padding
autoWrapForWidthCalc := config.Formatting.AutoWrap
if t.config.Stream.Widths.PerColumn != nil && t.config.Stream.Widths.PerColumn.Len() > 0 {
if t.config.Widths.PerColumn != nil && t.config.Widths.PerColumn.Len() > 0 {
t.logger.Debug("streamCalculateWidths: Using widths from StreamConfig.Widths.PerColumn")
for i := 0; i < t.streamNumCols; i++ {
width, ok := t.config.Stream.Widths.PerColumn.OK(i)
width, ok := t.config.Widths.PerColumn.OK(i)
if !ok {
width = 0
}
@@ -501,12 +501,12 @@ func (t *Table) streamCalculateWidths(sampleDataLines []string, sectionConfigFor
t.streamWidths.Set(i, width)
}
} else {
// No PerColumn config, derive from sampleDataLines intelligently
// No PerColumn config, derive from sampling intelligently
t.logger.Debug("streamCalculateWidths: Intelligently deriving widths from sample data content and padding.")
tempRequiredWidths := tw.NewMapper[int, int]() // Widths from updateWidths (content + padding)
if len(sampleDataLines) > 0 {
if len(sampling) > 0 {
// updateWidths calculates: DisplayWidth(content) + padLeft + padRight
t.updateWidths(sampleDataLines, tempRequiredWidths, paddingForWidthCalc)
t.updateWidths(sampling, tempRequiredWidths, paddingForWidthCalc)
}
ellipsisWidthBuffer := 0
@@ -522,13 +522,13 @@ func (t *Table) streamCalculateWidths(sampleDataLines []string, sectionConfigFor
// We need to deconstruct it to apply logic to content_width first.
sampleContent := ""
if i < len(sampleDataLines) {
sampleContent = t.Trimmer(sampleDataLines[i])
if i < len(sampling) {
sampleContent = t.Trimmer(sampling[i])
}
sampleContentDisplayWidth := tw.DisplayWidth(sampleContent)
colPad := paddingForWidthCalc.Global
if i < len(paddingForWidthCalc.PerColumn) && paddingForWidthCalc.PerColumn[i] != (tw.Padding{}) {
if i < len(paddingForWidthCalc.PerColumn) && paddingForWidthCalc.PerColumn[i].Paddable() {
colPad = paddingForWidthCalc.PerColumn[i]
}
currentPadLWidth := tw.DisplayWidth(colPad.Left)
@@ -584,8 +584,8 @@ func (t *Table) streamCalculateWidths(sampleDataLines []string, sectionConfigFor
}
// Apply Global Constraint (if t.config.Stream.Widths.Global > 0)
if t.config.Stream.Widths.Global > 0 && t.streamNumCols > 0 {
t.logger.Debug("streamCalculateWidths: Applying global stream width constraint %d", t.config.Stream.Widths.Global)
if t.config.Widths.Global > 0 && t.streamNumCols > 0 {
t.logger.Debug("streamCalculateWidths: Applying global stream width constraint %d", t.config.Widths.Global)
currentTotalColumnWidthsSum := 0
t.streamWidths.Each(func(_ int, w int) {
currentTotalColumnWidthsSum += w
@@ -606,11 +606,11 @@ func (t *Table) streamCalculateWidths(sampleDataLines []string, sectionConfigFor
totalWidthIncludingSeparators += (t.streamNumCols - 1) * separatorWidth
}
if t.config.Stream.Widths.Global < totalWidthIncludingSeparators && totalWidthIncludingSeparators > 0 { // Added check for total > 0
t.logger.Debug("streamCalculateWidths: Total calculated width (%d incl separators) exceeds global stream width (%d). Shrinking.", totalWidthIncludingSeparators, t.config.Stream.Widths.Global)
if t.config.Widths.Global < totalWidthIncludingSeparators && totalWidthIncludingSeparators > 0 { // Added check for total > 0
t.logger.Debug("streamCalculateWidths: Total calculated width (%d incl separators) exceeds global stream width (%d). Shrinking.", totalWidthIncludingSeparators, t.config.Widths.Global)
// Target sum for column widths only (global limit - total separator width)
targetSumForColumnWidths := t.config.Stream.Widths.Global
targetSumForColumnWidths := t.config.Widths.Global
if t.streamNumCols > 1 {
targetSumForColumnWidths -= (t.streamNumCols - 1) * separatorWidth
}
@@ -671,7 +671,7 @@ func (t *Table) streamCalculateWidths(sampleDataLines []string, sectionConfigFor
}
t.logger.Debug("streamCalculateWidths: Widths after scaling and distribution: %v", t.streamWidths)
} else {
t.logger.Debug("streamCalculateWidths: Total calculated width (%d) fits global stream width (%d). No scaling needed.", totalWidthIncludingSeparators, t.config.Stream.Widths.Global)
t.logger.Debug("streamCalculateWidths: Total calculated width (%d) fits global stream width (%d). No scaling needed.", totalWidthIncludingSeparators, t.config.Widths.Global)
}
}

View File

@@ -133,6 +133,16 @@ func NewTable(w io.Writer, opts ...Option) *Table {
return t
}
// NewWriter creates a new table with default settings for backward compatibility.
// It logs the creation if debugging is enabled.
func NewWriter(w io.Writer) *Table {
t := NewTable(w)
if t.logger != nil {
t.logger.Debug("NewWriter created buffered Table")
}
return t
}
// Caption sets the table caption (legacy method).
// Defaults to BottomCenter alignment, wrapping to table width.
// Use SetCaptionOptions for more control.
@@ -397,6 +407,11 @@ func (t *Table) Options(opts ...Option) *Table {
t.logger.Suspend()
}
// help resolve from deprecation
//if t.config.Stream.Enable {
// t.config.Widths = t.config.Stream.Widths
//}
// send logger to renderer
// this will overwrite the default logger
t.renderer.Logger(t.logger)
@@ -508,19 +523,40 @@ func (t *Table) appendSingle(row interface{}) error {
// Parameter config provides alignment settings for the section.
// Returns a map of column indices to alignment settings.
func (t *Table) buildAligns(config tw.CellConfig) map[int]tw.Align {
t.logger.Debugf("buildAligns INPUT: config.Formatting.Align=%s, config.ColumnAligns=%v", config.Formatting.Alignment, config.ColumnAligns)
// Start with global alignment, preferring deprecated Formatting.Alignment
effectiveGlobalAlign := config.Formatting.Alignment
if effectiveGlobalAlign == tw.Empty || effectiveGlobalAlign == tw.Skip {
effectiveGlobalAlign = config.Alignment.Global
if config.Formatting.Alignment != tw.Empty && config.Formatting.Alignment != tw.Skip {
t.logger.Warnf("Using deprecated CellFormatting.Alignment (%s). Migrate to CellConfig.Alignment.Global.", config.Formatting.Alignment)
}
}
// Use per-column alignments, preferring deprecated ColumnAligns
effectivePerColumn := config.ColumnAligns
if len(effectivePerColumn) == 0 && len(config.Alignment.PerColumn) > 0 {
effectivePerColumn = make([]tw.Align, len(config.Alignment.PerColumn))
copy(effectivePerColumn, config.Alignment.PerColumn)
if len(config.ColumnAligns) > 0 {
t.logger.Warnf("Using deprecated CellConfig.ColumnAligns (%v). Migrate to CellConfig.Alignment.PerColumn.", config.ColumnAligns)
}
}
// Log input for debugging
t.logger.Debugf("buildAligns INPUT: deprecated Formatting.Alignment=%s, deprecated ColumnAligns=%v, config.Alignment.Global=%s, config.Alignment.PerColumn=%v",
config.Formatting.Alignment, config.ColumnAligns, config.Alignment.Global, config.Alignment.PerColumn)
numColsToUse := t.getNumColsToUse()
colAlignsResult := make(map[int]tw.Align)
for i := 0; i < numColsToUse; i++ {
currentAlign := config.Formatting.Alignment
if i < len(config.ColumnAligns) {
colSpecificAlign := config.ColumnAligns[i]
if colSpecificAlign != tw.Empty && colSpecificAlign != tw.Skip {
currentAlign = colSpecificAlign
}
currentAlign := effectiveGlobalAlign
if i < len(effectivePerColumn) && effectivePerColumn[i] != tw.Empty && effectivePerColumn[i] != tw.Skip {
currentAlign = effectivePerColumn[i]
}
// Skip validation here; rely on rendering to handle invalid alignments
colAlignsResult[i] = currentAlign
}
t.logger.Debugf("Aligns built: %v (length %d)", colAlignsResult, len(colAlignsResult))
return colAlignsResult
}
@@ -532,7 +568,7 @@ func (t *Table) buildPadding(padding tw.CellPadding) map[int]tw.Padding {
numColsToUse := t.getNumColsToUse()
colPadding := make(map[int]tw.Padding)
for i := 0; i < numColsToUse; i++ {
if i < len(padding.PerColumn) && padding.PerColumn[i] != (tw.Padding{}) {
if i < len(padding.PerColumn) && padding.PerColumn[i].Paddable() {
colPadding[i] = padding.PerColumn[i]
} else {
colPadding[i] = padding.Global
@@ -699,8 +735,12 @@ func (t *Table) maxColumns() int {
return m
}
// printTopBottomCaption prints the table's caption at the specified top or bottom position.
// It wraps the caption text to fit the table width or a user-defined width, aligns it according
// to the specified alignment, and writes it to the provided writer. If the caption text is empty
// or the spot is invalid, it logs the issue and returns without printing. The function handles
// wrapping errors by falling back to splitting on newlines or using the original text.
func (t *Table) printTopBottomCaption(w io.Writer, actualTableWidth int) {
// Log the state of t.caption
t.logger.Debugf("[printCaption Entry] Text=%q, Spot=%v (type %T), Align=%q, UserWidth=%d, ActualTableWidth=%d",
t.caption.Text, t.caption.Spot, t.caption.Spot, t.caption.Align, t.caption.Width, actualTableWidth)
@@ -711,12 +751,11 @@ func (t *Table) printTopBottomCaption(w io.Writer, actualTableWidth int) {
return
}
// Determine captionWrapWidth
var captionWrapWidth int
if t.caption.Width > 0 {
captionWrapWidth = t.caption.Width
t.logger.Debugf("[printCaption] Using user-defined caption.Width %d for wrapping.", captionWrapWidth)
} else if actualTableWidth <= 4 { // Empty or minimal table
} else if actualTableWidth <= 4 {
captionWrapWidth = tw.DisplayWidth(t.caption.Text)
t.logger.Debugf("[printCaption] Empty table, no user caption.Width: Using natural caption width %d.", captionWrapWidth)
} else {
@@ -724,14 +763,12 @@ func (t *Table) printTopBottomCaption(w io.Writer, actualTableWidth int) {
t.logger.Debugf("[printCaption] Non-empty table, no user caption.Width: Using actualTableWidth %d for wrapping.", actualTableWidth)
}
// Ensure captionWrapWidth is positive
if captionWrapWidth <= 0 {
captionWrapWidth = 10 // Minimum sensible width
captionWrapWidth = 10
t.logger.Warnf("[printCaption] captionWrapWidth was %d (<=0). Setting to minimum %d.", captionWrapWidth, 10)
}
t.logger.Debugf("[printCaption] Final captionWrapWidth to be used by twwarp: %d", captionWrapWidth)
// Wrap the caption text
wrappedCaptionLines, count := twwarp.WrapString(t.caption.Text, captionWrapWidth)
if count == 0 {
t.logger.Errorf("[printCaption] Error from twwarp.WrapString (width %d): %v. Text: %q", captionWrapWidth, count, t.caption.Text)
@@ -754,7 +791,6 @@ func (t *Table) printTopBottomCaption(w io.Writer, actualTableWidth int) {
t.logger.Debugf("[printCaption] Wrapped caption into %d lines: %v", len(wrappedCaptionLines), wrappedCaptionLines)
}
// Determine padding target width
paddingTargetWidth := actualTableWidth
if t.caption.Width > 0 {
paddingTargetWidth = t.caption.Width
@@ -763,7 +799,6 @@ func (t *Table) printTopBottomCaption(w io.Writer, actualTableWidth int) {
}
t.logger.Debugf("[printCaption] Final paddingTargetWidth for tw.Pad: %d", paddingTargetWidth)
// Print each wrapped line
for i, line := range wrappedCaptionLines {
align := t.caption.Align
if align == "" || align == tw.AlignDefault || align == tw.AlignNone {
@@ -791,22 +826,11 @@ func (t *Table) printTopBottomCaption(w io.Writer, actualTableWidth int) {
// Parameters include cells to process and config for formatting rules.
// Returns a slice of string slices representing processed lines.
func (t *Table) prepareContent(cells []string, config tw.CellConfig) [][]string {
// force max width
isStreaming := t.config.Stream.Enable && t.hasPrinted
t.logger.Debugf("prepareContent: Processing cells=%v (streaming: %v)", cells, isStreaming)
initialInputCellCount := len(cells)
result := make([][]string, 0)
// ll.Dbg(t.config.MaxWidth)
// force max width
if t.config.MaxWidth > 0 {
// it has headers
if len(cells) > 0 {
config.ColMaxWidths.Global = int(math.Floor(float64(t.config.MaxWidth) / float64(len(cells))))
}
}
effectiveNumCols := initialInputCellCount
if isStreaming {
if t.streamNumCols > 0 {
@@ -830,6 +854,16 @@ func (t *Table) prepareContent(cells []string, config tw.CellConfig) [][]string
}
}
if t.config.MaxWidth > 0 && !t.config.Widths.Constrained() {
if effectiveNumCols > 0 {
derivedSectionGlobalMaxWidth := int(math.Floor(float64(t.config.MaxWidth) / float64(effectiveNumCols)))
config.ColMaxWidths.Global = derivedSectionGlobalMaxWidth
t.logger.Debugf("prepareContent: Table MaxWidth %d active and t.config.Widths not constrained. "+
"Derived section ColMaxWidths.Global: %d for %d columns. This will be used by calculateContentMaxWidth if no higher priority constraints exist.",
t.config.MaxWidth, config.ColMaxWidths.Global, effectiveNumCols)
}
}
for i := 0; i < effectiveNumCols; i++ {
cellContent := ""
if i < len(cells) {
@@ -841,7 +875,7 @@ func (t *Table) prepareContent(cells []string, config tw.CellConfig) [][]string
cellContent = t.Trimmer(cellContent)
colPad := config.Padding.Global
if i < len(config.Padding.PerColumn) && config.Padding.PerColumn[i] != (tw.Padding{}) {
if i < len(config.Padding.PerColumn) && config.Padding.PerColumn[i].Paddable() {
colPad = config.Padding.PerColumn[i]
}
@@ -881,50 +915,45 @@ func (t *Table) prepareContent(cells []string, config tw.CellConfig) [][]string
case tw.WrapBreak:
wrapped := make([]string, 0)
currentLine := line
breakCharWidth := tw.DisplayWidth(tw.CharBreak)
for tw.DisplayWidth(currentLine) > effectiveContentMaxWidth {
breakPoint := tw.BreakPoint(currentLine, effectiveContentMaxWidth)
if breakPoint <= 0 {
t.logger.Warnf("prepareContent: WrapBreak - BreakPoint <= 0 for line '%s' at width %d. Attempting manual break.", currentLine, effectiveContentMaxWidth)
runes := []rune(currentLine)
targetWidth := effectiveContentMaxWidth - breakCharWidth
if targetWidth < 0 {
targetWidth = 0
}
breakPoint := tw.BreakPoint(currentLine, targetWidth)
runes := []rune(currentLine)
if breakPoint <= 0 || breakPoint > len(runes) {
t.logger.Warnf("prepareContent: WrapBreak - Invalid BreakPoint %d for line '%s' at width %d. Attempting manual break.", breakPoint, currentLine, targetWidth)
actualBreakRuneCount := 0
tempWidth := 0
for charIdx, r := range currentLine {
for charIdx, r := range runes {
runeStr := string(r)
rw := tw.DisplayWidth(runeStr)
if tempWidth+rw > effectiveContentMaxWidth && charIdx > 0 {
if tempWidth+rw > targetWidth && charIdx > 0 {
break
}
tempWidth += rw
actualBreakRuneCount = charIdx + 1
if tempWidth >= effectiveContentMaxWidth && charIdx == 0 {
if tempWidth >= targetWidth && charIdx == 0 {
break
}
}
if actualBreakRuneCount == 0 && len(runes) > 0 {
actualBreakRuneCount = 1
}
if actualBreakRuneCount > 0 && actualBreakRuneCount <= len(runes) {
wrapped = append(wrapped, string(runes[:actualBreakRuneCount])+tw.CharBreak)
currentLine = string(runes[actualBreakRuneCount:])
} else {
if tw.DisplayWidth(currentLine) > 0 {
wrapped = append(wrapped, currentLine)
currentLine = ""
}
break
}
} else {
runes := []rune(currentLine)
if breakPoint <= len(runes) {
wrapped = append(wrapped, string(runes[:breakPoint])+tw.CharBreak)
currentLine = string(runes[breakPoint:])
} else {
t.logger.Warnf("prepareContent: WrapBreak - BreakPoint (%d) out of bounds for line runes (%d). Adding full line.", breakPoint, len(runes))
t.logger.Warnf("prepareContent: WrapBreak - Cannot break line '%s'. Adding as is.", currentLine)
wrapped = append(wrapped, currentLine)
currentLine = ""
break
}
} else {
wrapped = append(wrapped, string(runes[:breakPoint])+tw.CharBreak)
currentLine = string(runes[breakPoint:])
}
}
if tw.DisplayWidth(currentLine) > 0 {
@@ -959,7 +988,8 @@ func (t *Table) prepareContent(cells []string, config tw.CellConfig) [][]string
if i < len(result[j]) {
result[j][i] = cellLineContent
} else {
t.logger.Warnf("prepareContent: Column index %d out of bounds (%d) during result matrix population.", i, len(result[j]))
t.logger.Warnf("prepareContent: Column index %d out of bounds (%d) during result matrix population. EffectiveNumCols: %d. This indicates a logic error.",
i, len(result[j]), effectiveNumCols)
}
}
}
@@ -1304,6 +1334,7 @@ func (t *Table) prepareWithMerges(content [][]string, config tw.CellConfig, posi
// No parameters are required.
// Returns an error if rendering fails in any section.
func (t *Table) render() error {
t.ensureInitialized()
if t.config.Stream.Enable {
@@ -1339,9 +1370,8 @@ func (t *Table) render() error {
t.logger.Debugf("No caption detected. Rendering table core directly to writer.")
}
//Render Table Core ---
t.writer = targetWriter // Set writer only when necessary
//Render Table Core
t.writer = targetWriter
ctx, mctx, err := t.prepareContexts()
if err != nil {
t.writer = originalWriter
@@ -1387,7 +1417,6 @@ func (t *Table) render() error {
if renderError {
return firstRenderErr // Return error from core rendering if any
}
//End Render Table Core ---
//Caption Handling & Final Output ---
if isTopOrBottomCaption {
@@ -1877,17 +1906,54 @@ func (t *Table) renderHeader(ctx *renderContext, mctx *mergeContext) error {
if cfg.Settings.Lines.ShowHeaderLine.Enabled() && (len(ctx.rowLines) > 0 || len(ctx.footerLines) > 0) {
ctx.logger.Debug("Rendering header separator line")
hctx.rowIdx = 0
hctx.lineIdx = len(ctx.headerLines) - 1
hctx.line = padLine(ctx.headerLines[hctx.lineIdx], ctx.numCols)
hctx.location = tw.LocationMiddle
resp := t.buildCellContexts(ctx, mctx, hctx, colAligns, colPadding)
var nextSectionCells map[int]tw.CellContext
var nextSectionWidths tw.Mapper[int, int]
if len(ctx.rowLines) > 0 {
nextSectionWidths = ctx.widths[tw.Row]
rowColAligns := t.buildAligns(t.config.Row)
rowColPadding := t.buildPadding(t.config.Row.Padding)
firstRowHctx := &helperContext{
position: tw.Row,
rowIdx: 0,
lineIdx: 0,
}
if len(ctx.rowLines[0]) > 0 {
firstRowHctx.line = padLine(ctx.rowLines[0][0], ctx.numCols)
} else {
firstRowHctx.line = make([]string, ctx.numCols)
}
firstRowResp := t.buildCellContexts(ctx, mctx, firstRowHctx, rowColAligns, rowColPadding)
nextSectionCells = firstRowResp.cells
} else if len(ctx.footerLines) > 0 {
nextSectionWidths = ctx.widths[tw.Row]
footerColAligns := t.buildAligns(t.config.Footer)
footerColPadding := t.buildPadding(t.config.Footer.Padding)
firstFooterHctx := &helperContext{
position: tw.Footer,
rowIdx: 0,
lineIdx: 0,
}
if len(ctx.footerLines) > 0 {
firstFooterHctx.line = padLine(ctx.footerLines[0], ctx.numCols)
} else {
firstFooterHctx.line = make([]string, ctx.numCols)
}
firstFooterResp := t.buildCellContexts(ctx, mctx, firstFooterHctx, footerColAligns, footerColPadding)
nextSectionCells = firstFooterResp.cells
} else {
nextSectionWidths = ctx.widths[tw.Header]
nextSectionCells = nil
}
f.Line(tw.Formatting{
Row: tw.RowContext{
Widths: ctx.widths[tw.Header],
Widths: nextSectionWidths,
Current: resp.cells,
Previous: resp.prevCells,
Next: resp.nextCells,
Next: nextSectionCells,
Position: tw.Header,
Location: tw.LocationMiddle,
},

View File

@@ -2,13 +2,17 @@ package tw
// CellFormatting holds formatting options for table cells.
type CellFormatting struct {
Alignment Align // Text alignment within the cell (e.g., Left, Right, Center)
AutoWrap int // Wrapping behavior (e.g., WrapTruncate, WrapNormal)
MergeMode int // Bitmask for merge behavior (e.g., MergeHorizontal, MergeVertical)
AutoWrap int // Wrapping behavior (e.g., WrapTruncate, WrapNormal)
MergeMode int // Bitmask for merge behavior (e.g., MergeHorizontal, MergeVertical)
// Changed form bool to State
// See https://github.com/olekukonko/tablewriter/issues/261
AutoFormat State // Enables automatic formatting (e.g., title case for headers)
// Deprecated: kept for compatibility
// will be removed soon
Alignment Align // Text alignment within the cell (e.g., Left, Right, Center)
}
// CellPadding defines padding settings for table cells.
@@ -30,17 +34,31 @@ type CellCallbacks struct {
PerColumn []func() // Column-specific callbacks
}
// CellAlignment defines alignment settings for table cells.
type CellAlignment struct {
Global Align // Default alignment applied to all cells
PerColumn []Align // Column-specific alignment overrides
}
// CellConfig combines formatting, padding, and callback settings for a table section.
type CellConfig struct {
Formatting CellFormatting // Cell formatting options
Padding CellPadding // Padding configuration
Callbacks CellCallbacks // Callback functions (unused)
Filter CellFilter // Function to filter cell content (renamed from Filter Filter)
ColumnAligns []Align // Per-column alignment overrides
Alignment CellAlignment // Alignment configuration for cells
ColMaxWidths CellWidth // Per-column maximum width overrides
// Deprecated: use Alignment.PerColumn instead. Will be removed in a future version.
// will be removed soon
ColumnAligns []Align // Per-column alignment overrides
}
type CellWidth struct {
Global int
PerColumn Mapper[int, int]
}
func (c CellWidth) Constrained() bool {
return c.Global > 0 || c.PerColumn.Len() > 0
}

View File

@@ -245,7 +245,7 @@ func IsNumeric(s string) bool {
return err == nil
}
// SplitCamelCase splits a camelCase or PascalCase string into separate words.
// SplitCamelCase splits a camelCase or PascalCase or snake_case string into separate words.
// It detects transitions between uppercase, lowercase, digits, and other characters.
func SplitCamelCase(src string) (entries []string) {
// Validate UTF-8 input; return as single entry if invalid
@@ -284,10 +284,11 @@ func SplitCamelCase(src string) (entries []string) {
runes[i] = runes[i][:len(runes[i])-1]
}
}
// Convert rune groups to strings, excluding empty or whitespace-only groups
// Convert rune groups to strings, excluding empty, underscore or whitespace-only groups
for _, s := range runes {
if len(s) > 0 && strings.TrimSpace(string(s)) != "" {
entries = append(entries, string(s))
str := string(s)
if len(s) > 0 && strings.TrimSpace(str) != "" && str != "_" {
entries = append(entries, str)
}
}
return

18
vendor/github.com/olekukonko/tablewriter/tw/preset.go generated vendored Normal file
View File

@@ -0,0 +1,18 @@
package tw
// BorderNone defines a border configuration with all sides disabled.
var (
// PaddingNone represents explicitly empty padding (no spacing on any side)
// Equivalent to Padding{Overwrite: true}
PaddingNone = Padding{Left: Empty, Right: Empty, Top: Empty, Bottom: Empty, Overwrite: true}
BorderNone = Border{Left: Off, Right: Off, Top: Off, Bottom: Off}
LinesNone = Lines{ShowTop: Off, ShowBottom: Off, ShowHeaderLine: Off, ShowFooterLine: Off}
SeparatorsNone = Separators{ShowHeader: Off, ShowFooter: Off, BetweenRows: Off, BetweenColumns: Off}
)
var (
// PaddingDefault represents standard single-space padding on left/right
// Equivalent to Padding{Left: " ", Right: " ", Overwrite: true}
PaddingDefault = Padding{Left: " ", Right: " ", Overwrite: true}
)

View File

@@ -108,21 +108,18 @@ type Settings struct {
// Border defines the visibility states of table borders.
type Border struct {
Left State // Left border visibility
Right State // Right border visibility
Top State // Top border visibility
Bottom State // Bottom border visibility
Left State // Left border visibility
Right State // Right border visibility
Top State // Top border visibility
Bottom State // Bottom border visibility
Overwrite bool
}
// BorderNone defines a border configuration with all sides disabled.
var (
PaddingNone = Padding{Left: Empty, Right: Empty, Top: Empty, Bottom: Empty}
BorderNone = Border{Left: Off, Right: Off, Top: Off, Bottom: Off}
LinesNone = Lines{ShowTop: Off, ShowBottom: Off, ShowHeaderLine: Off, ShowFooterLine: Off}
SeparatorsNone = Separators{ShowHeader: Off, ShowFooter: Off, BetweenRows: Off, BetweenColumns: Off}
)
type StreamConfig struct {
Enable bool
Widths CellWidth // Cell/column widths
// Deprecated: Use top-level Config.Widths for streaming width control.
// This field will be removed in a future version. It will be respected if
// Config.Widths is not set and this field is.
Widths CellWidth
}

View File

@@ -15,6 +15,8 @@ const (
Skip = ""
Space = " "
NewLine = "\n"
Column = ":"
Dash = "-"
)
// Feature State Constants

View File

@@ -132,22 +132,68 @@ func (c Caption) WithWidth(width int) Caption {
return c
}
// Padding defines custom padding characters for a cell
type Control struct {
Hide State
}
// Compact configures compact width optimization for merged cells.
type Compact struct {
Merge State // Merge enables compact width calculation during cell merging, optimizing space allocation.
}
// Behavior defines settings that control table rendering behaviors, such as column visibility and content formatting.
type Behavior struct {
AutoHide State // AutoHide determines whether empty columns are hidden. Ignored in streaming mode.
TrimSpace State // TrimSpace enables trimming of leading and trailing spaces from cell content.
Header Control // Header specifies control settings for the table header.
Footer Control // Footer specifies control settings for the table footer.
// Compact enables optimized width calculation for merged cells, such as in horizontal merges,
// by systematically determining the most efficient width instead of scaling by the number of columns.
Compact Compact
}
// Padding defines the spacing characters around cell content in all four directions.
// A zero-value Padding struct will use the table's default padding unless Overwrite is true.
type Padding struct {
Left string
Right string
Top string
Bottom string
// Overwrite forces tablewriter to use this padding configuration exactly as specified,
// even when empty. When false (default), empty Padding fields will inherit defaults.
//
// For explicit no-padding, use the PaddingNone constant instead of setting Overwrite.
Overwrite bool
}
type Control struct {
Hide State
// Common padding configurations for convenience
// Equals reports whether two Padding configurations are identical in all fields.
// This includes comparing the Overwrite flag as part of the equality check.
func (p Padding) Equals(padding Padding) bool {
return p.Left == padding.Left &&
p.Right == padding.Right &&
p.Top == padding.Top &&
p.Bottom == padding.Bottom &&
p.Overwrite == padding.Overwrite
}
// Behavior defines table behavior settings that control features like auto-hiding columns and trimming spaces.
type Behavior struct {
AutoHide State // Controls whether empty columns are automatically hidden (ignored in streaming mode)
TrimSpace State // Controls whether leading/trailing spaces are trimmed from cell content
Header Control
Footer Control
// Empty reports whether all padding strings are empty (all fields == "").
// Note that an Empty padding may still take effect if Overwrite is true.
func (p Padding) Empty() bool {
return p.Left == "" && p.Right == "" && p.Top == "" && p.Bottom == ""
}
// Paddable reports whether this Padding configuration should override existing padding.
// Returns true if either:
// - Any padding string is non-empty (!p.Empty())
// - Overwrite flag is true (even with all strings empty)
//
// This is used internally during configuration merging to determine whether to
// apply the padding settings.
func (p Padding) Paddable() bool {
return !p.Empty() || p.Overwrite
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/olekukonko/errors"
"github.com/olekukonko/tablewriter/tw"
"io"
"math"
"reflect"
"strconv"
"strings"
@@ -576,26 +577,23 @@ func (t *Table) buildPaddingLineContents(padChar string, widths tw.Mapper[int, i
// Parameter ctx holds rendering state with width maps.
// Returns an error if width calculation fails.
func (t *Table) calculateAndNormalizeWidths(ctx *renderContext) error {
ctx.logger.Debugf("calculateAndNormalizeWidths: Computing and normalizing widths for %d columns", ctx.numCols)
ctx.logger.Debugf("calculateAndNormalizeWidths: Computing and normalizing widths for %d columns. Compact: %v",
ctx.numCols, t.config.Behavior.Compact.Merge.Enabled())
// Initialize width maps
t.headerWidths = tw.NewMapper[int, int]()
t.rowWidths = tw.NewMapper[int, int]()
t.footerWidths = tw.NewMapper[int, int]()
//t.headerWidths = tw.NewMapper[int, int]()
//t.rowWidths = tw.NewMapper[int, int]()
//t.footerWidths = tw.NewMapper[int, int]()
// Compute header widths
// Compute content-based widths for each section
for _, lines := range ctx.headerLines {
t.updateWidths(lines, t.headerWidths, t.config.Header.Padding)
}
ctx.logger.Debugf("Initial Header widths: %v", t.headerWidths)
// Cache row widths to avoid re-iteration
rowWidthCache := make([]tw.Mapper[int, int], len(ctx.rowLines))
for i, row := range ctx.rowLines {
rowWidthCache[i] = tw.NewMapper[int, int]()
for _, line := range row {
t.updateWidths(line, rowWidthCache[i], t.config.Row.Padding)
// Aggregate into t.rowWidths
for col, width := range rowWidthCache[i] {
currentMax, _ := t.rowWidths.OK(col)
if width > currentMax {
@@ -604,41 +602,348 @@ func (t *Table) calculateAndNormalizeWidths(ctx *renderContext) error {
}
}
}
ctx.logger.Debugf("Initial Row widths: %v", t.rowWidths)
// Compute footer widths
for _, lines := range ctx.footerLines {
t.updateWidths(lines, t.footerWidths, t.config.Footer.Padding)
}
ctx.logger.Debugf("Initial Footer widths: %v", t.footerWidths)
ctx.logger.Debugf("Content-based widths: header=%v, row=%v, footer=%v", t.headerWidths, t.rowWidths, t.footerWidths)
// Initialize width maps for normalization
ctx.widths[tw.Header] = tw.NewMapper[int, int]()
ctx.widths[tw.Row] = tw.NewMapper[int, int]()
ctx.widths[tw.Footer] = tw.NewMapper[int, int]()
// Normalize widths by taking the maximum across sections
for i := 0; i < ctx.numCols; i++ {
maxWidth := 0
for _, w := range []tw.Mapper[int, int]{t.headerWidths, t.rowWidths, t.footerWidths} {
if wd := w.Get(i); wd > maxWidth {
maxWidth = wd
// Analyze header merges for optimization
var headerMergeSpans map[int]int
if t.config.Header.Formatting.MergeMode&tw.MergeHorizontal != 0 && len(ctx.headerLines) > 0 {
headerMergeSpans = make(map[int]int)
visitedCols := make(map[int]bool)
firstHeaderLine := ctx.headerLines[0]
if len(firstHeaderLine) > 0 {
for i := 0; i < len(firstHeaderLine); {
if visitedCols[i] {
i++
continue
}
var currentLogicalCellContentBuilder strings.Builder
for _, hLine := range ctx.headerLines {
if i < len(hLine) {
currentLogicalCellContentBuilder.WriteString(hLine[i])
}
}
currentHeaderCellContent := t.Trimmer(currentLogicalCellContentBuilder.String())
span := 1
for j := i + 1; j < len(firstHeaderLine); j++ {
var nextLogicalCellContentBuilder strings.Builder
for _, hLine := range ctx.headerLines {
if j < len(hLine) {
nextLogicalCellContentBuilder.WriteString(hLine[j])
}
}
nextHeaderCellContent := t.Trimmer(nextLogicalCellContentBuilder.String())
if currentHeaderCellContent == nextHeaderCellContent && currentHeaderCellContent != "" && currentHeaderCellContent != "-" {
span++
} else {
break
}
}
if span > 1 {
headerMergeSpans[i] = span
for k := 0; k < span; k++ {
visitedCols[i+k] = true
}
}
i += span
}
}
ctx.widths[tw.Header].Set(i, maxWidth)
ctx.widths[tw.Row].Set(i, maxWidth)
ctx.widths[tw.Footer].Set(i, maxWidth)
if len(headerMergeSpans) > 0 {
ctx.logger.Debugf("Header merge spans: %v", headerMergeSpans)
}
}
ctx.logger.Debugf("Normalized widths: header=%v, row=%v, footer=%v", ctx.widths[tw.Header], ctx.widths[tw.Row], ctx.widths[tw.Footer])
// Determine natural column widths
naturalColumnWidths := tw.NewMapper[int, int]()
for i := 0; i < ctx.numCols; i++ {
width := 0
if colWidth, ok := t.config.Widths.PerColumn.OK(i); ok && colWidth >= 0 {
width = colWidth
ctx.logger.Debugf("Col %d width from Config.Widths.PerColumn: %d", i, width)
} else {
maxRowFooterWidth := tw.Max(t.rowWidths.Get(i), t.footerWidths.Get(i))
headerCellOriginalWidth := t.headerWidths.Get(i)
if t.config.Behavior.Compact.Merge.Enabled() &&
t.config.Header.Formatting.MergeMode&tw.MergeHorizontal != 0 &&
headerMergeSpans != nil {
isColInHeaderMerge := false
for startCol, span := range headerMergeSpans {
if i >= startCol && i < startCol+span {
isColInHeaderMerge = true
break
}
}
if isColInHeaderMerge {
width = maxRowFooterWidth
if width == 0 && headerCellOriginalWidth > 0 {
width = headerCellOriginalWidth
}
ctx.logger.Debugf("Col %d (in merge) width: %d (row/footer: %d, header: %d)", i, width, maxRowFooterWidth, headerCellOriginalWidth)
} else {
width = tw.Max(headerCellOriginalWidth, maxRowFooterWidth)
ctx.logger.Debugf("Col %d (not in merge) width: %d", i, width)
}
} else {
width = tw.Max(tw.Max(headerCellOriginalWidth, t.rowWidths.Get(i)), t.footerWidths.Get(i))
ctx.logger.Debugf("Col %d width (no merge): %d", i, width)
}
if width == 0 && (headerCellOriginalWidth > 0 || t.rowWidths.Get(i) > 0 || t.footerWidths.Get(i) > 0) {
width = tw.Max(tw.Max(headerCellOriginalWidth, t.rowWidths.Get(i)), t.footerWidths.Get(i))
}
if width == 0 {
width = 1
}
}
naturalColumnWidths.Set(i, width)
}
ctx.logger.Debugf("Natural column widths: %v", naturalColumnWidths)
// Expand columns for merged header content if needed
workingWidths := naturalColumnWidths.Clone()
if t.config.Header.Formatting.MergeMode&tw.MergeHorizontal != 0 && headerMergeSpans != nil {
if span, isOneBigMerge := headerMergeSpans[0]; isOneBigMerge && span == ctx.numCols && ctx.numCols > 0 {
var firstHeaderCellLogicalContentBuilder strings.Builder
for _, hLine := range ctx.headerLines {
if 0 < len(hLine) {
firstHeaderCellLogicalContentBuilder.WriteString(hLine[0])
}
}
mergedContentString := t.Trimmer(firstHeaderCellLogicalContentBuilder.String())
headerCellPadding := t.config.Header.Padding.Global
if 0 < len(t.config.Header.Padding.PerColumn) && t.config.Header.Padding.PerColumn[0].Paddable() {
headerCellPadding = t.config.Header.Padding.PerColumn[0]
}
actualMergedHeaderContentPhysicalWidth := tw.DisplayWidth(mergedContentString) +
tw.DisplayWidth(headerCellPadding.Left) +
tw.DisplayWidth(headerCellPadding.Right)
currentSumOfColumnWidths := 0
workingWidths.Each(func(_ int, w int) { currentSumOfColumnWidths += w })
numSeparatorsInFullSpan := 0
if ctx.numCols > 1 {
if t.renderer != nil && t.renderer.Config().Settings.Separators.BetweenColumns.Enabled() {
numSeparatorsInFullSpan = (ctx.numCols - 1) * tw.DisplayWidth(t.renderer.Config().Symbols.Column())
}
}
totalCurrentSpanPhysicalWidth := currentSumOfColumnWidths + numSeparatorsInFullSpan
if actualMergedHeaderContentPhysicalWidth > totalCurrentSpanPhysicalWidth {
ctx.logger.Debugf("Merged header content '%s' (width %d) exceeds total width %d. Expanding.",
mergedContentString, actualMergedHeaderContentPhysicalWidth, totalCurrentSpanPhysicalWidth)
shortfall := actualMergedHeaderContentPhysicalWidth - totalCurrentSpanPhysicalWidth
numNonZeroCols := 0
workingWidths.Each(func(_ int, w int) {
if w > 0 {
numNonZeroCols++
}
})
if numNonZeroCols == 0 && ctx.numCols > 0 {
numNonZeroCols = ctx.numCols
}
if numNonZeroCols > 0 && shortfall > 0 {
extraPerColumn := int(math.Ceil(float64(shortfall) / float64(numNonZeroCols)))
finalSumAfterExpansion := 0
workingWidths.Each(func(colIdx int, currentW int) {
if currentW > 0 || (numNonZeroCols == ctx.numCols && ctx.numCols > 0) {
newWidth := currentW + extraPerColumn
workingWidths.Set(colIdx, newWidth)
finalSumAfterExpansion += newWidth
ctx.logger.Debugf("Col %d expanded by %d to %d", colIdx, extraPerColumn, newWidth)
} else {
finalSumAfterExpansion += currentW
}
})
overDistributed := (finalSumAfterExpansion + numSeparatorsInFullSpan) - actualMergedHeaderContentPhysicalWidth
if overDistributed > 0 {
ctx.logger.Debugf("Correcting over-distribution of %d", overDistributed)
// Sort columns for deterministic reduction
sortedCols := workingWidths.SortedKeys()
for i := 0; i < overDistributed; i++ {
// Reduce from highest-indexed column
for j := len(sortedCols) - 1; j >= 0; j-- {
col := sortedCols[j]
if workingWidths.Get(col) > 1 && naturalColumnWidths.Get(col) < workingWidths.Get(col) {
workingWidths.Set(col, workingWidths.Get(col)-1)
ctx.logger.Debugf("Reduced col %d by 1 to %d", col, workingWidths.Get(col))
break
}
}
}
}
}
}
}
}
ctx.logger.Debugf("Widths after merged header expansion: %v", workingWidths)
// Apply global width constraint
finalWidths := workingWidths.Clone()
if t.config.Widths.Global > 0 {
ctx.logger.Debugf("Applying global width constraint: %d", t.config.Widths.Global)
currentSumOfFinalColWidths := 0
finalWidths.Each(func(_ int, w int) { currentSumOfFinalColWidths += w })
numSeparators := 0
if ctx.numCols > 1 && t.renderer != nil && t.renderer.Config().Settings.Separators.BetweenColumns.Enabled() {
numSeparators = (ctx.numCols - 1) * tw.DisplayWidth(t.renderer.Config().Symbols.Column())
}
totalCurrentTablePhysicalWidth := currentSumOfFinalColWidths + numSeparators
if totalCurrentTablePhysicalWidth > t.config.Widths.Global {
ctx.logger.Debugf("Table width %d exceeds global limit %d. Shrinking.", totalCurrentTablePhysicalWidth, t.config.Widths.Global)
targetTotalColumnContentWidth := t.config.Widths.Global - numSeparators
if targetTotalColumnContentWidth < 0 {
targetTotalColumnContentWidth = 0
}
if ctx.numCols > 0 && targetTotalColumnContentWidth < ctx.numCols {
targetTotalColumnContentWidth = ctx.numCols
}
hardMinimums := tw.NewMapper[int, int]()
sumOfHardMinimums := 0
isHeaderContentHardToWrap := !(t.config.Header.Formatting.AutoWrap == tw.WrapNormal || t.config.Header.Formatting.AutoWrap == tw.WrapBreak)
for i := 0; i < ctx.numCols; i++ {
minW := 1
if isHeaderContentHardToWrap && len(ctx.headerLines) > 0 {
headerColNaturalWidthWithPadding := t.headerWidths.Get(i)
if headerColNaturalWidthWithPadding > minW {
minW = headerColNaturalWidthWithPadding
}
}
hardMinimums.Set(i, minW)
sumOfHardMinimums += minW
}
ctx.logger.Debugf("Hard minimums: %v (sum: %d)", hardMinimums, sumOfHardMinimums)
if targetTotalColumnContentWidth < sumOfHardMinimums && sumOfHardMinimums > 0 {
ctx.logger.Warnf("Target width %d below minimums %d. Scaling.", targetTotalColumnContentWidth, sumOfHardMinimums)
scaleFactorMin := float64(targetTotalColumnContentWidth) / float64(sumOfHardMinimums)
if scaleFactorMin < 0 {
scaleFactorMin = 0
}
tempSum := 0
scaledHardMinimums := tw.NewMapper[int, int]()
hardMinimums.Each(func(colIdx int, currentMinW int) {
scaledMinW := int(math.Round(float64(currentMinW) * scaleFactorMin))
if scaledMinW < 1 && targetTotalColumnContentWidth > 0 {
scaledMinW = 1
} else if scaledMinW < 0 {
scaledMinW = 0
}
scaledHardMinimums.Set(colIdx, scaledMinW)
tempSum += scaledMinW
})
errorDiffMin := targetTotalColumnContentWidth - tempSum
if errorDiffMin != 0 && scaledHardMinimums.Len() > 0 {
sortedKeys := scaledHardMinimums.SortedKeys()
for i := 0; i < int(math.Abs(float64(errorDiffMin))); i++ {
keyToAdjust := sortedKeys[i%len(sortedKeys)]
val := scaledHardMinimums.Get(keyToAdjust)
adj := 1
if errorDiffMin < 0 {
adj = -1
}
if val+adj >= 1 || (val+adj == 0 && targetTotalColumnContentWidth == 0) {
scaledHardMinimums.Set(keyToAdjust, val+adj)
} else if adj > 0 {
scaledHardMinimums.Set(keyToAdjust, val+adj)
}
}
}
finalWidths = scaledHardMinimums.Clone()
ctx.logger.Debugf("Scaled minimums: %v", finalWidths)
} else {
finalWidths = hardMinimums.Clone()
widthAllocatedByMinimums := sumOfHardMinimums
remainingWidthToDistribute := targetTotalColumnContentWidth - widthAllocatedByMinimums
ctx.logger.Debugf("Target: %d, minimums: %d, remaining: %d", targetTotalColumnContentWidth, widthAllocatedByMinimums, remainingWidthToDistribute)
if remainingWidthToDistribute > 0 {
sumOfFlexiblePotentialBase := 0
flexibleColsOriginalWidths := tw.NewMapper[int, int]()
for i := 0; i < ctx.numCols; i++ {
naturalW := workingWidths.Get(i)
minW := hardMinimums.Get(i)
if naturalW > minW {
sumOfFlexiblePotentialBase += (naturalW - minW)
flexibleColsOriginalWidths.Set(i, naturalW)
}
}
ctx.logger.Debugf("Flexible potential: %d, flexible widths: %v", sumOfFlexiblePotentialBase, flexibleColsOriginalWidths)
if sumOfFlexiblePotentialBase > 0 {
distributedExtraSum := 0
sortedFlexKeys := flexibleColsOriginalWidths.SortedKeys()
for _, colIdx := range sortedFlexKeys {
naturalWOfCol := flexibleColsOriginalWidths.Get(colIdx)
hardMinOfCol := hardMinimums.Get(colIdx)
flexiblePartOfCol := naturalWOfCol - hardMinOfCol
proportion := 0.0
if sumOfFlexiblePotentialBase > 0 {
proportion = float64(flexiblePartOfCol) / float64(sumOfFlexiblePotentialBase)
} else if len(sortedFlexKeys) > 0 {
proportion = 1.0 / float64(len(sortedFlexKeys))
}
extraForThisCol := int(math.Round(float64(remainingWidthToDistribute) * proportion))
currentAssignedW := finalWidths.Get(colIdx)
finalWidths.Set(colIdx, currentAssignedW+extraForThisCol)
distributedExtraSum += extraForThisCol
}
errorInDist := remainingWidthToDistribute - distributedExtraSum
ctx.logger.Debugf("Distributed %d, error: %d", distributedExtraSum, errorInDist)
if errorInDist != 0 && len(sortedFlexKeys) > 0 {
for i := 0; i < int(math.Abs(float64(errorInDist))); i++ {
colToAdjust := sortedFlexKeys[i%len(sortedFlexKeys)]
w := finalWidths.Get(colToAdjust)
adj := 1
if errorInDist < 0 {
adj = -1
}
if !(adj < 0 && w+adj < hardMinimums.Get(colToAdjust)) {
finalWidths.Set(colToAdjust, w+adj)
} else if adj > 0 {
finalWidths.Set(colToAdjust, w+adj)
}
}
}
} else {
if ctx.numCols > 0 {
extraPerCol := remainingWidthToDistribute / ctx.numCols
rem := remainingWidthToDistribute % ctx.numCols
for i := 0; i < ctx.numCols; i++ {
currentW := finalWidths.Get(i)
add := extraPerCol
if i < rem {
add++
}
finalWidths.Set(i, currentW+add)
}
}
}
}
}
finalSumCheck := 0
finalWidths.Each(func(idx int, w int) {
if w < 1 && targetTotalColumnContentWidth > 0 {
finalWidths.Set(idx, 1)
} else if w < 0 {
finalWidths.Set(idx, 0)
}
finalSumCheck += finalWidths.Get(idx)
})
ctx.logger.Debugf("Final widths after scaling: %v (sum: %d, target: %d)", finalWidths, finalSumCheck, targetTotalColumnContentWidth)
}
}
// Assign final widths to context
ctx.widths[tw.Header] = finalWidths.Clone()
ctx.widths[tw.Row] = finalWidths.Clone()
ctx.widths[tw.Footer] = finalWidths.Clone()
ctx.logger.Debugf("Final normalized widths: header=%v, row=%v, footer=%v", ctx.widths[tw.Header], ctx.widths[tw.Row], ctx.widths[tw.Footer])
return nil
}
// calculateContentMaxWidth computes the maximum content width for a column, accounting for padding and mode-specific constraints.
// Returns the effective content width (after subtracting padding) for the given column index.
func (t *Table) calculateContentMaxWidth(colIdx int, config tw.CellConfig, padLeftWidth, padRightWidth int, isStreaming bool) int {
var effectiveContentMaxWidth int
if isStreaming {
// Existing streaming logic remains unchanged
totalColumnWidthFromStream := t.streamWidths.Get(colIdx)
if totalColumnWidthFromStream < 0 {
totalColumnWidthFromStream = 0
@@ -652,28 +957,57 @@ func (t *Table) calculateContentMaxWidth(colIdx int, config tw.CellConfig, padLe
if totalColumnWidthFromStream == 0 {
effectiveContentMaxWidth = 0
}
t.logger.Debugf("calculateContentMaxWidth: Streaming col %d, TotalColWd=%d, PadL=%d, PadR=%d -> ContentMaxWd=%d",
colIdx, totalColumnWidthFromStream, padLeftWidth, padRightWidth, effectiveContentMaxWidth)
t.logger.Debugf("calculateContentMaxWidth: Streaming col %d, TotalColWd=%d, PadL=%d, PadR=%d -> ContentMaxWd=%d", colIdx, totalColumnWidthFromStream, padLeftWidth, padRightWidth, effectiveContentMaxWidth)
} else {
hasConstraint := false
// New priority-based width constraint checking
constraintTotalCellWidth := 0
if config.ColMaxWidths.PerColumn != nil {
hasConstraint := false
// 1. Check new Widths.PerColumn (highest priority)
if t.config.Widths.Constrained() {
if colWidth, ok := t.config.Widths.PerColumn.OK(colIdx); ok && colWidth > 0 {
constraintTotalCellWidth = colWidth
hasConstraint = true
t.logger.Debugf("calculateContentMaxWidth: Using Widths.PerColumn[%d] = %d",
colIdx, constraintTotalCellWidth)
}
// 2. Check new Widths.Global
if !hasConstraint && t.config.Widths.Global > 0 {
constraintTotalCellWidth = t.config.Widths.Global
hasConstraint = true
t.logger.Debugf("calculateContentMaxWidth: Using Widths.Global = %d", constraintTotalCellWidth)
}
}
// 3. Fall back to legacy ColMaxWidths.PerColumn (backward compatibility)
if !hasConstraint && config.ColMaxWidths.PerColumn != nil {
if colMax, ok := config.ColMaxWidths.PerColumn.OK(colIdx); ok && colMax > 0 {
constraintTotalCellWidth = colMax
hasConstraint = true
t.logger.Debugf("calculateContentMaxWidth: Batch col %d using config.ColMaxWidths.PerColumn (as total cell width constraint): %d", colIdx, constraintTotalCellWidth)
t.logger.Debugf("calculateContentMaxWidth: Using legacy ColMaxWidths.PerColumn[%d] = %d",
colIdx, constraintTotalCellWidth)
}
}
// 4. Fall back to legacy ColMaxWidths.Global
if !hasConstraint && config.ColMaxWidths.Global > 0 {
constraintTotalCellWidth = config.ColMaxWidths.Global
hasConstraint = true
t.logger.Debugf("calculateContentMaxWidth: Batch col %d using config.Formatting.MaxWidth (as total cell width constraint): %d", colIdx, constraintTotalCellWidth)
t.logger.Debugf("calculateContentMaxWidth: Using legacy ColMaxWidths.Global = %d",
constraintTotalCellWidth)
}
// 5. Fall back to table MaxWidth if auto-wrapping
if !hasConstraint && t.config.MaxWidth > 0 && config.Formatting.AutoWrap != tw.WrapNone {
constraintTotalCellWidth = t.config.MaxWidth
hasConstraint = true
t.logger.Debugf("calculateContentMaxWidth: Batch col %d using t.config.MaxWidth (as total cell width constraint, due to AutoWrap != WrapNone): %d", colIdx, constraintTotalCellWidth)
t.logger.Debugf("calculateContentMaxWidth: Using table MaxWidth = %d (AutoWrap enabled)",
constraintTotalCellWidth)
}
// Calculate effective width based on found constraint
if hasConstraint {
effectiveContentMaxWidth = constraintTotalCellWidth - padLeftWidth - padRightWidth
if effectiveContentMaxWidth < 1 && constraintTotalCellWidth > (padLeftWidth+padRightWidth) {
@@ -681,13 +1015,14 @@ func (t *Table) calculateContentMaxWidth(colIdx int, config tw.CellConfig, padLe
} else if effectiveContentMaxWidth < 0 {
effectiveContentMaxWidth = 0
}
t.logger.Debugf("calculateContentMaxWidth: Batch col %d, ConstraintTotalCellWidth=%d, PadL=%d, PadR=%d -> EffectiveContentMaxWidth=%d",
colIdx, constraintTotalCellWidth, padLeftWidth, padRightWidth, effectiveContentMaxWidth)
t.logger.Debugf("calculateContentMaxWidth: ConstraintTotalCellWidth=%d, PadL=%d, PadR=%d -> EffectiveContentMaxWidth=%d",
constraintTotalCellWidth, padLeftWidth, padRightWidth, effectiveContentMaxWidth)
} else {
effectiveContentMaxWidth = 0
t.logger.Debugf("calculateContentMaxWidth: Batch col %d, No applicable MaxWidth constraint. EffectiveContentMaxWidth set to 0 (unlimited for this stage).", colIdx)
t.logger.Debugf("calculateContentMaxWidth: No width constraints found for column %d", colIdx)
}
}
return effectiveContentMaxWidth
}
@@ -698,6 +1033,8 @@ func (t *Table) convertToStringer(input interface{}) ([]string, error) {
return nil, errors.New("internal error: convertToStringer called with nil t.stringer")
}
t.logger.Debugf("convertToString attempt %v using %v", input, t.stringer)
inputType := reflect.TypeOf(input)
stringerFuncVal := reflect.ValueOf(t.stringer)
stringerFuncType := stringerFuncVal.Type()
@@ -1310,7 +1647,8 @@ func (t *Table) updateWidths(row []string, widths tw.Mapper[int, int], padding t
t.logger.Debugf("Updating widths for row: %v", row)
for i, cell := range row {
colPad := padding.Global
if i < len(padding.PerColumn) && padding.PerColumn[i] != (tw.Padding{}) {
if i < len(padding.PerColumn) && padding.PerColumn[i].Paddable() {
colPad = padding.PerColumn[i]
t.logger.Debugf(" Col %d: Using per-column padding: L:'%s' R:'%s'", i, colPad.Left, colPad.Right)
} else {

4
vendor/modules.txt vendored
View File

@@ -1034,12 +1034,12 @@ github.com/oklog/run
# github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6
## explicit; go 1.21
github.com/olekukonko/errors
# github.com/olekukonko/ll v0.0.8-0.20250516010636-22ea57d81985
# github.com/olekukonko/ll v0.0.8
## explicit; go 1.21
github.com/olekukonko/ll
github.com/olekukonko/ll/lh
github.com/olekukonko/ll/lx
# github.com/olekukonko/tablewriter v1.0.6
# github.com/olekukonko/tablewriter v1.0.7
## explicit; go 1.21
github.com/olekukonko/tablewriter
github.com/olekukonko/tablewriter/pkg/twwarp