/********************************************************************++ Copyright (c) Microsoft Corporation. All rights reserved. --********************************************************************/ using System.Collections.Specialized; using System.Management.Automation.Internal; using System.Text; namespace Microsoft.PowerShell.Commands.Internal.Format { internal class TableWriter { /// /// Information about each column boundaries /// private class ColumnInfo { internal int startCol = 0; internal int width = 0; internal int alignment = TextAlignment.Left; } /// /// Class containing information about the tabular layout /// private class ScreenInfo { internal int screenColumns = 0; internal const int separatorCharacterCount = 1; internal const int minimumScreenColumns = 5; internal const int minimumColumnWidth = 1; internal ColumnInfo[] columnInfo = null; } private ScreenInfo _si; internal static int ComputeWideViewBestItemsPerRowFit(int stringLen, int screenColumns) { if (stringLen <= 0 || screenColumns < 1) return 1; if (stringLen >= screenColumns) { // we too wide anyways, we might have to trim even for a single column return 1; } // we try to fit more than one: start increasing until we do not fit anymore int columnNumber = 1; while (true) { // would we fit with one more column? int nextValue = columnNumber + 1; // compute the width if we added the extra column int width = stringLen * nextValue + (nextValue - 1) * ScreenInfo.separatorCharacterCount; if (width >= screenColumns) { // we would not fit, so we are done return columnNumber; } // try another round columnNumber++; } } /// /// Initialize the table specifying the width of each column /// /// left margin indentation /// number of character columns on the screen /// array of specified column widths /// array of alignment flags /// if true, suppress header printing internal void Initialize(int leftMarginIndent, int screenColumns, int[] columnWidths, int[] alignment, bool suppressHeader) { //Console.WriteLine(" 1 2 3 4 5 6 7"); //Console.WriteLine("01234567890123456789012345678901234567890123456789012345678901234567890123456789"); if (screenColumns == int.MaxValue) { try { screenColumns = System.Console.WindowWidth; } catch { screenColumns = 120; } } if (leftMarginIndent < 0) { leftMarginIndent = 0; } if (screenColumns - leftMarginIndent < ScreenInfo.minimumScreenColumns) { _disabled = true; return; } _startColumn = leftMarginIndent; _hideHeader = suppressHeader; // make sure the column widths are correct; if not, take appropriate action ColumnWidthManager manager = new ColumnWidthManager(screenColumns - leftMarginIndent, ScreenInfo.minimumColumnWidth, ScreenInfo.separatorCharacterCount); manager.CalculateColumnWidths(columnWidths); // if all the columns are hidden, just disable bool oneValid = false; for (int k = 0; k < columnWidths.Length; k++) { if (columnWidths[k] >= ScreenInfo.minimumColumnWidth) { oneValid = true; break; } } if (!oneValid) { _disabled = true; return; } // now set the run time data structures _si = new ScreenInfo(); _si.screenColumns = screenColumns; _si.columnInfo = new ColumnInfo[columnWidths.Length]; int startCol = _startColumn; for (int k = 0; k < columnWidths.Length; k++) { _si.columnInfo[k] = new ColumnInfo(); _si.columnInfo[k].startCol = startCol; _si.columnInfo[k].width = columnWidths[k]; _si.columnInfo[k].alignment = alignment[k]; startCol += columnWidths[k] + ScreenInfo.separatorCharacterCount; //Console.WriteLine("start = {0} width = {1}", si.columnInfo[k].startCol, si.columnInfo[k].width); } } internal void GenerateHeader(string[] values, LineOutput lo) { if (_disabled) return; if (_hideHeader) return; // generate the row with the header labels GenerateRow(values, lo, true, null, lo.DisplayCells); // generate an array of "--" as header markers below // the column header labels string[] breakLine = new string[values.Length]; for (int k = 0; k < _si.columnInfo.Length; k++) { // the column can be hidden if (_si.columnInfo[k].width <= 0) { breakLine[k] = ""; continue; } // the title can be larger than the width int count = _si.columnInfo[k].width; if (!string.IsNullOrEmpty(values[k])) { int labelDisplayCells = lo.DisplayCells.Length(values[k]); if (labelDisplayCells < count) count = labelDisplayCells; } // NOTE: we can do this because "-" is a single cell character // on all devices. If changed to some other character, this assumption // would be invalidated breakLine[k] = new string('-', count); } GenerateRow(breakLine, lo, false, null, lo.DisplayCells); } internal void GenerateRow(string[] values, LineOutput lo, bool multiLine, int[] alignment, DisplayCells dc) { if (_disabled) return; // build the current row alignment settings int cols = _si.columnInfo.Length; int[] currentAlignment = new int[cols]; if (alignment == null) { for (int i = 0; i < cols; i++) { currentAlignment[i] = _si.columnInfo[i].alignment; } } else { for (int i = 0; i < cols; i++) { if (alignment[i] == TextAlignment.Undefined) currentAlignment[i] = _si.columnInfo[i].alignment; else currentAlignment[i] = alignment[i]; } } if (multiLine) { string[] lines = GenerateTableRow(values, currentAlignment, lo.DisplayCells); for (int k = 0; k < lines.Length; k++) { lo.WriteLine(lines[k]); } } else { lo.WriteLine(GenerateRow(values, currentAlignment, dc)); } } private string[] GenerateTableRow(string[] values, int[] alignment, DisplayCells ds) { // select the active columns (skip hidden ones) int[] validColumnArray = new int[_si.columnInfo.Length]; int validColumnCount = 0; for (int k = 0; k < _si.columnInfo.Length; k++) { if (_si.columnInfo[k].width > 0) { validColumnArray[validColumnCount++] = k; } } if (validColumnCount == 0) return null; StringCollection[] scArray = new StringCollection[validColumnCount]; for (int k = 0; k < scArray.Length; k++) { // obtain a set of tokens for each field scArray[k] = GenerateMultiLineRowField(values[validColumnArray[k]], validColumnArray[k], alignment[validColumnArray[k]], ds); // NOTE: the following padding operations assume that we // pad with a blank (or any character that ALWAYS maps to a single screen cell if (k > 0) { // skipping the first ones, add a separator for catenation for (int j = 0; j < scArray[k].Count; j++) { scArray[k][j] = StringUtil.Padding(ScreenInfo.separatorCharacterCount) + scArray[k][j]; } } else { // add indentation padding if needed if (_startColumn > 0) { for (int j = 0; j < scArray[k].Count; j++) { scArray[k][j] = StringUtil.Padding(_startColumn) + scArray[k][j]; } } } } // now we processed all the rows columns and we need to find the cell that occupies the most // rows int screenRows = 0; for (int k = 0; k < scArray.Length; k++) { if (scArray[k].Count > screenRows) screenRows = scArray[k].Count; } // add padding for the columns that are shorter for (int col = 0; col < scArray.Length; col++) { int paddingBlanks = _si.columnInfo[validColumnArray[col]].width; if (col > 0) paddingBlanks += ScreenInfo.separatorCharacterCount; else { paddingBlanks += _startColumn; } int paddingEntries = screenRows - scArray[col].Count; if (paddingEntries > 0) { for (int j = 0; j < paddingEntries; j++) { scArray[col].Add(StringUtil.Padding(paddingBlanks)); } } } // finally, build an array of strings string[] rows = new string[screenRows]; for (int row = 0; row < rows.Length; row++) { StringBuilder sb = new StringBuilder(); // for a give row, walk the columns for (int col = 0; col < scArray.Length; col++) { sb.Append(scArray[col][row]); } rows[row] = sb.ToString(); } return rows; } private StringCollection GenerateMultiLineRowField(string val, int k, int alignment, DisplayCells dc) { StringCollection sc = StringManipulationHelper.GenerateLines(dc, val, _si.columnInfo[k].width, _si.columnInfo[k].width); // if length is shorter, do some padding for (int col = 0; col < sc.Count; col++) { if (dc.Length(sc[col]) < _si.columnInfo[k].width) sc[col] = GenerateRowField(sc[col], _si.columnInfo[k].width, alignment, dc); } return sc; } private string GenerateRow(string[] values, int[] alignment, DisplayCells dc) { StringBuilder sb = new StringBuilder(); for (int k = 0; k < _si.columnInfo.Length; k++) { if (_si.columnInfo[k].width <= 0) { // skip columns that are not at least a single character wide continue; } int newRowIndex = sb.Length; // NOTE: the following padding operations assume that we // pad with a blank (or any character that ALWAYS maps to a single screen cell if (k > 0) { sb.Append(StringUtil.Padding(ScreenInfo.separatorCharacterCount)); } else { // add indentation padding if needed if (_startColumn > 0) { sb.Append(StringUtil.Padding(_startColumn)); } } sb.Append(GenerateRowField(values[k], _si.columnInfo[k].width, alignment[k], dc)); } return sb.ToString(); } private static string GenerateRowField(string val, int width, int alignment, DisplayCells dc) { // make sure the string does not have any embedded in it string s = StringManipulationHelper.TruncateAtNewLine(val) ?? ""; string currentValue = s; int currentValueDisplayLength = dc.Length(currentValue); if (currentValueDisplayLength < width) { // the string is shorter than the width of the column // need to pad with with blanks to reach the desired width int padCount = width - currentValueDisplayLength; switch (alignment) { case TextAlignment.Right: { s = StringUtil.Padding(padCount) + s; } break; case TextAlignment.Center: { // add a bit at both ends of the string int padLeft = padCount / 2; int padRight = padCount - padLeft; s = StringUtil.Padding(padLeft) + s + StringUtil.Padding(padRight); } break; default: { // left align is the default s += StringUtil.Padding(padCount); } break; } } else if (currentValueDisplayLength > width) { // the string is longer than the width of the column // truncate and add ellipsis if it's too long int truncationDisplayLength = width - ellipsis.Length; if (truncationDisplayLength > 0) { // we have space for the ellipsis, add it switch (alignment) { case TextAlignment.Right: { // get from "abcdef" to "...f" int tailCount = dc.GetTailSplitLength(s, truncationDisplayLength); s = s.Substring(s.Length - tailCount); s = ellipsis + s; } break; case TextAlignment.Center: { // get from "abcdef" to "a..." s = s.Substring(0, dc.GetHeadSplitLength(s, truncationDisplayLength)); s += ellipsis; } break; default: { // left align is the default // get from "abcdef" to "a..." s = s.Substring(0, dc.GetHeadSplitLength(s, truncationDisplayLength)); s += ellipsis; } break; } } else { // not enough space for the ellipsis, just truncate at the width int len = width; switch (alignment) { case TextAlignment.Right: { // get from "abcdef" to "f" int tailCount = dc.GetTailSplitLength(s, len); s = s.Substring(s.Length - tailCount, tailCount); } break; case TextAlignment.Center: { // get from "abcdef" to "a" s = s.Substring(0, dc.GetHeadSplitLength(s, len)); } break; default: { // left align is the default // get from "abcdef" to "a" s = s.Substring(0, dc.GetHeadSplitLength(s, len)); } break; } } } // we need to take into consideration that truncation left the string one // display cell short if a double cell character got truncated // in this case, we need to pad with a blank int finalValueDisplayLength = dc.Length(s); if (finalValueDisplayLength == width) { return s; } // we have to pad System.Diagnostics.Debug.Assert(finalValueDisplayLength == width - 1, "padding is not correct"); switch (alignment) { case TextAlignment.Right: { s = " " + s; } break; case TextAlignment.Center: { s += " "; } break; default: { // left align is the default s += " "; } break; } return s; } private const string ellipsis = "..."; private bool _disabled = false; private bool _hideHeader = false; private int _startColumn = 0; } }