mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-06 04:09:40 -06:00
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>
1164 lines
48 KiB
Go
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
|
|
}
|