/// <summary>Writes the specified <see cref="ConsoleColoredString"/> to the console.</summary> public static void Write(ConsoleColoredString value, bool stdErr = false) { if (value != null) value.writeTo(stdErr ? Console.Error : Console.Out); }
/// <summary> /// Writes the specified <see cref="ConsoleColoredString"/> followed by a newline to the console.</summary> /// <param name="value"> /// The string to print to the console.</param> /// <param name="stdErr"> /// <c>true</c> to print to Standard Error instead of Standard Output.</param> /// <param name="align"> /// Horizontal alignment of the string within the remaining space of the current line. If the string does not fit, /// it will be printed as if left-aligned.</param> public static void WriteLine(ConsoleColoredString value, bool stdErr = false, HorizontalTextAlignment align = HorizontalTextAlignment.Left) { var output = stdErr ? Console.Error : Console.Out; if (value != null) { var cursorLeft = 0; try { cursorLeft = Console.CursorLeft; } catch { } var width = WrapToWidth() - cursorLeft; if (align == HorizontalTextAlignment.Center && width > value.Length) output.Write(new string(' ', (width - value.Length) / 2)); else if (align == HorizontalTextAlignment.Right && width > value.Length) output.Write(new string(' ', width - value.Length)); value.writeTo(output); } output.WriteLine(); }
/// <summary> /// Places the specified content into the cell at the specified co-ordinates.</summary> /// <param name="col"> /// Column where to place the content.</param> /// <param name="row"> /// Row where to place the content.</param> /// <param name="content"> /// The content to place.</param> /// <param name="colSpan"> /// The number of columns to span.</param> /// <param name="rowSpan"> /// The number of rows to span.</param> /// <param name="noWrap"> /// If true, indicates that this cell should not be automatically word-wrapped except at explicit newlines in /// <paramref name="content"/>. The cell is word-wrapped only if doing so is necessary to fit all no-wrap cells /// into the table's total width. If false, the cell is automatically word-wrapped to optimise the table's layout.</param> /// <param name="alignment"> /// How to align the contents within the cell, or null to use <see cref="DefaultAlignment"/>.</param> /// <param name="background"> /// Specifies a background color for the whole cell, including its empty space. Characters with background colors /// in the input string take precedence for those characters only.</param> public void SetCell(int col, int row, ConsoleColoredString content, int colSpan = 1, int rowSpan = 1, bool noWrap = false, HorizontalTextAlignment? alignment = null, ConsoleColor? background = null) { setCell(col, row, content, colSpan, rowSpan, noWrap, alignment, background); }
/// <summary> /// Outputs the specified message to the console window, treating newlines as paragraph breaks. All paragraphs are /// word-wrapped to fit in the console buffer, or to a sensible width if redirected to a file. Each paragraph is /// indented by the number of spaces at the start of the corresponding line.</summary> /// <param name="message"> /// The message to output.</param> /// <param name="hangingIndent"> /// Specifies a number of spaces by which the message is indented in all but the first line of each paragraph.</param> public static void WriteParagraphs(ConsoleColoredString message, int hangingIndent = 0) { if (message == null) throw new ArgumentNullException("message"); int width; try { width = WrapToWidth(); } catch { ConsoleUtil.WriteLine(message); return; } foreach (var line in message.WordWrap(width, hangingIndent)) ConsoleUtil.WriteLine(line); }
private void toString(int? maxWidth, Action<string> outputString, Action<ConsoleColoredString> outputColoredString) { int rows = _cells.Count; if (rows == 0) return; int cols = _cells.Max(row => row.Count); // Create a lookup array which, for each column, and for each possible value of colspan, tells you which cells in that column have this colspan and end in this column var cellsByColspan = new SortedDictionary<int, List<int>>[cols]; for (var col = 0; col < cols; col++) { var cellsInThisColumn = new SortedDictionary<int, List<int>>(); for (int row = 0; row < rows; row++) { if (col >= _cells[row].Count) continue; var cel = _cells[row][col]; if (cel == null) continue; if (cel is surrogateCell && ((surrogateCell) cel).RealRow != row) continue; int realCol = cel is surrogateCell ? ((surrogateCell) cel).RealCol : col; var realCell = (trueCell) _cells[row][realCol]; if (realCol + realCell.ColSpan - 1 != col) continue; cellsInThisColumn.AddSafe(realCell.ColSpan, row); } cellsByColspan[col] = cellsInThisColumn; } // Find out the width that each column would have if the text wasn't wrapped. // If this fits into the total width, then we want each column to be at least this wide. var columnWidths = generateColumnWidths(cols, cellsByColspan, c => Math.Max(1, c.LongestParagraph())); var unwrapped = true; // If the table is now too wide, use the length of the longest word, or longest paragraph if nowrap if (maxWidth != null && columnWidths.Sum() > maxWidth - (cols - 1) * ColumnSpacing) { columnWidths = generateColumnWidths(cols, cellsByColspan, c => Math.Max(1, c.MinWidth())); unwrapped = false; } // If the table is still too wide, use the length of the longest paragraph if nowrap, otherwise 0 if (maxWidth != null && columnWidths.Sum() > maxWidth - (cols - 1) * ColumnSpacing) columnWidths = generateColumnWidths(cols, cellsByColspan, c => c.NoWrap ? Math.Max(1, c.LongestParagraph()) : 1); // If the table is still too wide, we will have to wrap like crazy. if (maxWidth != null && columnWidths.Sum() > maxWidth - (cols - 1) * ColumnSpacing) { columnWidths = new int[cols]; for (int i = 0; i < cols; i++) columnWidths[i] = 1; } // If the table is STILL too wide, all bets are off. if (maxWidth != null && columnWidths.Sum() > maxWidth - (cols - 1) * ColumnSpacing) throw new InvalidOperationException(@"The specified table width is too narrow. It is not possible to fit the {0} columns and the column spacing of {1} per column into a total width of {2} characters.".Fmt(cols, ColumnSpacing, maxWidth)); // If we have any extra width to spare... var missingTotalWidth = maxWidth == null ? 0 : maxWidth - columnWidths.Sum() - (cols - 1) * ColumnSpacing; if (missingTotalWidth > 0 && (UseFullWidth || !unwrapped)) { // Use the length of the longest paragraph in each column to calculate a proportion by which to enlarge each column var widthProportionByCol = new int[cols]; for (var col = 0; col < cols; col++) foreach (var kvp in cellsByColspan[col]) distributeEvenly( widthProportionByCol, col, kvp.Key, kvp.Value.Max(row => ((trueCell) _cells[row][col - kvp.Key + 1]).LongestParagraph()) - widthProportionByCol.Skip(col - kvp.Key + 1).Take(kvp.Key).Sum() - (unwrapped ? 0 : columnWidths.Skip(col - kvp.Key + 1).Take(kvp.Key).Sum()) ); var widthProportionTotal = widthProportionByCol.Sum(); // Adjust the width of the columns according to the calculated proportions so that they fill the missing width. // We do this in two steps. Step one: enlarge the column widths by the integer part of the calculated portion (round down). // After this the width remaining will be smaller than the number of columns, so each column is missing at most 1 character. var widthRemaining = missingTotalWidth; var fractionalParts = new double[cols]; for (int col = 0; col < cols; col++) { var widthToAdd = (double) (widthProportionByCol[col] * missingTotalWidth) / widthProportionTotal; var integerPart = (int) widthToAdd; columnWidths[col] += integerPart; fractionalParts[col] = widthToAdd - integerPart; widthRemaining -= integerPart; } // Step two: enlarge a few more columns by 1 character so that we reach the desired width. // The columns with the largest fractional parts here are the furthest from what we ideally want, so we favour those. foreach (var elem in fractionalParts.Select((frac, col) => new { Value = frac, Col = col }).OrderByDescending(e => e.Value)) { if (widthRemaining < 1) break; columnWidths[elem.Col]++; widthRemaining--; } } // Word-wrap all the contents of all the cells trueCell truCel; foreach (var row in _cells) for (int col = 0; col < row.Count; col++) if ((truCel = row[col] as trueCell) != null) truCel.Wordwrap(columnWidths.Skip(col).Take(truCel.ColSpan).Sum() + (truCel.ColSpan - 1) * ColumnSpacing); // Calculate the string index for each column var strIndexByCol = new int[cols + 1]; for (var i = 0; i < cols; i++) strIndexByCol[i + 1] = strIndexByCol[i] + columnWidths[i] + ColumnSpacing; var realWidth = strIndexByCol[cols] - ColumnSpacing; // Make sure we don't render rules if we can't bool verticalRules = VerticalRules && ColumnSpacing > 0; bool horizontalRules = HorizontalRules && RowSpacing > 0; // If we do render vertical rules, where should it be (at which string offset, counted backwards from the end of the column spacing) var vertRuleOffset = (ColumnSpacing + 1) / 2; // Finally, render the entire output List<ConsoleColoredString> currentLine = null; for (int row = 0; row < rows; row++) { var rowList = _cells[row]; var extraRows = RowSpacing + 1; var isFirstIteration = true; bool anyMoreContentInThisRow; do { ConsoleColoredString previousLine = currentLine == null ? null : new ConsoleColoredString(currentLine.ToArray()); currentLine = new List<ConsoleColoredString>(); anyMoreContentInThisRow = false; for (int col = 0; col < cols; col++) { var cel = col < rowList.Count ? rowList[col] : null; // For cells with colspan, consider only the first cell they're spanning and skip the rest if (cel is surrogateCell && ((surrogateCell) cel).RealCol != col) continue; // If the cell has rowspan, what row did this cell start in? var valueRow = cel is surrogateCell ? ((surrogateCell) cel).RealRow : row; // Retrieve the data for the cell var realCell = col < _cells[valueRow].Count ? (trueCell) _cells[valueRow][col] : null; var colspan = realCell == null ? 1 : realCell.ColSpan; var rowspan = realCell == null ? 1 : realCell.RowSpan; var rowBackground = row >= _rowBackgrounds.Count ? null : _rowBackgrounds[row]; // Does this cell end in this row? var isLastRow = valueRow + rowspan - 1 == row; // If we are inside the cell, render one line of the contents of the cell if (realCell != null && realCell.WordwrappedValue.Length > realCell.WordwrappedIndex) { var align = realCell.Alignment ?? DefaultAlignment; var curLineLength = currentLine.Sum(c => c.Length); var cellBackground = realCell.Background ?? rowBackground; if (strIndexByCol[col] > curLineLength) currentLine.Add(new string(' ', strIndexByCol[col] - curLineLength).Color(null, cellBackground)); object textRaw = realCell.WordwrappedValue[realCell.WordwrappedIndex]; ConsoleColoredString text = textRaw is ConsoleColoredString ? (ConsoleColoredString) textRaw : (string) textRaw; // implicit conversion to ConsoleColoredString if (align == HorizontalTextAlignment.Center) currentLine.Add(new string(' ', (strIndexByCol[col + colspan] - strIndexByCol[col] - ColumnSpacing - text.Length) / 2).Color(null, cellBackground)); else if (align == HorizontalTextAlignment.Right) currentLine.Add(new string(' ', strIndexByCol[col + colspan] - strIndexByCol[col] - ColumnSpacing - text.Length).Color(null, cellBackground)); if (cellBackground == null) currentLine.Add(text); else { currentLine.Add(text.ColorBackgroundWhereNull(cellBackground.Value)); if (align == HorizontalTextAlignment.Center) currentLine.Add(new string(' ', (strIndexByCol[col + colspan] - strIndexByCol[col] - ColumnSpacing - text.Length + 1) / 2).Color(null, cellBackground)); else if (align == HorizontalTextAlignment.Left) currentLine.Add(new string(' ', strIndexByCol[col + colspan] - strIndexByCol[col] - ColumnSpacing - text.Length).Color(null, cellBackground)); } realCell.WordwrappedIndex++; } // If we are at the end of a row, render horizontal rules var horizRuleStart = col > 0 ? strIndexByCol[col] - vertRuleOffset + 1 : 0; var horizRuleEnd = (col + colspan < cols) ? strIndexByCol[col + colspan] - vertRuleOffset + (verticalRules ? 0 : 1) : realWidth; var renderingHorizontalRules = horizontalRules && isLastRow && extraRows == 1; if (renderingHorizontalRules) { currentLine.Add(new string(' ', horizRuleStart - currentLine.Sum(c => c.Length))); currentLine.Add(new string((row == HeaderRows - 1) ? '=' : '-', horizRuleEnd - horizRuleStart)); } else { var subtract = (col + colspan == cols ? ColumnSpacing : vertRuleOffset) + currentLine.Sum(c => c.Length); currentLine.Add(new string(' ', strIndexByCol[col + colspan] - subtract).Color(null, (realCell == null ? null : realCell.Background) ?? rowBackground)); } // If we are at the beginning of a row, render the horizontal rules for the row above by modifying the previous line. // We want to do this because it may have an unwanted vertical rule if this is a cell with colspan and there are // multiple cells with smaller colspans above it. if (isFirstIteration && horizontalRules && row > 0 && cel is trueCell) previousLine = new ConsoleColoredString(previousLine.Substring(0, horizRuleStart), new string((row == HeaderRows) ? '=' : '-', horizRuleEnd - horizRuleStart), previousLine.Substring(horizRuleEnd)); // Render vertical rules if (verticalRules && (col + colspan < cols)) currentLine.Add((new string(' ', strIndexByCol[col + colspan] - vertRuleOffset - currentLine.Sum(c => c.Length)) + "|").Color(null, renderingHorizontalRules ? null : rowBackground)); // Does this cell still contain any more content that needs to be output before this row can be finished? anyMoreContentInThisRow = anyMoreContentInThisRow || (realCell != null && isLastRow && realCell.WordwrappedValue.Length > realCell.WordwrappedIndex); } if (previousLine != null) { if (LeftMargin > 0) outputString(new string(' ', LeftMargin)); outputColoredString(previousLine); outputString(Environment.NewLine); } isFirstIteration = false; // If none of the cells in this row contain any more content, start counting down the row spacing if (!anyMoreContentInThisRow) extraRows--; } while (anyMoreContentInThisRow || (extraRows > 0 && row < rows - 1)); } // Output the last line if (LeftMargin > 0) outputString(new string(' ', LeftMargin)); outputColoredString(new ConsoleColoredString(currentLine.ToArray())); outputString(Environment.NewLine); }
/// <summary> /// Returns a new <see cref="ConsoleColoredString"/> in which every occurrence of the text in <paramref /// name="oldValue"/> is replaced with a new colored string.</summary> /// <param name="oldValue"> /// The substring to search for.</param> /// <param name="newValue"> /// The new colored string to replace every occurrence with.</param> /// <param name="comparison"> /// A string comparison to use.</param> /// <returns> /// The new string after replacements.</returns> public ConsoleColoredString Replace(string oldValue, ConsoleColoredString newValue, StringComparison comparison = StringComparison.Ordinal) { if (oldValue == null) throw new ArgumentNullException("oldValue"); if (newValue == null) throw new ArgumentNullException("newValue"); var index = 0; var newText = new List<ConsoleColoredString>(); while (true) { var pos = _text.IndexOf(oldValue, index, comparison); if (pos == -1) { if (index < _text.Length) newText.Add(Substring(index)); return new ConsoleColoredString(newText); } if (pos > index) newText.Add(Substring(index, pos - index)); newText.Add(newValue); index = pos + oldValue.Length; } }
internal IEnumerable<object> FmtEnumerableInternal(FormatBehavior behavior, IFormatProvider provider, params object[] args) { var index = 0; var oldIndex = 0; var customFormatter = provider == null ? null : provider.GetFormat(typeof(ICustomFormatter)) as ICustomFormatter; var substring = behavior.HasFlag(FormatBehavior.Colored) ? Ut.Lambda((int ix, int length) => (object) Substring(ix, length)) : Ut.Lambda((int ix, int length) => (object) _text.Substring(ix, length)); while (index < _text.Length) { char ch = _text[index]; if (ch == '{' && index < _text.Length - 1 && _text[index + 1] == '{') { yield return substring(oldIndex, index + 1 - oldIndex); index++; oldIndex = index + 1; } else if (ch == '{' && index < _text.Length - 1 && _text[index + 1] >= '0' && _text[index + 1] <= '9') { var implicitForeground = _foreground[index]; var implicitBackground = _background[index]; if (index > oldIndex) yield return substring(oldIndex, index - oldIndex); var num = 0; var leftAlign = false; var align = 0; StringBuilder foregroundBuilder = null, backgroundBuilder = null, formatBuilder = null; // Syntax: {num[,alignment][/[foreground]][+[background]][:format]} // States: 0 = before first digit of num; 1 = during num; 2 = before align; 3 = during align; 4 = during foreground; 5 = during background; 6 = during format var state = 0; while (true) { index++; if (index == _text.Length) throw new FormatException("The specified format string is invalid."); ch = _text[index]; if (ch == '}') { if (index + 1 == _text.Length || _text[index + 1] != '}' || state == 1 || state == 3) break; index++; } if (state == 6 && ch == '{' && index + 1 < _text.Length && _text[index + 1] == '{') index++; if ((state == 0 || state == 1) && ch >= '0' && ch <= '9') { num = (num * 10) + (ch - '0'); state = 1; } else if (state == 1 && ch == ',') state = 2; else if (state == 2 && ch == '-') { leftAlign = true; state = 3; } else if ((state == 2 || state == 3) && ch >= '0' && ch <= '9') { align = (align * 10) + (ch - '0'); state = 3; } else if ((state == 1 || state == 3) && ch == '/') { foregroundBuilder = new StringBuilder(); state = 4; } else if ((state == 1 || state == 3 || state == 4) && ch == '+') { backgroundBuilder = new StringBuilder(); state = 5; } else if ((state == 1 || state == 3 || state == 4 || state == 5) && ch == ':') { formatBuilder = new StringBuilder(); state = 6; } else if (state == 4) foregroundBuilder.Append(ch); else if (state == 5) backgroundBuilder.Append(ch); else if (state == 6) formatBuilder.Append(ch); else throw new FormatException("The specified format string is invalid."); } if (num >= args.Length) throw new FormatException("The specified format string references an array index outside the bounds of the supplied arguments."); var formatString = formatBuilder == null ? null : formatBuilder.ToString(); if (behavior == (FormatBehavior.Stringify | FormatBehavior.Colored)) { if (args[num] != null) { var foregroundStr = foregroundBuilder == null ? null : foregroundBuilder.ToString(); var backgroundStr = backgroundBuilder == null ? null : backgroundBuilder.ToString(); ConsoleColor foreground = 0, background = 0; if (foregroundStr != null && foregroundStr != "" && !Enum.TryParse<ConsoleColor>(foregroundStr, true, out foreground)) throw new FormatException("The specified format string uses an invalid console color name ({0}).".Fmt(foregroundStr)); if (backgroundStr != null && backgroundStr != "" && !Enum.TryParse<ConsoleColor>(backgroundStr, true, out background)) throw new FormatException("The specified format string uses an invalid console color name ({0}).".Fmt(backgroundStr)); var objFormattable = args[num] as IFormattable; var result = args[num] as ConsoleColoredString; // If the object is a ConsoleColoredString, use it (and color it if a color is explicitly specified) if (result != null) { if (foregroundStr != null) result = result.Color(foregroundStr == "" ? (ConsoleColor?) null : foreground); else if (implicitForeground != null) result = result.ColorWhereNull(implicitForeground.Value); if (backgroundStr != null) result = result.ColorBackground(backgroundStr == "" ? (ConsoleColor?) null : background); else if (implicitBackground != null) result = result.ColorBackgroundWhereNull(implicitBackground.Value); } // ... otherwise use IFormattable and/or the custom formatter (and then color the result of that, if specified). else { result = new ConsoleColoredString( formatString != null && objFormattable != null ? objFormattable.ToString(formatString, provider) : formatString != null && customFormatter != null ? customFormatter.Format(formatString, args[num], provider) : args[num].ToString(), foregroundStr == null ? implicitForeground : foregroundStr == "" ? (ConsoleColor?) null : foreground, backgroundStr == null ? implicitBackground : backgroundStr == "" ? (ConsoleColor?) null : background); } // Alignment if (result.Length < align) result = leftAlign ? result + new string(' ', align - result.Length) : new string(' ', align - result.Length) + result; yield return result; } } else if (behavior == FormatBehavior.Stringify) { if (args[num] != null) { var objFormattable = args[num] as IFormattable; var result = formatString != null && objFormattable != null ? objFormattable.ToString(formatString, provider) : formatString != null && customFormatter != null ? customFormatter.Format(formatString, args[num], provider) : args[num].ToString(); // Alignment if (result.Length < align) result = leftAlign ? result + new string(' ', align - result.Length) : new string(' ', align - result.Length) + result; yield return result; } } else yield return args[num]; oldIndex = index + 1; } else if (ch == '}') { yield return substring(oldIndex, index + 1 - oldIndex); if (index < _text.Length - 1 && _text[index + 1] == '}') index++; oldIndex = index + 1; } index++; } if (index > oldIndex) yield return substring(oldIndex, index - oldIndex); }
/// <summary> /// Replaces the format item in a specified string with the string representation of a corresponding object in a /// specified array. A specified parameter supplies culture-specific formatting information.</summary> /// <param name="provider"> /// An object that supplies culture-specific formatting information.</param> /// <param name="format"> /// A composite format string.</param> /// <param name="args"> /// An object array that contains zero or more objects to format.</param> /// <returns> /// A copy of <paramref name="format"/> in which the format items have been replaced by the string representation /// of the corresponding objects in <paramref name="args"/>.</returns> public static ConsoleColoredString Format(ConsoleColoredString format, IFormatProvider provider, params object[] args) { if (format == null) throw new ArgumentNullException("format"); return format.Fmt(provider, args); }
/// <summary> /// Returns a new string in which a specified string is inserted at a specified index position in this instance.</summary> /// <param name="startIndex"> /// The zero-based index position of the insertion.</param> /// <param name="value"> /// The string to insert.</param> /// <returns> /// A new string that is equivalent to this instance, but with <paramref name="value"/> inserted at position /// <paramref name="startIndex"/>.</returns> public ConsoleColoredString Insert(int startIndex, ConsoleColoredString value) { if (startIndex < 0 || startIndex > Length) throw new ArgumentOutOfRangeException("startIndex", "startIndex cannot be negative or greater than the length of the string."); return Substring(0, startIndex) + value + Substring(startIndex); }