diff --git a/go.mod b/go.mod index 1bdff0b3..131b91d3 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/iancoleman/strcase v0.3.0 github.com/launchdarkly/json-patch v0.0.0-20180720210516-dd68d883319f github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 - github.com/olekukonko/tablewriter v1.0.7 + github.com/olekukonko/tablewriter v1.0.8 github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.6 diff --git a/go.sum b/go.sum index cf061672..6a493c2d 100644 --- a/go.sum +++ b/go.sum @@ -183,8 +183,8 @@ github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hS github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= 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 v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw= -github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs= +github.com/olekukonko/tablewriter v1.0.8 h1:f6wJzHg4QUtJdvrVPKco4QTrAylgaU0+b9br/lJxEiQ= +github.com/olekukonko/tablewriter v1.0.8/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= diff --git a/vendor/github.com/olekukonko/tablewriter/MIGRATION.md b/vendor/github.com/olekukonko/tablewriter/MIGRATION.md index 6500cd32..650a195b 100644 --- a/vendor/github.com/olekukonko/tablewriter/MIGRATION.md +++ b/vendor/github.com/olekukonko/tablewriter/MIGRATION.md @@ -416,6 +416,244 @@ func main() { ``` + +#### Custom Invoice Renderer + +```go + +package main + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/olekukonko/ll" + "github.com/olekukonko/tablewriter" + "github.com/olekukonko/tablewriter/tw" +) + +// InvoiceRenderer implements tw.Renderer for a basic invoice style. +type InvoiceRenderer struct { + writer io.Writer + logger *ll.Logger + rendition tw.Rendition +} + +func NewInvoiceRenderer() *InvoiceRenderer { + rendition := tw.Rendition{ + Borders: tw.BorderNone, + Symbols: tw.NewSymbols(tw.StyleNone), + Settings: tw.Settings{Separators: tw.SeparatorsNone, Lines: tw.LinesNone}, + Streaming: false, + } + defaultLogger := ll.New("simple-invoice-renderer") + return &InvoiceRenderer{logger: defaultLogger, rendition: rendition} +} + +func (r *InvoiceRenderer) Logger(logger *ll.Logger) { + if logger != nil { + r.logger = logger + } +} + +func (r *InvoiceRenderer) Config() tw.Rendition { + return r.rendition +} + +func (r *InvoiceRenderer) Start(w io.Writer) error { + r.writer = w + r.logger.Debug("InvoiceRenderer: Start") + return nil +} + +func (r *InvoiceRenderer) formatLine(cells []string, widths tw.Mapper[int, int], cellContexts map[int]tw.CellContext) string { + var sb strings.Builder + numCols := 0 + if widths != nil { // Ensure widths is not nil before calling Len + numCols = widths.Len() + } + + for i := 0; i < numCols; i++ { + data := "" + if i < len(cells) { + data = cells[i] + } + + width := 0 + if widths != nil { // Check again before Get + width = widths.Get(i) + } + + align := tw.AlignDefault + if cellContexts != nil { // Check cellContexts + if ctx, ok := cellContexts[i]; ok { + align = ctx.Align + } + } + + paddedCell := tw.Pad(data, " ", width, align) + sb.WriteString(paddedCell) + + if i < numCols-1 { + sb.WriteString(" ") // Column separator + } + } + return sb.String() +} + +func (r *InvoiceRenderer) Header(headers [][]string, ctx tw.Formatting) { + if r.writer == nil { + return + } + r.logger.Debugf("InvoiceRenderer: Header (lines: %d)", len(headers)) + + for _, headerLineCells := range headers { + lineStr := r.formatLine(headerLineCells, ctx.Row.Widths, ctx.Row.Current) + fmt.Fprintln(r.writer, lineStr) + } + + if len(headers) > 0 { + totalWidth := 0 + if ctx.Row.Widths != nil { + ctx.Row.Widths.Each(func(_ int, w int) { totalWidth += w }) + if ctx.Row.Widths.Len() > 1 { + totalWidth += (ctx.Row.Widths.Len() - 1) * 3 // Separator spaces + } + } + if totalWidth > 0 { + fmt.Fprintln(r.writer, strings.Repeat("-", totalWidth)) + } + } +} + +func (r *InvoiceRenderer) Row(row []string, ctx tw.Formatting) { + if r.writer == nil { + return + } + r.logger.Debug("InvoiceRenderer: Row") + lineStr := r.formatLine(row, ctx.Row.Widths, ctx.Row.Current) + fmt.Fprintln(r.writer, lineStr) +} + +func (r *InvoiceRenderer) Footer(footers [][]string, ctx tw.Formatting) { + if r.writer == nil { + return + } + r.logger.Debugf("InvoiceRenderer: Footer (lines: %d)", len(footers)) + + if len(footers) > 0 { + totalWidth := 0 + if ctx.Row.Widths != nil { + ctx.Row.Widths.Each(func(_ int, w int) { totalWidth += w }) + if ctx.Row.Widths.Len() > 1 { + totalWidth += (ctx.Row.Widths.Len() - 1) * 3 + } + } + if totalWidth > 0 { + fmt.Fprintln(r.writer, strings.Repeat("-", totalWidth)) + } + } + + for _, footerLineCells := range footers { + lineStr := r.formatLine(footerLineCells, ctx.Row.Widths, ctx.Row.Current) + fmt.Fprintln(r.writer, lineStr) + } +} + +func (r *InvoiceRenderer) Line(ctx tw.Formatting) { + r.logger.Debug("InvoiceRenderer: Line (no-op)") + // This simple renderer draws its own lines in Header/Footer. +} + +func (r *InvoiceRenderer) Close() error { + r.logger.Debug("InvoiceRenderer: Close") + r.writer = nil + return nil +} + +func main() { + data := [][]string{ + {"Product A", "2", "10.00", "20.00"}, + {"Super Long Product Name B", "1", "125.50", "125.50"}, + {"Item C", "10", "1.99", "19.90"}, + } + + header := []string{"Description", "Qty", "Unit Price", "Total Price"} + footer := []string{"", "", "Subtotal:\nTax (10%):\nGRAND TOTAL:", "165.40\n16.54\n181.94"} + invoiceRenderer := NewInvoiceRenderer() + + // Create table and set custom renderer using Options + table := tablewriter.NewTable(os.Stdout, + tablewriter.WithRenderer(invoiceRenderer), + tablewriter.WithAlignment([]tw.Align{ + tw.AlignLeft, tw.AlignCenter, tw.AlignRight, tw.AlignRight, + }), + ) + + table.Header(header) + for _, v := range data { + table.Append(v) + } + + // Use the Footer method with strings containing newlines for multi-line cells + table.Footer(footer) + + fmt.Println("Rendering with InvoiceRenderer:") + table.Render() + + // For comparison, render with default Blueprint renderer + // Re-create the table or reset it to use a different renderer + table2 := tablewriter.NewTable(os.Stdout, + tablewriter.WithAlignment([]tw.Align{ + tw.AlignLeft, tw.AlignCenter, tw.AlignRight, tw.AlignRight, + }), + ) + + table2.Header(header) + for _, v := range data { + table2.Append(v) + } + table2.Footer(footer) + fmt.Println("\nRendering with Default Blueprint Renderer (for comparison):") + table2.Render() +} + +``` + + +``` +Rendering with InvoiceRenderer: +DESCRIPTION QTY UNIT PRICE TOTAL PRICE +-------------------------------------------------------------------- +Product A 2 10.00 20.00 +Super Long Product Name B 1 125.50 125.50 +Item C 10 1.99 19.90 +-------------------------------------------------------------------- + Subtotal: 165.40 +-------------------------------------------------------------------- + Tax (10%): 16.54 +-------------------------------------------------------------------- + GRAND TOTAL: 181.94 +``` + + +``` +Rendering with Default Blueprint Renderer (for comparison): +┌───────────────────────────┬─────┬──────────────┬─────────────┐ +│ DESCRIPTION │ QTY │ UNIT PRICE │ TOTAL PRICE │ +├───────────────────────────┼─────┼──────────────┼─────────────┤ +│ Product A │ 2 │ 10.00 │ 20.00 │ +│ Super Long Product Name B │ 1 │ 125.50 │ 125.50 │ +│ Item C │ 10 │ 1.99 │ 19.90 │ +├───────────────────────────┼─────┼──────────────┼─────────────┤ +│ │ │ Subtotal: │ 165.40 │ +│ │ │ Tax (10%): │ 16.54 │ +│ │ │ GRAND TOTAL: │ 181.94 │ +└───────────────────────────┴─────┴──────────────┴─────────────┘ + +``` **Notes**: - The `renderer.NewBlueprint()` is sufficient for most text-based use cases. - Custom renderers require implementing all interface methods to handle table structure correctly. `tw.Formatting` (which includes `tw.RowContext`) provides cell content and metadata. @@ -1914,7 +2152,7 @@ func main() { - **Direct ANSI Codes**: Embed codes (e.g., `\033[32m` for green) in strings for manual control (`zoo.go:convertCellsToStrings`). - **tw.Formatter**: Implement `Format() string` on custom types to control cell output, including colors (`tw/types.go:Formatter`). - **tw.CellFilter**: Use `Config.
.Filter.Global` or `PerColumn` to apply transformations like coloring dynamically (`tw/cell.go:CellFilter`). -- **Width Handling**: `tw.DisplayWidth()` correctly calculates display width of ANSI-coded strings, ignoring escape sequences (`tw/fn.go:DisplayWidth`). +- **Width Handling**: `twdw.Width()` correctly calculates display width of ANSI-coded strings, ignoring escape sequences (`tw/fn.go:DisplayWidth`). - **No Built-In Color Presets**: Unlike v0.0.5’s potential `tablewriter.Colors`, v1.0.x requires manual ANSI code management or external libraries for color constants. **Migration Tips**: @@ -1928,7 +2166,7 @@ func main() { **Potential Pitfalls**: - **Terminal Support**: Some terminals may not support ANSI codes, causing artifacts; test in your environment or provide a non-colored fallback. - **Filter Overlap**: Combining `tw.CellFilter` with `AutoFormat` or other transformations can lead to unexpected results; prioritize filters for coloring (`zoo.go`). -- **Width Miscalculation**: Incorrect ANSI code handling (e.g., missing `Reset`) can skew width calculations; use `tw.DisplayWidth` (`tw/fn.go`). +- **Width Miscalculation**: Incorrect ANSI code handling (e.g., missing `Reset`) can skew width calculations; use `twdw.Width` (`tw/fn.go`). - **Streaming Consistency**: In streaming mode, ensure color codes are applied consistently across rows to avoid visual discrepancies (`stream.go`). - **Performance**: Applying filters to large datasets may add overhead; optimize filter logic for efficiency (`zoo.go`). @@ -2818,7 +3056,7 @@ func main() { **Notes**: - **Configuration**: Uses `tw.CellFilter` for per-column coloring, embedding ANSI codes (`tw/cell.go`). - **Migration from v0.0.5**: Replaces `SetColumnColor` with dynamic filters (`tablewriter.go`). -- **Key Features**: Flexible color application; `tw.DisplayWidth` handles ANSI codes correctly (`tw/fn.go`). +- **Key Features**: Flexible color application; `twdw.Width` handles ANSI codes correctly (`tw/fn.go`). - **Best Practices**: Test in ANSI-compatible terminals; use constants for code clarity. - **Potential Issues**: Non-ANSI terminals may show artifacts; provide fallbacks (`tw/fn.go`). @@ -3005,7 +3243,7 @@ This section addresses common migration issues with detailed solutions, covering | Merging not working | **Cause**: Streaming mode or mismatched data. **Solution**: Use batch mode for vertical/hierarchical merging; ensure identical content (`zoo.go`). | | Alignment ignored | **Cause**: `PerColumn` overrides `Global`. **Solution**: Check `Config.Section.Alignment.PerColumn` settings or `ConfigBuilder` calls (`tw/cell.go`). | | Padding affects widths | **Cause**: Padding included in `Config.Widths`. **Solution**: Adjust `Config.Widths` to account for `tw.CellPadding` (`zoo.go`). | -| Colors not rendering | **Cause**: Non-ANSI terminal or incorrect codes. **Solution**: Test in ANSI-compatible terminal; use `tw.DisplayWidth` (`tw/fn.go`). | +| Colors not rendering | **Cause**: Non-ANSI terminal or incorrect codes. **Solution**: Test in ANSI-compatible terminal; use `twdw.Width` (`tw/fn.go`). | | Caption missing | **Cause**: `Close()` not called in streaming or incorrect `Spot`. **Solution**: Ensure `Close()`; verify `tw.Caption.Spot` (`tablewriter.go`). | | Filters not applied | **Cause**: Incorrect `PerColumn` indexing or nil filters. **Solution**: Set filters correctly; test with sample data (`tw/cell.go`). | | Stringer cache overhead | **Cause**: Large datasets with diverse types. **Solution**: Disable `WithStringerCache` for small tables if not using `WithStringer` or if types vary greatly (`tablewriter.go`). | diff --git a/vendor/github.com/olekukonko/tablewriter/README.md b/vendor/github.com/olekukonko/tablewriter/README.md index f694340f..70480d69 100644 --- a/vendor/github.com/olekukonko/tablewriter/README.md +++ b/vendor/github.com/olekukonko/tablewriter/README.md @@ -86,6 +86,43 @@ func main() { Create a table with `NewTable` or `NewWriter`, configure it using options or a `Config` struct, add data with `Append` or `Bulk`, and render to an `io.Writer`. Use renderers like `Blueprint` (ASCII), `HTML`, `Markdown`, `Colorized`, or `Ocean` (streaming). +Here's how the API primitives map to the generated ASCII table: + +``` +API Call ASCII Table Component +-------- --------------------- + +table.Header([]string{"NAME", "AGE"}) ┌──────┬─────┐ ← Borders.Top + │ NAME │ AGE │ ← Header row + ├──────┼─────┤ ← Lines.ShowTop (header separator) + +table.Append([]string{"Alice", "25"}) │ Alice│ 25 │ ← Data row + ├──────┼─────┤ ← Separators.BetweenRows + +table.Append([]string{"Bob", "30"}) │ Bob │ 30 │ ← Data row + ├──────┼─────┤ ← Lines.ShowBottom (footer separator) + +table.Footer([]string{"Total", "2"}) │ Total│ 2 │ ← Footer row + └──────┴─────┘ ← Borders.Bottom +``` + +The core components include: + +- **Renderer** - Implements the core interface for converting table data into output formats. Available renderers include Blueprint (ASCII), HTML, Markdown, Colorized (ASCII with color), Ocean (streaming ASCII), and SVG. + +- **Config** - The root configuration struct that controls all table behavior and appearance + - **Behavior** - Controls high-level rendering behaviors including auto-hiding empty columns, trimming row whitespace, header/footer visibility, and compact mode for optimized merged cell calculations + - **CellConfig** - The comprehensive configuration template used for table sections (header, row, footer). Combines formatting, padding, alignment, filtering, callbacks, and width constraints with global and per-column control + - **StreamConfig** - Configuration for streaming mode including enable/disable state and strict column validation + +- **Rendition** - Defines how a renderer formats tables and contains the complete visual styling configuration + - **Borders** - Control the outer frame visibility (top, bottom, left, right edges) of the table + - **Lines** - Control horizontal boundary lines (above/below headers, above footers) that separate different table sections + - **Separators** - Control the visibility of separators between rows and between columns within the table content + - **Symbols** - Define the characters used for drawing table borders, corners, and junctions + +These components can be configured with various `tablewriter.With*()` functional options when creating a new table. + ## Examples ### Basic Examples diff --git a/vendor/github.com/olekukonko/tablewriter/deprecated.go b/vendor/github.com/olekukonko/tablewriter/deprecated.go index b61d507a..aa119e48 100644 --- a/vendor/github.com/olekukonko/tablewriter/deprecated.go +++ b/vendor/github.com/olekukonko/tablewriter/deprecated.go @@ -1,6 +1,8 @@ package tablewriter -import "github.com/olekukonko/tablewriter/tw" +import ( + "github.com/olekukonko/tablewriter/tw" +) // 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. diff --git a/vendor/github.com/olekukonko/tablewriter/option.go b/vendor/github.com/olekukonko/tablewriter/option.go index 1302462c..7270b768 100644 --- a/vendor/github.com/olekukonko/tablewriter/option.go +++ b/vendor/github.com/olekukonko/tablewriter/option.go @@ -1,7 +1,9 @@ package tablewriter import ( + "github.com/mattn/go-runewidth" "github.com/olekukonko/ll" + "github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/tw" "reflect" ) @@ -613,6 +615,31 @@ func WithRendition(rendition tw.Rendition) Option { } } +// WithEastAsian configures the global East Asian width calculation setting. +// - enable=true: Enables East Asian width calculations. CJK and ambiguous characters +// are typically measured as double width. +// - enable=false: Disables East Asian width calculations. Characters are generally +// measured as single width, subject to Unicode standards. +// +// This setting affects all subsequent display width calculations using the twdw package. +func WithEastAsian(enable bool) Option { + return func(target *Table) { + twwidth.SetEastAsian(enable) + } +} + +// WithCondition provides a way to set a custom global runewidth.Condition +// that will be used for all subsequent display width calculations by the twwidth (twdw) package. +// +// The runewidth.Condition object allows for more fine-grained control over how rune widths +// are determined, beyond just toggling EastAsianWidth. This could include settings for +// ambiguous width characters or other future properties of runewidth.Condition. +func WithCondition(condition *runewidth.Condition) Option { + return func(target *Table) { + twwidth.SetCondition(condition) + } +} + // 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. . @@ -682,8 +709,11 @@ func defaultConfig() Config { PerColumn: []tw.Align{}, }, }, - Stream: tw.StreamConfig{}, - Debug: false, + Stream: tw.StreamConfig{ + Enable: false, + StrictColumns: false, + }, + Debug: false, Behavior: tw.Behavior{ AutoHide: tw.Off, TrimSpace: tw.On, @@ -842,6 +872,8 @@ func mergeStreamConfig(dst, src tw.StreamConfig) tw.StreamConfig { if src.Enable { dst.Enable = true } + + dst.StrictColumns = src.StrictColumns return dst } diff --git a/vendor/github.com/olekukonko/tablewriter/pkg/twwarp/wrap.go b/vendor/github.com/olekukonko/tablewriter/pkg/twwarp/wrap.go index f32f0542..a577c1eb 100644 --- a/vendor/github.com/olekukonko/tablewriter/pkg/twwarp/wrap.go +++ b/vendor/github.com/olekukonko/tablewriter/pkg/twwarp/wrap.go @@ -8,12 +8,13 @@ package twwarp import ( - "github.com/rivo/uniseg" "math" "strings" "unicode" - "github.com/mattn/go-runewidth" + "github.com/olekukonko/tablewriter/pkg/twwidth" // IMPORT YOUR NEW PACKAGE + "github.com/rivo/uniseg" + // "github.com/mattn/go-runewidth" // This can be removed if all direct uses are gone ) const ( @@ -59,7 +60,8 @@ func WrapString(s string, lim int) ([]string, int) { var lines []string max := 0 for _, v := range words { - max = runewidth.StringWidth(v) + // max = runewidth.StringWidth(v) // OLD + max = twwidth.Width(v) // NEW: Use twdw.Width if max > lim { lim = max } @@ -82,12 +84,13 @@ func WrapStringWithSpaces(s string, lim int) ([]string, int) { return []string{""}, lim } if strings.TrimSpace(s) == "" { // All spaces - if runewidth.StringWidth(s) <= lim { - return []string{s}, runewidth.StringWidth(s) + // if runewidth.StringWidth(s) <= lim { // OLD + if twwidth.Width(s) <= lim { // NEW: Use twdw.Width + // return []string{s}, runewidth.StringWidth(s) // OLD + return []string{s}, twwidth.Width(s) // NEW: Use twdw.Width } // For very long all-space strings, "wrap" by truncating to the limit. if lim > 0 { - // Use our new helper function to get a substring of the correct display width substring, _ := stringToDisplayWidth(s, lim) return []string{substring}, lim } @@ -96,7 +99,6 @@ func WrapStringWithSpaces(s string, lim int) ([]string, int) { var leadingSpaces, trailingSpaces, coreContent string firstNonSpace := strings.IndexFunc(s, func(r rune) bool { return !unicode.IsSpace(r) }) - // firstNonSpace will not be -1 due to TrimSpace check above. leadingSpaces = s[:firstNonSpace] lastNonSpace := strings.LastIndexFunc(s, func(r rune) bool { return !unicode.IsSpace(r) }) trailingSpaces = s[lastNonSpace+1:] @@ -116,7 +118,8 @@ func WrapStringWithSpaces(s string, lim int) ([]string, int) { maxCoreWordWidth := 0 for _, v := range words { - w := runewidth.StringWidth(v) + // w := runewidth.StringWidth(v) // OLD + w := twwidth.Width(v) // NEW: Use twdw.Width if w > maxCoreWordWidth { maxCoreWordWidth = w } @@ -153,15 +156,14 @@ func stringToDisplayWidth(s string, targetWidth int) (substring string, actualWi g := uniseg.NewGraphemes(s) for g.Next() { grapheme := g.Str() - graphemeWidth := runewidth.StringWidth(grapheme) // Get width of the current grapheme cluster + // graphemeWidth := runewidth.StringWidth(grapheme) // OLD + graphemeWidth := twwidth.Width(grapheme) // NEW: Use twdw.Width if currentWidth+graphemeWidth > targetWidth { - // Adding this grapheme would exceed the target width break } currentWidth += graphemeWidth - // Get the end byte position of the current grapheme cluster _, e := g.Positions() endIndex = e } @@ -186,14 +188,15 @@ func WrapWords(words []string, spc, lim, pen int) [][]string { } lengths := make([]int, n) for i := 0; i < n; i++ { - lengths[i] = runewidth.StringWidth(words[i]) + // lengths[i] = runewidth.StringWidth(words[i]) // OLD + lengths[i] = twwidth.Width(words[i]) // NEW: Use twdw.Width } nbrk := make([]int, n) cost := make([]int, n) for i := range cost { cost[i] = math.MaxInt32 } - remainderLen := lengths[n-1] + remainderLen := lengths[n-1] // Uses updated lengths for i := n - 1; i >= 0; i-- { if i < n-1 { remainderLen += spc + lengths[i] diff --git a/vendor/github.com/olekukonko/tablewriter/pkg/twwidth/width.go b/vendor/github.com/olekukonko/tablewriter/pkg/twwidth/width.go new file mode 100644 index 00000000..d46ce4a8 --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/pkg/twwidth/width.go @@ -0,0 +1,321 @@ +package twwidth + +import ( + "bytes" + "github.com/mattn/go-runewidth" + "regexp" + "strings" + "sync" +) + +// condition holds the global runewidth configuration, including East Asian width settings. +var condition *runewidth.Condition + +// mu protects access to condition and widthCache for thread safety. +var mu sync.Mutex + +// ansi is a compiled regular expression for stripping ANSI escape codes from strings. +var ansi = Filter() + +func init() { + condition = runewidth.NewCondition() + widthCache = make(map[cacheKey]int) +} + +// cacheKey is used as a key for memoizing string width results in widthCache. +type cacheKey struct { + str string // Input string + eastAsianWidth bool // East Asian width setting +} + +// widthCache stores memoized results of Width calculations to improve performance. +var widthCache map[cacheKey]int + +// Filter compiles and returns a regular expression for matching ANSI escape sequences, +// including CSI (Control Sequence Introducer) and OSC (Operating System Command) sequences. +// The returned regex can be used to strip ANSI codes from strings. +func Filter() *regexp.Regexp { + var regESC = "\x1b" // ASCII escape character + var regBEL = "\x07" // ASCII bell character + + // ANSI string terminator: either ESC+\ or BEL + var regST = "(" + regexp.QuoteMeta(regESC+"\\") + "|" + regexp.QuoteMeta(regBEL) + ")" + // Control Sequence Introducer (CSI): ESC[ followed by parameters and a final byte + var regCSI = regexp.QuoteMeta(regESC+"[") + "[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]" + // Operating System Command (OSC): ESC] followed by arbitrary content until a terminator + var regOSC = regexp.QuoteMeta(regESC+"]") + ".*?" + regST + + // Combine CSI and OSC patterns into a single regex + return regexp.MustCompile("(" + regCSI + "|" + regOSC + ")") +} + +// SetEastAsian enables or disables East Asian width handling for width calculations. +// When the setting changes, the width cache is cleared to ensure accuracy. +// This function is thread-safe. +// +// Example: +// +// twdw.SetEastAsian(true) // Enable East Asian width handling +func SetEastAsian(enable bool) { + mu.Lock() + defer mu.Unlock() + if condition.EastAsianWidth != enable { + condition.EastAsianWidth = enable + widthCache = make(map[cacheKey]int) // Clear cache on setting change + } +} + +// SetCondition updates the global runewidth.Condition used for width calculations. +// When the condition is changed, the width cache is cleared. +// This function is thread-safe. +// +// Example: +// +// newCond := runewidth.NewCondition() +// newCond.EastAsianWidth = true +// twdw.SetCondition(newCond) +func SetCondition(newCond *runewidth.Condition) { + mu.Lock() + defer mu.Unlock() + condition = newCond + widthCache = make(map[cacheKey]int) // Clear cache on setting change +} + +// Width calculates the visual width of a string, excluding ANSI escape sequences, +// using the go-runewidth package for accurate Unicode handling. It accounts for the +// current East Asian width setting and caches results for performance. +// This function is thread-safe. +// +// Example: +// +// width := twdw.Width("Hello\x1b[31mWorld") // Returns 10 +func Width(str string) int { + mu.Lock() + key := cacheKey{str: str, eastAsianWidth: condition.EastAsianWidth} + if w, found := widthCache[key]; found { + mu.Unlock() + return w + } + mu.Unlock() + + // Use a temporary condition to avoid holding the lock during calculation + tempCond := runewidth.NewCondition() + tempCond.EastAsianWidth = key.eastAsianWidth + + stripped := ansi.ReplaceAllLiteralString(str, "") + calculatedWidth := tempCond.StringWidth(stripped) + + mu.Lock() + widthCache[key] = calculatedWidth + mu.Unlock() + + return calculatedWidth +} + +// WidthNoCache calculates the visual width of a string without using or +// updating the global cache. It uses the current global East Asian width setting. +// This function is intended for internal use (e.g., benchmarking) and is thread-safe. +// +// Example: +// +// width := twdw.WidthNoCache("Hello\x1b[31mWorld") // Returns 10 +func WidthNoCache(str string) int { + mu.Lock() + currentEA := condition.EastAsianWidth + mu.Unlock() + + tempCond := runewidth.NewCondition() + tempCond.EastAsianWidth = currentEA + + stripped := ansi.ReplaceAllLiteralString(str, "") + return tempCond.StringWidth(stripped) +} + +// Display calculates the visual width of a string, excluding ANSI escape sequences, +// using the provided runewidth condition. Unlike Width, it does not use caching +// and is intended for cases where a specific condition is required. +// This function is thread-safe with respect to the provided condition. +// +// Example: +// +// cond := runewidth.NewCondition() +// width := twdw.Display(cond, "Hello\x1b[31mWorld") // Returns 10 +func Display(cond *runewidth.Condition, str string) int { + return cond.StringWidth(ansi.ReplaceAllLiteralString(str, "")) +} + +// Truncate shortens a string to fit within a specified visual width, optionally +// appending a suffix (e.g., "..."). It preserves ANSI escape sequences and adds +// a reset sequence (\x1b[0m) if needed to prevent formatting bleed. The function +// respects the global East Asian width setting and is thread-safe. +// +// If maxWidth is negative, an empty string is returned. If maxWidth is zero and +// a suffix is provided, the suffix is returned. If the string's visual width is +// less than or equal to maxWidth, the string (and suffix, if provided and fits) +// is returned unchanged. +// +// Example: +// +// s := twdw.Truncate("Hello\x1b[31mWorld", 5, "...") // Returns "Hello..." +// s = twdw.Truncate("Hello", 10) // Returns "Hello" +func Truncate(s string, maxWidth int, suffix ...string) string { + if maxWidth < 0 { + return "" + } + + suffixStr := strings.Join(suffix, "") + sDisplayWidth := Width(s) // Uses global cached Width + suffixDisplayWidth := Width(suffixStr) // Uses global cached Width + + // Case 1: Original string is visually empty. + if sDisplayWidth == 0 { + // If suffix is provided and fits within maxWidth (or if maxWidth is generous) + if len(suffixStr) > 0 && suffixDisplayWidth <= maxWidth { + return suffixStr + } + // If s has ANSI codes (len(s)>0) but maxWidth is 0, can't display them. + if maxWidth == 0 && len(s) > 0 { + return "" + } + return s // Returns "" or original ANSI codes + } + + // Case 2: maxWidth is 0, but string has content. Cannot display anything. + if maxWidth == 0 { + return "" + } + + // Case 3: String fits completely or fits with suffix. + // Here, maxWidth is the total budget for the line. + if sDisplayWidth <= maxWidth { + if len(suffixStr) == 0 { // No suffix. + return s + } + // Suffix is provided. Check if s + suffix fits. + if sDisplayWidth+suffixDisplayWidth <= maxWidth { + return s + suffixStr + } + // s fits, but s + suffix is too long. Return s. + return s + } + + // Case 4: String needs truncation (sDisplayWidth > maxWidth). + // maxWidth is the total budget for the final string (content + suffix). + + // Capture the global EastAsianWidth setting once for consistent use + mu.Lock() + currentGlobalEastAsianWidth := condition.EastAsianWidth + mu.Unlock() + + // Special case for EastAsian true: if only suffix fits, return suffix. + // This was derived from previous test behavior. + if len(suffixStr) > 0 && currentGlobalEastAsianWidth { + provisionalContentWidth := maxWidth - suffixDisplayWidth + if provisionalContentWidth == 0 { // Exactly enough space for suffix only + return suffixStr // <<<< MODIFIED: No ANSI reset here + } + } + + // Calculate the budget for the content part, reserving space for the suffix. + targetContentForIteration := maxWidth + if len(suffixStr) > 0 { + targetContentForIteration -= suffixDisplayWidth + } + + // If content budget is negative, means not even suffix fits (or no suffix and no space). + // However, if only suffix fits, it should be handled. + if targetContentForIteration < 0 { + // Can we still fit just the suffix? + if len(suffixStr) > 0 && suffixDisplayWidth <= maxWidth { + if strings.Contains(s, "\x1b[") { + return "\x1b[0m" + suffixStr + } + return suffixStr + } + return "" // Cannot fit anything. + } + // If targetContentForIteration is 0, loop won't run, result will be empty string, then suffix is added. + + var contentBuf bytes.Buffer + var currentContentDisplayWidth int + var ansiSeqBuf bytes.Buffer + inAnsiSequence := false + ansiWrittenToContent := false + + localRunewidthCond := runewidth.NewCondition() + localRunewidthCond.EastAsianWidth = currentGlobalEastAsianWidth + + for _, r := range s { + if r == '\x1b' { + inAnsiSequence = true + ansiSeqBuf.Reset() + ansiSeqBuf.WriteRune(r) + } else if inAnsiSequence { + ansiSeqBuf.WriteRune(r) + seqBytes := ansiSeqBuf.Bytes() + seqLen := len(seqBytes) + terminated := false + if seqLen >= 2 { + introducer := seqBytes[1] + if introducer == '[' { + if seqLen >= 3 && r >= 0x40 && r <= 0x7E { + terminated = true + } + } else if introducer == ']' { + if r == '\x07' { + terminated = true + } else if seqLen > 1 && seqBytes[seqLen-2] == '\x1b' && r == '\\' { // Check for ST: \x1b\ + terminated = true + } + } + } + if terminated { + inAnsiSequence = false + contentBuf.Write(ansiSeqBuf.Bytes()) + ansiWrittenToContent = true + ansiSeqBuf.Reset() + } + } else { // Normal character + runeDisplayWidth := localRunewidthCond.RuneWidth(r) + if targetContentForIteration == 0 { // No budget for content at all + break + } + if currentContentDisplayWidth+runeDisplayWidth > targetContentForIteration { + break + } + contentBuf.WriteRune(r) + currentContentDisplayWidth += runeDisplayWidth + } + } + + result := contentBuf.String() + + // Suffix is added if: + // 1. A suffix string is provided. + // 2. Truncation actually happened (sDisplayWidth > maxWidth originally) + // OR if the content part is empty but a suffix is meant to be shown + // (e.g. targetContentForIteration was 0). + if len(suffixStr) > 0 { + // Add suffix if we are in the truncation path (sDisplayWidth > maxWidth) + // OR if targetContentForIteration was 0 (meaning only suffix should be shown) + // but we must ensure we don't exceed original maxWidth. + // The logic above for targetContentForIteration already ensures space. + + needsReset := false + // Condition for reset: if styling was active in 's' and might affect suffix + if (ansiWrittenToContent || (inAnsiSequence && strings.Contains(s, "\x1b["))) && (currentContentDisplayWidth > 0 || ansiWrittenToContent) { + if !strings.HasSuffix(result, "\x1b[0m") { + needsReset = true + } + } else if currentContentDisplayWidth > 0 && strings.Contains(result, "\x1b[") && !strings.HasSuffix(result, "\x1b[0m") && strings.Contains(s, "\x1b[") { + // If result has content and ANSI, and original had ANSI, and result not already reset + needsReset = true + } + + if needsReset { + result += "\x1b[0m" + } + result += suffixStr + } + return result +} diff --git a/vendor/github.com/olekukonko/tablewriter/renderer/blueprint.go b/vendor/github.com/olekukonko/tablewriter/renderer/blueprint.go index 355b3af3..42966ebc 100644 --- a/vendor/github.com/olekukonko/tablewriter/renderer/blueprint.go +++ b/vendor/github.com/olekukonko/tablewriter/renderer/blueprint.go @@ -2,6 +2,7 @@ package renderer import ( "github.com/olekukonko/ll" + "github.com/olekukonko/tablewriter/pkg/twwidth" "io" "strings" @@ -42,7 +43,7 @@ func NewBlueprint(configs ...tw.Rendition) *Blueprint { // Merge user settings with default settings cfg.Settings = mergeSettings(cfg.Settings, userCfg.Settings) } - return &Blueprint{config: cfg} + return &Blueprint{config: cfg, logger: ll.New("blueprint")} } // Close performs cleanup (no-op in this implementation). @@ -106,7 +107,7 @@ func (f *Blueprint) Line(ctx tw.Formatting) { } if prefix != tw.Empty || suffix != tw.Empty { line.WriteString(prefix + suffix + tw.NewLine) - totalLineWidth = tw.DisplayWidth(prefix) + tw.DisplayWidth(suffix) + totalLineWidth = twwidth.Width(prefix) + twwidth.Width(suffix) f.w.Write([]byte(line.String())) } f.logger.Debugf("Line: Handled empty row/widths case (total width %d)", totalLineWidth) @@ -119,13 +120,13 @@ func (f *Blueprint) Line(ctx tw.Formatting) { targetTotalWidth += ctx.Row.Widths.Get(colIdx) } if f.config.Borders.Left.Enabled() { - targetTotalWidth += tw.DisplayWidth(f.config.Symbols.Column()) + targetTotalWidth += twwidth.Width(f.config.Symbols.Column()) } if f.config.Borders.Right.Enabled() { - targetTotalWidth += tw.DisplayWidth(f.config.Symbols.Column()) + targetTotalWidth += twwidth.Width(f.config.Symbols.Column()) } if f.config.Settings.Separators.BetweenColumns.Enabled() && len(sortedKeys) > 1 { - targetTotalWidth += tw.DisplayWidth(f.config.Symbols.Column()) * (len(sortedKeys) - 1) + targetTotalWidth += twwidth.Width(f.config.Symbols.Column()) * (len(sortedKeys) - 1) } // Add left border if enabled @@ -133,7 +134,7 @@ func (f *Blueprint) Line(ctx tw.Formatting) { if f.config.Borders.Left.Enabled() { leftBorder := jr.RenderLeft() line.WriteString(leftBorder) - leftBorderWidth = tw.DisplayWidth(leftBorder) + leftBorderWidth = twwidth.Width(leftBorder) totalLineWidth += leftBorderWidth f.logger.Debugf("Line: Left border='%s' (f.width %d)", leftBorder, leftBorderWidth) } @@ -156,11 +157,11 @@ func (f *Blueprint) Line(ctx tw.Formatting) { // Adjust colWidth to account for wider borders adjustedColWidth := colWidth if f.config.Borders.Left.Enabled() && keyIndex == 0 { - adjustedColWidth -= leftBorderWidth - tw.DisplayWidth(f.config.Symbols.Column()) + adjustedColWidth -= leftBorderWidth - twwidth.Width(f.config.Symbols.Column()) } if f.config.Borders.Right.Enabled() && keyIndex == len(visibleColIndices)-1 { - rightBorderWidth := tw.DisplayWidth(jr.RenderRight(currentColIdx)) - adjustedColWidth -= rightBorderWidth - tw.DisplayWidth(f.config.Symbols.Column()) + rightBorderWidth := twwidth.Width(jr.RenderRight(currentColIdx)) + adjustedColWidth -= rightBorderWidth - twwidth.Width(f.config.Symbols.Column()) } if adjustedColWidth < 0 { adjustedColWidth = 0 @@ -172,7 +173,7 @@ func (f *Blueprint) Line(ctx tw.Formatting) { totalLineWidth += adjustedColWidth f.logger.Debugf("Line: Rendered spaces='%s' (f.width %d) for col %d", spaces, adjustedColWidth, currentColIdx) } else { - segmentWidth := tw.DisplayWidth(segment) + segmentWidth := twwidth.Width(segment) if segmentWidth == 0 { segmentWidth = 1 // Avoid division by zero f.logger.Warnf("Line: Segment='%s' has zero width, using 1", segment) @@ -183,11 +184,11 @@ func (f *Blueprint) Line(ctx tw.Formatting) { repeat = 1 } repeatedSegment := strings.Repeat(segment, repeat) - actualWidth := tw.DisplayWidth(repeatedSegment) + actualWidth := twwidth.Width(repeatedSegment) if actualWidth > adjustedColWidth { // Truncate if too long - repeatedSegment = tw.TruncateString(repeatedSegment, adjustedColWidth) - actualWidth = tw.DisplayWidth(repeatedSegment) + repeatedSegment = twwidth.Truncate(repeatedSegment, adjustedColWidth) + actualWidth = twwidth.Width(repeatedSegment) f.logger.Debugf("Line: Truncated segment='%s' to width %d", repeatedSegment, actualWidth) } else if actualWidth < adjustedColWidth { // Pad with segment character to match adjustedColWidth @@ -195,7 +196,7 @@ func (f *Blueprint) Line(ctx tw.Formatting) { for i := 0; i < remainingWidth/segmentWidth; i++ { repeatedSegment += segment } - actualWidth = tw.DisplayWidth(repeatedSegment) + actualWidth = twwidth.Width(repeatedSegment) if actualWidth < adjustedColWidth { repeatedSegment = tw.PadRight(repeatedSegment, tw.Space, adjustedColWidth) actualWidth = adjustedColWidth @@ -214,13 +215,13 @@ func (f *Blueprint) Line(ctx tw.Formatting) { nextColIdx := visibleColIndices[keyIndex+1] junction := jr.RenderJunction(currentColIdx, nextColIdx) // Use center symbol (❀) or column separator (|) to match data rows - if tw.DisplayWidth(junction) != tw.DisplayWidth(f.config.Symbols.Column()) { + if twwidth.Width(junction) != twwidth.Width(f.config.Symbols.Column()) { junction = f.config.Symbols.Center() - if tw.DisplayWidth(junction) != tw.DisplayWidth(f.config.Symbols.Column()) { + if twwidth.Width(junction) != twwidth.Width(f.config.Symbols.Column()) { junction = f.config.Symbols.Column() } } - junctionWidth := tw.DisplayWidth(junction) + junctionWidth := twwidth.Width(junction) line.WriteString(junction) totalLineWidth += junctionWidth f.logger.Debugf("Line: Junction between %d and %d: '%s' (f.width %d)", currentColIdx, nextColIdx, junction, junctionWidth) @@ -232,7 +233,7 @@ func (f *Blueprint) Line(ctx tw.Formatting) { if f.config.Borders.Right.Enabled() && len(visibleColIndices) > 0 { lastIdx := visibleColIndices[len(visibleColIndices)-1] rightBorder := jr.RenderRight(lastIdx) - rightBorderWidth = tw.DisplayWidth(rightBorder) + rightBorderWidth = twwidth.Width(rightBorder) line.WriteString(rightBorder) totalLineWidth += rightBorderWidth f.logger.Debugf("Line: Right border='%s' (f.width %d)", rightBorder, rightBorderWidth) @@ -276,7 +277,7 @@ func (f *Blueprint) formatCell(content string, width int, padding tw.Padding, al content, width, align, padding.Left, padding.Right) // Calculate display width of content - runeWidth := tw.DisplayWidth(content) + runeWidth := twwidth.Width(content) // Set default padding characters leftPadChar := padding.Left @@ -292,8 +293,8 @@ func (f *Blueprint) formatCell(content string, width int, padding tw.Padding, al //} // Calculate padding widths - padLeftWidth := tw.DisplayWidth(leftPadChar) - padRightWidth := tw.DisplayWidth(rightPadChar) + padLeftWidth := twwidth.Width(leftPadChar) + padRightWidth := twwidth.Width(rightPadChar) // Calculate available width for content availableContentWidth := width - padLeftWidth - padRightWidth @@ -304,8 +305,8 @@ func (f *Blueprint) formatCell(content string, width int, padding tw.Padding, al // Truncate content if it exceeds available width if runeWidth > availableContentWidth { - content = tw.TruncateString(content, availableContentWidth) - runeWidth = tw.DisplayWidth(content) + content = twwidth.Truncate(content, availableContentWidth) + runeWidth = twwidth.Width(content) f.logger.Debugf("Truncated content to fit %d: '%s' (new width %d)", availableContentWidth, content, runeWidth) } @@ -363,10 +364,10 @@ func (f *Blueprint) formatCell(content string, width int, padding tw.Padding, al } output := result.String() - finalWidth := tw.DisplayWidth(output) + finalWidth := twwidth.Width(output) // Adjust output to match target width if finalWidth > width { - output = tw.TruncateString(output, width) + output = twwidth.Truncate(output, width) f.logger.Debugf("formatCell: Truncated output to width %d", width) } else if finalWidth < width { output = tw.PadRight(output, tw.Space, width) @@ -374,9 +375,9 @@ func (f *Blueprint) formatCell(content string, width int, padding tw.Padding, al } // Log warning if final width doesn't match target - if f.logger.Enabled() && tw.DisplayWidth(output) != width { + if f.logger.Enabled() && twwidth.Width(output) != width { f.logger.Debugf("formatCell Warning: Final width %d does not match target %d for result '%s'", - tw.DisplayWidth(output), width, output) + twwidth.Width(output), width, output) } f.logger.Debugf("Formatted cell final result: '%s' (target width %d)", output, width) @@ -407,14 +408,14 @@ func (f *Blueprint) renderLine(ctx tw.Formatting) { totalLineWidth := 0 // Track total display width if prefix != tw.Empty { output.WriteString(prefix) - totalLineWidth += tw.DisplayWidth(prefix) - f.logger.Debugf("renderLine: Prefix='%s' (f.width %d)", prefix, tw.DisplayWidth(prefix)) + totalLineWidth += twwidth.Width(prefix) + f.logger.Debugf("renderLine: Prefix='%s' (f.width %d)", prefix, twwidth.Width(prefix)) } colIndex := 0 separatorDisplayWidth := 0 if f.config.Settings.Separators.BetweenColumns.Enabled() { - separatorDisplayWidth = tw.DisplayWidth(columnSeparator) + separatorDisplayWidth = twwidth.Width(columnSeparator) } // Process each column @@ -542,7 +543,7 @@ func (f *Blueprint) renderLine(ctx tw.Formatting) { formattedCell := f.formatCell(cellData, visualWidth, padding, align) if len(formattedCell) > 0 { output.WriteString(formattedCell) - cellWidth := tw.DisplayWidth(formattedCell) + cellWidth := twwidth.Width(formattedCell) totalLineWidth += cellWidth f.logger.Debugf("renderLine: Rendered col %d, formattedCell='%s' (f.width %d), totalLineWidth=%d", colIndex, formattedCell, cellWidth, totalLineWidth) } @@ -561,17 +562,19 @@ func (f *Blueprint) renderLine(ctx tw.Formatting) { // Add suffix and adjust total width if output.Len() > len(prefix) || f.config.Borders.Right.Enabled() { output.WriteString(suffix) - totalLineWidth += tw.DisplayWidth(suffix) - f.logger.Debugf("renderLine: Suffix='%s' (f.width %d)", suffix, tw.DisplayWidth(suffix)) + totalLineWidth += twwidth.Width(suffix) + f.logger.Debugf("renderLine: Suffix='%s' (f.width %d)", suffix, twwidth.Width(suffix)) } output.WriteString(tw.NewLine) f.w.Write([]byte(output.String())) f.logger.Debugf("renderLine: Final rendered line: '%s' (total width %d)", strings.TrimSuffix(output.String(), tw.NewLine), totalLineWidth) } +// Rendition updates the Blueprint's configuration. func (f *Blueprint) Rendition(config tw.Rendition) { f.config = mergeRendition(f.config, config) - f.logger.Debugf("Blueprint.Rendition updated. New internal config: %+v", f.config) + f.logger.Debugf("Blueprint.Rendition updated. New config: %+v", f.config) + } // Ensure Blueprint implements tw.Renditioning diff --git a/vendor/github.com/olekukonko/tablewriter/renderer/colorized.go b/vendor/github.com/olekukonko/tablewriter/renderer/colorized.go index 2f4a0863..5eaa1924 100644 --- a/vendor/github.com/olekukonko/tablewriter/renderer/colorized.go +++ b/vendor/github.com/olekukonko/tablewriter/renderer/colorized.go @@ -4,6 +4,7 @@ import ( "github.com/fatih/color" "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" + "github.com/olekukonko/tablewriter/pkg/twwidth" "io" "strings" @@ -254,7 +255,7 @@ func (c *Colorized) Line(ctx tw.Formatting) { line.WriteString(strings.Repeat(tw.Space, colWidth)) } else { // Calculate how many times to repeat the segment - segmentWidth := tw.DisplayWidth(segment) + segmentWidth := twwidth.Width(segment) if segmentWidth <= 0 { segmentWidth = 1 } @@ -266,7 +267,7 @@ func (c *Colorized) Line(ctx tw.Formatting) { line.WriteString(drawnSegment) // Adjust for width discrepancies - actualDrawnWidth := tw.DisplayWidth(drawnSegment) + actualDrawnWidth := twwidth.Width(drawnSegment) if actualDrawnWidth < colWidth { missingWidth := colWidth - actualDrawnWidth spaces := strings.Repeat(tw.Space, missingWidth) @@ -373,7 +374,7 @@ func (c *Colorized) formatCell(content string, width int, padding tw.Padding, al } // Calculate visual width of content - contentVisualWidth := tw.DisplayWidth(content) + contentVisualWidth := twwidth.Width(content) // Set default padding characters padLeftCharStr := padding.Left @@ -386,8 +387,8 @@ func (c *Colorized) formatCell(content string, width int, padding tw.Padding, al } // Calculate padding widths - definedPadLeftWidth := tw.DisplayWidth(padLeftCharStr) - definedPadRightWidth := tw.DisplayWidth(padRightCharStr) + definedPadLeftWidth := twwidth.Width(padLeftCharStr) + definedPadRightWidth := twwidth.Width(padRightCharStr) // Calculate available width for content and alignment availableForContentAndAlign := width - definedPadLeftWidth - definedPadRightWidth if availableForContentAndAlign < 0 { @@ -396,8 +397,8 @@ func (c *Colorized) formatCell(content string, width int, padding tw.Padding, al // Truncate content if it exceeds available width if contentVisualWidth > availableForContentAndAlign { - content = tw.TruncateString(content, availableForContentAndAlign) - contentVisualWidth = tw.DisplayWidth(content) + content = twwidth.Truncate(content, availableForContentAndAlign) + contentVisualWidth = twwidth.Width(content) c.logger.Debugf("Truncated content to fit %d: '%s' (new width %d)", availableForContentAndAlign, content, contentVisualWidth) } @@ -472,12 +473,12 @@ func (c *Colorized) formatCell(content string, width int, padding tw.Padding, al output := sb.String() // Adjust output width if necessary - currentVisualWidth := tw.DisplayWidth(output) + currentVisualWidth := twwidth.Width(output) if currentVisualWidth != width { c.logger.Debugf("formatCell MISMATCH: content='%s', target_w=%d. Calculated parts width = %d. String: '%s'", content, width, currentVisualWidth, output) if currentVisualWidth > width { - output = tw.TruncateString(output, width) + output = twwidth.Truncate(output, width) } else { paddingSpacesStr := strings.Repeat(tw.Space, width-currentVisualWidth) if len(tint.BG) > 0 { @@ -486,10 +487,10 @@ func (c *Colorized) formatCell(content string, width int, padding tw.Padding, al output += paddingSpacesStr } } - c.logger.Debugf("formatCell Post-Correction: Target %d, New Visual width %d. Output: '%s'", width, tw.DisplayWidth(output), output) + c.logger.Debugf("formatCell Post-Correction: Target %d, New Visual width %d. Output: '%s'", width, twwidth.Width(output), output) } - c.logger.Debugf("Formatted cell final result: '%s' (target width %d, display width %d)", output, width, tw.DisplayWidth(output)) + c.logger.Debugf("Formatted cell final result: '%s' (target width %d, display width %d)", output, width, twwidth.Width(output)) return output } @@ -529,7 +530,7 @@ func (c *Colorized) renderLine(ctx tw.Formatting, line []string, tint Tint) { separatorString := tw.Empty if c.config.Settings.Separators.BetweenColumns.Enabled() { separatorString = c.config.Separator.Apply(c.config.Symbols.Column()) - separatorDisplayWidth = tw.DisplayWidth(c.config.Symbols.Column()) + separatorDisplayWidth = twwidth.Width(c.config.Symbols.Column()) } // Process each column @@ -693,8 +694,7 @@ func (c *Colorized) renderLine(ctx tw.Formatting, line []string, tint Tint) { // Rendition updates the parts of ColorizedConfig that correspond to tw.Rendition // by merging the provided newRendition. Color-specific Tints are not modified. func (c *Colorized) Rendition(newRendition tw.Rendition) { // Method name matches interface - c.logger.Debug("Colorized.Rendition called. Current B/Sym/Set: B:%+v, Sym:%T, S:%+v. Override: %+v", - c.config.Borders, c.config.Symbols, c.config.Settings, newRendition) + c.logger.Debug("Colorized.Rendition called. Current B/Sym/Set: B:%+v, Sym:%T, S:%+v. Override: %+v", c.config.Borders, c.config.Symbols, c.config.Settings, newRendition) currentRenditionPart := tw.Rendition{ Borders: c.config.Borders, diff --git a/vendor/github.com/olekukonko/tablewriter/renderer/html.go b/vendor/github.com/olekukonko/tablewriter/renderer/html.go index 71c0cf5f..62430a17 100644 --- a/vendor/github.com/olekukonko/tablewriter/renderer/html.go +++ b/vendor/github.com/olekukonko/tablewriter/renderer/html.go @@ -63,6 +63,7 @@ func NewHTML(configs ...HTMLConfig) *HTML { tableStarted: false, tbodyStarted: false, tfootStarted: false, + logger: ll.New("html"), } } diff --git a/vendor/github.com/olekukonko/tablewriter/renderer/markdown.go b/vendor/github.com/olekukonko/tablewriter/renderer/markdown.go index 525e6338..a565cc38 100644 --- a/vendor/github.com/olekukonko/tablewriter/renderer/markdown.go +++ b/vendor/github.com/olekukonko/tablewriter/renderer/markdown.go @@ -2,6 +2,7 @@ package renderer import ( "github.com/olekukonko/ll" + "github.com/olekukonko/tablewriter/pkg/twwidth" "io" "strings" @@ -35,7 +36,7 @@ func NewMarkdown(configs ...tw.Rendition) *Markdown { if len(configs) > 0 { cfg = mergeMarkdownConfig(cfg, configs[0]) } - return &Markdown{config: cfg} + return &Markdown{config: cfg, logger: ll.New("markdown")} } // mergeMarkdownConfig combines user-provided config with Markdown defaults, enforcing Markdown-specific settings. @@ -157,7 +158,7 @@ func (m *Markdown) formatCell(content string, width int, align tw.Align, padding //if m.config.Settings.TrimWhitespace.Enabled() { // content = strings.TrimSpace(content) //} - contentVisualWidth := tw.DisplayWidth(content) + contentVisualWidth := twwidth.Width(content) // Use specified padding characters or default to spaces padLeftChar := padding.Left @@ -170,8 +171,8 @@ func (m *Markdown) formatCell(content string, width int, align tw.Align, padding } // Calculate padding widths - padLeftCharWidth := tw.DisplayWidth(padLeftChar) - padRightCharWidth := tw.DisplayWidth(padRightChar) + padLeftCharWidth := twwidth.Width(padLeftChar) + padRightCharWidth := twwidth.Width(padRightChar) minWidth := tw.Max(3, contentVisualWidth+padLeftCharWidth+padRightCharWidth) targetWidth := tw.Max(width, minWidth) @@ -212,7 +213,7 @@ func (m *Markdown) formatCell(content string, width int, align tw.Align, padding result := leftPadStr + content + rightPadStr // Adjust width if needed - finalWidth := tw.DisplayWidth(result) + finalWidth := twwidth.Width(result) if finalWidth != targetWidth { m.logger.Debugf("Markdown formatCell MISMATCH: content='%s', target_w=%d, paddingL='%s', paddingR='%s', align=%s -> result='%s', result_w=%d", content, targetWidth, padding.Left, padding.Right, align, result, finalWidth) @@ -229,9 +230,9 @@ func (m *Markdown) formatCell(content string, width int, align tw.Align, padding result += adjStr } } else { - result = tw.TruncateString(result, targetWidth) + result = twwidth.Truncate(result, targetWidth) } - m.logger.Debugf("Markdown formatCell Corrected: target_w=%d, result='%s', new_w=%d", targetWidth, result, tw.DisplayWidth(result)) + m.logger.Debugf("Markdown formatCell Corrected: target_w=%d, result='%s', new_w=%d", targetWidth, result, twwidth.Width(result)) } m.logger.Debugf("Markdown formatCell: content='%s', width=%d, align=%s, paddingL='%s', paddingR='%s' -> '%s' (target %d)", @@ -262,11 +263,11 @@ func (m *Markdown) formatSeparator(width int, align tw.Align) string { } result := sb.String() - currentLen := tw.DisplayWidth(result) + currentLen := twwidth.Width(result) if currentLen < targetWidth { result += strings.Repeat("-", targetWidth-currentLen) } else if currentLen > targetWidth { - result = tw.TruncateString(result, targetWidth) + result = twwidth.Truncate(result, targetWidth) } m.logger.Debugf("Markdown formatSeparator: width=%d, align=%s -> '%s'", width, align, result) @@ -314,7 +315,7 @@ func (m *Markdown) renderMarkdownLine(line []string, ctx tw.Formatting, isHeader output.WriteString(prefix) colIndex := 0 - separatorWidth := tw.DisplayWidth(separator) + separatorWidth := twwidth.Width(separator) for colIndex < numCols { cellCtx, ok := ctx.Row.Current[colIndex] diff --git a/vendor/github.com/olekukonko/tablewriter/renderer/ocean.go b/vendor/github.com/olekukonko/tablewriter/renderer/ocean.go index 58a6c9d6..7a943e9f 100644 --- a/vendor/github.com/olekukonko/tablewriter/renderer/ocean.go +++ b/vendor/github.com/olekukonko/tablewriter/renderer/ocean.go @@ -1,6 +1,7 @@ package renderer import ( + "github.com/olekukonko/tablewriter/pkg/twwidth" "io" "strings" @@ -106,17 +107,9 @@ func (o *Ocean) Header(headers [][]string, ctx tw.Formatting) { if !o.widthsFinalized { o.tryFinalizeWidths(ctx) } - // The batch renderer (table.go/renderHeader) will call Line() for the top border - // and for the header separator if its main config t.config says so. - // So, Ocean.Header should *not* draw these itself when in batch mode. - // For true streaming, table.go's streamRenderHeader would make these Line() calls. - - // Decision: Ocean.Header *only* renders header content. - // Lines (top border, header separator) are managed by the caller (batch or stream logic in table.go). if !o.widthsFinalized { o.logger.Error("Ocean.Header: Cannot render content, widths are not finalized.") - // o.headerContentRendered = true; // No, content wasn't rendered. return } @@ -133,10 +126,7 @@ func (o *Ocean) Header(headers [][]string, ctx tw.Formatting) { o.headerContentRendered = true } else { o.logger.Debug("Ocean.Header: No actual header content lines to render.") - // If header is empty, table.go's renderHeader might still call Line() for the separator. - // o.headerContentRendered remains false if no content. } - // DO NOT draw the header separator line here. Let table.go's renderHeader or streamRenderHeader call o.Line(). } func (o *Ocean) Row(row []string, ctx tw.Formatting) { @@ -145,15 +135,6 @@ func (o *Ocean) Row(row []string, ctx tw.Formatting) { if !o.widthsFinalized { o.tryFinalizeWidths(ctx) } - // Top border / header separator logic: - // If this is the very first output, table.go's batch renderHeader (or streamRenderHeader) - // should have already called Line() for top border and header separator. - // If Header() was called but rendered no content, table.go's renderHeader would still call Line() for the separator. - // If Header() was never called by table.go (e.g. streaming rows directly after Start()), - // then table.go's streamAppendRow needs to handle initial lines. - - // Decision: Ocean.Row *only* renders row content. - if !o.widthsFinalized { o.logger.Error("Ocean.Row: Cannot render content, widths are not finalized.") return @@ -171,11 +152,6 @@ func (o *Ocean) Footer(footers [][]string, ctx tw.Formatting) { o.tryFinalizeWidths(ctx) o.logger.Warn("Ocean.Footer: Widths finalized at Footer stage (unusual).") } - // Separator line before footer: - // This should be handled by table.go's renderFooter or streamRenderFooter calling o.Line(). - - // Decision: Ocean.Footer *only* renders footer content. - if !o.widthsFinalized { o.logger.Error("Ocean.Footer: Cannot render content, widths are not finalized.") return @@ -194,24 +170,19 @@ func (o *Ocean) Footer(footers [][]string, ctx tw.Formatting) { } else { o.logger.Debug("Ocean.Footer: No actual footer content lines to render.") } - // DO NOT draw the bottom border here. Let table.go's main Close or batch renderFooter call o.Line(). } func (o *Ocean) Line(ctx tw.Formatting) { - // This method is now called EXTERNALLY by table.go's batch or stream logic - // to draw all horizontal lines (top border, header sep, footer sep, bottom border). if !o.widthsFinalized { - // If Line is called before widths are known (e.g. table.go's batch renderHeader trying to draw top border) - // we must try to finalize widths from this context. o.tryFinalizeWidths(ctx) if !o.widthsFinalized { o.logger.Error("Ocean.Line: Called but widths could not be finalized. Skipping line rendering.") return } } + // Ensure Line uses the consistent fixedWidths for drawing ctx.Row.Widths = o.fixedWidths - o.logger.Debugf("Ocean.Line DRAWING: Level=%v, Loc=%s, Pos=%s, IsSubRow=%t, WidthsLen=%d", ctx.Level, ctx.Row.Location, ctx.Row.Position, ctx.IsSubRow, ctx.Row.Widths.Len()) jr := NewJunction(JunctionContext{ @@ -262,7 +233,7 @@ func (o *Ocean) Line(ctx tw.Formatting) { if segmentChar == tw.Empty { segmentChar = o.config.Symbols.Row() } - segmentDisplayWidth := tw.DisplayWidth(segmentChar) + segmentDisplayWidth := twwidth.Width(segmentChar) if segmentDisplayWidth <= 0 { segmentDisplayWidth = 1 } @@ -296,12 +267,6 @@ func (o *Ocean) Line(ctx tw.Formatting) { func (o *Ocean) Close() error { o.logger.Debug("Ocean.Close() called.") - // The actual bottom border drawing is expected to be handled by table.go's - // batch render logic (renderFooter) or stream logic (streamRenderBottomBorder) - // by making an explicit call to o.Line() with the correct context. - // Ocean.Close() itself does not draw the bottom border to avoid duplication. - - // Only reset state. o.resetState() return nil } @@ -374,7 +339,7 @@ func (o *Ocean) renderContentLine(ctx tw.Formatting, lineData []string) { } if k < hSpan-1 && o.config.Settings.Separators.BetweenColumns.Enabled() { - currentMergeTotalRenderWidth += tw.DisplayWidth(o.config.Symbols.Column()) + currentMergeTotalRenderWidth += twwidth.Width(o.config.Symbols.Column()) } } actualCellWidthToRender = currentMergeTotalRenderWidth @@ -428,7 +393,7 @@ func (o *Ocean) formatCellContent(content string, cellVisualWidth int, padding t return tw.Empty } - contentDisplayWidth := tw.DisplayWidth(content) + contentDisplayWidth := twwidth.Width(content) padLeftChar := padding.Left if padLeftChar == tw.Empty { @@ -439,8 +404,8 @@ func (o *Ocean) formatCellContent(content string, cellVisualWidth int, padding t padRightChar = tw.Space } - padLeftDisplayWidth := tw.DisplayWidth(padLeftChar) - padRightDisplayWidth := tw.DisplayWidth(padRightChar) + padLeftDisplayWidth := twwidth.Width(padLeftChar) + padRightDisplayWidth := twwidth.Width(padRightChar) spaceForContentAndAlignment := cellVisualWidth - padLeftDisplayWidth - padRightDisplayWidth if spaceForContentAndAlignment < 0 { @@ -448,8 +413,8 @@ func (o *Ocean) formatCellContent(content string, cellVisualWidth int, padding t } if contentDisplayWidth > spaceForContentAndAlignment { - content = tw.TruncateString(content, spaceForContentAndAlignment) - contentDisplayWidth = tw.DisplayWidth(content) + content = twwidth.Truncate(content, spaceForContentAndAlignment) + contentDisplayWidth = twwidth.Width(content) } remainingSpace := spaceForContentAndAlignment - contentDisplayWidth @@ -477,7 +442,7 @@ func (o *Ocean) formatCellContent(content string, cellVisualWidth int, padding t sb.WriteString(PR) sb.WriteString(padRightChar) - currentFormattedWidth := tw.DisplayWidth(sb.String()) + currentFormattedWidth := twwidth.Width(sb.String()) if currentFormattedWidth < cellVisualWidth { if align == tw.AlignRight { prefixSpaces := strings.Repeat(tw.Space, cellVisualWidth-currentFormattedWidth) @@ -490,7 +455,7 @@ func (o *Ocean) formatCellContent(content string, cellVisualWidth int, padding t } else if currentFormattedWidth > cellVisualWidth { tempStr := sb.String() sb.Reset() - sb.WriteString(tw.TruncateString(tempStr, cellVisualWidth)) + sb.WriteString(twwidth.Truncate(tempStr, cellVisualWidth)) o.logger.Warnf("formatCellContent: Final string '%s' (width %d) exceeded target %d. Force truncated.", tempStr, currentFormattedWidth, cellVisualWidth) } return sb.String() diff --git a/vendor/github.com/olekukonko/tablewriter/renderer/svg.go b/vendor/github.com/olekukonko/tablewriter/renderer/svg.go index 013e8b8b..f49e2d77 100644 --- a/vendor/github.com/olekukonko/tablewriter/renderer/svg.go +++ b/vendor/github.com/olekukonko/tablewriter/renderer/svg.go @@ -138,6 +138,7 @@ func NewSVG(configs ...SVGConfig) *SVG { allVisualLineData: make([][][]string, 3), allVisualLineCtx: make([][]tw.Formatting, 3), vMergeTrack: make(map[int]int), + logger: ll.New("svg"), } for i := 0; i < 3; i++ { r.allVisualLineData[i] = make([][]string, 0) diff --git a/vendor/github.com/olekukonko/tablewriter/stream.go b/vendor/github.com/olekukonko/tablewriter/stream.go index 1b230d04..7467e9cb 100644 --- a/vendor/github.com/olekukonko/tablewriter/stream.go +++ b/vendor/github.com/olekukonko/tablewriter/stream.go @@ -3,6 +3,7 @@ package tablewriter import ( "fmt" "github.com/olekukonko/errors" + "github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/tw" "math" ) @@ -220,21 +221,34 @@ func (t *Table) streamAppendRow(row interface{}) error { } if err := t.ensureStreamWidthsCalculated(rawCellsSlice, t.config.Row); err != nil { - return err + return fmt.Errorf("failed to establish stream column count/widths: %w", err) } - if t.streamNumCols > 0 && len(rawCellsSlice) != t.streamNumCols { - t.logger.Warnf("streamAppendRow: Input row column count (%d) != stream column count (%d). Padding/Truncating.", len(rawCellsSlice), t.streamNumCols) - if len(rawCellsSlice) < t.streamNumCols { - paddedCells := make([]string, t.streamNumCols) - copy(paddedCells, rawCellsSlice) - for i := len(rawCellsSlice); i < t.streamNumCols; i++ { - paddedCells[i] = tw.Empty + // Now, check for column mismatch if a column count has been established. + if t.streamNumCols > 0 { + if len(rawCellsSlice) != t.streamNumCols { + if t.config.Stream.StrictColumns { + err := errors.Newf("input row column count (%d) does not match established stream column count (%d) and StrictColumns is enabled", len(rawCellsSlice), t.streamNumCols) + t.logger.Error(err.Error()) + return err + } + // If not strict, retain the old lenient behavior (warn and pad/truncate) + t.logger.Warnf("streamAppendRow: Input row column count (%d) != stream column count (%d). Padding/Truncating (StrictColumns is false).", len(rawCellsSlice), t.streamNumCols) + if len(rawCellsSlice) < t.streamNumCols { + paddedCells := make([]string, t.streamNumCols) + copy(paddedCells, rawCellsSlice) + for i := len(rawCellsSlice); i < t.streamNumCols; i++ { + paddedCells[i] = tw.Empty + } + rawCellsSlice = paddedCells + } else { + rawCellsSlice = rawCellsSlice[:t.streamNumCols] } - rawCellsSlice = paddedCells - } else { - rawCellsSlice = rawCellsSlice[:t.streamNumCols] } + } else if len(rawCellsSlice) > 0 && t.config.Stream.StrictColumns { + err := errors.Newf("failed to establish stream column count from first data row (%d cells) and StrictColumns is enabled", len(rawCellsSlice)) + t.logger.Error(err.Error()) + return err } if t.streamNumCols == 0 { @@ -511,7 +525,7 @@ func (t *Table) streamCalculateWidths(sampling []string, config tw.CellConfig) i ellipsisWidthBuffer := 0 if autoWrapForWidthCalc == tw.WrapTruncate { - ellipsisWidthBuffer = tw.DisplayWidth(tw.CharEllipsis) + ellipsisWidthBuffer = twwidth.Width(tw.CharEllipsis) } varianceBuffer := 2 // Your suggested variance minTotalColWidth := tw.MinimumColumnWidth @@ -525,14 +539,14 @@ func (t *Table) streamCalculateWidths(sampling []string, config tw.CellConfig) i if i < len(sampling) { sampleContent = t.Trimmer(sampling[i]) } - sampleContentDisplayWidth := tw.DisplayWidth(sampleContent) + sampleContentDisplayWidth := twwidth.Width(sampleContent) colPad := paddingForWidthCalc.Global if i < len(paddingForWidthCalc.PerColumn) && paddingForWidthCalc.PerColumn[i].Paddable() { colPad = paddingForWidthCalc.PerColumn[i] } - currentPadLWidth := tw.DisplayWidth(colPad.Left) - currentPadRWidth := tw.DisplayWidth(colPad.Right) + currentPadLWidth := twwidth.Width(colPad.Left) + currentPadRWidth := twwidth.Width(colPad.Right) currentTotalPaddingWidth := currentPadLWidth + currentPadRWidth // Start with the target content width logic @@ -595,7 +609,7 @@ func (t *Table) streamCalculateWidths(sampling []string, config tw.CellConfig) i if t.renderer != nil { rendererConfig := t.renderer.Config() if rendererConfig.Settings.Separators.BetweenColumns.Enabled() { - separatorWidth = tw.DisplayWidth(rendererConfig.Symbols.Column()) + separatorWidth = twwidth.Width(rendererConfig.Symbols.Column()) } } else { separatorWidth = 1 // Default if renderer not available yet diff --git a/vendor/github.com/olekukonko/tablewriter/tablewriter.go b/vendor/github.com/olekukonko/tablewriter/tablewriter.go index 4eca55d7..4aed1a64 100644 --- a/vendor/github.com/olekukonko/tablewriter/tablewriter.go +++ b/vendor/github.com/olekukonko/tablewriter/tablewriter.go @@ -7,11 +7,14 @@ import ( "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" "github.com/olekukonko/tablewriter/pkg/twwarp" + "github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "io" "math" + "os" "reflect" + "runtime" "strings" "sync" ) @@ -407,10 +410,14 @@ 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 - //} + // Get additional system information for debugging + goVersion := runtime.Version() + goOS := runtime.GOOS + goArch := runtime.GOARCH + numCPU := runtime.NumCPU() + + t.logger.Infof("Environment: LC_CTYPE=%s, LANG=%s, TERM=%s", os.Getenv("LC_CTYPE"), os.Getenv("LANG"), os.Getenv("TERM")) + t.logger.Infof("Go Runtime: Version=%s, OS=%s, Arch=%s, CPUs=%d", goVersion, goOS, goArch, numCPU) // send logger to renderer // this will overwrite the default logger @@ -635,7 +642,7 @@ func (t *Table) finalizeHierarchicalMergeBlock(ctx *renderContext, mctx *mergeCo startState.Hierarchical.Present = true startState.Hierarchical.Start = true startState.Hierarchical.Span = finalSpan - startState.Hierarchical.End = (finalSpan == 1) + startState.Hierarchical.End = finalSpan == 1 mctx.rowMerges[startRow][col] = startState } @@ -756,7 +763,7 @@ func (t *Table) printTopBottomCaption(w io.Writer, actualTableWidth int) { captionWrapWidth = t.caption.Width t.logger.Debugf("[printCaption] Using user-defined caption.Width %d for wrapping.", captionWrapWidth) } else if actualTableWidth <= 4 { - captionWrapWidth = tw.DisplayWidth(t.caption.Text) + captionWrapWidth = twwidth.Width(t.caption.Text) t.logger.Debugf("[printCaption] Empty table, no user caption.Width: Using natural caption width %d.", captionWrapWidth) } else { captionWrapWidth = actualTableWidth @@ -879,8 +886,8 @@ func (t *Table) prepareContent(cells []string, config tw.CellConfig) [][]string colPad = config.Padding.PerColumn[i] } - padLeftWidth := tw.DisplayWidth(colPad.Left) - padRightWidth := tw.DisplayWidth(colPad.Right) + padLeftWidth := twwidth.Width(colPad.Left) + padRightWidth := twwidth.Width(colPad.Right) effectiveContentMaxWidth := t.calculateContentMaxWidth(i, config, padLeftWidth, padRightWidth, isStreaming) @@ -902,12 +909,12 @@ func (t *Table) prepareContent(cells []string, config tw.CellConfig) [][]string } finalLinesForCell = append(finalLinesForCell, wrapped...) case tw.WrapTruncate: - if tw.DisplayWidth(line) > effectiveContentMaxWidth { - ellipsisWidth := tw.DisplayWidth(tw.CharEllipsis) + if twwidth.Width(line) > effectiveContentMaxWidth { + ellipsisWidth := twwidth.Width(tw.CharEllipsis) if effectiveContentMaxWidth >= ellipsisWidth { - finalLinesForCell = append(finalLinesForCell, tw.TruncateString(line, effectiveContentMaxWidth-ellipsisWidth, tw.CharEllipsis)) + finalLinesForCell = append(finalLinesForCell, twwidth.Truncate(line, effectiveContentMaxWidth-ellipsisWidth, tw.CharEllipsis)) } else { - finalLinesForCell = append(finalLinesForCell, tw.TruncateString(line, effectiveContentMaxWidth, "")) + finalLinesForCell = append(finalLinesForCell, twwidth.Truncate(line, effectiveContentMaxWidth, "")) } } else { finalLinesForCell = append(finalLinesForCell, line) @@ -915,8 +922,8 @@ 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 { + breakCharWidth := twwidth.Width(tw.CharBreak) + for twwidth.Width(currentLine) > effectiveContentMaxWidth { targetWidth := effectiveContentMaxWidth - breakCharWidth if targetWidth < 0 { targetWidth = 0 @@ -929,7 +936,7 @@ func (t *Table) prepareContent(cells []string, config tw.CellConfig) [][]string tempWidth := 0 for charIdx, r := range runes { runeStr := string(r) - rw := tw.DisplayWidth(runeStr) + rw := twwidth.Width(runeStr) if tempWidth+rw > targetWidth && charIdx > 0 { break } @@ -956,10 +963,10 @@ func (t *Table) prepareContent(cells []string, config tw.CellConfig) [][]string currentLine = string(runes[breakPoint:]) } } - if tw.DisplayWidth(currentLine) > 0 { + if twwidth.Width(currentLine) > 0 { wrapped = append(wrapped, currentLine) } - if len(wrapped) == 0 && tw.DisplayWidth(line) > 0 && len(finalLinesForCell) == 0 { + if len(wrapped) == 0 && twwidth.Width(line) > 0 && len(finalLinesForCell) == 0 { finalLinesForCell = append(finalLinesForCell, line) } else { finalLinesForCell = append(finalLinesForCell, wrapped...) @@ -1441,7 +1448,7 @@ func (t *Table) render() error { actualTableWidth := 0 trimmedBuffer := strings.TrimRight(renderedTableContent, "\r\n \t") for _, line := range strings.Split(trimmedBuffer, "\n") { - w := tw.DisplayWidth(line) + w := twwidth.Width(line) if w > actualTableWidth { actualTableWidth = w } @@ -1713,7 +1720,7 @@ func (t *Table) renderFooter(ctx *renderContext, mctx *mergeContext) error { if j == 0 || representativePadChar == " " { representativePadChar = padChar } - padWidth := tw.DisplayWidth(padChar) + padWidth := twwidth.Width(padChar) if padWidth < 1 { padWidth = 1 } @@ -1728,12 +1735,12 @@ func (t *Table) renderFooter(ctx *renderContext, mctx *mergeContext) error { repeatCount = 0 } rawPaddingContent := strings.Repeat(padChar, repeatCount) - currentWd := tw.DisplayWidth(rawPaddingContent) + currentWd := twwidth.Width(rawPaddingContent) if currentWd < colWd { rawPaddingContent += strings.Repeat(" ", colWd-currentWd) } if currentWd > colWd && colWd > 0 { - rawPaddingContent = tw.TruncateString(rawPaddingContent, colWd) + rawPaddingContent = twwidth.Truncate(rawPaddingContent, colWd) } if colWd == 0 { rawPaddingContent = "" diff --git a/vendor/github.com/olekukonko/tablewriter/tw/fn.go b/vendor/github.com/olekukonko/tablewriter/tw/fn.go index 9610b273..d962ff8d 100644 --- a/vendor/github.com/olekukonko/tablewriter/tw/fn.go +++ b/vendor/github.com/olekukonko/tablewriter/tw/fn.go @@ -3,146 +3,14 @@ package tw import ( - "bytes" // For buffering string output - "github.com/mattn/go-runewidth" // For calculating display width of Unicode characters - "math" // For mathematical operations like ceiling - "regexp" // For regular expression handling of ANSI codes - "strconv" // For string-to-number conversions - "strings" // For string manipulation utilities - "unicode" // For Unicode character classification - "unicode/utf8" // For UTF-8 rune handling + "github.com/olekukonko/tablewriter/pkg/twwidth" + "math" // For mathematical operations like ceiling + "strconv" // For string-to-number conversions + "strings" // For string manipulation utilities + "unicode" // For Unicode character classification + "unicode/utf8" // For UTF-8 rune handling ) -// ansi is a compiled regex pattern used to strip ANSI escape codes. -// These codes are used in terminal output for styling and are invisible in rendered text. -var ansi = CompileANSIFilter() - -// CompileANSIFilter constructs and compiles a regex for matching ANSI sequences. -// It supports both control sequences (CSI) and operating system commands (OSC) like hyperlinks. -func CompileANSIFilter() *regexp.Regexp { - var regESC = "\x1b" // ASCII escape character - var regBEL = "\x07" // ASCII bell character - - // ANSI string terminator: either ESC+\ or BEL - var regST = "(" + regexp.QuoteMeta(regESC+"\\") + "|" + regexp.QuoteMeta(regBEL) + ")" - // Control Sequence Introducer (CSI): ESC[ followed by parameters and a final byte - var regCSI = regexp.QuoteMeta(regESC+"[") + "[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]" - // Operating System Command (OSC): ESC] followed by arbitrary content until a terminator - var regOSC = regexp.QuoteMeta(regESC+"]") + ".*?" + regST - - // Combine CSI and OSC patterns into a single regex - return regexp.MustCompile("(" + regCSI + "|" + regOSC + ")") -} - -// DisplayWidth calculates the visual width of a string, excluding ANSI escape sequences. -// It uses go-runewidth to handle Unicode characters correctly. -func DisplayWidth(str string) int { - // Strip ANSI codes before calculating width to avoid counting invisible characters - return runewidth.StringWidth(ansi.ReplaceAllLiteralString(str, "")) -} - -// TruncateString shortens a string to a specified maximum display width while preserving ANSI color codes. -// An optional suffix (e.g., "...") is appended if truncation occurs. -func TruncateString(s string, maxWidth int, suffix ...string) string { - // If maxWidth is 0 or negative, return an empty string - if maxWidth <= 0 { - return "" - } - - // Join suffix slices into a single string and calculate its display width - suffixStr := strings.Join(suffix, " ") - suffixDisplayWidth := 0 - if len(suffixStr) > 0 { - // Strip ANSI from suffix for accurate width calculation - suffixDisplayWidth = runewidth.StringWidth(ansi.ReplaceAllLiteralString(suffixStr, "")) - } - - // Check if the string (without ANSI) plus suffix fits within maxWidth - strippedS := ansi.ReplaceAllLiteralString(s, "") - if runewidth.StringWidth(strippedS)+suffixDisplayWidth <= maxWidth { - // If it fits, return the original string (with ANSI) plus suffix - return s + suffixStr - } - - // Handle edge case: maxWidth is too small for even the suffix - if maxWidth < suffixDisplayWidth { - // Try truncating the string without suffix - return TruncateString(s, maxWidth) // Recursive call without suffix - } - // Handle edge case: maxWidth exactly equals suffix width - if maxWidth == suffixDisplayWidth { - if runewidth.StringWidth(strippedS) > 0 { - // If there's content, it's fully truncated; return suffix - return suffixStr - } - return "" // No content and no space for content; return empty string - } - - // Calculate the maximum width available for the content (excluding suffix) - targetContentDisplayWidth := maxWidth - suffixDisplayWidth - - var contentBuf bytes.Buffer // Buffer for building truncated content - var currentContentDisplayWidth int // Tracks display width of content - var ansiSeqBuf bytes.Buffer // Buffer for collecting ANSI sequences - inAnsiSequence := false // Tracks if we're inside an ANSI sequence - - // Iterate over runes to build content while respecting maxWidth - for _, r := range s { - if r == '\x1b' { // Start of ANSI escape sequence - if inAnsiSequence { - // Unexpected new ESC; flush existing sequence - contentBuf.Write(ansiSeqBuf.Bytes()) - ansiSeqBuf.Reset() - } - inAnsiSequence = true - ansiSeqBuf.WriteRune(r) - } else if inAnsiSequence { - ansiSeqBuf.WriteRune(r) - // Detect end of common ANSI sequences (e.g., SGR 'm' or CSI terminators) - if r == 'm' || (ansiSeqBuf.Len() > 2 && ansiSeqBuf.Bytes()[1] == '[' && r >= '@' && r <= '~') { - inAnsiSequence = false - contentBuf.Write(ansiSeqBuf.Bytes()) // Append completed sequence - ansiSeqBuf.Reset() - } else if ansiSeqBuf.Len() > 128 { // Prevent buffer overflow for malformed sequences - inAnsiSequence = false - contentBuf.Write(ansiSeqBuf.Bytes()) - ansiSeqBuf.Reset() - } - } else { - // Handle displayable characters - runeDisplayWidth := runewidth.RuneWidth(r) - if currentContentDisplayWidth+runeDisplayWidth > targetContentDisplayWidth { - // Adding this rune would exceed the content width; stop here - break - } - contentBuf.WriteRune(r) - currentContentDisplayWidth += runeDisplayWidth - } - } - - // Append any unterminated ANSI sequence - if ansiSeqBuf.Len() > 0 { - contentBuf.Write(ansiSeqBuf.Bytes()) - } - - finalContent := contentBuf.String() - - // Append suffix if content was truncated or if suffix is provided and content exists - if runewidth.StringWidth(ansi.ReplaceAllLiteralString(finalContent, "")) < runewidth.StringWidth(strippedS) { - // Content was truncated; append suffix - return finalContent + suffixStr - } else if len(suffixStr) > 0 && len(finalContent) > 0 { - // No truncation but suffix exists; append it - return finalContent + suffixStr - } else if len(suffixStr) > 0 && len(strippedS) == 0 { - // Original string was empty; return suffix - return suffixStr - } - - // Return content as is (with preserved ANSI codes) - return finalContent -} - // Title normalizes and uppercases a label string for use in headers. // It replaces underscores and certain dots with spaces and trims whitespace. func Title(name string) string { @@ -172,7 +40,7 @@ func Title(name string) string { // PadCenter centers a string within a specified width using a padding character. // Extra padding is split between left and right, with slight preference to left if uneven. func PadCenter(s, pad string, width int) string { - gap := width - DisplayWidth(s) + gap := width - twwidth.Width(s) if gap > 0 { // Calculate left and right padding; ceil ensures left gets extra if gap is odd gapLeft := int(math.Ceil(float64(gap) / 2)) @@ -185,7 +53,7 @@ func PadCenter(s, pad string, width int) string { // PadRight left-aligns a string within a specified width, filling remaining space on the right with padding. func PadRight(s, pad string, width int) string { - gap := width - DisplayWidth(s) + gap := width - twwidth.Width(s) if gap > 0 { // Append padding to the right return s + strings.Repeat(pad, gap) @@ -196,7 +64,7 @@ func PadRight(s, pad string, width int) string { // PadLeft right-aligns a string within a specified width, filling remaining space on the left with padding. func PadLeft(s, pad string, width int) string { - gap := width - DisplayWidth(s) + gap := width - twwidth.Width(s) if gap > 0 { // Prepend padding to the left return strings.Repeat(pad, gap) + s @@ -208,9 +76,9 @@ func PadLeft(s, pad string, width int) string { // Pad aligns a string within a specified width using a padding character. // It truncates if the string is wider than the target width. func Pad(s string, padChar string, totalWidth int, alignment Align) string { - sDisplayWidth := DisplayWidth(s) + sDisplayWidth := twwidth.Width(s) if sDisplayWidth > totalWidth { - return TruncateString(s, totalWidth) // Only truncate if necessary + return twwidth.Truncate(s, totalWidth) // Only truncate if necessary } switch alignment { case AlignLeft: @@ -334,7 +202,7 @@ func BreakPoint(s string, limit int) int { runeCount := 0 // Iterate over runes, accumulating display width for _, r := range s { - runeWidth := DisplayWidth(string(r)) // Calculate width of individual rune + runeWidth := twwidth.Width(string(r)) // Calculate width of individual rune if currentWidth+runeWidth > limit { // Adding this rune would exceed the limit; breakpoint is before this rune if currentWidth == 0 { diff --git a/vendor/github.com/olekukonko/tablewriter/tw/renderer.go b/vendor/github.com/olekukonko/tablewriter/tw/renderer.go index 6caf05e4..cf2779b2 100644 --- a/vendor/github.com/olekukonko/tablewriter/tw/renderer.go +++ b/vendor/github.com/olekukonko/tablewriter/tw/renderer.go @@ -118,6 +118,13 @@ type Border struct { type StreamConfig struct { Enable bool + // StrictColumns, if true, causes Append() to return an error + // in streaming mode if the number of cells in an appended row + // does not match the established number of columns for the stream. + // If false (default), rows with mismatched column counts will be + // padded or truncated with a warning log. + StrictColumns bool + // 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. diff --git a/vendor/github.com/olekukonko/tablewriter/tw/tw.go b/vendor/github.com/olekukonko/tablewriter/tw/tw.go index 020de742..f1cbb9e5 100644 --- a/vendor/github.com/olekukonko/tablewriter/tw/tw.go +++ b/vendor/github.com/olekukonko/tablewriter/tw/tw.go @@ -56,7 +56,7 @@ const ( ) const ( - SectionHeader = "heder" + SectionHeader = "header" SectionRow = "row" SectionFooter = "footer" ) diff --git a/vendor/github.com/olekukonko/tablewriter/zoo.go b/vendor/github.com/olekukonko/tablewriter/zoo.go index 04a0c15a..b24f230c 100644 --- a/vendor/github.com/olekukonko/tablewriter/zoo.go +++ b/vendor/github.com/olekukonko/tablewriter/zoo.go @@ -4,6 +4,7 @@ import ( "database/sql" "fmt" "github.com/olekukonko/errors" + "github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/tw" "io" "math" @@ -165,7 +166,7 @@ func (t *Table) applyHorizontalMergeWidths(position tw.Position, ctx *renderCont if t.renderer != nil { rendererConfig := t.renderer.Config() if rendererConfig.Settings.Separators.BetweenColumns.Enabled() { - separatorWidth = tw.DisplayWidth(rendererConfig.Symbols.Column()) + separatorWidth = twwidth.Width(rendererConfig.Symbols.Column()) } } @@ -541,7 +542,7 @@ func (t *Table) buildCoreCellContexts(line []string, merges map[int]tw.MergeStat // It generates a []string where each element is the padding content for a column, using the specified padChar. func (t *Table) buildPaddingLineContents(padChar string, widths tw.Mapper[int, int], numCols int, merges map[int]tw.MergeState) []string { line := make([]string, numCols) - padWidth := tw.DisplayWidth(padChar) + padWidth := twwidth.Width(padChar) if padWidth < 1 { padWidth = 1 } @@ -715,15 +716,15 @@ func (t *Table) calculateAndNormalizeWidths(ctx *renderContext) error { 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) + actualMergedHeaderContentPhysicalWidth := twwidth.Width(mergedContentString) + + twwidth.Width(headerCellPadding.Left) + + twwidth.Width(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()) + numSeparatorsInFullSpan = (ctx.numCols - 1) * twwidth.Width(t.renderer.Config().Symbols.Column()) } } totalCurrentSpanPhysicalWidth := currentSumOfColumnWidths + numSeparatorsInFullSpan @@ -784,7 +785,7 @@ func (t *Table) calculateAndNormalizeWidths(ctx *renderContext) error { 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()) + numSeparators = (ctx.numCols - 1) * twwidth.Width(t.renderer.Config().Symbols.Column()) } totalCurrentTablePhysicalWidth := currentSumOfFinalColWidths + numSeparators if totalCurrentTablePhysicalWidth > t.config.Widths.Global { @@ -1655,16 +1656,16 @@ func (t *Table) updateWidths(row []string, widths tw.Mapper[int, int], padding t t.logger.Debugf(" Col %d: Using global padding: L:'%s' R:'%s'", i, padding.Global.Left, padding.Global.Right) } - padLeftWidth := tw.DisplayWidth(colPad.Left) - padRightWidth := tw.DisplayWidth(colPad.Right) + padLeftWidth := twwidth.Width(colPad.Left) + padRightWidth := twwidth.Width(colPad.Right) // Split cell into lines and find maximum content width lines := strings.Split(cell, tw.NewLine) contentWidth := 0 for _, line := range lines { - lineWidth := tw.DisplayWidth(line) + lineWidth := twwidth.Width(line) if t.config.Behavior.TrimSpace.Enabled() { - lineWidth = tw.DisplayWidth(t.Trimmer(line)) + lineWidth = twwidth.Width(t.Trimmer(line)) } if lineWidth > contentWidth { contentWidth = lineWidth diff --git a/vendor/modules.txt b/vendor/modules.txt index 3d358b45..b1d05a3c 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -183,10 +183,11 @@ github.com/olekukonko/errors github.com/olekukonko/ll github.com/olekukonko/ll/lh github.com/olekukonko/ll/lx -# github.com/olekukonko/tablewriter v1.0.7 +# github.com/olekukonko/tablewriter v1.0.8 ## explicit; go 1.21 github.com/olekukonko/tablewriter github.com/olekukonko/tablewriter/pkg/twwarp +github.com/olekukonko/tablewriter/pkg/twwidth github.com/olekukonko/tablewriter/renderer github.com/olekukonko/tablewriter/tw # github.com/pelletier/go-toml/v2 v2.2.3