summary history files

vendor/github.com/olekukonko/tablewriter/renderer/colorized.go
package renderer

import (
	"io"
	"strings"

	"github.com/fatih/color"
	"github.com/olekukonko/ll"
	"github.com/olekukonko/ll/lh"
	"github.com/olekukonko/tablewriter/pkg/twwidth"

	"github.com/olekukonko/tablewriter/tw"
)

// ColorizedConfig holds configuration for the Colorized table renderer.
type ColorizedConfig struct {
	Borders   tw.Border   // Border visibility settings
	Settings  tw.Settings // Rendering behavior settings (e.g., separators, whitespace)
	Header    Tint        // Colors for header cells
	Column    Tint        // Colors for row cells
	Footer    Tint        // Colors for footer cells
	Border    Tint        // Colors for borders and lines
	Separator Tint        // Colors for column separators
	Symbols   tw.Symbols  // Symbols for table drawing (e.g., corners, lines)
}

// Colors is a slice of color attributes for use with fatih/color, such as color.FgWhite or color.Bold.
type Colors []color.Attribute

// Tint defines foreground and background color settings for table elements, with optional per-column overrides.
type Tint struct {
	FG      Colors // Foreground color attributes
	BG      Colors // Background color attributes
	Columns []Tint // Per-column color settings
}

// Apply applies the Tint's foreground and background colors to the given text, returning the text unchanged if no colors are set.
func (t Tint) Apply(text string) string {
	if len(t.FG) == 0 && len(t.BG) == 0 {
		return text
	}
	// Combine foreground and background colors
	combinedColors := append(t.FG, t.BG...)
	// Create a color function and apply it to the text
	c := color.New(combinedColors...).SprintFunc()
	return c(text)
}

// Colorized renders colored ASCII tables with customizable borders, colors, and alignments.
type Colorized struct {
	config       ColorizedConfig          // Renderer configuration
	trace        []string                 // Debug trace messages
	newLine      string                   // Newline character
	defaultAlign map[tw.Position]tw.Align // Default alignments for header, row, and footer
	logger       *ll.Logger               // Logger for debug messages
	w            io.Writer
}

// NewColorized creates a Colorized renderer with the specified configuration, falling back to defaults if none provided.
// Only the first config is used if multiple are passed.
func NewColorized(configs ...ColorizedConfig) *Colorized {
	// Initialize with default configuration
	baseCfg := defaultColorized()

	if len(configs) > 0 {
		userCfg := configs[0]

		// Override border settings if provided
		if userCfg.Borders.Left != 0 {
			baseCfg.Borders.Left = userCfg.Borders.Left
		}
		if userCfg.Borders.Right != 0 {
			baseCfg.Borders.Right = userCfg.Borders.Right
		}
		if userCfg.Borders.Top != 0 {
			baseCfg.Borders.Top = userCfg.Borders.Top
		}
		if userCfg.Borders.Bottom != 0 {
			baseCfg.Borders.Bottom = userCfg.Borders.Bottom
		}

		// Merge separator and line settings
		baseCfg.Settings.Separators = mergeSeparators(baseCfg.Settings.Separators, userCfg.Settings.Separators)
		baseCfg.Settings.Lines = mergeLines(baseCfg.Settings.Lines, userCfg.Settings.Lines)

		// Override compact mode if specified
		if userCfg.Settings.CompactMode != 0 {
			baseCfg.Settings.CompactMode = userCfg.Settings.CompactMode
		}

		// Override color settings for various table elements
		if len(userCfg.Header.FG) > 0 || len(userCfg.Header.BG) > 0 || userCfg.Header.Columns != nil {
			baseCfg.Header = userCfg.Header
		}
		if len(userCfg.Column.FG) > 0 || len(userCfg.Column.BG) > 0 || userCfg.Column.Columns != nil {
			baseCfg.Column = userCfg.Column
		}
		if len(userCfg.Footer.FG) > 0 || len(userCfg.Footer.BG) > 0 || userCfg.Footer.Columns != nil {
			baseCfg.Footer = userCfg.Footer
		}
		if len(userCfg.Border.FG) > 0 || len(userCfg.Border.BG) > 0 || userCfg.Border.Columns != nil {
			baseCfg.Border = userCfg.Border
		}
		if len(userCfg.Separator.FG) > 0 || len(userCfg.Separator.BG) > 0 || userCfg.Separator.Columns != nil {
			baseCfg.Separator = userCfg.Separator
		}

		// Override symbols if provided
		if userCfg.Symbols != nil {
			baseCfg.Symbols = userCfg.Symbols
		}
	}

	cfg := baseCfg
	// Ensure symbols are initialized
	if cfg.Symbols == nil {
		cfg.Symbols = tw.NewSymbols(tw.StyleLight)
	}

	// Initialize the Colorized renderer
	f := &Colorized{
		config:  cfg,
		newLine: tw.NewLine,
		defaultAlign: map[tw.Position]tw.Align{
			tw.Header: tw.AlignCenter,
			tw.Row:    tw.AlignLeft,
			tw.Footer: tw.AlignRight,
		},
		logger: ll.New("colorized", ll.WithHandler(lh.NewMemoryHandler())),
	}
	// Log initialization details
	f.logger.Debugf("Initialized Colorized renderer with symbols: Center=%q, Row=%q, Column=%q", f.config.Symbols.Center(), f.config.Symbols.Row(), f.config.Symbols.Column())
	f.logger.Debugf("Final ColorizedConfig.Settings.Lines: %+v", f.config.Settings.Lines)
	f.logger.Debugf("Final ColorizedConfig.Borders: %+v", f.config.Borders)
	return f
}

// Close performs cleanup (no-op in this implementation).
func (c *Colorized) Close() error {
	c.logger.Debug("Colorized.Close() called (no-op).")
	return nil
}

// Config returns the renderer's configuration as a Rendition.
func (c *Colorized) Config() tw.Rendition {
	return tw.Rendition{
		Borders:   c.config.Borders,
		Settings:  c.config.Settings,
		Symbols:   c.config.Symbols,
		Streaming: true,
	}
}

// Debug returns the accumulated debug trace messages.
func (c *Colorized) Debug() []string {
	return c.trace
}

// Footer renders the table footer with configured colors and formatting.
func (c *Colorized) Footer(footers [][]string, ctx tw.Formatting) {
	c.logger.Debugf("Starting Footer render: IsSubRow=%v, Location=%v, Pos=%s",
		ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position)

	// Check if there are footers to render
	if len(footers) == 0 || len(footers[0]) == 0 {
		c.logger.Debug("Footer: No footers to render")
		return
	}

	// Render the footer line
	c.renderLine(ctx, footers[0], c.config.Footer)
	c.logger.Debug("Completed Footer render")
}

// Header renders the table header with configured colors and formatting.
func (c *Colorized) Header(headers [][]string, ctx tw.Formatting) {
	c.logger.Debugf("Starting Header render: IsSubRow=%v, Location=%v, Pos=%s, lines=%d, widths=%v",
		ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position, len(headers), ctx.Row.Widths)

	// Check if there are headers to render
	if len(headers) == 0 || len(headers[0]) == 0 {
		c.logger.Debug("Header: No headers to render")
		return
	}

	// Render the header line
	c.renderLine(ctx, headers[0], c.config.Header)
	c.logger.Debug("Completed Header render")
}

// Line renders a horizontal row line with colored junctions and segments, skipping zero-width columns.
func (c *Colorized) Line(ctx tw.Formatting) {
	c.logger.Debugf("Line: Starting with Level=%v, Location=%v, IsSubRow=%v, Widths=%v", ctx.Level, ctx.Row.Location, ctx.IsSubRow, ctx.Row.Widths)

	// Initialize junction renderer
	jr := NewJunction(JunctionContext{
		Symbols:       c.config.Symbols,
		Ctx:           ctx,
		ColIdx:        0,
		BorderTint:    c.config.Border,
		SeparatorTint: c.config.Separator,
		Logger:        c.logger,
	})

	var line strings.Builder

	// Get sorted column indices and filter out zero-width columns
	allSortedKeys := ctx.Row.Widths.SortedKeys()
	effectiveKeys := []int{}
	keyWidthMap := make(map[int]int)

	for _, k := range allSortedKeys {
		width := ctx.Row.Widths.Get(k)
		keyWidthMap[k] = width
		if width > 0 {
			effectiveKeys = append(effectiveKeys, k)
		}
	}
	c.logger.Debugf("Line: All keys=%v, Effective keys (width>0)=%v", allSortedKeys, effectiveKeys)

	// Handle case with no effective columns
	if len(effectiveKeys) == 0 {
		prefix := tw.Empty
		suffix := tw.Empty
		if c.config.Borders.Left.Enabled() {
			prefix = jr.RenderLeft()
		}
		if c.config.Borders.Right.Enabled() {
			originalLastColIdx := -1
			if len(allSortedKeys) > 0 {
				originalLastColIdx = allSortedKeys[len(allSortedKeys)-1]
			}
			suffix = jr.RenderRight(originalLastColIdx)
		}
		if prefix != tw.Empty || suffix != tw.Empty {
			line.WriteString(prefix + suffix + tw.NewLine)
			c.w.Write([]byte(line.String()))
		}
		c.logger.Debug("Line: Handled empty row/widths case (no effective keys)")
		return
	}

	// Add left border if enabled
	if c.config.Borders.Left.Enabled() {
		line.WriteString(jr.RenderLeft())
	}

	// Render segments for each effective column
	for keyIndex, currentColIdx := range effectiveKeys {
		jr.colIdx = currentColIdx
		segment := jr.GetSegment()
		colWidth := keyWidthMap[currentColIdx]
		c.logger.Debugf("Line: Drawing segment for Effective colIdx=%d, segment='%s', width=%d", currentColIdx, segment, colWidth)

		if segment == tw.Empty {
			line.WriteString(strings.Repeat(tw.Space, colWidth))
		} else {
			// Calculate how many times to repeat the segment
			segmentWidth := twwidth.Width(segment)
			if segmentWidth <= 0 {
				segmentWidth = 1
			}
			repeat := 0
			if colWidth > 0 && segmentWidth > 0 {
				repeat = colWidth / segmentWidth
			}
			drawnSegment := strings.Repeat(segment, repeat)
			line.WriteString(drawnSegment)

			// Adjust for width discrepancies
			actualDrawnWidth := twwidth.Width(drawnSegment)
			if actualDrawnWidth < colWidth {
				missingWidth := colWidth - actualDrawnWidth
				spaces := strings.Repeat(tw.Space, missingWidth)
				if len(c.config.Border.BG) > 0 {
					line.WriteString(Tint{BG: c.config.Border.BG}.Apply(spaces))
				} else {
					line.WriteString(spaces)
				}
				c.logger.Debugf("Line: colIdx=%d corrected segment width, added %d spaces", currentColIdx, missingWidth)
			} else if actualDrawnWidth > colWidth {
				c.logger.Debugf("Line: WARNING colIdx=%d segment draw width %d > target %d", currentColIdx, actualDrawnWidth, colWidth)
			}
		}

		// Add junction between columns if not the last visible column
		isLastVisible := keyIndex == len(effectiveKeys)-1
		if !isLastVisible && c.config.Settings.Separators.BetweenColumns.Enabled() {
			nextVisibleColIdx := effectiveKeys[keyIndex+1]
			originalPrecedingCol := -1
			foundCurrent := false
			for _, k := range allSortedKeys {
				if k == currentColIdx {
					foundCurrent = true
				}
				if foundCurrent && k < nextVisibleColIdx {
					originalPrecedingCol = k
				}
				if k >= nextVisibleColIdx {
					break
				}
			}

			if originalPrecedingCol != -1 {
				jr.colIdx = originalPrecedingCol
				junction := jr.RenderJunction(originalPrecedingCol, nextVisibleColIdx)
				c.logger.Debugf("Line: Junction between visible %d (orig preceding %d) and next visible %d: '%s'", currentColIdx, originalPrecedingCol, nextVisibleColIdx, junction)
				line.WriteString(junction)
			} else {
				c.logger.Debugf("Line: Could not determine original preceding column for junction before visible %d", nextVisibleColIdx)
				line.WriteString(c.config.Separator.Apply(jr.sym.Center()))
			}
		}
	}

	// Add right border if enabled
	if c.config.Borders.Right.Enabled() {
		originalLastColIdx := -1
		if len(allSortedKeys) > 0 {
			originalLastColIdx = allSortedKeys[len(allSortedKeys)-1]
		}
		jr.colIdx = originalLastColIdx
		line.WriteString(jr.RenderRight(originalLastColIdx))
	}

	// Write the final line
	line.WriteString(c.newLine)
	c.w.Write([]byte(line.String()))
	c.logger.Debugf("Line rendered: %s", strings.TrimSuffix(line.String(), c.newLine))
}

// Logger sets the logger for the Colorized instance.
func (c *Colorized) Logger(logger *ll.Logger) {
	c.logger = logger.Namespace("colorized")
}

// Reset clears the renderer's internal state, including debug traces.
func (c *Colorized) Reset() {
	c.trace = nil
	c.logger.Debugf("Reset: Cleared debug trace")
}

// Row renders a table data row with configured colors and formatting.
func (c *Colorized) Row(row []string, ctx tw.Formatting) {
	c.logger.Debugf("Starting Row render: IsSubRow=%v, Location=%v, Pos=%s, hasFooter=%v",
		ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position, ctx.HasFooter)

	// Check if there is data to render
	if len(row) == 0 {
		c.logger.Debugf("Row: No data to render")
		return
	}

	// Render the row line
	c.renderLine(ctx, row, c.config.Column)
	c.logger.Debugf("Completed Row render")
}

// Start initializes the rendering process (no-op in this implementation).
func (c *Colorized) Start(w io.Writer) error {
	c.w = w
	c.logger.Debugf("Colorized.Start() called (no-op).")
	return nil
}

// formatCell formats a cell's content with color, width, padding, and alignment, handling whitespace trimming and truncation.
func (c *Colorized) formatCell(content string, width int, padding tw.Padding, align tw.Align, tint Tint) string {
	c.logger.Debugf("Formatting cell: content='%s', width=%d, align=%s, paddingL='%s', paddingR='%s', tintFG=%v, tintBG=%v",
		content, width, align, padding.Left, padding.Right, tint.FG, tint.BG)

	// Return empty string if width is non-positive
	if width <= 0 {
		c.logger.Debugf("formatCell: width %d <= 0, returning empty string", width)
		return tw.Empty
	}

	// Calculate visual width of content
	contentVisualWidth := twwidth.Width(content)

	// Set default padding characters
	padLeftCharStr := padding.Left
	// if padLeftCharStr == tw.Empty {
	//	padLeftCharStr = tw.Space
	//}
	padRightCharStr := padding.Right
	// if padRightCharStr == tw.Empty {
	//	padRightCharStr = tw.Space
	//}

	// Calculate padding widths
	definedPadLeftWidth := twwidth.Width(padLeftCharStr)
	definedPadRightWidth := twwidth.Width(padRightCharStr)
	// Calculate available width for content and alignment
	availableForContentAndAlign := max(width-definedPadLeftWidth-definedPadRightWidth, 0)

	// Truncate content if it exceeds available width
	if contentVisualWidth > availableForContentAndAlign {
		content = twwidth.Truncate(content, availableForContentAndAlign)
		contentVisualWidth = twwidth.Width(content)
		c.logger.Debugf("Truncated content to fit %d: '%s' (new width %d)", availableForContentAndAlign, content, contentVisualWidth)
	}

	// Calculate remaining space for alignment
	remainingSpaceForAlignment := max(availableForContentAndAlign-contentVisualWidth, 0)

	// Apply alignment padding
	leftAlignmentPadSpaces := tw.Empty
	rightAlignmentPadSpaces := tw.Empty
	switch align {
	case tw.AlignLeft:
		rightAlignmentPadSpaces = strings.Repeat(tw.Space, remainingSpaceForAlignment)
	case tw.AlignRight:
		leftAlignmentPadSpaces = strings.Repeat(tw.Space, remainingSpaceForAlignment)
	case tw.AlignCenter:
		leftSpacesCount := remainingSpaceForAlignment / 2
		rightSpacesCount := remainingSpaceForAlignment - leftSpacesCount
		leftAlignmentPadSpaces = strings.Repeat(tw.Space, leftSpacesCount)
		rightAlignmentPadSpaces = strings.Repeat(tw.Space, rightSpacesCount)
	default:
		// Default to left alignment
		rightAlignmentPadSpaces = strings.Repeat(tw.Space, remainingSpaceForAlignment)
	}

	// Apply colors to content and padding
	coloredContent := tint.Apply(content)
	coloredPadLeft := padLeftCharStr
	coloredPadRight := padRightCharStr
	coloredAlignPadLeft := leftAlignmentPadSpaces
	coloredAlignPadRight := rightAlignmentPadSpaces

	if len(tint.BG) > 0 {
		bgTint := Tint{BG: tint.BG}
		// Apply foreground color to non-space padding if foreground is defined
		if len(tint.FG) > 0 && padLeftCharStr != tw.Space {
			coloredPadLeft = tint.Apply(padLeftCharStr)
		} else {
			coloredPadLeft = bgTint.Apply(padLeftCharStr)
		}
		if len(tint.FG) > 0 && padRightCharStr != tw.Space {
			coloredPadRight = tint.Apply(padRightCharStr)
		} else {
			coloredPadRight = bgTint.Apply(padRightCharStr)
		}
		// Apply background color to alignment padding
		if leftAlignmentPadSpaces != tw.Empty {
			coloredAlignPadLeft = bgTint.Apply(leftAlignmentPadSpaces)
		}
		if rightAlignmentPadSpaces != tw.Empty {
			coloredAlignPadRight = bgTint.Apply(rightAlignmentPadSpaces)
		}
	} else if len(tint.FG) > 0 {
		// Apply foreground color to non-space padding
		if padLeftCharStr != tw.Space {
			coloredPadLeft = tint.Apply(padLeftCharStr)
		}
		if padRightCharStr != tw.Space {
			coloredPadRight = tint.Apply(padRightCharStr)
		}
	}

	// Build final cell string
	var sb strings.Builder
	sb.WriteString(coloredPadLeft)
	sb.WriteString(coloredAlignPadLeft)
	sb.WriteString(coloredContent)
	sb.WriteString(coloredAlignPadRight)
	sb.WriteString(coloredPadRight)
	output := sb.String()

	// Adjust output width if necessary
	currentVisualWidth := twwidth.Width(output)
	if currentVisualWidth != width {
		c.logger.Debugf("formatCell MISMATCH: content='%s', target_w=%d. Calculated parts width = %d. String: '%s'",
			content, width, currentVisualWidth, output)
		if currentVisualWidth > width {
			output = twwidth.Truncate(output, width)
		} else {
			paddingSpacesStr := strings.Repeat(tw.Space, width-currentVisualWidth)
			if len(tint.BG) > 0 {
				output += Tint{BG: tint.BG}.Apply(paddingSpacesStr)
			} else {
				output += paddingSpacesStr
			}
		}
		c.logger.Debugf("formatCell Post-Correction: Target %d, New Visual width %d. Output: '%s'", width, twwidth.Width(output), output)
	}

	c.logger.Debugf("Formatted cell final result: '%s' (target width %d, display width %d)", output, width, twwidth.Width(output))
	return output
}

// renderLine renders a single line (header, row, or footer) with colors, handling merges and separators.
func (c *Colorized) renderLine(ctx tw.Formatting, line []string, tint Tint) {
	// Determine number of columns
	numCols := 0
	if len(ctx.Row.Current) > 0 {
		maxKey := -1
		for k := range ctx.Row.Current {
			if k > maxKey {
				maxKey = k
			}
		}
		numCols = maxKey + 1
	} else {
		maxKey := -1
		for k := range ctx.Row.Widths {
			if k > maxKey {
				maxKey = k
			}
		}
		numCols = maxKey + 1
	}

	var output strings.Builder

	// Add left border if enabled
	prefix := tw.Empty
	if c.config.Borders.Left.Enabled() {
		prefix = c.config.Border.Apply(c.config.Symbols.Column())
	}
	output.WriteString(prefix)

	// Set up separator
	separatorDisplayWidth := 0
	separatorString := tw.Empty
	if c.config.Settings.Separators.BetweenColumns.Enabled() {
		separatorString = c.config.Separator.Apply(c.config.Symbols.Column())
		separatorDisplayWidth = twwidth.Width(c.config.Symbols.Column())
	}

	// Process each column
	for i := 0; i < numCols; {
		// Determine if a separator is needed
		shouldAddSeparator := false
		if i > 0 && c.config.Settings.Separators.BetweenColumns.Enabled() {
			cellCtx, ok := ctx.Row.Current[i]
			if !ok || (!cellCtx.Merge.Horizontal.Present || cellCtx.Merge.Horizontal.Start) {
				shouldAddSeparator = true
			}
		}
		if shouldAddSeparator {
			output.WriteString(separatorString)
			c.logger.Debugf("renderLine: Added separator '%s' before col %d", separatorString, i)
		} else if i > 0 {
			c.logger.Debugf("renderLine: Skipped separator before col %d due to HMerge continuation", i)
		}

		// Get cell context, use default if not present
		cellCtx, ok := ctx.Row.Current[i]
		if !ok {
			cellCtx = tw.CellContext{
				Data:    tw.Empty,
				Align:   c.defaultAlign[ctx.Row.Position],
				Padding: tw.Padding{Left: tw.Space, Right: tw.Space},
				Width:   ctx.Row.Widths.Get(i),
				Merge:   tw.MergeState{},
			}
		}

		// Handle merged cells
		visualWidth := 0
		span := 1
		isHMergeStart := ok && cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start

		if isHMergeStart {
			span = cellCtx.Merge.Horizontal.Span
			if ctx.Row.Position == tw.Row {
				// Calculate dynamic width for row merges
				dynamicTotalWidth := 0
				for k := 0; k < span && i+k < numCols; k++ {
					colToSum := i + k
					normWidth := max(ctx.NormalizedWidths.Get(colToSum), 0)
					dynamicTotalWidth += normWidth
					if k > 0 && separatorDisplayWidth > 0 {
						dynamicTotalWidth += separatorDisplayWidth
					}
				}
				visualWidth = dynamicTotalWidth
				c.logger.Debugf("renderLine: Row HMerge col %d, span %d, dynamic visualWidth %d", i, span, visualWidth)
			} else {
				visualWidth = ctx.Row.Widths.Get(i)
				c.logger.Debugf("renderLine: H/F HMerge col %d, span %d, pre-adjusted visualWidth %d", i, span, visualWidth)
			}
		} else {
			visualWidth = ctx.Row.Widths.Get(i)
			c.logger.Debugf("renderLine: Regular col %d, visualWidth %d", i, visualWidth)
		}
		if visualWidth < 0 {
			visualWidth = 0
		}

		// Skip processing for non-start merged cells
		if ok && cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start {
			c.logger.Debugf("renderLine: Skipping col %d processing (part of HMerge)", i)
			i++
			continue
		}

		// Handle empty cell context with non-zero width
		if !ok && visualWidth > 0 {
			spaces := strings.Repeat(tw.Space, visualWidth)
			if len(tint.BG) > 0 {
				output.WriteString(Tint{BG: tint.BG}.Apply(spaces))
			} else {
				output.WriteString(spaces)
			}
			c.logger.Debugf("renderLine: No cell context for col %d, writing %d spaces", i, visualWidth)
			i += span
			continue
		}

		// Set cell alignment
		padding := cellCtx.Padding
		align := cellCtx.Align
		if align == tw.AlignNone {
			align = c.defaultAlign[ctx.Row.Position]
			c.logger.Debugf("renderLine: col %d using default renderer align '%s' for position %s because cellCtx.Align was AlignNone", i, align, ctx.Row.Position)
		}

		// Detect and handle TOTAL pattern
		isTotalPattern := false
		if i == 0 && isHMergeStart && cellCtx.Merge.Horizontal.Span >= 3 && strings.TrimSpace(cellCtx.Data) == "TOTAL" {
			isTotalPattern = true
			c.logger.Debugf("renderLine: Detected 'TOTAL' HMerge pattern at col 0")
		}
		// Override alignment for footer merges or TOTAL pattern
		if (ctx.Row.Position == tw.Footer && isHMergeStart) || isTotalPattern {
			if align == tw.AlignNone {
				c.logger.Debugf("renderLine: Applying AlignRight override for Footer HMerge/TOTAL pattern at col %d. Original/default align was: %s", i, align)
				align = tw.AlignRight
			}
		}

		// Handle vertical/hierarchical merges
		content := cellCtx.Data
		if (cellCtx.Merge.Vertical.Present && !cellCtx.Merge.Vertical.Start) ||
			(cellCtx.Merge.Hierarchical.Present && !cellCtx.Merge.Hierarchical.Start) {
			content = tw.Empty
			c.logger.Debugf("renderLine: Blanked data for col %d (non-start V/Hierarchical)", i)
		}

		// Apply per-column tint if available
		cellTint := tint
		if i < len(tint.Columns) {
			columnTint := tint.Columns[i]
			if len(columnTint.FG) > 0 || len(columnTint.BG) > 0 {
				cellTint = columnTint
			}
		}

		// Format and render the cell
		formattedCell := c.formatCell(content, visualWidth, padding, align, cellTint)
		if len(formattedCell) > 0 {
			output.WriteString(formattedCell)
		} else if visualWidth == 0 && isHMergeStart {
			c.logger.Debugf("renderLine: Rendered HMerge START col %d resulted in 0 visual width, wrote nothing.", i)
		} else if visualWidth == 0 {
			c.logger.Debugf("renderLine: Rendered regular col %d resulted in 0 visual width, wrote nothing.", i)
		}

		// Log rendering details
		if isHMergeStart {
			c.logger.Debugf("renderLine: Rendered HMerge START col %d (span %d, visualWidth %d, align %s): '%s'",
				i, span, visualWidth, align, formattedCell)
		} else {
			c.logger.Debugf("renderLine: Rendered regular col %d (visualWidth %d, align %s): '%s'",
				i, visualWidth, align, formattedCell)
		}

		i += span
	}

	// Add right border if enabled
	suffix := tw.Empty
	if c.config.Borders.Right.Enabled() {
		suffix = c.config.Border.Apply(c.config.Symbols.Column())
	}
	output.WriteString(suffix)

	// Write the final line
	output.WriteString(c.newLine)
	c.w.Write([]byte(output.String()))
	c.logger.Debugf("renderLine: Final rendered line: %s", strings.TrimSuffix(output.String(), c.newLine))
}

// Rendition updates the parts of ColorizedConfig that correspond to tw.Rendition
// by merging the provided newRendition. Color-specific Tints are not modified.
func (c *Colorized) Rendition(newRendition tw.Rendition) { // Method name matches interface
	c.logger.Debug("Colorized.Rendition called. Current B/Sym/Set: B:%+v, Sym:%T, S:%+v. Override: %+v", c.config.Borders, c.config.Symbols, c.config.Settings, newRendition)

	currentRenditionPart := tw.Rendition{
		Borders:  c.config.Borders,
		Symbols:  c.config.Symbols,
		Settings: c.config.Settings,
	}

	mergedRenditionPart := mergeRendition(currentRenditionPart, newRendition)

	c.config.Borders = mergedRenditionPart.Borders
	c.config.Symbols = mergedRenditionPart.Symbols
	if c.config.Symbols == nil {
		c.config.Symbols = tw.NewSymbols(tw.StyleLight)
	}
	c.config.Settings = mergedRenditionPart.Settings

	c.logger.Debugf("Colorized.Rendition updated. New B/Sym/Set: B:%+v, Sym:%T, S:%+v",
		c.config.Borders, c.config.Symbols, c.config.Settings)
}

// Ensure Colorized implements tw.Renditioning
var _ tw.Renditioning = (*Colorized)(nil)