1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163
|
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
}
|