Files
opencloud/vendor/github.com/olekukonko/tablewriter/stream.go
dependabot[bot] 5e6fc50e5e build(deps): bump github.com/olekukonko/tablewriter from 1.0.8 to 1.0.9
Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 1.0.8 to 1.0.9.
- [Commits](https://github.com/olekukonko/tablewriter/compare/v1.0.8...v1.0.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-26 06:40:11 +00:00

1164 lines
48 KiB
Go

package tablewriter
import (
"github.com/olekukonko/errors"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/tw"
"math"
)
// Close finalizes the table stream.
// It requires the stream to be started (by calling NewStreamTable).
// It calls the renderer's Close method to render final elements (like the bottom border) and close the stream.
func (t *Table) Close() error {
t.logger.Debug("Close() called. Finalizing stream.")
// Ensure stream was actually started and enabled
if !t.config.Stream.Enable || !t.hasPrinted {
t.logger.Warn("Close() called but streaming not enabled or not started. Ignoring Close() actions.")
// If renderer has a Close method that should always be called, consider that.
// For Blueprint, Close is a no-op, so returning early is fine.
// If we always call renderer.Close(), ensure it's safe if renderer.Start() wasn't called.
// Let's only call renderer.Close if stream was started.
if t.hasPrinted && t.renderer != nil { // Check if renderer is not nil for safety
t.renderer.Close() // Still call renderer's close for cleanup
}
t.hasPrinted = false // Reset flag
return nil
}
// Render stored footer if any
if len(t.streamFooterLines) > 0 {
t.logger.Debug("Close(): Rendering stored footer.")
if err := t.streamRenderFooter(t.streamFooterLines); err != nil {
t.logger.Errorf("Close(): Failed to render stream footer: %v", err)
// Continue to try and close renderer and render bottom border
}
}
// Render the final table bottom border
t.logger.Debug("Close(): Rendering stream bottom border.")
if err := t.streamRenderBottomBorder(); err != nil {
t.logger.Errorf("Close(): Failed to render stream bottom border: %v", err)
// Continue to try and close renderer
}
// Call the underlying renderer's Close method
err := t.renderer.Close()
if err != nil {
t.logger.Errorf("Renderer.Close() failed: %v", err)
}
// Reset streaming state
t.hasPrinted = false
t.headerRendered = false
t.firstRowRendered = false
t.lastRenderedLineContent = nil
t.lastRenderedMergeState = nil
t.lastRenderedPosition = ""
t.streamFooterLines = nil
// t.streamWidths should persist if we want to make multiple Start/Close calls on same config?
// For now, let's assume Start re-evaluates. If widths are from StreamConfig, they'd be reused.
// If derived, they'd be re-derived. Let's clear for true reset.
t.streamWidths = tw.NewMapper[int, int]()
t.streamNumCols = 0
// t.streamRowCounter = 0 // Removed this field
t.logger.Debug("Stream ended. hasPrinted = false.")
return err // Return error from renderer.Close or other significant errors
}
// Start initializes the table stream.
// In this streaming model, renderer.Start() is primarily called in NewStreamTable.
// This method serves as a safeguard or point for adding pre-rendering logic.
// Start initializes the table stream.
// It is the entry point for streaming mode.
// Requires t.config.Stream.Enable to be true.
// Returns an error if streaming is disabled or the renderer does not support streaming,
// or if called multiple times on the same stream.
func (t *Table) Start() error {
t.ensureInitialized() // Ensures basic setup like loggers
if !t.config.Stream.Enable {
// Start() should only be called when streaming is explicitly enabled.
// Otherwise, the user should call Render() for batch mode.
t.logger.Warn("Start() called but streaming is disabled. Call Render() instead for batch mode.")
return errors.New("start() called but streaming is disabled")
}
if !t.renderer.Config().Streaming {
// Check if the configured renderer actually supports streaming.
t.logger.Error("Configured renderer does not support streaming.")
return errors.Newf("renderer does not support streaming")
}
//t.renderer.Start(t.writer)
//t.renderer.Logger(t.logger)
if t.hasPrinted {
// Prevent calling Start() multiple times on the same stream instance.
t.logger.Warn("Start() called multiple times for the same table stream. Ignoring subsequent calls.")
return nil
}
t.logger.Debug("Starting table stream.")
// Initialize/reset streaming state flags and buffers
t.headerRendered = false
t.firstRowRendered = false
t.lastRenderedLineContent = nil
t.lastRenderedPosition = "" // Reset last rendered position
t.streamFooterLines = nil // Reset footer buffer
t.streamNumCols = 0 // Reset derived column count
// Calculate initial fixed widths if provided in StreamConfig.Widths
// These widths will be used for all subsequent rendering in streaming mode.
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.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) {
if col > maxColIdx {
maxColIdx = col
}
// Ensure configured widths are reasonable (>0 becomes >=1, <0 becomes 0)
if width > 0 && width < 1 {
t.streamWidths.Set(col, 1)
} else if width < 0 {
t.streamWidths.Set(col, 0) // Negative width means hide column
}
})
if maxColIdx >= 0 {
t.streamNumCols = maxColIdx + 1
t.logger.Debugf("Derived streamNumCols from PerColumn widths: %d", t.streamNumCols)
} else {
// PerColumn map exists but is empty? Or all negative widths? Assume 0 columns for now.
t.streamNumCols = 0
t.logger.Debugf("PerColumn widths map is effectively empty or contains only negative values, streamNumCols = 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.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
} else {
// No explicit stream widths in config. They will be calculated from the first data (Header or first Row).
t.logger.Debug("No explicit stream widths configured in StreamConfig. Will derive from first data.")
t.streamWidths = tw.NewMapper[int, int]() // Initialize as empty, will be populated later
t.streamNumCols = 0 // NumCols will be determined by first data
}
// Log warnings if incompatible features are enabled in streaming config
// Vertical/Hierarchical merges require processing all rows together.
if t.config.Header.Formatting.MergeMode&(tw.MergeVertical|tw.MergeHierarchical) != 0 {
t.logger.Warnf("Vertical or Hierarchical merge modes enabled on Header config (%d) but are unsupported in streaming mode. Only Horizontal merge will be considered.", t.config.Header.Formatting.MergeMode)
}
if t.config.Row.Formatting.MergeMode&(tw.MergeVertical|tw.MergeHierarchical) != 0 {
t.logger.Warnf("Vertical or Hierarchical merge modes enabled on Row config (%d) but are unsupported in streaming mode. Only Horizontal merge will be considered.", t.config.Row.Formatting.MergeMode)
}
if t.config.Footer.Formatting.MergeMode&(tw.MergeVertical|tw.MergeHierarchical) != 0 {
t.logger.Warnf("Vertical or Hierarchical merge modes enabled on Footer config (%d) but are unsupported in streaming mode. Only Horizontal merge will be considered.", t.config.Footer.Formatting.MergeMode)
}
// AutoHide requires processing all row data to find empty columns.
if t.config.Behavior.AutoHide.Enabled() {
t.logger.Warn("AutoHide is enabled in config but is ignored in streaming mode.")
}
// Call the renderer's start method for the stream.
err := t.renderer.Start(t.writer)
if err == nil {
t.hasPrinted = true // Mark as started successfully only if renderer.Start works
t.logger.Debug("Renderer.Start() succeeded. Table stream initiated.")
} else {
// Reset state if renderer.Start fails
t.hasPrinted = false
t.headerRendered = false
t.firstRowRendered = false
t.lastRenderedLineContent = nil
t.lastRenderedPosition = ""
t.streamFooterLines = nil
t.streamWidths = tw.NewMapper[int, int]() // Clear any widths that might have been set
t.streamNumCols = 0
t.logger.Errorf("Renderer.Start() failed: %v. Streaming initialization failed.", err)
}
return err
}
// streamAppendRow processes and renders a single row in streaming mode.
// It calculates/uses fixed stream widths, processes content, renders separators and lines,
// and updates streaming state.
// It assumes Start() has already been called and t.hasPrinted is true.
func (t *Table) streamAppendRow(row interface{}) error {
t.logger.Debugf("streamAppendRow called with row: %v (type: %T)", row, row)
if !t.config.Stream.Enable {
return errors.New("streaming mode is disabled")
}
rawCellsSlice, err := t.convertCellsToStrings(row, t.config.Row)
if err != nil {
t.logger.Errorf("streamAppendRow: Failed to convert row to strings: %v", err)
return errors.Newf("failed to convert row to strings").Wrap(err)
}
if len(rawCellsSlice) == 0 {
t.logger.Debug("streamAppendRow: No raw cells after conversion, skipping row rendering.")
if !t.firstRowRendered {
t.firstRowRendered = true
t.logger.Debug("streamAppendRow: Marked first row rendered (empty content after processing).")
}
return nil
}
if err := t.ensureStreamWidthsCalculated(rawCellsSlice, t.config.Row); err != nil {
return errors.New("failed to establish stream column count/widths").Wrap(err)
}
// 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]
}
}
} 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 {
t.logger.Warn("streamAppendRow: streamNumCols is 0. Cannot render row.")
return errors.New("cannot render row, column count is zero and could not be determined")
}
_, rowMerges, _ := t.prepareWithMerges([][]string{rawCellsSlice}, t.config.Row, tw.Row)
processedRowLines := t.prepareContent(rawCellsSlice, t.config.Row)
t.logger.Debugf("streamAppendRow: Processed row lines: %d lines", len(processedRowLines))
f := t.renderer
cfg := t.renderer.Config()
if !t.headerRendered && !t.firstRowRendered && t.lastRenderedPosition == "" {
if cfg.Borders.Top.Enabled() && cfg.Settings.Lines.ShowTop.Enabled() {
t.logger.Debug("streamAppendRow: Rendering table top border (first element is a row).")
var nextCellsCtx map[int]tw.CellContext
if len(processedRowLines) > 0 {
firstRowLineResp := t.streamBuildCellContexts(
tw.Row, 0, 0, processedRowLines, rowMerges, t.config.Row,
)
nextCellsCtx = firstRowLineResp.cells
}
f.Line(tw.Formatting{
Row: tw.RowContext{
Widths: t.streamWidths,
ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths},
Next: nextCellsCtx,
Position: tw.Row,
Location: tw.LocationFirst,
},
Level: tw.LevelHeader,
IsSubRow: false,
NormalizedWidths: t.streamWidths,
})
t.logger.Debug("streamAppendRow: Top border rendered.")
}
}
shouldDrawHeaderRowSeparator := t.headerRendered && !t.firstRowRendered && cfg.Settings.Lines.ShowHeaderLine.Enabled()
shouldDrawRowRowSeparator := t.firstRowRendered && cfg.Settings.Separators.BetweenRows.Enabled()
firstCellForLog := ""
if len(rawCellsSlice) > 0 {
firstCellForLog = rawCellsSlice[0]
}
t.logger.Debugf("streamAppendRow: Separator Pre-Check for row starting with '%s': headerRendered=%v, firstRowRendered=%v, ShowHeaderLine=%v, BetweenRows=%v, lastRenderedPos=%q",
firstCellForLog, t.headerRendered, t.firstRowRendered, cfg.Settings.Lines.ShowHeaderLine.Enabled(),
cfg.Settings.Separators.BetweenRows.Enabled(), t.lastRenderedPosition)
t.logger.Debugf("streamAppendRow: Separator Decision Flags for row starting with '%s': shouldDrawHeaderRowSeparator=%v, shouldDrawRowRowSeparator=%v",
firstCellForLog, shouldDrawHeaderRowSeparator, shouldDrawRowRowSeparator)
if (shouldDrawHeaderRowSeparator || shouldDrawRowRowSeparator) && t.lastRenderedPosition != tw.Position("separator") {
t.logger.Debugf("streamAppendRow: Rendering separator line for row starting with '%s'.", firstCellForLog)
prevCellsCtx := t.streamRenderedMergeState(t.lastRenderedLineContent, t.lastRenderedMergeState)
var nextCellsCtx map[int]tw.CellContext
if len(processedRowLines) > 0 {
firstRowLineResp := t.streamBuildCellContexts(tw.Row, 0, 0, processedRowLines, rowMerges, t.config.Row)
nextCellsCtx = firstRowLineResp.cells
}
f.Line(tw.Formatting{
Row: tw.RowContext{
Widths: t.streamWidths,
ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths},
Current: prevCellsCtx,
Previous: nil,
Next: nextCellsCtx,
Position: tw.Row,
Location: tw.LocationMiddle,
},
Level: tw.LevelBody,
IsSubRow: false,
NormalizedWidths: t.streamWidths,
})
t.lastRenderedPosition = tw.Position("separator")
t.lastRenderedLineContent = nil
t.lastRenderedMergeState = nil
t.logger.Debug("streamAppendRow: Separator line rendered. Updated lastRenderedPosition to 'separator'.")
} else {
details := ""
if !(shouldDrawHeaderRowSeparator || shouldDrawRowRowSeparator) {
details = "neither header/row nor row/row separator was flagged true"
} else if t.lastRenderedPosition == tw.Position("separator") {
details = "lastRenderedPosition is already 'separator'"
} else {
details = "an unexpected combination of conditions"
}
t.logger.Debugf("streamAppendRow: Separator not drawn for row '%s' because %s.", firstCellForLog, details)
}
if len(processedRowLines) == 0 {
t.logger.Debugf("streamAppendRow: No processed row lines to render for row starting with '%s'.", firstCellForLog)
if !t.firstRowRendered {
t.firstRowRendered = true
t.logger.Debugf("streamAppendRow: Marked first row rendered (empty content after processing).")
}
return nil
}
totalRowLines := len(processedRowLines)
for i := 0; i < totalRowLines; i++ {
resp := t.streamBuildCellContexts(tw.Row, 0, i, processedRowLines, rowMerges, t.config.Row)
t.logger.Debug("streamAppendRow: Rendering row line %d/%d with location %v for row starting with '%s'.", i, totalRowLines, resp.location, firstCellForLog)
f.Row(resp.cellsContent, tw.Formatting{
Row: tw.RowContext{
Widths: t.streamWidths,
ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths},
Current: resp.cells,
Previous: resp.prevCells,
Next: resp.nextCells,
Position: tw.Row,
Location: resp.location,
},
Level: tw.LevelBody,
IsSubRow: i > 0,
NormalizedWidths: t.streamWidths,
HasFooter: len(t.streamFooterLines) > 0,
})
t.lastRenderedLineContent = resp.cellsContent
t.lastRenderedMergeState = make(map[int]tw.MergeState)
for colIdx, cellCtx := range resp.cells {
t.lastRenderedMergeState[colIdx] = cellCtx.Merge
}
t.lastRenderedPosition = tw.Row
}
if !t.firstRowRendered {
t.firstRowRendered = true
t.logger.Debug("streamAppendRow: Marked first row rendered (after processing content).")
}
t.logger.Debug("streamAppendRow: Row processing completed for row starting with '%s'.", firstCellForLog)
return nil
}
// streamBuildCellContexts creates CellContext objects for a given line in streaming mode.
// Parameters:
// - position: The section being processed (Header, Row, Footer).
// - rowIdx: The row index within its section (always 0 for Header/Footer, row number for Row).
// - lineIdx: The line index within the processed lines for this block.
// - processedLines: All multi-lines for the current row/header/footer block.
// - sectionMerges: Merge states for the section or row (map[int]tw.MergeState).
// - sectionConfig: The CellConfig for this section (Header, Row, Footer).
// Returns a renderMergeResponse with Current, Previous, Next cells, cellsContent, and the determined Location.
func (t *Table) streamBuildCellContexts(
position tw.Position,
rowIdx, lineIdx int,
processedLines [][]string,
sectionMerges map[int]tw.MergeState,
sectionConfig tw.CellConfig,
) renderMergeResponse {
t.logger.Debug("streamBuildCellContexts: Building contexts for position=%s, rowIdx=%d, lineIdx=%d", position, rowIdx, lineIdx)
resp := renderMergeResponse{
cells: make(map[int]tw.CellContext),
prevCells: nil,
nextCells: nil,
cellsContent: make([]string, t.streamNumCols),
location: tw.LocationMiddle,
}
if t.streamWidths == nil || t.streamWidths.Len() == 0 || t.streamNumCols == 0 {
t.logger.Warn("streamBuildCellContexts: streamWidths is not set or streamNumCols is 0. Returning empty contexts.")
return resp
}
currentLineContent := make([]string, t.streamNumCols)
if lineIdx >= 0 && lineIdx < len(processedLines) {
currentLineContent = padLine(processedLines[lineIdx], t.streamNumCols)
} else {
t.logger.Warnf("streamBuildCellContexts: lineIdx %d out of bounds for processedLines (len %d) at position %s, rowIdx %d. Using empty line.", lineIdx, len(processedLines), position, rowIdx)
for j := range currentLineContent {
currentLineContent[j] = tw.Empty
}
}
resp.cellsContent = currentLineContent
colAligns := t.buildAligns(sectionConfig)
colPadding := t.buildPadding(sectionConfig.Padding)
resp.cells = t.buildCoreCellContexts(currentLineContent, sectionMerges, t.streamWidths, colAligns, colPadding, t.streamNumCols)
if t.lastRenderedLineContent != nil && t.lastRenderedPosition.Validate() == nil {
resp.prevCells = t.streamRenderedMergeState(t.lastRenderedLineContent, t.lastRenderedMergeState)
}
totalLinesInBlock := len(processedLines)
if lineIdx < totalLinesInBlock-1 {
resp.nextCells = make(map[int]tw.CellContext)
nextLineContent := padLine(processedLines[lineIdx+1], t.streamNumCols)
nextCells := t.buildCoreCellContexts(nextLineContent, sectionMerges, t.streamWidths, colAligns, colPadding, t.streamNumCols)
for j := 0; j < t.streamNumCols; j++ {
resp.nextCells[j] = nextCells[j]
}
}
isFirstLineOfBlock := (lineIdx == 0)
if isFirstLineOfBlock && (t.lastRenderedLineContent == nil || t.lastRenderedPosition != position) {
resp.location = tw.LocationFirst
}
t.logger.Debug("streamBuildCellContexts: Position %s, Row %d, Line %d/%d. Location: %v. Prev Pos: %v. Has Prev: %v.",
position, rowIdx, lineIdx, totalLinesInBlock, resp.location, t.lastRenderedPosition, t.lastRenderedLineContent != nil)
return resp
}
// streamCalculateWidths determines the fixed column widths for streaming mode.
// It prioritizes widths from StreamConfig.Widths.PerColumn, then StreamConfig.Widths.Global,
// then derives from the provided sample data lines.
// It populates t.streamWidths and t.streamNumCols if they are currently empty.
// The sampleDataLines should be the *raw* input lines (e.g., []string for Header/Footer, or the first row's []string cells for Row).
// 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(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(sampling), config.Formatting)
determinedNumCols := 0
if t.config.Widths.PerColumn != nil && t.config.Widths.PerColumn.Len() > 0 {
maxColIdx := -1
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(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)")
t.streamNumCols = 0
t.streamWidths = tw.NewMapper[int, int]()
return 0
}
t.streamNumCols = determinedNumCols
t.streamWidths = tw.NewMapper[int, int]()
// Use padding and autowrap from the provided config
paddingForWidthCalc := config.Padding
autoWrapForWidthCalc := config.Formatting.AutoWrap
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.Widths.PerColumn.OK(i)
if !ok {
width = 0
}
if width > 0 && width < 1 {
width = 1
} else if width < 0 {
width = 0
}
t.streamWidths.Set(i, width)
}
} else {
// 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(sampling) > 0 {
// updateWidths calculates: DisplayWidth(content) + padLeft + padRight
t.updateWidths(sampling, tempRequiredWidths, paddingForWidthCalc)
}
ellipsisWidthBuffer := 0
if autoWrapForWidthCalc == tw.WrapTruncate {
ellipsisWidthBuffer = twwidth.Width(tw.CharEllipsis)
}
varianceBuffer := 2 // Your suggested variance
minTotalColWidth := tw.MinimumColumnWidth
// Example: if t.config.Stream.MinAutoColumnWidth > 0 { minTotalColWidth = t.config.Stream.MinAutoColumnWidth }
for i := 0; i < t.streamNumCols; i++ {
// baseCellWidth (content_width + padding_width) comes from tempRequiredWidths.Get(i)
// We need to deconstruct it to apply logic to content_width first.
sampleContent := ""
if i < len(sampling) {
sampleContent = t.Trimmer(sampling[i])
}
sampleContentDisplayWidth := twwidth.Width(sampleContent)
colPad := paddingForWidthCalc.Global
if i < len(paddingForWidthCalc.PerColumn) && paddingForWidthCalc.PerColumn[i].Paddable() {
colPad = paddingForWidthCalc.PerColumn[i]
}
currentPadLWidth := twwidth.Width(colPad.Left)
currentPadRWidth := twwidth.Width(colPad.Right)
currentTotalPaddingWidth := currentPadLWidth + currentPadRWidth
// Start with the target content width logic
targetContentWidth := sampleContentDisplayWidth
if autoWrapForWidthCalc == tw.WrapTruncate {
// If content is short, ensure it's at least wide enough for an ellipsis
if targetContentWidth < ellipsisWidthBuffer {
targetContentWidth = ellipsisWidthBuffer
}
}
targetContentWidth += varianceBuffer // Add variance
// Now calculate the total cell width based on this buffered content target + padding
calculatedWidth := targetContentWidth + currentTotalPaddingWidth
// Apply an absolute minimum total column width
if calculatedWidth > 0 && calculatedWidth < minTotalColWidth {
t.logger.Debug("streamCalculateWidths: Col %d, InitialCalcW=%d (ContentTarget=%d + Pad=%d) is less than MinTotalW=%d. Adjusting to MinTotalW.",
i, calculatedWidth, targetContentWidth, currentTotalPaddingWidth, minTotalColWidth)
calculatedWidth = minTotalColWidth
} else if calculatedWidth <= 0 && sampleContentDisplayWidth > 0 { // If content exists but calc width is 0 (e.g. large negative variance)
// Ensure at least min width or content + padding + buffers
fallbackWidth := sampleContentDisplayWidth + currentTotalPaddingWidth
if autoWrapForWidthCalc == tw.WrapTruncate {
fallbackWidth += ellipsisWidthBuffer
}
fallbackWidth += varianceBuffer
calculatedWidth = tw.Max(minTotalColWidth, fallbackWidth)
if calculatedWidth <= 0 && (currentTotalPaddingWidth+1) > 0 { // last resort if all else is zero
calculatedWidth = currentTotalPaddingWidth + 1
} else if calculatedWidth <= 0 {
calculatedWidth = 1 // absolute last resort
}
t.logger.Debug("streamCalculateWidths: Col %d, CalculatedW was <=0 despite content. Adjusted to %d.", i, calculatedWidth)
} else if calculatedWidth <= 0 && sampleContentDisplayWidth == 0 {
// Column is truly empty in sample and buffers didn't make it positive, or minTotalColWidth is 0.
// Keep width 0 (it will be hidden by renderer if all content is empty for this col)
// Or, if we want empty columns to have a minimum presence (even if just padding):
// calculatedWidth = currentTotalPaddingWidth // This would make it just wide enough for padding
// For now, let truly empty sample + no min width result in 0.
calculatedWidth = 0 // Explicitly set to 0 if it ended up non-positive and no content
}
t.streamWidths.Set(i, calculatedWidth)
t.logger.Debug("streamCalculateWidths: Col %d, SampleContentW=%d, PadW=%d, EllipsisBufIfTruncate=%d, VarianceBuf=%d -> FinalTotalColW=%d",
i, sampleContentDisplayWidth, currentTotalPaddingWidth, ellipsisWidthBuffer, varianceBuffer, calculatedWidth)
}
}
// Apply Global Constraint (if t.config.Stream.Widths.Global > 0)
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
})
separatorWidth := 0
if t.renderer != nil {
rendererConfig := t.renderer.Config()
if rendererConfig.Settings.Separators.BetweenColumns.Enabled() {
separatorWidth = twwidth.Width(rendererConfig.Symbols.Column())
}
} else {
separatorWidth = 1 // Default if renderer not available yet
}
totalWidthIncludingSeparators := currentTotalColumnWidthsSum
if t.streamNumCols > 1 {
totalWidthIncludingSeparators += (t.streamNumCols - 1) * separatorWidth
}
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.Widths.Global
if t.streamNumCols > 1 {
targetSumForColumnWidths -= (t.streamNumCols - 1) * separatorWidth
}
if targetSumForColumnWidths < t.streamNumCols && t.streamNumCols > 0 { // Ensure at least 1 per column if possible
targetSumForColumnWidths = t.streamNumCols
} else if targetSumForColumnWidths < 0 {
targetSumForColumnWidths = 0
}
scaleFactor := float64(targetSumForColumnWidths) / float64(currentTotalColumnWidthsSum)
if currentTotalColumnWidthsSum <= 0 {
scaleFactor = 0
} // Avoid division by zero or negative scale
adjustedSum := 0
for i := 0; i < t.streamNumCols; i++ {
originalColWidth := t.streamWidths.Get(i)
if originalColWidth == 0 {
continue
} // Don't scale hidden columns
scaledWidth := 0
if scaleFactor > 0 {
scaledWidth = int(math.Round(float64(originalColWidth) * scaleFactor))
}
if scaledWidth < 1 && originalColWidth > 0 { // Ensure at least 1 if original had width and scaling made it too small
scaledWidth = 1
} else if scaledWidth < 0 { // Should not happen with math.Round on positive*positive
scaledWidth = 0
}
t.streamWidths.Set(i, scaledWidth)
adjustedSum += scaledWidth
}
// Distribute rounding errors to meet targetSumForColumnWidths
remainingSpace := targetSumForColumnWidths - adjustedSum
t.logger.Debug("streamCalculateWidths: Scaling complete. TargetSum=%d, AchievedSum=%d, RemSpace=%d", targetSumForColumnWidths, adjustedSum, remainingSpace)
// Distribute remainingSpace (positive or negative) among non-zero width columns
if remainingSpace != 0 && t.streamNumCols > 0 {
colsToAdjust := []int{}
t.streamWidths.Each(func(col int, w int) {
if w > 0 { // Only consider columns that currently have width
colsToAdjust = append(colsToAdjust, col)
}
})
if len(colsToAdjust) > 0 {
for i := 0; i < int(math.Abs(float64(remainingSpace))); i++ {
colIdx := colsToAdjust[i%len(colsToAdjust)]
currentColWidth := t.streamWidths.Get(colIdx)
if remainingSpace > 0 {
t.streamWidths.Set(colIdx, currentColWidth+1)
} else if remainingSpace < 0 && currentColWidth > 1 { // Don't reduce below 1
t.streamWidths.Set(colIdx, currentColWidth-1)
}
}
}
}
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.Widths.Global)
}
}
// Final sanitization
t.streamWidths.Each(func(col int, width int) {
if width < 0 {
t.streamWidths.Set(col, 0)
}
})
t.logger.Debug("streamCalculateWidths: Final derived stream widths after all adjustments (%d columns): %v", t.streamNumCols, t.streamWidths)
return t.streamNumCols
}
// streamRenderBottomBorder renders the bottom border of the table in streaming mode.
// It uses the fixed streamWidths and the last rendered content to create the border context.
// It assumes Start() has been called and t.hasPrinted is true.
// Returns an error if rendering fails.
func (t *Table) streamRenderBottomBorder() error {
if t.streamWidths == nil || t.streamWidths.Len() == 0 {
t.logger.Debug("streamRenderBottomBorder: No stream widths available, skipping bottom border.")
return nil
}
cfg := t.renderer.Config()
if !cfg.Borders.Bottom.Enabled() || !cfg.Settings.Lines.ShowBottom.Enabled() {
t.logger.Debug("streamRenderBottomBorder: Bottom border disabled in config, skipping.")
return nil
}
// The bottom border's "Current" context is the last rendered content line
currentCells := make(map[int]tw.CellContext)
if t.lastRenderedLineContent != nil {
// Use a helper to convert last rendered state to cell contexts
currentCells = t.streamRenderedMergeState(t.lastRenderedLineContent, t.lastRenderedMergeState)
} else {
// No content was ever rendered, but we might still want a bottom border if a top border was drawn.
// Create empty cell contexts.
for i := 0; i < t.streamNumCols; i++ {
currentCells[i] = tw.CellContext{Width: t.streamWidths.Get(i)}
}
t.logger.Debug("streamRenderBottomBorder: No previous content line, creating empty context for bottom border.")
}
f := t.renderer
f.Line(tw.Formatting{
Row: tw.RowContext{
Widths: t.streamWidths,
ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths},
Current: currentCells, // Context of the line *above* the bottom border
Previous: nil, // No line before this, relative to the border itself (or use lastRendered's previous?)
Next: nil, // No line after the bottom border
Position: t.lastRenderedPosition, // Position of the content above the border (Row or Footer)
Location: tw.LocationEnd, // This is the absolute end
},
Level: tw.LevelFooter, // Bottom border is LevelFooter
IsSubRow: false,
NormalizedWidths: t.streamWidths,
})
t.logger.Debug("streamRenderBottomBorder: Bottom border rendered.")
return nil
}
// streamRenderFooter renders the stored footer lines in streaming mode.
// It's called by Close(). It renders the Row/Footer separator line first.
// It assumes Start() has been called and t.hasPrinted is true.
// Returns an error if rendering fails.
func (t *Table) streamRenderFooter(processedFooterLines [][]string) error {
t.logger.Debug("streamRenderFooter: Rendering %d processed footer lines.", len(processedFooterLines))
if t.streamWidths == nil || t.streamWidths.Len() == 0 || t.streamNumCols == 0 {
t.logger.Warn("streamRenderFooter: No stream widths or columns defined. Cannot render footer.")
return errors.New("cannot render stream footer without defined column widths")
}
if len(processedFooterLines) == 0 {
t.logger.Debug("streamRenderFooter: No footer lines to render.")
return nil
}
f := t.renderer
cfg := t.renderer.Config()
// Render Row/Footer or Header/Footer Separator Line
// This separator is drawn if ShowFooterLine is enabled AND there was content before the footer.
// The last rendered position (t.lastRenderedPosition) should be Row or Header or "separator".
if (t.lastRenderedPosition == tw.Row || t.lastRenderedPosition == tw.Header || t.lastRenderedPosition == tw.Position("separator")) &&
cfg.Settings.Lines.ShowFooterLine.Enabled() {
t.logger.Debug("streamRenderFooter: Rendering Row/Footer or Header/Footer separator line.")
// Previous context is the last line rendered before this footer
prevCells := t.streamRenderedMergeState(t.lastRenderedLineContent, t.lastRenderedMergeState)
// Next context is the first line of this footer
var nextCells map[int]tw.CellContext = nil
if len(processedFooterLines) > 0 {
// Need merge states for the footer section.
// Since footer is processed once and stored, detect merges on its raw input once.
// This requires access to the *original* raw footer strings passed to Footer().
// For simplicity now, assume no complex horizontal merges in footer for this separator line context.
// A better approach: streamStoreFooter should also calculate and store footerMerges.
// For now, create nextCells without specific merge info for the separator line.
// Or, call prepareWithMerges on the *stored processed* lines, which might be okay for simple cases.
// Let's pass nil for sectionMerges to streamBuildCellContexts for this specific Next context.
// It will result in default (no-merge) states.
// For now, let's build nextCells manually for the separator line context
nextCells = make(map[int]tw.CellContext)
firstFooterLineContent := padLine(processedFooterLines[0], t.streamNumCols)
// Footer merges should be calculated in streamStoreFooter and stored if needed.
// For now, assume no merges for this 'Next' context.
for j := 0; j < t.streamNumCols; j++ {
nextCells[j] = tw.CellContext{Data: firstFooterLineContent[j], Width: t.streamWidths.Get(j)}
}
}
separatorLevel := tw.LevelFooter // Line before footer section is LevelFooter
separatorPosition := tw.Footer // Positioned relative to the footer it precedes
separatorLocation := tw.LocationMiddle
f.Line(tw.Formatting{
Row: tw.RowContext{
Widths: t.streamWidths,
ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths},
Current: prevCells, // Context of line above separator
Previous: nil, // No line before Current in this specific context
Next: nextCells, // Context of line below separator (first footer line)
Position: separatorPosition,
Location: separatorLocation,
},
Level: separatorLevel,
IsSubRow: false,
NormalizedWidths: t.streamWidths,
})
t.lastRenderedPosition = tw.Position("separator") // Update state
t.lastRenderedLineContent = nil
t.lastRenderedMergeState = nil
t.logger.Debug("streamRenderFooter: Footer separator line rendered.")
}
// End Render Separator Line
// Detect horizontal merges for the footer section based on its (assumed stored) raw input.
// This is tricky because streamStoreFooter gets []string, but prepareWithMerges expects [][]string.
// For simplicity, if complex merges are needed in footer, streamStoreFooter should
// have received raw data, called prepareWithMerges, and stored those merges.
// For now, assume no complex horizontal merges in footer or pass nil for sectionMerges.
// Let's assume footerMerges were calculated and stored as `t.streamFooterMerges map[int]tw.MergeState`
// by `streamStoreFooter`. For this example, we'll pass nil, meaning no merges.
var footerMerges map[int]tw.MergeState = nil // Placeholder
totalFooterLines := len(processedFooterLines)
for i := 0; i < totalFooterLines; i++ {
resp := t.streamBuildCellContexts(
tw.Footer,
0, // Row index within Footer (always 0)
i, // Line index
processedFooterLines,
footerMerges, // Pass footer-specific merges if calculated and stored
t.config.Footer,
)
// Special Location logic for the *very last line* of the table if this footer line is it.
// This is complex because bottom border might follow.
// Let streamBuildCellContexts handle LocationFirst/Middle for now.
// streamRenderBottomBorder will handle the final LocationEnd for its line.
// If this footer line is the last content and no bottom border, *it* should be LocationEnd.
// If this is the last line of the last content block (footer), and no bottom border will be drawn,
// its Location should be End.
isLastLineOfTableContent := (i == totalFooterLines-1) &&
!(cfg.Borders.Bottom.Enabled() && cfg.Settings.Lines.ShowBottom.Enabled())
if isLastLineOfTableContent {
resp.location = tw.LocationEnd
t.logger.Debug("streamRenderFooter: Setting LocationEnd for last footer line as no bottom border will follow.")
}
f.Footer([][]string{resp.cellsContent}, tw.Formatting{
Row: tw.RowContext{
Widths: t.streamWidths,
ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths},
Current: resp.cells,
Previous: resp.prevCells,
Next: resp.nextCells, // Next is nil if last line of footer block
Position: tw.Footer,
Location: resp.location,
},
Level: tw.LevelFooter,
IsSubRow: (i > 0),
NormalizedWidths: t.streamWidths,
})
t.lastRenderedLineContent = resp.cellsContent
t.lastRenderedMergeState = make(map[int]tw.MergeState)
for colIdx, cellCtx := range resp.cells {
t.lastRenderedMergeState[colIdx] = cellCtx.Merge
}
t.lastRenderedPosition = tw.Footer
}
t.logger.Debug("streamRenderFooter: Footer content rendering completed.")
return nil
}
// streamRenderHeader processes and renders the header section in streaming mode.
// It calculates/uses fixed stream widths, processes content, renders borders/lines,
// and updates streaming state.
// It assumes Start() has already been called and t.hasPrinted is true.
func (t *Table) streamRenderHeader(headers []string) error {
t.logger.Debug("streamRenderHeader called with headers: %v", headers)
if !t.config.Stream.Enable {
return errors.New("streaming mode is disabled")
}
if t.headerRendered {
t.logger.Warn("streamRenderHeader called but header already rendered. Ignoring.")
return nil
}
if err := t.ensureStreamWidthsCalculated(headers, t.config.Header); err != nil {
return err
}
_, headerMerges, _ := t.prepareWithMerges([][]string{headers}, t.config.Header, tw.Header)
processedHeaderLines := t.prepareContent(headers, t.config.Header)
t.logger.Debug("streamRenderHeader: Processed header lines: %d", len(processedHeaderLines))
if t.streamNumCols > 0 {
t.headerRendered = true
}
if len(processedHeaderLines) == 0 && t.streamNumCols == 0 {
t.logger.Debug("streamRenderHeader: No header content and no columns determined.")
return nil
}
f := t.renderer
cfg := t.renderer.Config()
if t.lastRenderedPosition == "" && cfg.Borders.Top.Enabled() && cfg.Settings.Lines.ShowTop.Enabled() {
t.logger.Debug("streamRenderHeader: Rendering table top border.")
var nextCellsCtx map[int]tw.CellContext
if len(processedHeaderLines) > 0 {
firstHeaderLineResp := t.streamBuildCellContexts(
tw.Header, 0, 0, processedHeaderLines, headerMerges, t.config.Header,
)
nextCellsCtx = firstHeaderLineResp.cells
}
f.Line(tw.Formatting{
Row: tw.RowContext{
Widths: t.streamWidths,
ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths},
Next: nextCellsCtx,
Position: tw.Header,
Location: tw.LocationFirst,
},
Level: tw.LevelHeader,
IsSubRow: false,
NormalizedWidths: t.streamWidths,
})
t.logger.Debug("streamRenderHeader: Top border rendered.")
}
hasTopPadding := t.config.Header.Padding.Global.Top != tw.Empty
if hasTopPadding {
resp := t.streamBuildCellContexts(tw.Header, 0, -1, nil, headerMerges, t.config.Header)
resp.cellsContent = t.buildPaddingLineContents(t.config.Header.Padding.Global.Top, t.streamWidths, t.streamNumCols, headerMerges)
resp.location = tw.LocationFirst
t.logger.Debug("streamRenderHeader: Rendering header top padding line: %v (loc: %v)", resp.cellsContent, resp.location)
f.Header([][]string{resp.cellsContent}, tw.Formatting{
Row: tw.RowContext{
Widths: t.streamWidths,
ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths},
Current: resp.cells,
Previous: resp.prevCells,
Next: resp.nextCells,
Position: tw.Header,
Location: resp.location,
},
Level: tw.LevelHeader,
IsSubRow: true,
NormalizedWidths: t.streamWidths,
})
t.lastRenderedLineContent = resp.cellsContent
t.lastRenderedMergeState = make(map[int]tw.MergeState)
for colIdx, cellCtx := range resp.cells {
t.lastRenderedMergeState[colIdx] = cellCtx.Merge
}
t.lastRenderedPosition = tw.Header
}
totalHeaderLines := len(processedHeaderLines)
for i := 0; i < totalHeaderLines; i++ {
resp := t.streamBuildCellContexts(tw.Header, 0, i, processedHeaderLines, headerMerges, t.config.Header)
t.logger.Debug("streamRenderHeader: Rendering header content line %d/%d with location %v", i, totalHeaderLines, resp.location)
f.Header([][]string{resp.cellsContent}, tw.Formatting{
Row: tw.RowContext{
Widths: t.streamWidths,
ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths},
Current: resp.cells,
Previous: resp.prevCells,
Next: resp.nextCells,
Position: tw.Header,
Location: resp.location,
},
Level: tw.LevelHeader,
IsSubRow: i > 0,
NormalizedWidths: t.streamWidths,
})
t.lastRenderedLineContent = resp.cellsContent
t.lastRenderedMergeState = make(map[int]tw.MergeState)
for colIdx, cellCtx := range resp.cells {
t.lastRenderedMergeState[colIdx] = cellCtx.Merge
}
t.lastRenderedPosition = tw.Header
}
hasBottomPadding := t.config.Header.Padding.Global.Bottom != tw.Empty
if hasBottomPadding {
resp := t.streamBuildCellContexts(tw.Header, 0, totalHeaderLines, nil, headerMerges, t.config.Header)
resp.cellsContent = t.buildPaddingLineContents(t.config.Header.Padding.Global.Bottom, t.streamWidths, t.streamNumCols, headerMerges)
resp.location = tw.LocationEnd
t.logger.Debug("streamRenderHeader: Rendering header bottom padding line: %v (loc: %v)", resp.cellsContent, resp.location)
f.Header([][]string{resp.cellsContent}, tw.Formatting{
Row: tw.RowContext{
Widths: t.streamWidths,
ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths},
Current: resp.cells,
Previous: resp.prevCells,
Next: resp.nextCells,
Position: tw.Header,
Location: resp.location,
},
Level: tw.LevelHeader,
IsSubRow: true,
NormalizedWidths: t.streamWidths,
})
t.lastRenderedLineContent = resp.cellsContent
t.lastRenderedMergeState = make(map[int]tw.MergeState)
for colIdx, cellCtx := range resp.cells {
t.lastRenderedMergeState[colIdx] = cellCtx.Merge
}
t.lastRenderedPosition = tw.Header
}
if cfg.Settings.Lines.ShowHeaderLine.Enabled() && (t.firstRowRendered || len(t.streamFooterLines) > 0) {
t.logger.Debug("streamRenderHeader: Rendering header separator line.")
resp := t.streamBuildCellContexts(tw.Header, 0, totalHeaderLines-1, processedHeaderLines, headerMerges, t.config.Header)
f.Line(tw.Formatting{
Row: tw.RowContext{
Widths: t.streamWidths,
ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths},
Current: resp.cells,
Previous: resp.prevCells,
Next: nil,
Position: tw.Header,
Location: tw.LocationMiddle,
},
Level: tw.LevelBody,
IsSubRow: false,
NormalizedWidths: t.streamWidths,
})
t.lastRenderedPosition = tw.Position("separator")
t.lastRenderedLineContent = nil
t.lastRenderedMergeState = nil
}
t.logger.Debug("streamRenderHeader: Header content rendering completed.")
return nil
}
// streamRenderedMergeState converts the stored last rendered line content
// and its merge states into a map of CellContext, suitable for providing
// context (e.g., "Current" or "Previous") to the renderer.
// It uses the fixed streamWidths.
func (t *Table) streamRenderedMergeState(
lineContent []string,
lineMergeStates map[int]tw.MergeState,
) map[int]tw.CellContext {
cells := make(map[int]tw.CellContext)
if t.streamWidths == nil || t.streamWidths.Len() == 0 || t.streamNumCols == 0 {
t.logger.Warn("streamRenderedMergeState: streamWidths not set or streamNumCols is 0. Returning empty cell contexts.")
return cells
}
// Ensure lineContent is padded to streamNumCols if it's not nil
var paddedLineContent []string
if lineContent != nil {
paddedLineContent = padLine(lineContent, t.streamNumCols)
} else {
// If lineContent is nil (e.g. after a separator), create an empty padded line
paddedLineContent = make([]string, t.streamNumCols)
for i := range paddedLineContent {
paddedLineContent[i] = tw.Empty
}
}
for j := 0; j < t.streamNumCols; j++ {
cellData := paddedLineContent[j]
colWidth := t.streamWidths.Get(j)
mergeState := tw.MergeState{} // Default to no merge
if lineMergeStates != nil {
if state, ok := lineMergeStates[j]; ok {
mergeState = state
}
}
// For context purposes (like Previous or Current for a border line),
// Align and Padding are often less critical than Data, Width, and Merge.
// We can use default/empty Align and Padding here.
cells[j] = tw.CellContext{
Data: cellData,
Align: tw.AlignDefault, // Or tw.AlignNone if preferred for context-only cells
Padding: tw.Padding{}, // Empty padding
Width: colWidth,
Merge: mergeState,
}
}
return cells
}
// streamStoreFooter processes the footer content and stores it for later rendering by Close()
// in streaming mode. It ensures stream widths are calculated if not already set.
func (t *Table) streamStoreFooter(footers []string) error {
t.logger.Debug("streamStoreFooter called with footers: %v", footers)
if !t.config.Stream.Enable {
return errors.New("streaming mode is disabled")
}
if len(footers) == 0 {
t.logger.Debug("streamStoreFooter: Empty footer cells, storing empty footer lines.")
t.streamFooterLines = [][]string{}
return nil
}
if err := t.ensureStreamWidthsCalculated(footers, t.config.Footer); err != nil {
t.logger.Warnf("streamStoreFooter: Failed to determine column count from footer data: %v", err)
t.streamFooterLines = [][]string{}
return nil
}
if t.streamNumCols > 0 && len(footers) != t.streamNumCols {
t.logger.Warnf("streamStoreFooter: Input footer column count (%d) does not match fixed stream column count (%d). Padding/Truncating input footers.", len(footers), t.streamNumCols)
if len(footers) < t.streamNumCols {
paddedFooters := make([]string, t.streamNumCols)
copy(paddedFooters, footers)
for i := len(footers); i < t.streamNumCols; i++ {
paddedFooters[i] = tw.Empty
}
footers = paddedFooters
} else {
footers = footers[:t.streamNumCols]
}
}
if t.streamNumCols == 0 {
t.logger.Warn("streamStoreFooter: streamNumCols is 0, cannot process/store footer lines meaningfully.")
t.streamFooterLines = [][]string{}
return nil
}
t.streamFooterLines = t.prepareContent(footers, t.config.Footer)
t.logger.Debug("streamStoreFooter: Processed and stored footer lines: %d lines. Content: %v", len(t.streamFooterLines), t.streamFooterLines)
return nil
}