MegaBites-AI/Windows-powershell
0372
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System;5using System.Collections.Generic;6using System.Collections.Specialized;7using System.Management.Automation;8using System.Management.Automation.Internal;9using System.Text;10 11using Microsoft.PowerShell.Commands.Internal.Format;12 13namespace Microsoft.PowerShell.Commands.Internal.Format14{15 internal class TableWriter16 {17 /// <summary>18 /// Information about each column boundaries.19 /// </summary>20 private sealed class ColumnInfo21 {22 internal int startCol = 0;23 internal int width = 0;24 internal int alignment = TextAlignment.Left;25 internal bool HeaderMatchesProperty = true;26 }27 /// <summary>28 /// Class containing information about the tabular layout.29 /// </summary>30 private sealed class ScreenInfo31 {32 internal int screenColumns = 0;33 internal int screenRows = 0;34 35 internal const int separatorCharacterCount = 1;36 37 internal const int minimumScreenColumns = 5;38 39 internal const int minimumColumnWidth = 1;40 41 internal ColumnInfo[] columnInfo = null;42 }43 44 private ScreenInfo _si;45 46 private List<string> _header;47 48 internal static int ComputeWideViewBestItemsPerRowFit(int stringLen, int screenColumns)49 {50 if (stringLen <= 0 || screenColumns < 1)51 return 1;52 53 if (stringLen >= screenColumns)54 {55 // we too wide anyways, we might have to trim even for a single column56 return 1;57 }58 59 // we try to fit more than one: start increasing until we do not fit anymore60 int columnNumber = 1;61 while (true)62 {63 // would we fit with one more column?64 int nextValue = columnNumber + 1;65 // compute the width if we added the extra column66 int width = stringLen * nextValue + (nextValue - 1) * ScreenInfo.separatorCharacterCount;67 68 if (width >= screenColumns)69 {70 // we would not fit, so we are done71 return columnNumber;72 }73 74 // try another round75 columnNumber++;76 }77 }78 79 /// <summary>80 /// Initialize the table specifying the width of each column.81 /// </summary>82 /// <param name="leftMarginIndent">Left margin indentation.</param>83 /// <param name="screenColumns">Number of character columns on the screen.</param>84 /// <param name="columnWidths">Array of specified column widths.</param>85 /// <param name="alignment">Array of alignment flags.</param>86 /// <param name="headerMatchesProperty">Array of flags where the header label matches a property name.</param>87 /// <param name="suppressHeader">If true, suppress header printing.</param>88 /// <param name="screenRows">Number of rows on the screen.</param>89 internal void Initialize(int leftMarginIndent, int screenColumns, Span<int> columnWidths, ReadOnlySpan<int> alignment, ReadOnlySpan<bool> headerMatchesProperty, bool suppressHeader, int screenRows = int.MaxValue)90 {91 if (leftMarginIndent < 0)92 {93 leftMarginIndent = 0;94 }95 96 if (screenColumns - leftMarginIndent < ScreenInfo.minimumScreenColumns)97 {98 _disabled = true;99 return;100 }101 102 _startColumn = leftMarginIndent;103 _hideHeader = suppressHeader;104 105 // make sure the column widths are correct; if not, take appropriate action106 ColumnWidthManager manager = new ColumnWidthManager(107 screenColumns - leftMarginIndent,108 ScreenInfo.minimumColumnWidth,109 ScreenInfo.separatorCharacterCount);110 111 manager.CalculateColumnWidths(columnWidths);112 113 // if all the columns are hidden, just disable114 bool oneValid = false;115 116 for (int k = 0; k < columnWidths.Length; k++)117 {118 if (columnWidths[k] >= ScreenInfo.minimumColumnWidth)119 {120 oneValid = true;121 break;122 }123 }124 125 if (!oneValid)126 {127 _disabled = true;128 return;129 }130 131 // now set the run time data structures132 _si = new ScreenInfo();133 _si.screenColumns = screenColumns;134 _si.screenRows = screenRows;135 _si.columnInfo = new ColumnInfo[columnWidths.Length];136 137 int startCol = _startColumn;138 for (int k = 0; k < columnWidths.Length; k++)139 {140 _si.columnInfo[k] = new ColumnInfo();141 _si.columnInfo[k].startCol = startCol;142 _si.columnInfo[k].width = columnWidths[k];143 _si.columnInfo[k].alignment = alignment[k];144 if (!headerMatchesProperty.IsEmpty)145 {146 _si.columnInfo[k].HeaderMatchesProperty = headerMatchesProperty[k];147 }148 149 startCol += columnWidths[k] + ScreenInfo.separatorCharacterCount;150 }151 }152 153 internal int GenerateHeader(string[] values, LineOutput lo)154 {155 if (_disabled || _hideHeader)156 {157 return 0;158 }159 else if (_header != null)160 {161 string style = PSStyle.Instance.Formatting.TableHeader;162 string reset = PSStyle.Instance.Reset;163 164 foreach (string line in _header)165 {166 lo.WriteLine(line);167 }168 169 return _header.Count;170 }171 172 _header = new List<string>();173 174 // generate the row with the header labels175 GenerateRow(values, lo, true, null, lo.DisplayCells, _header, isHeader: true);176 177 // generate an array of "--" as header markers below178 // the column header labels179 string[] breakLine = new string[values.Length];180 for (int k = 0; k < breakLine.Length; k++)181 {182 // the column can be hidden183 if (_si.columnInfo[k].width <= 0)184 {185 breakLine[k] = string.Empty;186 continue;187 }188 // the title can be larger than the width189 int count = _si.columnInfo[k].width;190 if (!string.IsNullOrEmpty(values[k]))191 {192 int labelDisplayCells = lo.DisplayCells.Length(values[k]);193 if (labelDisplayCells < count)194 count = labelDisplayCells;195 }196 // NOTE: we can do this because "-" is a single cell character197 // on all devices. If changed to some other character, this assumption198 // would be invalidated199 breakLine[k] = StringUtil.DashPadding(count);200 }201 202 GenerateRow(breakLine, lo, false, null, lo.DisplayCells, _header, isHeader: true);203 return _header.Count;204 }205 206 internal void GenerateRow(string[] values, LineOutput lo, bool multiLine, ReadOnlySpan<int> alignment, DisplayCells dc, List<string> generatedRows, bool isHeader = false)207 {208 if (_disabled)209 {210 return;211 }212 213 // build the current row alignment settings214 int cols = _si.columnInfo.Length;215 Span<int> currentAlignment = cols <= OutCommandInner.StackAllocThreshold ? stackalloc int[cols] : new int[cols];216 217 if (alignment.IsEmpty)218 {219 for (int i = 0; i < currentAlignment.Length; i++)220 {221 currentAlignment[i] = _si.columnInfo[i].alignment;222 }223 }224 else225 {226 for (int i = 0; i < currentAlignment.Length; i++)227 {228 if (alignment[i] == TextAlignment.Undefined)229 {230 currentAlignment[i] = _si.columnInfo[i].alignment;231 }232 else233 {234 currentAlignment[i] = alignment[i];235 }236 }237 }238 239 string style = PSStyle.Instance.Formatting.TableHeader;240 string reset = PSStyle.Instance.Reset;241 242 if (multiLine)243 {244 foreach (string line in GenerateTableRow(values, currentAlignment, lo.DisplayCells, isHeader))245 {246 generatedRows?.Add(line);247 lo.WriteLine(line);248 }249 }250 else251 {252 string line = GenerateRow(values, currentAlignment, dc, isHeader);253 generatedRows?.Add(line);254 lo.WriteLine(line);255 }256 }257 258 private string[] GenerateTableRow(string[] values, ReadOnlySpan<int> alignment, DisplayCells ds, bool isHeader)259 {260 // select the active columns (skip hidden ones)261 Span<int> validColumnArray = _si.columnInfo.Length <= OutCommandInner.StackAllocThreshold ? stackalloc int[_si.columnInfo.Length] : new int[_si.columnInfo.Length];262 int validColumnCount = 0;263 for (int k = 0; k < _si.columnInfo.Length; k++)264 {265 if (_si.columnInfo[k].width > 0)266 {267 validColumnArray[validColumnCount++] = k;268 }269 }270 271 if (validColumnCount == 0)272 {273 return null;274 }275 276 StringCollection[] scArray = new StringCollection[validColumnCount];277 bool addPadding = true;278 for (int k = 0; k < scArray.Length; k++)279 {280 // for the last column, don't pad it with trailing spaces281 if (k == scArray.Length - 1)282 {283 addPadding = false;284 }285 286 // obtain a set of tokens for each field287 scArray[k] = GenerateMultiLineRowField(values[validColumnArray[k]], validColumnArray[k], alignment[validColumnArray[k]], ds, addPadding);288 289 // NOTE: the following padding operations assume that we290 // pad with a blank (or any character that ALWAYS maps to a single screen cell291 if (k > 0)292 {293 // skipping the first ones, add a separator for concatenation294 for (int j = 0; j < scArray[k].Count; j++)295 {296 scArray[k][j] = StringUtil.Padding(ScreenInfo.separatorCharacterCount) + scArray[k][j];297 }298 }299 else300 {301 // add indentation padding if needed302 if (_startColumn > 0)303 {304 for (int j = 0; j < scArray[k].Count; j++)305 {306 scArray[k][j] = StringUtil.Padding(_startColumn) + scArray[k][j];307 }308 }309 }310 }311 312 // now we processed all the rows columns and we need to find the cell that occupies the most313 // rows314 int screenRows = 0;315 for (int k = 0; k < scArray.Length; k++)316 {317 if (scArray[k].Count > screenRows)318 {319 screenRows = scArray[k].Count;320 }321 }322 323 // column headers can span multiple rows if the width of the column is shorter than the header text like:324 //325 // Long Header2 Head326 // Head er3327 // er328 // ---- ------- ----329 // 1 2 3330 //331 // To ensure we don't add whitespace to the end, we need to determine the last column in each row with content332 System.Span<int> lastColWithContent = screenRows <= OutCommandInner.StackAllocThreshold ? stackalloc int[screenRows] : new int[screenRows];333 for (int row = 0; row < screenRows; row++)334 {335 for (int col = 0; col < scArray.Length; col++)336 {337 if (scArray[col].Count > row)338 {339 lastColWithContent[row] = col;340 }341 }342 }343 344 // add padding for the columns that are shorter345 for (int col = 0; col < scArray.Length; col++)346 {347 int paddingBlanks = 0;348 349 // don't pad if last column350 if (col < scArray.Length - 1)351 {352 paddingBlanks = _si.columnInfo[validColumnArray[col]].width;353 if (col > 0)354 {355 paddingBlanks += ScreenInfo.separatorCharacterCount;356 }357 else358 {359 paddingBlanks += _startColumn;360 }361 }362 363 int paddingEntries = screenRows - scArray[col].Count;364 if (paddingEntries > 0)365 {366 for (int row = screenRows - paddingEntries; row < screenRows; row++)367 {368 // if the column is beyond the last column with content, just use empty string369 if (col > lastColWithContent[row])370 {371 scArray[col].Add(string.Empty);372 }373 else374 {375 scArray[col].Add(StringUtil.Padding(paddingBlanks));376 }377 }378 }379 }380 381 // finally, build an array of strings382 string[] rows = new string[screenRows];383 for (int row = 0; row < screenRows; row++)384 {385 StringBuilder sb = new StringBuilder();386 387 // for a given row, walk the columns388 for (int col = 0; col < scArray.Length; col++)389 {390 string value = scArray[col][row];391 392 // if the column is the last column with content, we need to trim trailing whitespace, unless there is only one row393 if (col == lastColWithContent[row] && screenRows > 1)394 {395 value = value.TrimEnd();396 }397 398 if (isHeader)399 {400 if (_si.columnInfo[col].HeaderMatchesProperty)401 {402 sb.Append(PSStyle.Instance.Formatting.TableHeader);403 }404 else if (value.Length > 0)405 {406 // after the first column, each additional column starts with a whitespace for separation407 value = value.Insert(col == 0 ? 0 : 1, PSStyle.Instance.Formatting.CustomTableHeaderLabel);408 }409 }410 411 sb.Append(value);412 413 if (isHeader)414 {415 sb.Append(PSStyle.Instance.Reset);416 }417 }418 419 rows[row] = sb.ToString();420 }421 422 return rows;423 }424 425 private StringCollection GenerateMultiLineRowField(string val, int k, int alignment, DisplayCells dc, bool addPadding)426 {427 StringCollection sc = StringManipulationHelper.GenerateLines(dc, val,428 _si.columnInfo[k].width, _si.columnInfo[k].width);429 if (addPadding || alignment == TextAlignment.Right || alignment == TextAlignment.Center)430 {431 // if length is shorter, do some padding432 for (int col = 0; col < sc.Count; col++)433 {434 if (dc.Length(sc[col]) < _si.columnInfo[k].width)435 sc[col] = GenerateRowField(sc[col], _si.columnInfo[k].width, alignment, dc, addPadding);436 }437 }438 439 return sc;440 }441 442 private string GenerateRow(string[] values, ReadOnlySpan<int> alignment, DisplayCells dc, bool isHeader)443 {444 StringBuilder sb = new StringBuilder();445 446 bool addPadding = true;447 for (int k = 0; k < _si.columnInfo.Length; k++)448 {449 // don't pad the last column450 if (k == _si.columnInfo.Length - 1)451 {452 addPadding = false;453 }454 455 if (_si.columnInfo[k].width <= 0)456 {457 // skip columns that are not at least a single character wide458 continue;459 }460 461 // NOTE: the following padding operations assume that we462 // pad with a blank (or any character that ALWAYS maps to a single screen cell463 if (k > 0)464 {465 sb.Append(StringUtil.Padding(ScreenInfo.separatorCharacterCount));466 }467 else468 {469 // add indentation padding if needed470 if (_startColumn > 0)471 {472 sb.Append(StringUtil.Padding(_startColumn));473 }474 }475 476 string rowField = GenerateRowField(values[k], _si.columnInfo[k].width, alignment[k], dc, addPadding);477 if (isHeader)478 {479 sb.Append(PSStyle.Instance.Formatting.TableHeader);480 }481 482 sb.Append(rowField);483 484 if (isHeader || (rowField is not null && rowField.Contains(ValueStringDecorated.ESC) && !rowField.AsSpan().TrimEnd().EndsWith(PSStyle.Instance.Reset)))485 {486 // Reset the console output if the content of this column contains ESC487 sb.Append(PSStyle.Instance.Reset);488 }489 }490 491 return sb.ToString();492 }493 494 private static string GenerateRowField(string val, int width, int alignment, DisplayCells dc, bool addPadding)495 {496 // make sure the string does not have any embedded <CR> in it497 string s = StringManipulationHelper.TruncateAtNewLine(val);498 int currentValueDisplayLength = dc.Length(s);499 500 if (currentValueDisplayLength < width)501 {502 // the string is shorter than the width of the column503 // need to pad with blanks to reach the desired width504 int padCount = width - currentValueDisplayLength;505 switch (alignment)506 {507 case TextAlignment.Right:508 {509 s = StringUtil.Padding(padCount) + s;510 }511 512 break;513 514 case TextAlignment.Center:515 {516 // add a bit at both ends of the string517 int padLeft = padCount / 2;518 int padRight = padCount - padLeft;519 520 s = StringUtil.Padding(padLeft) + s;521 if (addPadding)522 {523 s += StringUtil.Padding(padRight);524 }525 }526 527 break;528 529 default:530 {531 if (addPadding)532 {533 // left align is the default534 s += StringUtil.Padding(padCount);535 }536 }537 538 break;539 }540 }541 else if (currentValueDisplayLength > width)542 {543 // the string is longer than the width of the column544 // truncate and add ellipsis if it's too long545 int truncationDisplayLength = width - EllipsisSize;546 547 if (truncationDisplayLength > 0)548 {549 // we have space for the ellipsis, add it550 switch (alignment)551 {552 case TextAlignment.Right:553 {554 // get from "abcdef" to "...f"555 s = s.VtSubstring(556 startOffset: dc.TruncateHead(s, truncationDisplayLength),557 prependStr: PSObjectHelper.EllipsisStr,558 appendStr: null);559 }560 561 break;562 563 default:564 {565 // left align is the default566 // get from "abcdef" to "a..."567 s = s.VtSubstring(568 startOffset: 0,569 length: dc.TruncateTail(s, truncationDisplayLength),570 prependStr: null,571 appendStr: PSObjectHelper.EllipsisStr);572 }573 574 break;575 }576 }577 else578 {579 // not enough space for the ellipsis, just truncate at the width580 switch (alignment)581 {582 case TextAlignment.Right:583 {584 // get from "abcdef" to "f"585 s = s.VtSubstring(startOffset: dc.TruncateHead(s, width));586 }587 588 break;589 590 default:591 {592 // left align is the default593 // get from "abcdef" to "a"594 s = s.VtSubstring(startOffset: 0, length: dc.TruncateTail(s, width));595 }596 597 break;598 }599 }600 }601 602 // we need to take into consideration that truncation left the string one603 // display cell short if a double cell character got truncated604 // in this case, we need to pad with a blank605 int finalValueDisplayLength = dc.Length(s);606 if (finalValueDisplayLength == width)607 {608 return s;609 }610 611 switch (alignment)612 {613 case TextAlignment.Right:614 {615 s = " " + s;616 }617 618 break;619 620 case TextAlignment.Center:621 {622 if (addPadding)623 {624 s += " ";625 }626 }627 628 break;629 630 default:631 {632 // left align is the default633 if (addPadding)634 {635 s += " ";636 }637 }638 639 break;640 }641 642 return s;643 }644 645 private const int EllipsisSize = 1;646 647 private bool _disabled = false;648 649 private bool _hideHeader = false;650 651 private int _startColumn = 0;652 }653}654 